OkHttp阅读笔记(三)

先上前两篇的地址
第一篇
第二篇
接着看拦截器链条,在RetryAndFollow拦截器之后

Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    //尝试建立一个拦截器列表,之后会进行一次链式调用
    //简单说就是从上到下执行获取response之前的过程,
    //然后获取到response之后,再从下到上执行
    List<Interceptor> interceptors = new ArrayList<>();
    //首先执行自定义的拦截器
    interceptors.addAll(client.interceptors());
    //处理重试逻辑
    interceptors.add(retryAndFollowUpInterceptor);
    //添加一些预定义头部之类的数据
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    //处理http协议的缓存逻辑
    interceptors.add(new CacheInterceptor(client.internalCache()));
    //处理socket建立连接或者复用连接过程,总之这里会建立连接
    interceptors.add(new ConnectInterceptor(client));
    if (!forWebSocket) {
      interceptors.addAll(client.networkInterceptors());
    }
    //在已经建立的链接上进行参数发送和获取响应封装等操作
    interceptors.add(new CallServerInterceptor(forWebSocket));

    Interceptor.Chain chain = new RealInterceptorChain(
        interceptors, null, null, null, 0, originalRequest);
    return chain.proceed(originalRequest);
  }

下一个是BridgeInterceptor,桥接拦截器

BridgeInterceptor

桥接拦截器,顾名思义应该是做一些数据拼接工作的拦截器,看一下实现

@Override public Response intercept(Chain chain) throws IOException {
    Request userRequest = chain.request();
    //在构建一个对应Call的时候首先要构建一个request,并且可以设置一些参数
    //通过构造器模式可以完成一些基础请求头等参数的补足
    //url:请求地址
    //method:请求方式(POST/GET)
    //body:请求内容(比方说数据、文件二进制等)
    //tag:当前request请求标记
    //headers:当前request已经配置的头部报文参数
    //newBuilder会保留之前request中的数据
    Request.Builder requestBuilder = userRequest.newBuilder();
    RequestBody body = userRequest.body();
    //如果有正文体,设置一些正文相关的头部报文信息
    if (body != null) {
      //注意此处是覆盖,这就意味着这些信息不需要在设置request的时候手动设置,设置了也可能被覆盖
      MediaType contentType = body.contentType();
      if (contentType != null) {//标记当前内容的媒体类型
        requestBuilder.header("Content-Type", contentType.toString());
      }

      long contentLength = body.contentLength();
      if (contentLength != -1) {//一般来说,推荐这种传输大小固定的模式
        requestBuilder.header("Content-Length", Long.toString(contentLength));
        requestBuilder.removeHeader("Transfer-Encoding");//移除分块传输的头部
      } else {//当无法获取传输内容的大小的时候,标记当前传输内容不定
        requestBuilder.header("Transfer-Encoding", "chunked");
        requestBuilder.removeHeader("Content-Length");
      }
    }
    //如果没有手动设置头部报文中的Host,则会采用设置的url中的域名,添加不是覆盖
    //否则使用你手动设置的值,这个一般可以用于HttpDNS服务
    //在不修改域名的情况下改变Host进行IP直连访问,用于避开一些DNS服务异常的情况
    if (userRequest.header("Host") == null) {
      //这里默认填充链接的host:port格式
      requestBuilder.header("Host", hostHeader(userRequest.url(), false));
    }
    //这个是用于标志长短连接的,一般close标志短连接,用于请求一些文档之类的,标志TCP可以很快进行4次握手结束连接
    //Keep-Alive表示长连接,在TCP进行完3次握手之后,这个过程中可以传输数据之类的,并不会很快进行4次握手结束连接
    //长连接可以用于连接复用
    if (userRequest.header("Connection") == null) {
      //默认用长连接,提倡连接复用
      requestBuilder.header("Connection", "Keep-Alive");
    }

    // If we add an "Accept-Encoding: gzip" header field we're responsible for also decompressing
    // the transfer stream.
    boolean transparentGzip = false;
    // 当前没有主动告知服务端客户端所能够接收的编码类型,并且没有指定获取的数据范围
    // 比方说分段下载的时候可以通过Range: bytes=1-10来说明要获取第1到第10个字节的数据
    if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
      transparentGzip = true;
      //用于告知服务器客户端所能够接收的编码类型,默认为GZIP
      requestBuilder.header("Accept-Encoding", "gzip");
    }
    //尝试从存储中获取cookies
    List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());
    if (!cookies.isEmpty()) {//如果存有cookie,放入请求头部
      requestBuilder.header("Cookie", cookieHeader(cookies));
    }

    if (userRequest.header("User-Agent") == null) {
      requestBuilder.header("User-Agent", Version.userAgent());
    }
    //再次回调RealInterceptorChain的proceed方法,通过其它拦截器发起请求
    Response networkResponse = chain.proceed(requestBuilder.build());
    //此处一般来说获取response成功
    //当请求完成之后,要尝试将cookie存入cookieJar当中
    HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());
    //关联当前响应和请求
    Response.Builder responseBuilder = networkResponse.newBuilder()
        .request(userRequest);

    if (transparentGzip
        && "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding"))
        && HttpHeaders.hasBody(networkResponse)) {//这里是处理Gzip返回格式的数据
      //通过GZIP的Source来处理数据的读取
      GzipSource responseBody = new GzipSource(networkResponse.body().source());
      Headers strippedHeaders = networkResponse.headers().newBuilder()
          .removeAll("Content-Encoding")
          .removeAll("Content-Length")
          .build();
      responseBuilder.headers(strippedHeaders);
      responseBuilder.body(new RealResponseBody(strippedHeaders, Okio.buffer(responseBody)));
    }

    return responseBuilder.build();
  }

