Zuul 源码分析

zuul是spring cloud 微服务体系中的网关,可以路由请求到具体的服务,同时做一些验签,解密等的与业务无关的事情。今天我们从一个注解@EnableZuulProxy开始讲述。这个注解导入了一个配置文件ZuulProxyConfiguration,他继承了ZuulConfiguration,整个zuul的流程的定义就在这两个类中。

@Configuration
@EnableConfigurationProperties({ ZuulProperties.class })
@ConditionalOnClass(ZuulServlet.class)
// Make sure to get the ServerProperties from the same place as a normal web app would
@Import(ServerPropertiesAutoConfiguration.class)
public class ZuulConfiguration {

   @Autowired
   protected ZuulProperties zuulProperties;

   @Autowired
   protected ServerProperties server;

   @Autowired(required = false)
   private ErrorController errorController;

 

   @Bean
   public ZuulController zuulController() {
      return new ZuulController();
   }

   @Bean
   public ZuulHandlerMapping zuulHandlerMapping(RouteLocator routes) {
      ZuulHandlerMapping mapping = new ZuulHandlerMapping(routes, zuulController());
      mapping.setErrorController(this.errorController);
      return mapping;
   }

   @Bean
   @ConditionalOnMissingBean(name = "zuulServlet")
   public ServletRegistrationBean zuulServlet() {
      ServletRegistrationBean servlet = new ServletRegistrationBean(new ZuulServlet(),
            this.zuulProperties.getServletPattern());
      // The whole point of exposing this servlet is to provide a route that doesn't
      // buffer requests.
      servlet.addInitParameter("buffer-requests", "false");
      return servlet;
   }

   // pre filters

   @Bean
   public ServletDetectionFilter servletDetectionFilter() {
      return new ServletDetectionFilter();
   }

   @Bean
   public FormBodyWrapperFilter formBodyWrapperFilter() {
      return new FormBodyWrapperFilter();
   }

   @Bean
   public DebugFilter debugFilter() {
      return new DebugFilter();
   }

   @Bean
   public Servlet30WrapperFilter servlet30WrapperFilter() {
      return new Servlet30WrapperFilter();
   }

   // post filters

   @Bean
   public SendResponseFilter sendResponseFilter() {
      return new SendResponseFilter();
   }

   @Bean
   public SendErrorFilter sendErrorFilter() {
      return new SendErrorFilter();
   }

   @Bean
   public SendForwardFilter sendForwardFilter() {
      return new SendForwardFilter();
   }
 
   ...
}

zuulProperties 是用户通过配置文件编写的服务路由等信息,表明请求是哪种模式时需要跳转到哪个具体的服务,这是主要的内容,当然还有一些其他的信息。具体的看一下这个类的内容就可以了,都可以配置,这是我们定制化zuul的一个基础配置文件。剩下的zuulController,zuulHandlerMapping,zuulServlet就是spring mvc 的组件了,通过zuulHandlerMapping可以发现所有的请求都交给了zuulController来处理,它里面包装的Servlet就是zuulServlet,由他的service方法来处理。这个下面细说。例外还配置了ZuulFilter的过滤器,着重看一下pre类型的servletDetectionFilter与post类型的sendResponseFilter。

@Configuration
public class ZuulProxyConfiguration extends ZuulConfiguration {

   @Autowired(required = false)
   private TraceRepository traces;

   @Autowired
   private SpringClientFactory clientFactory;

   @Autowired
   private DiscoveryClient discovery;

   @Autowired
   private ServiceRouteMapper serviceRouteMapper;

   @Bean
   @Override
   @ConditionalOnMissingBean(RouteLocator.class)
   public DiscoveryClientRouteLocator routeLocator() {
      return new DiscoveryClientRouteLocator(this.server.getServletPrefix(),
            this.discovery, this.zuulProperties, this.serviceRouteMapper);
   }

   @Bean
   @ConditionalOnMissingBean
   public RibbonCommandFactory<?> ribbonCommandFactory() {
      return new RestClientRibbonCommandFactory(this.clientFactory);
   }

   // pre filters
   @Bean
   public PreDecorationFilter preDecorationFilter(RouteLocator routeLocator,
         ProxyRequestHelper proxyRequestHelper) {
      return new PreDecorationFilter(routeLocator,
            this.server.getServletPrefix(),
            this.zuulProperties,
            proxyRequestHelper);
   }

   // route filter
   @Bean
   public RibbonRoutingFilter ribbonRoutingFilter(ProxyRequestHelper helper,
         RibbonCommandFactory<?> ribbonCommandFactory) {
      RibbonRoutingFilter filter = new RibbonRoutingFilter(helper,
            ribbonCommandFactory);
      return filter;
   }

