浅析okhttp和Retrofit(一)

概述

okhttp,一个处理网络请求的开源项目,是安卓端最火热的轻量级框架,由移动支付Square公司贡献用于替代HttpUrlConnection和Apache HttpClient,而Retrofit也是Square开源的一款适用于Android网络请求的框架。Retrofit底层是基于OkHttp实现的,与其他网络框架不同的是,它更多使用运行时注解的方式提供功能。

现在比较常用的网络请求方式大概有四种:Android-Async-Http、Volley、OkHttp、Retrofit,下面借用一张图来让你大概了解全他们的特点和他们之间的区别

HttpClient简介:

HttpClient 是Apache的一个三方网络框架,网络请求做了完善的封装,api众多,用起来比较方便,开发快。实现比较稳定,bug比较少,但是正式由于其api众多,是我们很难再不破坏兼容性的情况下对其进行扩展。所以,Android团队对提升和优化httpclient积极性并不高。android5.0被废弃,6.0逐渐删除。

HttpURLConnection简介

HttpURLConnection是一个多用途、轻量级的http客户端。它对网络请求的封装没有HttpClient彻底,api比较简单,用起来没有那么方便。但是正是由于此,使得我们能更容易的扩展和优化的HttpURLConnection。不过,再android2.2之前一直存在着一些令人烦的bug,比如一个人可读的inputstream调用它的close方法的时候,会使得连接池实效,通常的做法就是禁用连接池。因此,在android2.2之前建议使用稳定的HttpClient,android2.2之后使用更容易扩展和优化的HttpURLConnection。

这篇文章我们先说说okhttp
OkHttp官网地址:http://square.github.io/okhttp/
OkHttp GitHub地址:https://github.com/square/okhttp

基本使用

配置

导入Jar包
点击下面链接下载最新 JAR
http://square.github.io/okhttp/#download
或者
GRADLE
在build.gradle中引用compile 'com.squareup.okhttp3:okhttp:(insert latest version)'

使用

在日常开发中最常用到的网络请求就是GET和POST两种请求方式。
HTTP GET
创建一个普通的同步get请求代码如下:

String run(String url) throws IOException {
    Request request = new Request.Builder().url(url).build();
    OkHttpClient client = new OkHttpClient();
    Response response = client.newCall(request).execute();
    if (response.isSuccessful()) {
        return response.body().string();
    } else {
        throw new IOException("Unexpected code " + response);
    }
}

异步get:

Request request = new Request.Builder()
        .url("http://xxxxxx")
        .build();
OkHttpClient client = new OkHttpClient();
client.newCall(request).enqueue(new Callback() {
    @Override
    public void onFailure(Call call, IOException e) {
    }
    @Override
    public void onResponse(Call call, Response response) throws IOException {
        if(response.isSuccessful()){//回调的方法执行在子线程。

        }
    }
});

HTTP POST
创建一个普通的post提交json数据代码如下:

String post(String url, String json) throws IOException {
    RequestBody body = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), json);
    Request request = new Request.Builder()
            .url(url)
            .post(body)
            .build();
    OkHttpClient client = new OkHttpClient();
    Response response = client.newCall(request).execute();
    if (response.isSuccessful()) {
        return response.body().string();
    } else {
        throw new IOException("Unexpected code " + response);
    }
}

Request是OkHttp中访问的请求,Builder是辅助类,Response即OkHttp中的响应。 从上面可以看出发起一个网络请求主要三个步骤:1.创建OkHttpClient实例;2.使用构造器创建请求;3.提交请求接收返回数据。整个处理流程如下:


通过使用我们在具体分析下我们使用的功能在内部是怎么实现的:

首先创建一个Request请求,设置相应的url,和具体的请求参数和一些其他的配置,Request是通过Builder来完成构建的。虽然说是构建Request,但实际上这些数据是不足以构建一个合法的Request的,其他待补全的信息其实是OkHttp在后面某个环节帮你加上去,我们开发者只需要设置一些必要的参数就行了。

创建一个OkHttpClient 对象

OkHttpClient client = new OkHttpClient();

进入源码我们可以看到她的构造方法