可以看到,桥接拦截器主要的工作是拼接请求和响应的头部报文,这里可以看一下OkHttp的默认头部报文数据:
1.Content-Type:根据Request中的RequestBody决定
2.Content-Length:根据Request中RequestBody决定
3.Host:优先使用自定义的,否则默认使用host:port格式(其中port为默认的则不添加)
4.Connection:优先使用自定义的,否则默认使用长连接
5.Accept-Encoding:当前客户端可以接收的编码格式,优先使用自定义的,默认使用GZIP编码(主要是有一定的压缩作用,降低传输数据的大小)
6.Cookie/User-Agent:这个Android一般不用,不细讲
默认就是处理以上6种请求头部报文
接下来就是通过其它拦截器发出请求,然后接收响应结果,然后再对响应报文进行处理
1.处理Cookie
2.如果当前返回数据为GZIP编码,通过GZIPSource处理当前响应体(编码格式不同,读取的时候有所差异)
桥接拦截器的工作就是对头部报文进行一些必要的加工处理
接着看CacheInterceptor

CacheInterceptor

缓存拦截器,这个实际上对应的是Http1.1的报文中的Cache-Control处理机制
很多时候,请求的数据量很大但是数据变化的可能性很小,那么一般会使用缓存的方式来节省流量和优化加载速度,体验会好不少,Http1.1协议也提供了这个功能,接下来看一下细节:

@Override public Response intercept(Chain chain) throws IOException {
    //需要在OkHttpClient.Builder中指定所使用的硬盘缓存,否则默认为null
    //默认的话可以使用OkHttp提供的Cache类,里面默认使用DiskLruCache进行最近最少使用管理
    //缓存的文件名默认是请求连接的md5编码
    Response cacheCandidate = cache != null
        ? cache.get(chain.request())
        : null;
    //获取当前时间戳
    long now = System.currentTimeMillis();
    //创建一个默认的缓存策略
    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
    //在不需要发出请求的时候,request为null,比方说成功获取缓存或者request中设置了only-if-cached
    //在没有自定义缓存的情况下,cacheResponse为null,当然此时request可能null
    //注意此处如果cacheResponse不为空,它至少也是cacheCandidate的一个深拷贝
    Request networkRequest = strategy.networkRequest;//为空意味着不需要发出请求
    Response cacheResponse = strategy.cacheResponse;//为空意味着没有集中缓存数据

    if (cache != null) {
      //此处统计用的,可以记录请求次数和击中缓存次数和网络请求次数
      cache.trackResponse(strategy);
    }
    //当前缓存不可用,释放cacheCandidate中的数据
    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.
    // 此处可以认为在only-if-cached的情况下获取缓存失败
    // 当前有且只从缓存中获取数据,但是缓存中没有数据
    if (networkRequest == null && cacheResponse == null) {
      //这里返回504请求超时,说明服务端没有在合适时间内收到客户端的请求
      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();
    }
    // only-if-cached处理完成
    // If we don't need the network, we're done.
    // 如果此时不需要发出网络请求,比方说击中缓存
    if (networkRequest == null) {
      //返回缓存即可,这里的cacheResponse记录一个没有实际body数据的自己
      return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build();
    }
    //来到这里说明没有击中缓存,并且要继续发起请求
    Response networkResponse = null;
    try {
      //回调下一个拦截器进而发起请求
      networkResponse = chain.proceed(networkRequest);
      //此处返回的是服务端的响应
    } finally {
      if (networkResponse == null && cacheCandidate != null) {
        closeQuietly(cacheCandidate.body());
      }
    }

    // If we have a cache response too, then we're doing a conditional get.
    // 如果当前有本地缓存
    if (cacheResponse != null) {
      //注意此时的逻辑,完整来说
      //首先客户端通过request和缓存response中header的标志认为缓存对于客户端来说已经过期了
      //然后又没有设置only-if-cached,则尝试发出了请求,这个请求中会附带缓存response的last-modified之类的信息
      //服务端会通过这个信息来判断当前服务端的数据是不是最新的,如果不是则返回304
      if (networkResponse.code() == HTTP_NOT_MODIFIED) {
        //这时候说明客户端的缓存其实还是服务端最新的数据,那么重新组装一下header等信息即可
        //此时服务端是不应该返回数据的
        Response response = cacheResponse.newBuilder()
                //组合拼装响应报文,主要是哪部分属于缓存响应报文,哪部分属于当前响应报文的问题
            .headers(combine(cacheResponse.headers(), networkResponse.headers()))
                //修改当前请求的发出时间为此次命中缓存的请求的发出时间
            .sentRequestAtMillis(networkResponse.sentRequestAtMillis())
                //修改当前响应接收到响应的时间
            .receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
                //标记缓存响应和网络响应体,但是都需要清空body部分
            .cacheResponse(stripBody(cacheResponse))
            .networkResponse(stripBody(networkResponse))
            .build();
        //注意此时又拷贝了一份,那么网络请求回来的response就可以关闭了
        networkResponse.body().close();

        // Update the cache after combining headers but before stripping the
        // Content-Encoding header (as performed by initContentStream()).
        //击中缓存次数+1,虽然还是发出了请求
        cache.trackConditionalCacheHit();
        //更新缓存,主要是刷新头部之类的信息
        //因为当前响应可能更新了Cache-Control
        //需要修改当前缓存体的策略
        cache.update(cacheResponse, response);
        return response;
      } else {
        //说明服务端返回的response数据是最新的,那么缓存数据已经无效,可以释放资源
        closeQuietly(cacheResponse.body());
      }
    }
    //当前接受从服务端返回的response
    Response response = networkResponse.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build();

    if (HttpHeaders.hasBody(response)) {//这里是判断当前响应报文是否有内容体
      //判断当前网络响应是否可以缓存,注意一下这里如果可以缓存的话会先将起始行和header之类的写入cache当中
      CacheRequest cacheRequest = maybeCache(response, networkResponse.request(), cache);
      //这里是将response的body写入cacheRequest的body当中
      response = cacheWritingResponse(cacheRequest, response);
    }

    return response;
  }

先描述一下结果:
1.当前本地有缓存,但是可能缓存过期或者要刷新,那么要向服务器请求,如果当前网络连接异常,那么会进行异常回调,否则根据服务端的响应码处理
304:说明当前缓存数据和服务端数据一致,那么此时服务端不需要返回数据,本地通过缓存数据拼接最新的响应报文构成此次请求的响应结果即可
其它:使用服务端返回的响应数据,并且根据Cache-Control中的no-store决定是否缓存
2.当前本地有缓存,而且缓存没有过期也不需要刷新,直接使用缓存中的数据
3.当前请求头部指定only-if-cached,那么此次只从缓存中获取数据,如果缓存中数据没有过期,直接使用缓存中的数据,否则返回一个504的空响应体
当然,从CacheInterceptor中没有看出Cache-Control的具体处理逻辑,实际上处理逻辑都在CacheStratrgy中,接下来看一下细节

