OkHttp 源码分析系列(三)- 5大Interceptor

  今天我们来看看OkHttp中另外一个大头--InterceptorOkhttp真正的网络请求都在这个拦截器链中执行的,所以分析OkHttp的拦截器链是非常有必要的。

1. 概述

  在正式分析拦截器链之前,我们先来每一个拦截器有一个大概的认识,同时对拦截器链的调用过程有一个宏观上的理解。
  在OkHttp中,给我们分配了5种拦截器,它们分别是:RetryAndFollowUpInterceptorBridgeInterceptorCacheInterceptorConnectInterceptorCallServerInterceptor。这里先对每一种拦截器的作用做一个总结。

类名 作用
RetryAndFollowUpInterceptor 主要负责网络请求的失败重连
BridgeInterceptor 主要是将用户的Request转换为能够真正进行网络请求的Request,负责添加一些相应头;其次就是将服务器返回的Response解析成为用户用够真正使用的Response,负责GZip解压之类的
CacheInterceptor 主要负责网络请求的Response缓存管理
ConnectInterceptor 主要负责打开与服务器的连接,正式进行网络请求
CallServerInterceptor 主要负责往网络数据流写入数据,同时接收服务器返回的数据

  在这里,再对整个拦截器链做一个小小的总结。
  拦截器链的调用是从RealCallgetResponseWithInterceptorChain方法开始的,在这个方法里面,会创建一个RealInterceptorChain对象,然后调用了RealInterceptorChainproceed方法,进而,在RealInterceptorChainproceed方法里面会调用第一个拦截器的intercept方法。而在拦截器的intercept方法里面,会再创建一个RealInterceptorChain对象,然后调用proceed方法,而此时,在proceed方法里面,调用就是第二个拦截器的intercept方法。如下图:

2. RetryAndFollowUpInterceptor

  在getResponseWithInterceptorChain方法里面,除了我们自定义的拦截器之外,第一个调用的就是RetryAndFollowUpInterceptor,所以,我们来看看RetryAndFollowUpInterceptor究竟为我们做了那些事情。
  RetryAndFollowUpInterceptor主要是负责网络请求的失败重连。

  @Override public Response intercept(Chain chain) throws IOException {
    Request request = chain.request();
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Call call = realChain.call();
    EventListener eventListener = realChain.eventListener();

    StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
        createAddress(request.url()), call, eventListener, callStackTrace);
    this.streamAllocation = streamAllocation;

    int followUpCount = 0;
    Response priorResponse = null;
    while (true) {
        //...代码省略
    }
  }

  从上面的代码中,我们可以看到,创建了StreamAllocation对象,这个类主要负责网络数据流的管理。在RetryAndFollowUpInterceptor类里面只是对StreamAllocation对象进行了初始化,并没有使用它来进行网络请求,真正使用它的地方是在ConnectInterceptor里面。
  其次,就是在while循环里面,这里就不详细的展开了,只是对一个进行简单的看看:

      if (++followUpCount > MAX_FOLLOW_UPS) {
        streamAllocation.release();
        throw new ProtocolException("Too many follow-up requests: " + followUpCount);
      }

  这句话非常的简单,就是如果当前重试的次数超过最大次数,就是结束当前的重连操作,并且抛出异常。

3. BridgeInterceptor

  在前面的总结表中,已经对BridgeInterceptor做了一个小小的总结。
  BridgeInterceptor在调用RealInterceptorChainproceed方法之前,会向RequestBuilder里面添加很多的信息,包括Content-TypeContent-Length等等。这些都是网络请求的必要信息,这里我们就不需要过多的关注。这个在后面讲解ConnectionPool时,会详细的解释
  但是,有一个参数需要的特别注意,那就是Connection在不为null时,会设置为Keep-Alive,这个参数表示,一些TCP连接是否保持连接,如果保持连接,就可以达到复用的效果。
  当调用proceed返回结果时,此时还需要一步操作,那就是如果Response就是经过GZip压缩的,就需要解压。如下代码:

    if (transparentGzip
        && "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding"))
        && HttpHeaders.hasBody(networkResponse)) {
      GzipSource responseBody = new GzipSource(networkResponse.body().source());
      Headers strippedHeaders = networkResponse.headers().newBuilder()
          .removeAll("Content-Encoding")
          .removeAll("Content-Length")
          .build();
      responseBuilder.headers(strippedHeaders);
      String contentType = networkResponse.header("Content-Type");
      responseBuilder.body(new RealResponseBody(contentType, -1L, Okio.buffer(responseBody)));
    }

  当然这个服务端是否返回GZip压缩的数据,还要取决于在添加Request的响应头时,是否告诉服务端,客户端支持Gzip压缩;如果客户端支持,那么服务端就会返回Gzip压缩过的数据。