public OkHttpClient() {
    this(new Builder());
}
public Builder() {
  dispatcher = new Dispatcher(); //请求的执行器,负责请求的同步执行、异步执行、取消、以及最多可以同时执行的请求数
  protocols = DEFAULT_PROTOCOLS;  //支持的Protocol协议,Protocol和Url类似
  connectionSpecs = DEFAULT_CONNECTION_SPECS;  //支持套接字连接协议.
  eventListenerFactory = EventListener.factory(EventListener.NONE);
  proxySelector = ProxySelector.getDefault();  //代理服务器选择器
  cookieJar = CookieJar.NO_COOKIES;  //提供cookie可持久化操作。
  socketFactory = SocketFactory.getDefault();  //创建套接字的工厂类
  hostnameVerifier = OkHostnameVerifier.INSTANCE;  //主机名验证
  certificatePinner = CertificatePinner.DEFAULT;  //约束所信任的证书
  proxyAuthenticator = Authenticator.NONE;  //身份验证代理服务器
  authenticator = Authenticator.NONE;  //身份验证代理服务器
  connectionPool = new ConnectionPool();  //连接池用于回收利用HTTP和HTTPS连接。
  dns = Dns.SYSTEM;   //dns服务器;默认使用系统的
  followSslRedirects = true;  //是否支持sslhttp重定向,默认true
  followRedirects = true;  //是否支持http重定向,默认true
  retryOnConnectionFailure = true;  //连接失败时是否重试,默认true
  connectTimeout = 10_000;
  readTimeout = 10_000;
  writeTimeout = 10_000;
  pingInterval = 0;
}

为了方便我们使用,okhttp提供了一个“快捷操作”,全部使用了默认的配置,比如指定了 Dispatcher (管理线程池)、链接池、超时时间等。OkHttpClient依然是通过Builder来进行构建的,我们开发者也可以自己去设置一些参数,比如:

 HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();
 loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
 OkHttpClient okHttpClient = new OkHttpClient.Builder()
            .addInterceptor(loggingInterceptor)
            .addInterceptor(new ParamInterceptor(mApplication))
            .connectTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS)
            .writeTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS)
            .readTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS)
            .build();

值得注意的是OKHttpClient实现了 Call.Factory接口,创建一个RealCall类的实例(Call的实现类)。前面的请求我们看到,在发送请求之前,需要调用newCall()方法,创建一个指向RealCall实现类的Call对象,实际上RealCall包装了Request和OKHttpClient这两个类的实例。使得后面的方法中可以很方便的使用这两者。

创建完Request和OKHttpClient,接着我们看下具体是怎样发起请求的,根据流程图我们看到我们先有一个RealCall对象,接着看看RealCall对象是怎么被创建的,OKHttpClient实现了Call.Factory接口创建了一个call对象:

 /**
 * Prepares the {@code request} to be executed at some point in the future.
 */
@Override 
public Call newCall(Request request) {
    return RealCall.newRealCall(this, request, false /* for web socket */);
}

通过前面代码可以看到得到RealCall对象,执行同步的execute()方法或者异步的enqueue我们就可以发起请求了,虽然只有一个简简单单的方法,但是这里面却是最为复杂的,我们来看看我 RealCall里面的execute()方法和enqueue方法:

@Override 
public Response execute() throws IOException {
    synchronized (this) {
        if (executed) throw new IllegalStateException("Already Executed");
        executed = true;
    }
    captureCallStackTrace();
    eventListener.callStart(this);
    try {
        client.dispatcher().executed(this);
        Response result = getResponseWithInterceptorChain();
        if (result == null) throw new IOException("Canceled");
        return result;
    } catch (IOException e) {
        eventListener.callFailed(this, e);
        throw e;
    } finally {
        client.dispatcher().finished(this);
    }
}
@Override 
public void enqueue(Callback responseCallback) {
    synchronized (this) {
        if (executed) throw new IllegalStateException("Already Executed");
        executed = true;
    }
    captureCallStackTrace();
    eventListener.callStart(this);
    client.dispatcher().enqueue(new AsyncCall(responseCallback));
}

enqueue方法里面引入了一个新的类AsyncCall,这个类继承于NamedRunnable,实现了Runnable接口。NamedRunnable可以给当前的线程设置名字,并且用模板方法将线程的执行体放到了execute方法中。

enqueue/execute 方法中涉及好几个新的类和方法,如下:

同步 execute:

captureCallStackTrace()
dispatcher()
getResponseWithInterceptorChain()

异步 enqueue:

captureCallStackTrace()
dispatcher()
AsyncCall

