OkHttp 原理

OKHttp 请求的整体流程是怎样的?

   val okHttpClient = OkHttpClient()
   val request: Request = Request.Builder()
       .url("https://www.google.com/")
       .build()

   okHttpClient.newCall(request).enqueue(object :Callback{
       override fun onFailure(call: Call, e: IOException) {
       }

       override fun onResponse(call: Call, response: Response) {
       }
   })

所有网络请求的逻辑大部分集中在拦截器中,但是在进入拦截器之前还需要依靠分发器来调配请求任务。

分发器 -> 内部维护队列与线程池,完成请求调配;拦截器 -> 五大默认拦截器完成整个请求过程。

整个网络请求过程大致如上所示:

1,通过建造者模式构建 OKHttpClientRequest

2,OKHttpClient 通过 newCall 发起一个新的请求

3,通过分发器维护请求队列与线程池,完成请求调配

4,通过五大默认拦截器完成请求重试,缓存处理,建立连接等一系列操作

5,得到网络请求结果

OKHttp 分发器是怎样工作的?

分发器的主要作用是维护请求队列与线程池,比如我们有100个异步请求,肯定不能把它们同时请求,而是应该把它们排队分个类,分为 正在请求中的列表正在等待的列表, 待正在请求中的列表的所有请求完成后,即可从等待中的列表中取出等待的请求,从而完成所有的请求。

同步请求:

同步请求不需要线程池,也不存在任何限制。所以分发器仅做一下记录。后续按照加入队列的顺序同步请求即可。

synchronized void executed(RealCall call) {
    runningSyncCalls.add(call);
}

异步请求:

synchronized void enqueue(AsyncCall call) {
    // 当正在执行的任务未超过最大限制64,并且同一Host的请求不超过5个,则会添加到正在执行队列
    if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost)     {
        runningAsyncCalls.add(call);
        executorService().execute(call); // 提交给线程池
    } else {
                // 加入等待队列
        readyAsyncCalls.add(call);
    }
}

每个任务完成后,最后都会调用分发器的 dispatcher.finished(this) 方法,这里面会取出等待队列中的任务继续执行。

OKHttp 拦截器是怎样工作的?

# RealCall
  override fun execute(): Response {
    try {
      client.dispatcher.executed(this)
      return getResponseWithInterceptorChain()
    } finally {
      client.dispatcher.finished(this)
    }
  }

// 构建了一个 OkHttp 拦截器的责任链
 internal fun getResponseWithInterceptorChain(): Response {
    // Build a full stack of interceptors.
    val interceptors = mutableListOf<Interceptor>()
    interceptors += client.interceptors
    interceptors += RetryAndFollowUpInterceptor(client)
    interceptors += BridgeInterceptor(client.cookieJar)
    interceptors += CacheInterceptor(client.cache)
    interceptors += ConnectInterceptor
    if (!forWebSocket) {
      interceptors += client.networkInterceptors
    }
    interceptors += CallServerInterceptor(forWebSocket)

    val chain = RealInterceptorChain(
        call = this,
        interceptors = interceptors,
        index = 0,
        exchange = null,
        request = originalRequest,
        connectTimeoutMillis = client.connectTimeoutMillis,
        readTimeoutMillis = client.readTimeoutMillis,
        writeTimeoutMillis = client.writeTimeoutMillis
    )
      ...
    try {
      // 将请求移交给下一个拦截器
      val response = chain.proceed(originalRequest)
      ...
      return response
    } 
    ...
  }

责任链,顾名思义,就是用来处理相关事务责任的一条执行链,执行链上有多个节点,每个节点都有机会(条件匹配)处理请求事务,如果某个节点处理完了就可以根据实际业务需求传递给下一个节点继续处理或者返回处理完毕。

如上所示责任链添加的顺序及作用如下表所示:

我们的网络请求就是这样经过责任链一级一级的递推下去,最终会执行到CallServerInterceptorintercept 方法,此方法会将网络响应的结果封装成一个 Response 对象并`return。

RetryAndFollowUpInterceptor 出错和重定向的判断标准也简单说一下:

  • 判断出错的标准: 利用 try-catch 块对请求进行异常捕获,这里会捕获RouteExceptionIOException,并且在出错后都会先判断当前请求是否能够进行重试的操作。

  • 重定向标准:对 Response 的状态码 Code 进行审查,当状态码为 3xx 时,则表示需要重定向,而后创建一个新的 Request,进行重试操作。

BridgeInterceptor 帮用户处理网络请求,它会帮助用户填写服务器请求所需要的配置信息(例如:请求头),如上面所展示的 User-AgentConnectionHostAccept-Encoding 等。同时也会对请求的结果进行相应处理。

GET /wxarticle/chapters/json HTTP/1.1
Host: wanandroid.com
Accept: application/json, text/plain, /
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Connection: keep-alive
User-Agent: Mozilla/5.0 xxx

BridgeInterceptor的内部实现主要分为以下三步:

1,为用户网络请求设置 Content-TypeContent-LengthHostConnectionCookie 等参数,也就是将一般请求转换为适合服务器解析的格式,以适应服务器端;

2,通过 chain.proceed(requestBuilder.build())方法,将转换后的请求移交给下一个拦截器 CacheInterceptor,并接收返回的结果 Response

3,对结果 Response 进行 gzip、Content-Type 等转换,以适应应用程序端。

所以说 BridgeInterceptor 是应用程序和服务器端的一个桥梁。

CacheInterceptor

CacheInterceptor 是一个处理网络请求缓存的拦截器。它的内部处理和一些图片缓存的逻辑相似,首先会判断是否存在可用的缓存,如果存在,则直接返回缓存,反之,调用 chain.proceed(networkRequest) 方法将请求移交给下一个拦截器,有了结果后,将结果 put 到 cache 中。

** ConnectInterceptor**

  internal fun initExchange(chain: RealInterceptorChain): Exchange {
    ...
    val exchangeFinder = this.exchangeFinder!!
    val codec = exchangeFinder.find(client, chain)
    val result = Exchange(this, eventListener, exchangeFinder, codec)
    this.interceptorScopedExchange = result
    this.exchange = result
    ...

    if (canceled) throw IOException("Canceled")
    return result
  }

它首先会通过 ExchangeFinder 查询到 codec,这个 ExchangeFinder 是不是很熟悉?在上面 RetryAndFollowUpInterceptor 分析中,每次循环都会先做创建 ExchangeFinder 的准备工作。而这个 codec 是什么?它是一个编码解码器,来确定是用 Http1 的方式还是以 Http2 的方式进行请求。在找到合适的codec 后,作为参数创建 ExchangeExchange 内部涉及了很多网络连接的实现,这个后面再详细说,我们先看看是如何找到合适的 codec

利用编解码器 codec 创建了一个 Exchange,而这个 Exchange 的内部其实是利用 Http1 解码器或者 Http2 解码器,分别进行请求头的编写writeRequestHeaders,或者创建 Request Body,发送给服务器。

找到合适的 codec,就必须先找到一个可用的网络连接,再利用这个可用的连接创建一个新的 codec。 为了找到可用的连接,内部使用了大概5种方式进行筛选。

1,从连接池中查找

if (connectionPool.callAcquirePooledConnection(address, call, null, false)) {
      val result = call.connection!!
      return result
    }

尝试在连接池中查找可用的连接,在遍历连接池中的连接时,就会判断每个连接是否可用,而判断连接是否可用的条件如下:请求数要小于该连接最大能承受的请求数,Http2 以下,最大请求数为1个,并且此连接上可创建新的交换;
该连接的主机和请求的主机一致;如果从连接池中拿到了合格的 connection,则直接返回。如果没有拿到,那就进行第二种拿可用连接的方式。

2,传入Route,从连接池中查找

 if (connectionPool.callAcquirePooledConnection(address, call, routes, false)) {
        val result = call.connection!!
        return result
      }

依然是从连接池中拿,但是这次不同的是,参数里传入了 routes,这个routes 是包含路由 Route 的一个 List 集合,而 Route 其实指的是连接的 IP 地址、TCP 端口以及代理模式。而这次从连接池中拿,主要是针对 Http2,路由必须共用一个 IP 地址,此连接的服务器证书必须包含新主机且证书必须与主机匹配。

3,自己创建连接

如果前两次从连接池里都没有拿到可用连接,那么就自己创建连接。

 val newConnection = RealConnection(connectionPool, route)
    call.connectionToCancel = newConnection
    try {
      newConnection.connect(
          connectTimeout,
          readTimeout,
          writeTimeout,
          pingIntervalMillis,
          connectionRetryEnabled,
          call,
          eventListener
      )
    } 

4,多路复用置为 true,依然从连接池中查找

 if (connectionPool.callAcquirePooledConnection(address, call, routes, true)) {
      val result = call.connection!!
      newConnection.socket().closeQuietly()
      return result
    }

这次从连接池中查找,requireMultiplexed 置为了true,只查找支持多路复用的连接。并且在建立连接后,将新的连接保存到连接池中。

建立连接,找到合适的 codec 后,又将请求移交给了下一个拦截器CallServerInterceptor

CallServerInterceptor

CallServerInterceptor 是链中最后一个拦截器,主要用于向服务器发送内容,主要传输 http 的头部和 body 信息。其内部利用上面创建的 Exchange 进行请求头编写,创建 Request body,发送请求,得到结果后,对结果进行解析并回传。

应用拦截器网络拦截器 有什么区别?

从整个责任链路来看,应用拦截器 是最先执行的拦截器,也就是用户自己设置 request 属性后的原始请求,而网络拦截器位于 ConnectInterceptorCallServerInterceptor 之间,此时网络链路已经准备好,只等待发送请求数据。它们主要有以下区别:

1,应用拦截器永远只会触发一次

2,每个拦截器都应该至少调用一次 realChain.proceed 方法。

OKHttp 如何复用 TCP 连接?

ConnectInterceptor 的主要工作就是负责建立 TCP 连接,建立 TCP 连接需要经历三次握手四次挥手等操作,如果每个 HTTP 请求都要新建一个 TCP,那么消耗资源比较多。而 Http1.1 已经支持 keep-alive,即多个 Http 请求复用一个 TCP 连接,OKHttp 也做了相应的优化,下面我们来看下 OKHttp 是怎么复用TCP 连接的:

ConnectInterceptor 中查找连接的代码会最终会调用到ExchangeFinder.findConnection 方法,具体如下:

# ExchangeFinder
//为承载新的数据流 寻找 连接。寻找顺序是 已分配的连接、连接池、新建连接
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
    int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
  synchronized (connectionPool) {
    // 1.尝试使用 已给数据流分配的连接.(例如重定向请求时,可以复用上次请求的连接)
    releasedConnection = transmitter.connection;
    result = transmitter.connection;

    if (result == null) {
      // 2. 没有已分配的可用连接,就尝试从连接池获取。(连接池稍后详细讲解)
      if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, null, false)) {
        result = transmitter.connection;
      }
    }
  }

  synchronized (connectionPool) {
    if (newRouteSelection) {
      //3. 现在有了IP地址,再次尝试从连接池获取。可能会因为连接合并而匹配。(这里传入了routes,上面的传的null)
      routes = routeSelection.getAll();
      if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, false)) {
        foundPooledConnection = true;
        result = transmitter.connection;
      }
    }

  // 4.第二次没成功,就把新建的连接,进行TCP + TLS 握手,与服务端建立连接. 是阻塞操作
  result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
      connectionRetryEnabled, call, eventListener);

  synchronized (connectionPool) {
    // 5. 最后一次尝试从连接池获取,注意最后一个参数为true,即要求 多路复用(http2.0)
    //意思是,如果本次是http2.0,那么为了保证 多路复用性,(因为上面的握手操作不是线程安全)会再次确认连接池中此时是否已有同样连接
    if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, true)) {
      // 如果获取到,就关闭我们创建里的连接,返回获取的连接
      result = transmitter.connection;
    } else {
      //最后一次尝试也没有的话,就把刚刚新建的连接存入连接池
      connectionPool.put(result);
    }
  }
 
  return result;
}

上面精简了部分代码,可以看出,连接拦截器使用了5种方法查找连接:

1,首先会尝试使用已分配的连接。(已分配连接的情况例如重定向时的再次请求,说明上次已经有了连接)

2,若没有已分配的可用连接,就尝试从连接池中匹配获取。因为此时没有路由信息,所以匹配条件:address一致——host、port、代理等一致,且匹配的连接可以接受新的请求。

3,若从连接池没有获取到,则传入 routes 再次尝试获取,这主要是针对 Http2.0 的一个操作,Http2.0 可以复用 square.com 与 square.ca 的连接
若第二次也没有获取到,就创建 RealConnection 实例,进行 TCP + TLS 握手,与服务端建立连接。

4,此时为了确保 Http2.0 连接的多路复用性,会第三次从连接池匹配。因为新建立的连接的握手过程是非线程安全的,所以此时可能连接池新存入了相同的连接。

5,第三次若匹配到,就使用已有连接,释放刚刚新建的连接;若未匹配到,则把新连接存入连接池并返回。

如何建立TCP/TLS连接?

TCP 连接:

fun connect(
    connectTimeout: Int,
    readTimeout: Int,
    writeTimeout: Int,
    pingIntervalMillis: Int,
    connectionRetryEnabled: Boolean,
    call: Call,
    eventListener: EventListener
  ) {
    ...

    while (true) {
      try {
        if (route.requiresTunnel()) {
          connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener)
          if (rawSocket == null) {
            // We were unable to connect the tunnel but properly closed down our resources.
            break
          }
        } else {
          connectSocket(connectTimeout, readTimeout, call, eventListener)
        }
        establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener)
        eventListener.connectEnd(call, route.socketAddress, route.proxy, protocol)
        break
      } catch (e: IOException) {
        ...
  }

route.requiresTunnel() 这个方法表示该请求是否使用了 Proxy.Type.HTTP 代理且目标是 Https 连接;如果是,则创建一个代理隧道连接 Tunnel(connectTunnel)。创建这个隧道的目的在于利用 Http 来代理请求 Https;如果不是,则直接建立一个 TCP 连接(connectSocket)

代理隧道是如何创建的?

它的内部会先通过 Http 代理创建一个 TLS 的请求,也就是在地址 url 上增加Host、Proxy-Connection、User-Agent 首部。接着最多21次的尝试,利用connectSocket 开启 TCP 连接且利用 TLS 请求创建一个代理隧道。

从这里可以看见,不管是否需要代理隧道,都会开始建立一个 TCP 连接(connectSocket),那又是如何建立 TCP 连接的?

 private fun connectSocket(
    connectTimeout: Int,
    readTimeout: Int,
    call: Call,
    eventListener: EventListener
  ) {
    val proxy = route.proxy
    val address = route.address

    val rawSocket = when (proxy.type()) {
      Proxy.Type.DIRECT, Proxy.Type.HTTP -> address.socketFactory.createSocket()!!
      else -> Socket(proxy)
    }
    this.rawSocket = rawSocket // 这个就代表着TCP连接

    eventListener.connectStart(call, route.socketAddress, proxy)
    rawSocket.soTimeout = readTimeout
    try {
      // 调用socket的connect方法来打开一个TCP连接。
      Platform.get().connectSocket(rawSocket, route.socketAddress, connectTimeout)
    } catch (e: ConnectException) {
      throw ConnectException("Failed to connect to ${route.socketAddress}").apply {
        initCause(e)
      }
    }
   ...
  }

TLS 连接

在建立 TCP 连接或者创建 Http 代理隧道后,就会开始建立连接协议(establishProtocol)。

  private fun establishProtocol(
    connectionSpecSelector: ConnectionSpecSelector,
    pingIntervalMillis: Int,
    call: Call,
    eventListener: EventListener
  ) {

    // 判断当前地址是否是HTTPS;
    if (route.address.sslSocketFactory == null) {

      // 如果不是HTTPS,判断当前协议是否是明文HTTP2
      if (Protocol.H2_PRIOR_KNOWLEDGE in route.address.protocols) {
        socket = rawSocket
        protocol = Protocol.H2_PRIOR_KNOWLEDGE

       // 开始Http2的握手动作
        startHttp2(pingIntervalMillis)
        return
      }

      // 如果是Http/1.1,则直接return返回;
      socket = rawSocket
      protocol = Protocol.HTTP_1_1
      return
    }

    // 如果是HTTPS,就开始建立TLS安全协议连接了(connectTls);
    eventListener.secureConnectStart(call)
    connectTls(connectionSpecSelector)
    eventListener.secureConnectEnd(call, handshake)

    if (protocol === Protocol.HTTP_2) {
      // 如果是 HTTPS 且为 HTTP2,除了建立TLS连接外,还会调用startHttp2,开始Http2的握手动作。
      startHttp2(pingIntervalMillis)
    }
  }

上述提到了 TLS 的连接(connectTls),那我们就来看一下它的内部实现:

private fun connectTls(connectionSpecSelector: ConnectionSpecSelector) {
    val address = route.address
    val sslSocketFactory = address.sslSocketFactory
    var success = false
    var sslSocket: SSLSocket? = null
    try {
      // Create the wrapper over the connected socket.
      sslSocket = sslSocketFactory!!.createSocket(
          rawSocket, address.url.host, address.url.port, true /* autoClose */) as SSLSocket

      // Configure the socket's ciphers, TLS versions, and extensions.
      val connectionSpec = connectionSpecSelector.configureSecureSocket(sslSocket)
      if (connectionSpec.supportsTlsExtensions) {
        Platform.get().configureTlsExtensions(sslSocket, address.url.host, address.protocols)
      }

      // Force handshake. This can throw!
      sslSocket.startHandshake()
      // block for session establishment
      val sslSocketSession = sslSocket.session
      val unverifiedHandshake = sslSocketSession.handshake()

      // Verify that the socket's certificates are acceptable for the target host.
      if (!address.hostnameVerifier!!.verify(address.url.host, sslSocketSession)) {
        val peerCertificates = unverifiedHandshake.peerCertificates
        if (peerCertificates.isNotEmpty()) {
          val cert = peerCertificates[0] as X509Certificate
          throw SSLPeerUnverifiedException("""
              |Hostname ${address.url.host} not verified:
              |    certificate: ${CertificatePinner.pin(cert)}
              |    DN: ${cert.subjectDN.name}
              |    subjectAltNames: ${OkHostnameVerifier.allSubjectAltNames(cert)}
              """.trimMargin())
        } else {
          throw SSLPeerUnverifiedException(
              "Hostname ${address.url.host} not verified (no certificates)")
        }
      }

      val certificatePinner = address.certificatePinner!!

      handshake = Handshake(unverifiedHandshake.tlsVersion, unverifiedHandshake.cipherSuite,
          unverifiedHandshake.localCertificates) {
        certificatePinner.certificateChainCleaner!!.clean(unverifiedHandshake.peerCertificates,
            address.url.host)
      }

      // Check that the certificate pinner is satisfied by the certificates presented.
      certificatePinner.check(address.url.host) {
        handshake!!.peerCertificates.map { it as X509Certificate }
      }

      // Success! Save the handshake and the ALPN protocol.
      val maybeProtocol = if (connectionSpec.supportsTlsExtensions) {
        Platform.get().getSelectedProtocol(sslSocket)
      } else {
        null
      }
      socket = sslSocket
      source = sslSocket.source().buffer()
      sink = sslSocket.sink().buffer()
      protocol = if (maybeProtocol != null) Protocol.get(maybeProtocol) else Protocol.HTTP_1_1
      success = true
    } finally {
      ...
    }
  }

这段代码很长,具体逻辑总结了以下几点:

1,利用请求地址host,端口以及TCP socket共同创建sslSocket;

2,为Socket 配置加密算法,TLS版本等;

3,调用startHandshake()进行强制握手;

4,验证服务器证书的合法性;

5,利用握手记录进行证书锁定校验(Pinner);

6,连接成功则保存握手记录和ALPN协议。

OKHttp 空闲连接如何清除?

上面说到我们会建立一个 TCP 连接池,但如果没有任务了,空闲的连接也应该及时清除,OKHttp 是如何做到的呢?

  # RealConnectionPool

  private val cleanupQueue: TaskQueue = taskRunner.newQueue()
  private val cleanupTask = object : Task("$okHttpName ConnectionPool") {
    override fun runOnce(): Long = cleanup(System.nanoTime())
  }

  long cleanup(long now) {
    int inUseConnectionCount = 0;//正在使用的连接数
    int idleConnectionCount = 0;//空闲连接数
    RealConnection longestIdleConnection = null;//空闲时间最长的连接
    long longestIdleDurationNs = Long.MIN_VALUE;//最长的空闲时间

    //遍历连接:找到待清理的连接, 找到下一次要清理的时间(还未到最大空闲时间)
    synchronized (this) {
      for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
        RealConnection connection = i.next();

        //若连接正在使用,continue,正在使用连接数+1
        if (pruneAndGetAllocationCount(connection, now) > 0) {
          inUseConnectionCount++;
          continue;
        }
        //空闲连接数+1
        idleConnectionCount++;

        // 赋值最长的空闲时间和对应连接
        long idleDurationNs = now - connection.idleAtNanos;
        if (idleDurationNs > longestIdleDurationNs) {
          longestIdleDurationNs = idleDurationNs;
          longestIdleConnection = connection;
        }
      }
      //若最长的空闲时间大于5分钟 或 空闲数 大于5,就移除并关闭这个连接
      if (longestIdleDurationNs >= this.keepAliveDurationNs
          || idleConnectionCount > this.maxIdleConnections) {
        connections.remove(longestIdleConnection);
      } else if (idleConnectionCount > 0) {
        // else,就返回 还剩多久到达5分钟,然后wait这个时间再来清理
        return keepAliveDurationNs - longestIdleDurationNs;
      } else if (inUseConnectionCount > 0) {
        //连接没有空闲的,就5分钟后再尝试清理.
        return keepAliveDurationNs;
      } else {
        // 没有连接,不清理
        cleanupRunning = false;
        return -1;
      }
    }
    //关闭移除的连接
    closeQuietly(longestIdleConnection.socket());

    //关闭移除后 立刻 进行下一次的 尝试清理
    return 0;
  }

思路还是很清晰的:

1,在将连接加入连接池时就会启动定时任务

2,有空闲连接的话,如果最长的空闲时间大于5分钟或空闲数大于5,就移除关闭这个最长空闲连接;

3,如果空闲数不大于5 且最长的空闲时间不大于5分钟,就返回到5分钟的剩余时间,然后等待这个时间再来清理。

4,没有空闲连接就等5分钟后再尝试清理。

5,没有连接不清理。

OKHttp 有哪些优点?

1,使用简单,在设计时使用了外观模式,将整个系统的复杂性给隐藏起来,将子系统接口通过一个客户端 OkHttpClient 统一暴露出来。

2,扩展性强,可以通过自定义应用拦截器与网络拦截器,完成用户各种自定义的需求。

3,功能强大,支持 Spdy、Http1.X、Http2、以及 WebSocket 等多种协议。

4,通过连接池复用底层TCP(Socket),减少请求延时。

5,无缝的支持 GZIP 减少数据流量。

6,支持数据缓存,减少重复的网络请求。

7,支持请求失败自动重试主机的其他 ip,自动重定向。

OKHttp 框架中用到了哪些设计模式?

1,构建者模式:OkHttpClientRequest 的构建都用到了构建者模式。

2,外观模式: OkHttp 使用了外观模式,将整个系统的复杂性给隐藏起来,将子系统接口通过一个客户端 OkHttpClient 统一暴露出来。

3,责任链模式: OKHttp 的核心就是责任链模式,通过5个默认拦截器构成的责任链完成请求的配置。

4,享元模式:享元模式的核心即池中复用,OKHttp 复用 TCP 连接时用到了连接池,同时在异步请求中也用到了线程池。

Ref:

https://juejin.cn/post/6979729429228421134

https://juejin.cn/post/7020027832977850381

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

推荐阅读更多精彩内容