4. CacheInterceptor

  CacheInterceptor的职责我们从这个类的名字就可以知道,它主要负责Response的缓存管理,包括哪些请求可以从缓存中取,哪些数据需要重新进行网络请求;哪些数据可以缓存,哪些数据不能被缓存等等。这些操作都是由CacheInterceptor来管理的。

(1).缓存原理

  在正式了解CacheInterceptor之前,我们还是先来看看OkHttp怎么使用Cache,同时了解OkHttp的缓存原理。

  private final OkHttpClient mOkHttpClient = new OkHttpClient.Builder()
    .readTimeout(50, TimeUnit.SECONDS)
    .cache(new Cache(new File("pby123"),1000))
    .build();

  如果我们需要将某些Response缓存,可以直接在创建OkHttpClient的对象时,调用cache方法来进行配置。
  CacheInterceptor在使用缓存时,是通过调用Cacheget方法和put方法来进行数据的获取和缓存。我们分别来看看。

(2).put方法

  在Cache类里面,我们需要重点关注的是put方法和get方法,因为CacheInterceptor是通过调用这个方法来进行数据缓存的相关操作的。我们先来看看put方法:

  @Nullable CacheRequest put(Response response) {
    String requestMethod = response.request().method();

    if (HttpMethod.invalidatesCache(response.request().method())) {
      try {
        remove(response.request());
      } catch (IOException ignored) {
        // The cache cannot be written.
      }
      return null;
    }
    if (!requestMethod.equals("GET")) {
      // Don't cache non-GET responses. We're technically allowed to cache
      // HEAD requests and some POST requests, but the complexity of doing
      // so is high and the benefit is low.
      return null;
    }

    if (HttpHeaders.hasVaryAll(response)) {
      return null;
    }

    Entry entry = new Entry(response);
    DiskLruCache.Editor editor = null;
    try {
      editor = cache.edit(key(response.request().url()));
      if (editor == null) {
        return null;
      }
      entry.writeTo(editor);
      return new CacheRequestImpl(editor);
    } catch (IOException e) {
      abortQuietly(editor);
      return null;
    }
  }

  我将put方法分为两个大步骤。
  首先是判断当前的Response是否支持缓存,比如当前的请求方式是Post,就不支持缓存:

    if (!requestMethod.equals("GET")) {
      // Don't cache non-GET responses. We're technically allowed to cache
      // HEAD requests and some POST requests, but the complexity of doing
      // so is high and the benefit is low.
      return null;
    }

  如果当前的Response支持缓存,那么就可以进行缓存的操作,这就是第二步。首先是,将当前的Response使用Entry包装起来,然后将创建DiskLruCache.Editor的对象,最后就是使用DiskLruCache.Editor对象来对数据进行写入。
  总体上来说,还是比较简单的。不过在在这里我们发现,OkHttp是DiskLruCache来实现缓存的操作,这一点需要特别的注意。

(3).get方法

  说了put方法,get方法就更加的简单了。put方法是缓存数据,那么get方法就是获取缓存数据。我们还是简单的看看:

  @Nullable Response get(Request request) {
    String key = key(request.url());
    DiskLruCache.Snapshot snapshot;
    Entry entry;
    try {
      snapshot = cache.get(key);
      if (snapshot == null) {
        return null;
      }
    } catch (IOException e) {
      // Give up because the cache cannot be read.
      return null;
    }

    try {
      entry = new Entry(snapshot.getSource(ENTRY_METADATA));
    } catch (IOException e) {
      Util.closeQuietly(snapshot);
      return null;
    }

    Response response = entry.response(snapshot);

    if (!entry.matches(request, response)) {
      Util.closeQuietly(response.body());
      return null;
    }

    return response;
  }

  get方法总体来说比较,这里就不做过多的解释了。
  OkHttp的缓存原理也没有传说中那么高大上,就是一些普通的操作。在简单的了解缓存原理之后,我们来看看CacheInterceptor

(4). CacheInterceptor

  CacheInterceptorintercept方法比较长,这里就不完全贴出来,这里以分段的形式解释。
  我觉得,可以将CacheInterceptorintercept过程分为三个步骤。