我们看到不管是同步还是异步都会调用两个相同的方法 captureCallStackTrace() 和 dispatcher() ,先看下 captureCallStackTrace() ,通过看源码这个应该是追踪栈信息的。

Dispatcher是做什么的呢?文章开头就提到了okhttp有个优势就是内置连接池,支持连接复用,减少延迟。Dispatcher类就是负责管理调度的,我们可以看看Dispatcher的源码:

public final class Dispatcher {
    /** 最大并发请求数为64 */
    private int maxRequests = 64;
    /** 每个主机最大请求数为5 */
    private int maxRequestsPerHost = 5;
    private @Nullable Runnable idleCallback;

    /** 线程池 */
    private @Nullable ExecutorService executorService;

    /** 准备执行的异步请求 */
    private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();

    /** 正在执行的异步请求,包含已经取消但未执行完的请求 */
    private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();

    /** 正在执行的同步请求,包含已经取消单未执行完的请求 */
    private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();

    public Dispatcher(ExecutorService executorService) {
        this.executorService = executorService;
    }

    public Dispatcher() {
    }

    public synchronized ExecutorService executorService() {
        if (executorService == null) {
            executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
                    new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false));
        }
        return executorService;
    }
    .....
}

从ExecutorService executorService()方法可以看出线程池实现了对象复用,降低线程创建开销,从设计模式上来讲,使用了享元模式。

代码太多了,我只贴出了部分,有兴趣大家可以去研究下,代码不多。言归正传,从源码我们可以看出Dispatcher就像是一个调度中心,这里负责所有的任务执行,值得注意的是我们看到executorService这个对象是个线程池,配置如下:

核心线程数:0
最大线程数:Iteger.MAX_VALUE,其实就是不限制
空闲线程保活时间:60s
任务队列:SynchronousQueue(没有缓存大小的阻塞队列),为有界队列,且队列大小为 0,不存 Runnable,只是用来进行生产者和消费者之间的传递任务。

实际上这里和我们在 Executors 使用的 缓存线程池的配置完全一样的,唯一的区别仅仅是修改了线程名,我们简要分析一下缓存线程池的运行机制,进入到 ThreadPoolExecutor 中的 execute 方法的源码中,假设线程池处于运行状态,会有这样的流程:

新过来一个任务,由于核心线程数为 0,不需要创建核心线程,所以尝试加入任务队列
如果线程池中有线程刚好空闲可以接收任务,因为任务队列的类型是 SynchronousQueue,实际大小为 0,只做消费者和生产者的中转站,所以这时候,就可以入列成功,通过 SynchronousQueue 中转任务给空闲的线程执行。
如果当前线程池所有线程工作饱和,会入列失败。这时候 ThreadPoolExecutor 会调用 addWorker(command, false) 来创建并且启动新线程。

如果这样就会产生一个问题,如果客户端一次发起1000个请求,那肯定就会产生1000个线程。所以在 OkHttp 中线程池只是一个辅助作用,仅仅是用来做线程缓存,便于复用的。真正对这些请求的并发数量限制,执行时机等等都是调度器 Dispatcher 承担的。在 OkHttp 这里,线程池只是个带缓存功能的执行器,而真正的调度是外部包了一个调度策略的:

首先,是线程数量的约束。最大并发数 maxRequests 默认为 64,单个主机最大请求数 maxRequestsPerHost 默认为 5 个。所以极端情况下,才会开启 64 个线程。一般场景是不会出现的,如果超过了约束,我们从代码里面可以看到有两个异步请求队列readyAsyncCalls 和runningAsyncCalls ,如果没有超过约束则把异步请求加入 runningAsyncCalls ,并在线程池中执行(线程池会根据当前负载自动创建,销毁,缓存相应的线程)。否则就会加入 readyAsyncCalls 缓冲排队。

那么,等待队列中的请求什么时候会执行呢?

回到之前提到的 AsyncCalls 中,在 execute 结束的时候会再调用一下 Dispatcher 的 finished 方法,如下

继续看源码我们会看到在finished方法中会执行一个方法


所以,OkHttp 的调度器默认配置下,在保证每个域名可以快速响应请求的情况下,还限制了每个域名的并发数和总体的并发数。实际每个应用的请求用到的域名都不多。线程池仅仅做线程缓存功能,调度策略应该外部自己去实现。

接着我们来分析下getResponseWithInterceptorChain()

getResponseWithInterceptorChain()应该是最为复杂的,我们先来看看源码:

Response getResponseWithInterceptorChain() throws IOException {
// Build a full stack of interceptors.
List<Interceptor> interceptors = new ArrayList<>();
interceptors.addAll(client.interceptors());
interceptors.add(retryAndFollowUpInterceptor);
interceptors.add(new BridgeInterceptor(client.cookieJar()));
interceptors.add(new CacheInterceptor(client.internalCache()));
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, this, eventListener, client.connectTimeoutMillis(),
    client.readTimeoutMillis(), client.writeTimeoutMillis());

return chain.proceed(originalRequest);

}

OkHttp 开发者之一介绍 OkHttp 的文章里面,作者讲到:the whole thing is just a stack of built-in interceptors.
可见 Interceptor 是 OkHttp 最核心的一个东西,不要误以为它只负责拦截请求进行一些额外的处理(例如 cookie),实际上它把实际的网络请求、缓存、透明压缩等功能都统一了起来,每一个功能都只是一个 Interceptor,它们再连接成一个 Interceptor.Chain,环环相扣,最终圆满完成一次网络请求。

从上面我们可以分析出,其逻辑大致分为两部分:

1、创建一系列拦截器,并将其放入一个拦截器数组中。这部分拦截器即包括用户自定义的拦截器也包括框架内部拦截器
2、创建一个拦截器链RealInterceptorChain,并执行拦截器链的proceed方法

这里okhttp用到了一个很经典的设计模式:责任链模式,它在okhttp中得到了很好的实践。实际上责任链模式在安卓系统中也有比较典型的实践,例如 view 系统对点击事件(TouchEvent)的处理,有兴趣可以去看下。

从 getResponseWithInterceptorChain 函数我们可以看到,借用一张图,Interceptor.Chain 的分布依次是:


从流程图我们可以看出

1、首先是在创建OkHttpClient 自定义的interceptors
2、用来实现连接失败的重试和重定向的RetryAndFollowUpInterceptor(在网络请求失败后进行重试
当服务器返回当前请求需要进行重定向时直接发起新的请求,并在条件允许情况下复用当前连接)
3、用来修改请求和响应的 header 信息的BridgeInterceptor(设置内容长度,内容编码
设置gzip压缩,并在接收到内容后进行解压。省去了应用层处理数据解压的麻烦
添加cookie,设置其他报头,如User-Agent,Host,Keep-alive等。其中Keep-Alive是实现多路复用的必要步骤)
4、负责读取缓存直接返回、更新缓存的 CacheInterceptor(当网络请求有符合要求的Cache时直接返回Cache,当服务器返回内容有改变时更新当前cache,如果当前cache失效,删除)
5、负责和服务器建立连接的 ConnectInterceptor(其实是调用了 StreamAllocation 的newStream 方法来打开连接的。建联的 TCP 握手,TLS 握手都发生该阶段。过了这个阶段,和服务端的 socket 连接打通)
6、配置 OkHttpClient 时设置的 networkInterceptors
7、负责向服务器发送请求数据、从服务器读取响应数据的 CallServerInterceptor(上一个阶段已经握手成功,HttpStream 流已经打开,所以这个阶段把 Request 的请求信息传入流中,并且从流中读取数据封装成 Response 返回)

这就构成了okhttp最核心的一块东西,把Request 变成 Response。每个 Interceptor 都可能完成这件事,所以我们循着链条让每个 Interceptor 自行决定能否完成任务以及怎么完成任务(自力更生或者交给下一个 Interceptor)。这样一来,完成网络请求这件事就彻底从 RealCall 类中剥离了出来,简化了各自的责任和逻辑。

在这里我们简单分析下CallServerInterceptor,看看 OkHttp 是怎么进行和服务器的实际通信的

CallServerInterceptor###

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

long sentRequestMillis = System.currentTimeMillis();

realChain.eventListener().requestHeadersStart(realChain.call());
httpCodec.writeRequestHeaders(request);
realChain.eventListener().requestHeadersEnd(realChain.call(), request);