   @Bean
   public SimpleHostRoutingFilter simpleHostRoutingFilter(ProxyRequestHelper helper,
         ZuulProperties zuulProperties) {
      return new SimpleHostRoutingFilter(helper, zuulProperties);
   }

   @Bean
   public ProxyRequestHelper proxyRequestHelper() {     //作为一个帮助类,主要用来设置值
      ProxyRequestHelper helper = new ProxyRequestHelper();
      if (this.traces != null) {
         helper.setTraces(this.traces);
      }
      helper.setIgnoredHeaders(this.zuulProperties.getIgnoredHeaders());
      helper.setTraceRequestBody(this.zuulProperties.isTraceRequestBody());
      return helper;
   }

   @Bean
   @ConditionalOnMissingBean(ServiceRouteMapper.class)
   public ServiceRouteMapper serviceRouteMapper() {
      return new SimpleServiceRouteMapper();
   }

   @Configuration
   @ConditionalOnClass(Endpoint.class)
   protected static class RoutesEndpointConfiguration {

      @Bean
      public RoutesEndpoint zuulEndpoint(RouteLocator routeLocator) {
         return new RoutesEndpoint(routeLocator);
      }
   }
}

routeLocator是一个路由的匹配器,他的实现是一个
DiscoveryClientRouteLocator,我这边使用的是Consul作为服务注册的容器,他会从consul中拉取各个服务的信息,比如我们再zuul中配置了要路由的serviceId,那么从consul中寻找,如果有对应的serviceId,那么就是这个服务来处理这个请求。proxyRequestHelper是一个帮助类,用来再context中set值。还有两个route类型zuulFilter,虽然都是Bean,但不一定都启用。simpleHostRoutingFilter再设置了url的情况下使用,ribbonRoutingFilter在设置了serviceId的情况下使用,有客户端负载均衡的作用。还有一个pre类型的zuulFilter
preDecorationFilter,主要是填充一些数据。OK,准备工作做的差不多了,我们来详细看一下流程。请求过来了,交给了ZuulServlet进行处理。

public class ZuulServlet extends HttpServlet {

    private static final long serialVersionUID = -3374242278843351500L;
    private ZuulRunner zuulRunner;


    @Override
    public void init(ServletConfig config) throws ServletException {
        super.init(config);

        String bufferReqsStr = config.getInitParameter("buffer-requests");
        boolean bufferReqs = bufferReqsStr != null && bufferReqsStr.equals("true") ? true : false;

        zuulRunner = new ZuulRunner(bufferReqs);
    }

    @Override
    public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
        try {
            init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);

            // Marks this request as having passed through the "Zuul engine", as opposed to servlets
            // explicitly bound in web.xml, for which requests will not have the same data attached
            RequestContext context = RequestContext.getCurrentContext();
            context.setZuulEngineRan();

            try {
                preRoute();
            } catch (ZuulException e) {
                error(e);
                postRoute();
                return;
            }
            try {
                route();
            } catch (ZuulException e) {
                error(e);
                postRoute();
                return;
            }
            try {
                postRoute();
            } catch (ZuulException e) {
                error(e);
                return;
            }

        } catch (Throwable e) {
            error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName()));
        } finally {
            RequestContext.getCurrentContext().unset();
        }
    }

    /**
     * executes "post" ZuulFilters
     *
     * @throws ZuulException
     */
    void postRoute() throws ZuulException {
        zuulRunner.postRoute();
    }

    /**
     * executes "route" filters
     *
     * @throws ZuulException
     */
    void route() throws ZuulException {
        zuulRunner.route();
    }

    /**
     * executes "pre" filters
     *
     * @throws ZuulException
     */
    void preRoute() throws ZuulException {
        zuulRunner.preRoute();
    }

    /**
     * initializes request
     *
     * @param servletRequest
     * @param servletResponse
     */
    void init(HttpServletRequest servletRequest, HttpServletResponse servletResponse) {
        zuulRunner.init(servletRequest, servletResponse);
    }

    /**
     * sets error context info and executes "error" filters
     *
     * @param e
     */
    void error(ZuulException e) {
        RequestContext.getCurrentContext().setThrowable(e);
        zuulRunner.error();
    }
}

在这个servlet初始化的时候会调用init方法,里面有个属性
buffer-requests用来判断是否走spring mvc。不知道大家有没有遇到过这种情况,上传文件走网关时,需要在链接上加上/zuul,加上就不需要走spring mvc ,这样不会对数据的大小有限制,所以可以传递大数据量的文件。另外初始化了一个zuulRunner,他所作的事情
第一点是在RequestContext设置请求按照buffer-requests这个参数来进行,如果为真,request被HttpServletRequestWrapper包装一下 ,第二点的作用是处理按照类型处理filter.。