1.从缓存中获取数据,如果缓存数据有效的,之前返回,不进行网络请求。
2.调用RealInterceptorChainproceed方法,进行网络请求,获取数据。
3.如果请求的数据支持缓存的话,那么缓存起来。

  我们一个一个的来看看。首先来看获取缓存数据这部分。

    Response cacheCandidate = cache != null
        ? cache.get(chain.request())
        : null;

    long now = System.currentTimeMillis();

    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
    Request networkRequest = strategy.networkRequest;
    Response cacheResponse = strategy.cacheResponse;

    if (cache != null) {
      cache.trackResponse(strategy);
    }

    if (cacheCandidate != null && cacheResponse == null) {
      closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
    }

    // If we're forbidden from using the network and the cache is insufficient, fail.
    if (networkRequest == null && cacheResponse == null) {
      return new Response.Builder()
          .request(chain.request())
          .protocol(Protocol.HTTP_1_1)
          .code(504)
          .message("Unsatisfiable Request (only-if-cached)")
          .body(Util.EMPTY_RESPONSE)
          .sentRequestAtMillis(-1L)
          .receivedResponseAtMillis(System.currentTimeMillis())
          .build();
    }

    // If we don't need the network, we're done.
    if (networkRequest == null) {
      return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build();
    }

  在整个过程中,思路还是比较清晰的。首先是调用Cacheget方法获取相应的缓存数据;然后就是创建一个缓存策略对象,这个缓存策略对象在后面有很大的作用,后面对它单独的讲解,这里就先对它有一个了解吧。
  接下就有四个判断,需要重点解释一下。

    if (cache != null) {
      cache.trackResponse(strategy);
    }

  上面代码表示的意思是,如果当前的Cache不为空,那么就追踪一下当前的缓存数据,主要是更新一下,获取缓存的次数,以及缓存命中率。

  synchronized void trackResponse(CacheStrategy cacheStrategy) {
    requestCount++;

    if (cacheStrategy.networkRequest != null) {
      // If this is a conditional request, we'll increment hitCount if/when it hits.
      networkCount++;
    } else if (cacheStrategy.cacheResponse != null) {
      // This response uses the cache and not the network. That's a cache hit.
      hitCount++;
    }
  }

  然后就是第二个判断。

    if (cacheCandidate != null && cacheResponse == null) {
      closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
    }

  上面代码的意思表示,如果从缓存里面获取的数据不为空,但是缓存策略里面的Response为空,就表示当前从缓存中获取数据无效的,所以调用closeQuietly方法关闭缓存相应的数据。
  我们再来看看第三个判断。

    if (networkRequest == null && cacheResponse == null) {
      return new Response.Builder()
          .request(chain.request())
          .protocol(Protocol.HTTP_1_1)
          .code(504)
          .message("Unsatisfiable Request (only-if-cached)")
          .body(Util.EMPTY_RESPONSE)
          .sentRequestAtMillis(-1L)
          .receivedResponseAtMillis(System.currentTimeMillis())
          .build();
    }

  如果缓存策略里面的RequestResponse都为空,表示当前网络不可用,并且没有缓存数据,所以就是返回504。
  最后我们来看看判断。

    if (networkRequest == null) {
      return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build();
    }

  这里就非常简单了,如果当前的网络请求不可用,并且缓存数据不为空,那么就返回缓存的数据。
  第二个过程就是调用RealInterceptorChainproceed方法,这里就不详细的解释。我们直接来看看第三个过程。

    // If we have a cache response too, then we're doing a conditional get.
    if (cacheResponse != null) {
      if (networkResponse.code() == HTTP_NOT_MODIFIED) {
        Response response = cacheResponse.newBuilder()
            .headers(combine(cacheResponse.headers(), networkResponse.headers()))
            .sentRequestAtMillis(networkResponse.sentRequestAtMillis())
            .receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
            .cacheResponse(stripBody(cacheResponse))
            .networkResponse(stripBody(networkResponse))
            .build();
        networkResponse.body().close();

        // Update the cache after combining headers but before stripping the
        // Content-Encoding header (as performed by initContentStream()).
        cache.trackConditionalCacheHit();
        cache.update(cacheResponse, response);
        return response;
      } else {
        closeQuietly(cacheResponse.body());
      }
    }

    Response response = networkResponse.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build();

    if (cache != null) {
      if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
        // Offer this request to the cache.
        CacheRequest cacheRequest = cache.put(response);
        return cacheWritingResponse(cacheRequest, response);
      }

      if (HttpMethod.invalidatesCache(networkRequest.method())) {
        try {
          cache.remove(networkRequest);
        } catch (IOException ignored) {
          // The cache cannot be written.
        }
      }
    }

  缓存数据的过程也是非常的简单的。
  首先返回的数据如果是HTTP_NOT_MODIFIED并且缓存数据不为空,那么更新一下缓存数据,否则的话,就关闭缓存的数据。
  其次,就是返回的数据支持缓存,那么就是调用cacheWritingResponse方法来缓存。
  如上就是整个RealInterceptorChain的缓存过程。接下来,我们来分析一下整个过程中贯穿的缓存策略类CacheStrategy