Response.Builder responseBuilder = null;
if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
  // If there's a "Expect: 100-continue" header on the request, wait for a "HTTP/1.1 100
  // Continue" response before transmitting the request body. If we don't get that, return
  // what we did get (such as a 4xx response) without ever transmitting the request body.
  if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
    httpCodec.flushRequest();
    realChain.eventListener().responseHeadersStart(realChain.call());
    responseBuilder = httpCodec.readResponseHeaders(true);
  }

  if (responseBuilder == null) {
    // Write the request body if the "Expect: 100-continue" expectation was met.
    realChain.eventListener().requestBodyStart(realChain.call());
    long contentLength = request.body().contentLength();
    CountingSink requestBodyOut =
        new CountingSink(httpCodec.createRequestBody(request, contentLength));
    BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);

    request.body().writeTo(bufferedRequestBody);
    bufferedRequestBody.close();
    realChain.eventListener()
        .requestBodyEnd(realChain.call(), requestBodyOut.successfulCount);
  } else if (!connection.isMultiplexed()) {
    // If the "Expect: 100-continue" expectation wasn't met, prevent the HTTP/1 connection
    // from being reused. Otherwise we're still obligated to transmit the request body to
    // leave the connection in a consistent state.
    streamAllocation.noNewStreams();
  }
}

httpCodec.finishRequest();

if (responseBuilder == null) {
  realChain.eventListener().responseHeadersStart(realChain.call());
  responseBuilder = httpCodec.readResponseHeaders(false);
}

Response response = responseBuilder
    .request(request)
    .handshake(streamAllocation.connection().handshake())
    .sentRequestAtMillis(sentRequestMillis)
    .receivedResponseAtMillis(System.currentTimeMillis())
    .build();

int code = response.code();
if (code == 100) {
  // server sent a 100-continue even though we did not request one.
  // try again to read the actual response
  responseBuilder = httpCodec.readResponseHeaders(false);

  response = responseBuilder
          .request(request)
          .handshake(streamAllocation.connection().handshake())
          .sentRequestAtMillis(sentRequestMillis)
          .receivedResponseAtMillis(System.currentTimeMillis())
          .build();

  code = response.code();
}

realChain.eventListener()
        .responseHeadersEnd(realChain.call(), response);

if (forWebSocket && code == 101) {
  // Connection is upgrading, but we need to ensure interceptors see a non-null response body.
  response = response.newBuilder()
      .body(Util.EMPTY_RESPONSE)
      .build();
} else {
  response = response.newBuilder()
      .body(httpCodec.openResponseBody(response))
      .build();
}

if ("close".equalsIgnoreCase(response.request().header("Connection"))
    || "close".equalsIgnoreCase(response.header("Connection"))) {
  streamAllocation.noNewStreams();
}

if ((code == 204 || code == 205) && response.body().contentLength() > 0) {
  throw new ProtocolException(
      "HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());
}

return response;
}

我们抓住主干部分:

首先向服务器发送 request header和request body;
读取 response header,先构造一个 Response 对象;
如果有 response body,就在 3 的基础上加上 body 构造一个新的 Response 对象;

这里我们可以看到,核心工作都由 HttpCodec 对象完成,而 HttpCodec 实际上利用的是 Okio,而 Okio 实际上还是用的 Socket。

我们再来说说自定义Interceptor ,我们可以用自定义的Interceptor 来干什么?比如,像我们请求接口都要加上token或者一些其他的一些公共参数,我们就可以利用自定义Interceptor 来实现:

public class ParamInterceptor implements Interceptor {

 private Context mContext;

 public ParamInterceptor(Context context ) {
    mContext = context;
  }


@Override
public Response intercept(Chain chain) throws IOException {
    Request oldrequest = chain.request();
    Request.Builder requestBuilder = oldrequest.newBuilder()
            .addHeader("Content-Type", "application/json");
    if (isLogin) {
        requestBuilder.addHeader("TOKEN","登录返回的token");
    }
    Request request = requestBuilder.build();
    return chain.proceed(request);
  } }

这样就实现了添加公共参数的功能

其实 Interceptor 的设计也是一种分层的思想,每个 Interceptor 就是一层。为什么要套这么多层呢?分层的思想在 TCP/IP 协议中就体现得淋漓尽致,分层简化了每一层的逻辑,每层只需要关注自己的责任(单一原则思想也在此体现),而各层之间通过约定的接口/协议进行合作(面向接口编程思想),共同完成复杂的任务。

总结

OkHttp 还有很多细节部分没有在本文探讨,但建立一个清晰的概览非常重要。对整体有了清晰认识之后,细节部分如有需要,再单独深入将更加容易。下一篇文章我们接着介绍Retrofit。

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

推荐阅读更多精彩内容