/**
 * executes "route" filterType  ZuulFilters
 *
 * @throws ZuulException
 */
public void route() throws ZuulException {
    FilterProcessor.getInstance().route();
}
 
/**
 * Runs all "route" filters. These filters route calls to an origin.
 *
 * @throws ZuulException if an exception occurs.
 */
public void route() throws ZuulException {
    try {
        runFilters("route");
    } catch (Throwable e) {
        if (e instanceof ZuulException) {
            throw (ZuulException) e;
        }
        throw new ZuulException(e, 500, "UNCAUGHT_EXCEPTION_IN_ROUTE_FILTER_" + e.getClass().getName());
    }
}
 
/**
 * runs all filters of the filterType sType/ Use this method within filters to run custom filters by type
 *
 * @param sType the filterType.
 * @return
 * @throws Throwable throws up an arbitrary exception
 */
public Object runFilters(String sType) throws Throwable {
    if (RequestContext.getCurrentContext().debugRouting()) {
        Debug.addRoutingDebug("Invoking {" + sType + "} type filters");
    }
    boolean bResult = false;
    List<ZuulFilter> list = FilterLoader.getInstance().getFiltersByType(sType);
    if (list != null) {
        for (int i = 0; i < list.size(); i++) {
            ZuulFilter zuulFilter = list.get(i);
            Object result = processZuulFilter(zuulFilter);
            if (result != null && result instanceof Boolean) {
                bResult |= ((Boolean) result);
            }
        }
    }
    return bResult;
}

当调用zuulRunner中的route方法时,就会搜索这个类型的zuulFilter,依次执行。当我们知道了这些,在回头看一下zuulServlet就特别容易理解了,就是根据流程走的先pre,再route,再post,再一直走这些filter啊。流程大抵知道了。我们再看一下,数据真实来了,怎么流转。我们就从pre(PreDecorationFilter),route(SimpleHostRoutingFilter),post(SendResponseFilter) 这三个部分来看一下。
pre: PreDecorationFilter

public class PreDecorationFilter extends ZuulFilter {

   private RouteLocator routeLocator;

   private String dispatcherServletPath;

   private ZuulProperties properties;

   private UrlPathHelper urlPathHelper = new UrlPathHelper();

   private ProxyRequestHelper proxyRequestHelper;

   public PreDecorationFilter(RouteLocator routeLocator, String dispatcherServletPath,
         ZuulProperties properties, ProxyRequestHelper proxyRequestHelper) {
      this.routeLocator = routeLocator;
      this.properties = properties;
      this.urlPathHelper
            .setRemoveSemicolonContent(properties.isRemoveSemicolonContent());
      this.dispatcherServletPath = dispatcherServletPath;
      this.proxyRequestHelper = proxyRequestHelper;
   }

   ...