(5).CacheStrategy

  我们直接来看CacheStrategy.Factoryget方法。看看这个方法到底做了什么。
  不过在看get方法之前,我们必须得有认知,那就是CacheStrategy里面的networkRequestcacheResponse到底代表着什么。

  1. networkRequest:表示一个网络请求,如果这个对象不为空的话,那么在CacheStrategy里面肯定会进行网络请求,至于最后是选择缓存数据还是请求回来的数据,得看具体的情况。
  2. cacheResponse:表示缓存的Response,如果当前的网络不可用,也就是networkRequest为空,那么会直接返回缓存的数据;如果networkRequest不为空,那么就得跟请求回来的数据比较,具体的比较,可以参考上面的第三个过程。

  现在,我们来正式看看get方法。

    public CacheStrategy get() {
      CacheStrategy candidate = getCandidate();

      if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
        // We're forbidden from using the network and the cache is insufficient.
        return new CacheStrategy(null, null);
      }

      return candidate;
    }

  其实get方法也没有做什么,核心操作还是在getCandidate里面。由于getCandidate方法过于长,这里直接贴出代码,在代码中解释。

    private CacheStrategy getCandidate() {
      // 没有缓存,返回一个request和一个为null的cacheResponse
      if (cacheResponse == null) {
        return new CacheStrategy(request, null);
      }

      // 如果当前是Https的请求,并且没有握手
      if (request.isHttps() && cacheResponse.handshake() == null) {
        return new CacheStrategy(request, null);
      }

      //如果当前的cacheResponse不支持缓存
      if (!isCacheable(cacheResponse, request)) {
        return new CacheStrategy(request, null);
      }

      CacheControl requestCaching = request.cacheControl();
      // 如果当前没有缓存或者请求有条件
      if (requestCaching.noCache() || hasConditions(request)) {
        return new CacheStrategy(request, null);
      }

      CacheControl responseCaching = cacheResponse.cacheControl();
      // 如果当前的Response不可以被改变
      if (responseCaching.immutable()) {
        return new CacheStrategy(null, cacheResponse);
      }
      // 省略代码
    }

  其实,到最后来看,缓存策略CacheStrategy并不是那么的可怕,还是比较通俗易懂的。