public Factory(long nowMillis, Request request, Response cacheResponse) {
      //当前创建策略的时间戳
      this.nowMillis = nowMillis;
      //当前请求策略的请求
      this.request = request;
      //当前从硬盘缓存中获得的当前请求的缓存响应数据
      this.cacheResponse = cacheResponse;
      //如果从缓存中获取成功
      if (cacheResponse != null) {//初始化头部中有关缓存的一些数据
        //该响应为本地当中的缓存
        //注意这个时间其实是在CallServerInterceptor中调用intercept时候初始化的时间,简称发送request的时间
        this.sentRequestMillis = cacheResponse.sentRequestAtMillis();
        //客户端接收到缓存的时间戳
        this.receivedResponseMillis = cacheResponse.receivedResponseAtMillis();
        Headers headers = cacheResponse.headers();
        for (int i = 0, size = headers.size(); i < size; i++) {//遍历response的头部报文,获取与缓存相关的字段
          String fieldName = headers.name(i);
          String value = headers.value(i);
          if ("Date".equalsIgnoreCase(fieldName)) {//消息发送的时间(创建报文的时间)
            servedDate = HttpDate.parse(value);
            servedDateString = value;
          } else if ("Expires".equalsIgnoreCase(fieldName)) {//消息应该过期的准确时间
            expires = HttpDate.parse(value);
          } else if ("Last-Modified".equalsIgnoreCase(fieldName)) {//客户端请求的资源在服务端的最后一次修改时间
            lastModified = HttpDate.parse(value);
            lastModifiedString = value;
          } else if ("ETag".equalsIgnoreCase(fieldName)) {//和Last-Modified之类的作用一致,用于判断服务端缓存是否过期
            etag = value;//资源的匹配信息,类似于身份证
          } else if ("Age".equalsIgnoreCase(fieldName)) {//从原始服务器到代理缓存形成的估算时间
            ageSeconds = HttpHeaders.parseSeconds(value, -1);
          }
        }
      }
    }

首先是解析本地缓存中的响应体中的cache-control中的数据并记录

public CacheStrategy get() {
      //通过header参数来判断当前缓存是否过期,并且设置对应的request和response
      //如果缓存没有过期,则request:null,response:cacheResponse
      CacheStrategy candidate = getCandidate();
      //当前没有获得的缓存,request中设置only-if-cached
      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;
    }