   @Override
   public Object run() {
      RequestContext ctx = RequestContext.getCurrentContext();
      final String requestURI = this.urlPathHelper
            .getPathWithinApplication(ctx.getRequest());
      Route route = this.routeLocator.getMatchingRoute(requestURI);
      if (route != null) {
         String location = route.getLocation();
         if (location != null) {
            ctx.put("requestURI", route.getPath());
            ctx.put("proxy", route.getId());
            if (!route.isCustomSensitiveHeaders()) {
               this.proxyRequestHelper.addIgnoredHeaders(
                     this.properties.getSensitiveHeaders().toArray(new String[0]));
            }
            else {
               this.proxyRequestHelper.addIgnoredHeaders(
                     route.getSensitiveHeaders().toArray(new String[0]));
            }

            if (route.getRetryable() != null) {
               ctx.put("retryable", route.getRetryable());
            }

            if (location.startsWith("http:") || location.startsWith("https:")) {
               ctx.setRouteHost(getUrl(location));
               ctx.addOriginResponseHeader("X-Zuul-Service", location);
            }
            else if (location.startsWith("forward:")) {
               ctx.set("forward.to", StringUtils.cleanPath(
                     location.substring("forward:".length()) + route.getPath()));
               ctx.setRouteHost(null);
               return null;
            }
            else {
               // set serviceId for use in filters.route.RibbonRequest
               ctx.set("serviceId", location);
               ctx.setRouteHost(null);
               ctx.addOriginResponseHeader("X-Zuul-ServiceId", location);
            }
            if (this.properties.isAddProxyHeaders()) {
               ctx.addZuulRequestHeader("X-Forwarded-Host",
                     ctx.getRequest().getServerName());
               ctx.addZuulRequestHeader("X-Forwarded-Port",
                     String.valueOf(ctx.getRequest().getServerPort()));
               ctx.addZuulRequestHeader(ZuulHeaders.X_FORWARDED_PROTO,
                     ctx.getRequest().getScheme());
               if (StringUtils.hasText(route.getPrefix())) {
                  String existingPrefix = ctx.getRequest()
                        .getHeader("X-Forwarded-Prefix");
                  StringBuilder newPrefixBuilder = new StringBuilder();
                  if (StringUtils.hasLength(existingPrefix)) {
                     if (existingPrefix.endsWith("/")
                           && route.getPrefix().startsWith("/")) {
                        newPrefixBuilder.append(existingPrefix, 0,
                              existingPrefix.length() - 1);
                     }
                     else {
                        newPrefixBuilder.append(existingPrefix);
                     }
                  }
                  newPrefixBuilder.append(route.getPrefix());
                  ctx.addZuulRequestHeader("X-Forwarded-Prefix",
                        newPrefixBuilder.toString());
               }
               String xforwardedfor = ctx.getRequest().getHeader("X-Forwarded-For");
               String remoteAddr = ctx.getRequest().getRemoteAddr();
               if (xforwardedfor == null) {
                  xforwardedfor = remoteAddr;
               }
               else if (!xforwardedfor.contains(remoteAddr)) { // Prevent duplicates
                  xforwardedfor += ", " + remoteAddr;
               }
               ctx.addZuulRequestHeader("X-Forwarded-For", xforwardedfor);
            }
         }
      }
      else {
         log.warn("No route found for uri: " + requestURI);

         String fallBackUri = requestURI;
         String fallbackPrefix = this.dispatcherServletPath; // default fallback
                                                // servlet is
                                                // DispatcherServlet

         if (RequestUtils.isZuulServletRequest()) {
            // remove the Zuul servletPath from the requestUri
            log.debug("zuulServletPath=" + this.properties.getServletPath());
            fallBackUri = fallBackUri.replaceFirst(this.properties.getServletPath(),
                  "");
            log.debug("Replaced Zuul servlet path:" + fallBackUri);
         }
         else {
            // remove the DispatcherServlet servletPath from the requestUri
            log.debug("dispatcherServletPath=" + this.dispatcherServletPath);
            fallBackUri = fallBackUri.replaceFirst(this.dispatcherServletPath, "");
            log.debug("Replaced DispatcherServlet servlet path:" + fallBackUri);
         }
         if (!fallBackUri.startsWith("/")) {
            fallBackUri = "/" + fallBackUri;
         }
         String forwardURI = fallbackPrefix + fallBackUri;
         forwardURI = forwardURI.replaceAll("//", "/");
         ctx.set("forward.to", forwardURI);
      }
      return null;
   }

   private URL getUrl(String target) {
      try {
         return new URL(target);
      }
      catch (MalformedURLException ex) {
         throw new IllegalStateException("Target URL is malformed", ex);
      }
   }
}

这个pre就是在上下文中set一系列的值。
route: SimpleHostRoutingFilter

