Zuul 源碼分析

zuul是spring cloud 微服務(wù)體系中的網(wǎng)關(guān)望迎,可以路由請(qǐng)求到具體的服務(wù)障癌,同時(shí)做一些驗(yàn)簽混弥,解密等的與業(yè)務(wù)無(wú)關(guān)的事情晾捏。今天我們從一個(gè)注解@EnableZuulProxy開(kāi)始講述惦辛。這個(gè)注解導(dǎo)入了一個(gè)配置文件ZuulProxyConfiguration胖齐,他繼承了ZuulConfiguration,整個(gè)zuul的流程的定義就在這兩個(gè)類中剿另。

@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 是用戶通過(guò)配置文件編寫的服務(wù)路由等信息阳准,表明請(qǐng)求是哪種模式時(shí)需要跳轉(zhuǎn)到哪個(gè)具體的服務(wù)讼稚,這是主要的內(nèi)容乱灵,當(dāng)然還有一些其他的信息。具體的看一下這個(gè)類的內(nèi)容就可以了蝉稳,都可以配置耘戚,這是我們定制化zuul的一個(gè)基礎(chǔ)配置文件。剩下的zuulController,zuulHandlerMapping长捧,zuulServlet就是spring mvc 的組件了串结,通過(guò)zuulHandlerMapping可以發(fā)現(xiàn)所有的請(qǐng)求都交給了zuulController來(lái)處理,它里面包裝的Servlet就是zuulServlet把敞,由他的service方法來(lái)處理先巴。這個(gè)下面細(xì)說(shuō)摩渺。例外還配置了ZuulFilter的過(guò)濾器横侦,著重看一下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() {     //作為一個(gè)幫助類帜矾,主要用來(lái)設(shè)置值
      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是一個(gè)路由的匹配器珍剑,他的實(shí)現(xiàn)是一個(gè)
DiscoveryClientRouteLocator,我這邊使用的是Consul作為服務(wù)注冊(cè)的容器,他會(huì)從consul中拉取各個(gè)服務(wù)的信息饰序,比如我們?cè)賨uul中配置了要路由的serviceId,那么從consul中尋找菌羽,如果有對(duì)應(yīng)的serviceId,那么就是這個(gè)服務(wù)來(lái)處理這個(gè)請(qǐng)求。proxyRequestHelper是一個(gè)幫助類是晨,用來(lái)再context中set值罩缴。還有兩個(gè)route類型zuulFilter,雖然都是Bean,但不一定都啟用檬寂。simpleHostRoutingFilter再設(shè)置了url的情況下使用,ribbonRoutingFilter在設(shè)置了serviceId的情況下使用镣屹,有客戶端負(fù)載均衡的作用女蜈。還有一個(gè)pre類型的zuulFilter
preDecorationFilter,主要是填充一些數(shù)據(jù)惰许。OK汹买,準(zhǔn)備工作做的差不多了生巡,我們來(lái)詳細(xì)看一下流程。請(qǐng)求過(guò)來(lái)了须揣,交給了ZuulServlet進(jìn)行處理疯汁。

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();
    }
}

在這個(gè)servlet初始化的時(shí)候會(huì)調(diào)用init方法,里面有個(gè)屬性
buffer-requests用來(lái)判斷是否走spring mvc沫换。不知道大家有沒(méi)有遇到過(guò)這種情況垮兑,上傳文件走網(wǎng)關(guān)時(shí)雀哨,需要在鏈接上加上/zuul,加上就不需要走spring mvc 捌浩,這樣不會(huì)對(duì)數(shù)據(jù)的大小有限制进统,所以可以傳遞大數(shù)據(jù)量的文件。另外初始化了一個(gè)zuulRunner,他所作的事情
第一點(diǎn)是在RequestContext設(shè)置請(qǐng)求按照buffer-requests這個(gè)參數(shù)來(lái)進(jìn)行掉分,如果為真,request被HttpServletRequestWrapper包裝一下 褥民,第二點(diǎn)的作用是處理按照類型處理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;
}

當(dāng)調(diào)用zuulRunner中的route方法時(shí)惫叛,就會(huì)搜索這個(gè)類型的zuulFilter,依次執(zhí)行妻熊。當(dāng)我們知道了這些,在回頭看一下zuulServlet就特別容易理解了亿胸,就是根據(jù)流程走的先pre,再route,再post,再一直走這些filter啊序仙。流程大抵知道了洋丐。我們?cè)倏匆幌拢瑪?shù)據(jù)真實(shí)來(lái)了迁客,怎么流轉(zhuǎn)。我們就從pre(PreDecorationFilter),route(SimpleHostRoutingFilter),post(SendResponseFilter) 這三個(gè)部分來(lái)看一下卜范。
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("http://", "/");
         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);
      }
   }
}

這個(gè)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調(diào)用服務(wù)拿到數(shù)據(jù),里面有個(gè)setReponse,就是借助helper,還記得我們有個(gè)Helper的Bean,不記得畏线,請(qǐng)看上文寝殴,把response設(shè)置到了上下文中市咽。
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());
      }
   }
}

重點(diǎn)看writeResponse,從上下文中找到response,將數(shù)據(jù)利用輸出流寫出去就行了押蚤。這個(gè)流程就結(jié)束了

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末园匹,一起剝皮案震驚了整個(gè)濱河市掖桦,隨后出現(xiàn)的幾起案子枪汪,更是在濱河造成了極大的恐慌,老刑警劉巖赖捌,帶你破解...
    沈念sama閱讀 211,639評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件罩锐,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡境氢,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,277評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)亭螟,“玉大人,你說(shuō)我怎么就攤上這事扁掸。” “怎么了牺蹄?”我有些...
    開(kāi)封第一講書(shū)人閱讀 157,221評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵翘魄,是天一觀的道長(zhǎng)训措。 經(jīng)常有香客問(wèn)我绩鸣,道長(zhǎng),這世上最難降的妖魔是什么捡多? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,474評(píng)論 1 283
  • 正文 為了忘掉前任倒信,我火速辦了婚禮榜掌,結(jié)果婚禮上憎账,老公的妹妹穿的比我還像新娘。我一直安慰自己朴恳,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,570評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般耐齐。 火紅的嫁衣襯著肌膚如雪耸携。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,816評(píng)論 1 290
  • 那天沟沙,我揣著相機(jī)與錄音,去河邊找鬼。 笑死贪染,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的痰憎。 我是一名探鬼主播,決...
    沈念sama閱讀 38,957評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼炉媒!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起白粉,我...
    開(kāi)封第一講書(shū)人閱讀 37,718評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后惯豆,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,176評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,511評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片太援。...
    茶點(diǎn)故事閱讀 38,646評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡笋敞,死狀恐怖振亮,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情澎怒,我是刑警寧澤星瘾,帶...
    沈念sama閱讀 34,322評(píng)論 4 330
  • 正文 年R本政府宣布,位于F島的核電站困食,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏瘩例。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,934評(píng)論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望部凑。 院中可真熱鬧,春花似錦比勉、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,755評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)梧兼。三九已至,卻和暖如春考赛,著一層夾襖步出監(jiān)牢的瞬間集灌,已是汗流浹背腌零。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,987評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留驯鳖,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,358評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像鸽捻,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子厚满,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,514評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容