5. ConnectInterceptor

  现在,我们来看看ConnectInterceptor这个类。我们先来看看ConnectInterceptorproceed方法:

  @Override public Response intercept(Chain chain) throws IOException {
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Request request = realChain.request();
    StreamAllocation streamAllocation = realChain.streamAllocation();

    // We need the network to satisfy this request. Possibly for validating a conditional GET.
    boolean doExtensiveHealthChecks = !request.method().equals("GET");
    HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
    RealConnection connection = streamAllocation.connection();

    return realChain.proceed(request, streamAllocation, httpCodec, connection);
  }

  整个过程还是简单的,不过有几个地方还是需要我们注意的。

    HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);

  还记得在RetryAndFollowUpInterceptor里面,在那里创建了StreamAllocation对象,但是没有用。当时,我也说了,StreamAllocation对象真正的使用实在ConnectInterceptor里面,这也正证实了当时的描述。
  然而这一句有什么用呢?通过调用StreamAllocationnewStream方法,返回了一个HttpCodec对象。这个HttpCodec对象的作用是对网络请求进行编码和对网络请求回来的数据进行解码,这些操作都是HttpCodec类给我们分装好的。
  然后就是调用StreamAllocationconnection方法获取RealConnection对象。
  最后就调用RealInterceptorChainproceed方法。这一步是常规操作。
  看到这里来,是不是感觉心里面有一点失落?感觉ConnectInterceptorintercept方法里面并没有做什么事。
  其实真正的操作并没有在intercept方法里面,而是在StreamAllocationnewStream方法里面。我们来看看newStream方法。

  public HttpCodec newStream(
      OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
    int connectTimeout = chain.connectTimeoutMillis();
    int readTimeout = chain.readTimeoutMillis();
    int writeTimeout = chain.writeTimeoutMillis();
    int pingIntervalMillis = client.pingIntervalMillis();
    boolean connectionRetryEnabled = client.retryOnConnectionFailure();

    try {
      RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
          writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
      HttpCodec resultCodec = resultConnection.newCodec(client, chain, this);

      synchronized (connectionPool) {
        codec = resultCodec;
        return resultCodec;
      }
    } catch (IOException e) {
      throw new RouteException(e);
    }
  }

  其实newStream方法也是非常的简单,重点还是在findHealthyConnection方法,我们来看看。

  private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
      int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled,
      boolean doExtensiveHealthChecks) throws IOException {
    while (true) {
      RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
          pingIntervalMillis, connectionRetryEnabled);

      // If this is a brand new connection, we can skip the extensive health checks.
      synchronized (connectionPool) {
        if (candidate.successCount == 0) {
          return candidate;
        }
      }

      // Do a (potentially slow) check to confirm that the pooled connection is still good. If it
      // isn't, take it out of the pool and start again.
      if (!candidate.isHealthy(doExtensiveHealthChecks)) {
        noNewStreams();
        continue;
      }

      return candidate;
    }
  }

   findHealthyConnection方法的作用是找到一个健康的连接,在findHealthyConnection方法里面,我们可以看到,不断调用findConnection方法来找到一个连接,然后判断当前这个连接是否是健康,如果是健康的,那么就返回,否则就重新寻找。
   这里的健康是一个宏观的概念,那什么表示不健康呢?如果一个连接没有关闭或者相关的流没有关闭都表示该连接是不健康的。
  我们再来看看findConnection方法,由于findConnection方法过长,这里就不全部贴出来了。简单的解释一下这个方法整个执行流程。
  findConnection方法主要分为两步:

  1. 找到一个可以使用的Connection
  2. 调用Connectionconnect方法进行连接。

  先来看看第一步。

      // Attempt to use an already-allocated connection. We need to be careful here because our
      // already-allocated connection may have been restricted from creating new streams.
      releasedConnection = this.connection;
      toClose = releaseIfNoNewStreams();
      if (this.connection != null) {
        // We had an already-allocated connection and it's good.
        result = this.connection;
        releasedConnection = null;
      }
      if (!reportedAcquired) {
        // If the connection was never reported acquired, don't report it as released!
        releasedConnection = null;
      }

      if (result == null) {
        // Attempt to get a connection from the pool.
        Internal.instance.get(connectionPool, address, this, null);
        if (connection != null) {
          foundPooledConnection = true;
          result = connection;
        } else {
          selectedRoute = route;
        }
      }
    }

  寻找一个可以使用的Connection过程看上去还是比较简单的。总体上来说,先尝试复用当前StreamAllocationconnection,如果可以复用的话,直接拿来用;否则的话,就去连接池 connectionPool里面去取得一个可以使用的连接。
  第二步操作就是调用Connectionconnect方法。这连接方法里面就涉及到非常多的东西,包括连接方式(socket连接还是隧道连接)等等。这里不进行展开了。
  最后在findConnection方法里面,我们可以又把获得的连接放回了连接池中去了:

    synchronized (connectionPool) {
      reportedAcquired = true;

      // Pool the connection.
      Internal.instance.put(connectionPool, result);

      // If another multiplexed connection to the same address was created concurrently, then
      // release this connection and acquire that one.
      if (result.isMultiplexed()) {
        socket = Internal.instance.deduplicate(connectionPool, address, this);
        result = connection;
      }
    }

6. CallServerInterceptor

  CallServerInterceptorOkHttp中的5大拦截器最后一个拦截器。CallServerInterceptor拦截器的作用在上面做了一个简单的介绍,在这里将结合代码来说明CallServerInterceptor到底做了那些事情。
  CallServerInterceptor的执行过程,我们可以分为两步

  1. 往网络请求中写入数据,包括写入头部数据和body数据。
  2. 接收服务器返回的数据。

  具体的细节这里就不在详细的解释了,都是一些常规操作。

7. 总结

  InterceptorOkhttp里面比较核心的东西,同时也是比较复杂的东西,这里对拦截器做一个简单的总结。

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