@Override
public Object run() {
   RequestContext context = RequestContext.getCurrentContext();
   HttpServletRequest request = context.getRequest();
   MultiValueMap<String, String> headers = this.helper
         .buildZuulRequestHeaders(request);
   MultiValueMap<String, String> params = this.helper
         .buildZuulRequestQueryParams(request);
   String verb = getVerb(request);
   InputStream requestEntity = getRequestBody(request);
   if (request.getContentLength() < 0) {
      context.setChunkedRequestBody();
   }

   String uri = this.helper.buildZuulRequestURI(request);
   this.helper.addIgnoredHeaders();

   try {
      HttpResponse response = forward(this.httpClient, verb, uri, request, headers,
            params, requestEntity);
      setResponse(response);
      setErrorCodeFor4xx(context, response);
   }
   catch (Exception ex) {
      context.set(ERROR_STATUS_CODE,
            HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
      context.set("error.exception", ex);
   }
   return null;
}
 
 
private void setResponse(HttpResponse response) throws IOException {
   this.helper.setResponse(response.getStatusLine().getStatusCode(),
         response.getEntity() == null ? null : response.getEntity().getContent(),
         revertHeaders(response.getAllHeaders()));
}

利用httpClient调用服务拿到数据,里面有个setReponse,就是借助helper,还记得我们有个Helper的Bean,不记得,请看上文,把response设置到了上下文中。
post :SendResponseFilter

@Override
public Object run() {
   try {
      addResponseHeaders();
      writeResponse();
   }
   catch (Exception ex) {
      ReflectionUtils.rethrowRuntimeException(ex);
   }
   return null;
}

private void writeResponse() throws Exception {
   RequestContext context = RequestContext.getCurrentContext();
   // there is no body to send
   if (context.getResponseBody() == null
         && context.getResponseDataStream() == null) {
      return;
   }
   HttpServletResponse servletResponse = context.getResponse();
   if (servletResponse.getCharacterEncoding() == null) { // only set if not set
      servletResponse.setCharacterEncoding("UTF-8");
   }
   OutputStream outStream = servletResponse.getOutputStream();
   InputStream is = null;
   try {
      if (RequestContext.getCurrentContext().getResponseBody() != null) {
         String body = RequestContext.getCurrentContext().getResponseBody();
         writeResponse(
               new ByteArrayInputStream(
                     body.getBytes(servletResponse.getCharacterEncoding())),
               outStream);
         return;
      }
      boolean isGzipRequested = false;
      final String requestEncoding = context.getRequest()
            .getHeader(ZuulHeaders.ACCEPT_ENCODING);

      if (requestEncoding != null
            && HTTPRequestUtils.getInstance().isGzipped(requestEncoding)) {
         isGzipRequested = true;
      }
      is = context.getResponseDataStream();
      InputStream inputStream = is;
      if (is != null) {
         if (context.sendZuulResponse()) {
            // if origin response is gzipped, and client has not requested gzip,
            // decompress stream
            // before sending to client
            // else, stream gzip directly to client
            if (context.getResponseGZipped() && !isGzipRequested) {
               // If origin tell it's GZipped but the content is ZERO bytes,
               // don't try to uncompress
               final Long len = context.getOriginContentLength();
               if (len == null || len > 0) {
                  try {
                     inputStream = new GZIPInputStream(is);
                  }
                  catch (java.util.zip.ZipException ex) {
                     log.debug(
                           "gzip expected but not "
                                 + "received assuming unencoded response "
                                 + RequestContext.getCurrentContext()
                                       .getRequest().getRequestURL()
                                       .toString());
                     inputStream = is;
                  }
               }
               else {
                  // Already done : inputStream = is;
               }
            }
            else if (context.getResponseGZipped() && isGzipRequested) {
               servletResponse.setHeader(ZuulHeaders.CONTENT_ENCODING, "gzip");
            }
            writeResponse(inputStream, outStream);
         }
      }
   }
   finally {
      try {
         if (is != null) {
            is.close();
         }
         outStream.flush();
         // The container will close the stream for us
      }
      catch (IOException ex) {
      }
   }
}

private void writeResponse(InputStream zin, OutputStream out) throws Exception {
   byte[] bytes = new byte[INITIAL_STREAM_BUFFER_SIZE.get()];
   int bytesRead = -1;
   while ((bytesRead = zin.read(bytes)) != -1) {
      try {
         out.write(bytes, 0, bytesRead);
         out.flush();
      }
      catch (IOException ex) {
         // ignore
      }
      // doubles buffer size if previous read filled it
      if (bytesRead == bytes.length) {
         bytes = new byte[bytes.length * 2];
      }
   }
}

private void addResponseHeaders() {
   RequestContext context = RequestContext.getCurrentContext();
   HttpServletResponse servletResponse = context.getResponse();
   List<Pair<String, String>> zuulResponseHeaders = context.getZuulResponseHeaders();
   @SuppressWarnings("unchecked")
   List<String> rd = (List<String>) RequestContext.getCurrentContext()
         .get("routingDebug");
   if (rd != null) {
      StringBuilder debugHeader = new StringBuilder();
      for (String it : rd) {
         debugHeader.append("[[[" + it + "]]]");
      }
      if (INCLUDE_DEBUG_HEADER.get()) {
         servletResponse.addHeader("X-Zuul-Debug-Header", debugHeader.toString());
      }
   }
   if (zuulResponseHeaders != null) {
      for (Pair<String, String> it : zuulResponseHeaders) {
         servletResponse.addHeader(it.first(), it.second());
      }
   }
   RequestContext ctx = RequestContext.getCurrentContext();
   Long contentLength = ctx.getOriginContentLength();
   // Only inserts Content-Length if origin provides it and origin response is not
   // gzipped
   if (SET_CONTENT_LENGTH.get()) {
      if (contentLength != null && !ctx.getResponseGZipped()) {
         servletResponse.setContentLength(contentLength.intValue());
      }
   }
}

重点看writeResponse,从上下文中找到response,将数据利用输出流写出去就行了。这个流程就结束了

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容