private CacheStrategy getCandidate() {
      // No cached response.
      //没有从自定义cache中通过request获得了对应的缓存response
      if (cacheResponse == null) {
        //对于没有自定义缓存来说,这里就简单返回null的response
        return new CacheStrategy(request, null);
      }

      // Drop the cached response if it's missing a required handshake.
      //从自定义cache中获取到response
      //该请求为https,但是TLS握手失败,可以直接认为该response是无效的
      if (request.isHttps() && cacheResponse.handshake() == null) {
        return new CacheStrategy(request, null);
      }

      // If this response shouldn't have been stored, it should never be used
      // as a response source. This check should be redundant as long as the
      // persistence store is well-behaved and the rules are constant.
      //检查当前request和response是否允许缓存,如果不允许缓存,则需要废除response
      //一般来说就是检查request和response的no-store标志
      if (!isCacheable(cacheResponse, request)) {
        return new CacheStrategy(request, null);
      }
      //下面通过获取request中的cache-control来实现最终的缓存策略
      //获取请求报头中cache-control对象
      CacheControl requestCaching = request.cacheControl();
      //在request中的cache-control指定不使用缓存no-cache的时候,缓存无效
      //在头部报文中指定了If-Modified-Since/If-None-Match的时候,
      //此时应该将之前响应的Last-Modified的时间通过If-Modified-Since传递给服务器判断当前缓存是否有效
      //那么在此之前,缓存无效
      if (requestCaching.noCache() || hasConditions(request)) {
        return new CacheStrategy(request, null);
      }
      //获取缓存到当前时间总共经过的时间戳
      long ageMillis = cacheResponseAge();
      //获取response可以存活的最长时间戳
      long freshMillis = computeFreshnessLifetime();
      //当前可以存活时间戳应该为request和response中设置的max-age的最小值
      //一般可以通过这种形式直接让缓存过期
      if (requestCaching.maxAgeSeconds() != -1) {
        freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
      }

      long minFreshMillis = 0;
      //最少应该剩余的有效时间,如果剩余时间小于该时间,认为缓存过期,比方说原本10s后过期,如果这个设置3s,则实际上7s后就被认为过期了,也就是说至少要保留3秒的有效期
      if (requestCaching.minFreshSeconds() != -1) {
        minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
      }

      long maxStaleMillis = 0;
      CacheControl responseCaching = cacheResponse.cacheControl();
      //在最长过期时间的基础上可以额外接收的时间,比方说原本10s后过期,如果这个设置3s,则实际上13s后才会过期
      //注意request中的max-stale只有在response中没有返回must-revalidate的前提下有效
      if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
        maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
      }
      //如果允许使用缓存,并且当前缓存时间小于最长存活时间,认为该缓存依然有效
      if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
        Response.Builder builder = cacheResponse.newBuilder();
        if (ageMillis + minFreshMillis >= freshMillis) {
          //当前缓存是在request的max-stale前提下有效,否则已然过期
          builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"");
        }
        long oneDayMillis = 24 * 60 * 60 * 1000L;
        if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
          builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
        }
        //当前缓存有效,返回当前缓存
        return new CacheStrategy(null, builder.build());
      }
      // 缓存已经过期
      // Find a condition to add to the request. If the condition is satisfied, the response body
      // will not be transmitted.
      String conditionName;
      String conditionValue;
      if (etag != null) {
        //如果当前设置了ETAG缓存标示,通过If-None-Match可以让服务端进行校验
        conditionName = "If-None-Match";
        conditionValue = etag;
      } else if (lastModified != null) {//在没有使用ETAG标志的前提下,通过发送上一次修改或者发送时间给服务端,让服务端判断当前数据是否已经过期
        conditionName = "If-Modified-Since";
        conditionValue = lastModifiedString;
      } else if (servedDate != null) {
        conditionName = "If-Modified-Since";
        conditionValue = servedDateString;
      } else {//没有必要让服务端校验response的过期情况
        return new CacheStrategy(request, null); // No condition! Make a regular request.
      }
      //将缓存过期需要设置的信息放入request的头部报文之中
      //主要就是客户端缓存的response的时间,用于服务端判断是否过期来使用
      Headers.Builder conditionalRequestHeaders = request.headers().newBuilder();
      Internal.instance.addLenient(conditionalRequestHeaders, conditionName, conditionValue);

      Request conditionalRequest = request.newBuilder()
          .headers(conditionalRequestHeaders.build())
          .build();
      return new CacheStrategy(conditionalRequest, cacheResponse);
    }

1.当前请求没有本地缓存,如果请求头部报文指定了only-if-cached,那么后续不再发请求,并且缓存无效。
2.当前请求有本地缓存,有几种情况下缓存无效:
(1)请求为Https,但是SSL握手失败
(2)当前请求报文头部指定no-cache,说明不使用缓存
(3)当前请求报文头部指定if-since-modified,说明要去校验当前缓存是否过期
以上三种情况没有命中缓存,然后需要发起请求。
3.当前请求有本地缓存,然后需要根据头部字段来判断当前缓存是否过期
(1)缓存过期,但是要根据服务端响应来判断缓存是否真正过期,这需要指定If-Modified-Since头部,后续根据304之类的状态码判断是否使用本地缓存,但是请求是一定要发的。
(2)缓存没有过期,那么直接使用当前缓存作为请求结果,不发请求。
稍微关注一下几个数据:
1.本地缓存的存活时间:这里是估算,通过当前时间-接收请求的时间+接收请求的时间-请求发送的时间+缓存创建的时间(第一次会是响应的创建时间,后续可能会是缓存代理所记录的缓存存活时间)。
2.缓存的过期时间:通过Cache-Control指定,比方说Expires会指定缓存过期的具体时刻,然后减去Date可以得到缓存的最大存活时间,或者通过max-age来指定缓存的最大存活时间
3.缓存的提前刷新时间:通过Cache-Control指定,min-fresh可以指定在缓存过期时间之前多少秒之内的请求都必须发起请求去刷新缓存。
4.缓存的过期拖延时间:通过Cache-Control指定,max-stale可以指定缓存拖延一段时间再过期,不过如果指定must-revalidate的话无效。

总结

这一篇主要是讲解缓存相关,其实就是Cache-Control,其实如果可以很好的利用Http1.1的缓存,可以减少很多人为代码。

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

推荐阅读更多精彩内容