OkHttp 源碼學(xué)習(xí)筆記(三) 數(shù)據(jù)交換的流 HTTPCodec

在上一篇文章中介紹了okhttp中連接概念以及連接建立和管理,其中在攔截器鏈中的ConnectInterceptor負(fù)責(zé)建立連接,并在該連接上建立流,將其放置在攔截器鏈中,在攔截器鏈中的最后一個(gè)攔截器CallServerInterceptor,通過(guò)使用流的操作完成網(wǎng)絡(luò)請(qǐng)求的數(shù)據(jù)交換。下面從該攔截器開(kāi)始學(xué)習(xí)okhttp時(shí)如果通過(guò)流的操作完成網(wǎng)絡(luò)通信的。

1. 最后一個(gè)攔截器CallServerInterceptor

CallServerInterceptor是okhttp中的最后一個(gè)攔截器,在攔截器鏈中,攔截器的順序是用戶定義的應(yīng)用攔截器,RetryAndFollowUpInterceptor, BridgeInterceptor, CacheInterceptor, ConnectInterceptor, 網(wǎng)絡(luò)攔截器,以及最后的CallServerInterceptor。前面的若干攔截器的作用主要包括兩個(gè)方面,一是在遞歸調(diào)用鏈中對(duì)Request和返回的Response進(jìn)行處理,這主要是針對(duì)用戶自定義的攔截器,完成我們?cè)趹?yīng)用中不同的需求;二是在攔截器鏈中完成一定功能,為最終的網(wǎng)絡(luò)請(qǐng)求提供幫助,這主要是針對(duì)okhttp中定義的攔截器,比如ConnectInterceptor主要作用就是根據(jù)請(qǐng)求建立連接和流對(duì)象,從而幫助最終完成網(wǎng)絡(luò)請(qǐng)求。雖然作用不同,但是這兩類攔截器的代碼結(jié)構(gòu)都有共同的特點(diǎn),即基本包括三個(gè)步驟,從chain對(duì)象中獲取request對(duì)象以及其他對(duì)象,對(duì)請(qǐng)求做處理,然后調(diào)用chain.proceed()方法獲取網(wǎng)絡(luò)請(qǐng)求response, 最后在第三步中對(duì)響應(yīng)做響應(yīng)處理并返回處理之后的Response,當(dāng)然部分?jǐn)r截器可以沒(méi)有第一步和第三步,但是基本結(jié)構(gòu)都是一致的。然而,最后的一個(gè)攔截器CallServerInterceptor,其結(jié)構(gòu)則與上述攔截器的結(jié)構(gòu)有所不同,它的主要功能就是建立好的流對(duì)象,完成數(shù)據(jù)通信。
這里首先簡(jiǎn)單介紹一下流對(duì)象,在okhttp中,流對(duì)象對(duì)應(yīng)著HttpCodec, 它有對(duì)應(yīng)的兩個(gè)子類, Http1Codec和Http2Codec, 分別對(duì)應(yīng)Http1.1協(xié)議以及Http2.0協(xié)議,本文主要學(xué)習(xí)前者。在Http1Codec中主要包括兩個(gè)重要的屬性,即source和sink,它們分別封裝了socket的輸入和輸出,CallServerInterceptor正是利用HttpCodec提供的I/O操作完成網(wǎng)絡(luò)通信。下面來(lái)看CallServerInterceptor的源碼。
對(duì)于攔截器,依然是學(xué)習(xí)它的intercept方法:

 @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();
    //1. 向socket中寫(xiě)入請(qǐng)求header信息
    httpCodec.writeRequestHeaders(request);

    Response.Builder responseBuilder = null;
    if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
      //a. 省略部分
      ...

      if (responseBuilder == null) {
        //2. 向socket中寫(xiě)入請(qǐng)求body信息
        Sink requestBodyOut = httpCodec.createRequestBody(request, request.body().contentLength());
        BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
        request.body().writeTo(bufferedRequestBody);
        bufferedRequestBody.close();
      } else if (!connection.isMultiplexed()) {
        //b.省略部分
      }
    }
    //3. 完成網(wǎng)絡(luò)請(qǐng)求的寫(xiě)入
    httpCodec.finishRequest();

    //4. 讀取網(wǎng)絡(luò)響應(yīng)header信息
    if (responseBuilder == null) {
      responseBuilder = httpCodec.readResponseHeaders(false);
    }

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

    int code = response.code();
    if (forWebSocket && code == 101) {
        //c. 省略部分
    } else {
      //5. 讀取網(wǎng)絡(luò)響應(yīng)的body信息
      response = response.newBuilder()
          .body(httpCodec.openResponseBody(response))
          .build();
    }
    // d. 省略部分 
    return response;
  }

本文對(duì)代碼做部分的省略,突出主要的流程,省略已經(jīng)做了a, b, c, b的標(biāo)注,后面再分別對(duì)其學(xué)習(xí)分析,然后是最主要的流程做了注釋,主要是分成5步,在執(zhí)行網(wǎng)絡(luò)請(qǐng)求之前,首先時(shí)從攔截器鏈中獲取連接,流以及它們二者的管理者streamAllocation,還有原始的網(wǎng)絡(luò)請(qǐng)求request,然后執(zhí)行以下五個(gè)步驟

  • 1 向socket中寫(xiě)入請(qǐng)求header信息
  • 2 向socket中寫(xiě)入請(qǐng)求body信息
  • 3 完成網(wǎng)絡(luò)請(qǐng)求的寫(xiě)入
  • 4 讀取網(wǎng)絡(luò)響應(yīng)header信息
  • 5 讀取網(wǎng)絡(luò)響應(yīng)的body信息
    注意在一次網(wǎng)絡(luò)請(qǐng)求中可能并不包括所有的這五個(gè)步驟,比如第二不,寫(xiě)入請(qǐng)求體,只有請(qǐng)求方法中有請(qǐng)求體的時(shí)候才會(huì)寫(xiě)入,而且有些情況只有在得到服務(wù)器允許的時(shí)候才會(huì)寫(xiě)入請(qǐng)求體,這一點(diǎn)后面會(huì)有提到;另外這里所使用的寫(xiě)入和讀取兩個(gè)詞并不完全準(zhǔn)確,比如第二步只是內(nèi)存與socket的輸出流建立關(guān)系,并沒(méi)有真正寫(xiě)入,直到第三步刷新時(shí)才會(huì)將請(qǐng)求的信息寫(xiě)入到socket的輸出流中,同樣地,第五步中獲取到響應(yīng)的body信息,只是獲取一個(gè)流對(duì)象,只有在應(yīng)用代碼中調(diào)用流對(duì)象的讀方法或者response.string()方法等,才會(huì)從socket的輸入流中讀取信息到應(yīng)用的內(nèi)存中使用。
    這五步邏輯很清晰,也很容易理解,下面中逐個(gè)學(xué)習(xí)省略的四個(gè)部分。首先看省略部分a的代碼:
      // 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();
        responseBuilder = httpCodec.readResponseHeaders(true);
      }

這里時(shí)處理一種特殊情況,即首先發(fā)送詢問(wèn)服務(wù)器是否可以發(fā)送帶有請(qǐng)求體的請(qǐng)求,在該請(qǐng)求中請(qǐng)求的頭部信息中添加Expect:100-continue字段,服務(wù)器如果可以接受請(qǐng)求體則可以返回一個(gè)100的響應(yīng)碼,客戶端繼續(xù)發(fā)送請(qǐng)求,具體的可以參考相關(guān)文章對(duì)100響應(yīng)碼的介紹。
這里我們看到如果請(qǐng)求中有該頭部信息會(huì)跳過(guò)第二步,直接執(zhí)行三四步,獲取響應(yīng)信息,我們繼續(xù)往下看okhttp對(duì)該特殊情況的處理邏輯。這里我們有必要提前看一下HttpCodec的具體實(shí)現(xiàn),當(dāng)然我們這里是分析Http1Codec的readResponseHeaders(boolean)代碼:

@Override public Response.Builder readResponseHeaders(boolean expectContinue) throws IOException {
    ...
    try {
      StatusLine statusLine = StatusLine.parse(source.readUtf8LineStrict());

      Response.Builder responseBuilder = new Response.Builder()
          .protocol(statusLine.protocol)
          .code(statusLine.code)
          .message(statusLine.message)
          .headers(readHeaders());

      if (expectContinue && statusLine.code == HTTP_CONTINUE) {
        return null;
      }
      ...
    } catch (EOFException e) {
      ...
    }
  }

這里因?yàn)闀r(shí)分析特殊情況,所以這里只看參數(shù)為true的情況,從代碼中可以看到獲取響應(yīng)頭部信息包括獲取響應(yīng)行和響應(yīng)頭兩部分,具體代碼可以自行查看,這里不再展開(kāi),當(dāng)服務(wù)器同意接收請(qǐng)求體時(shí)回返回100的響應(yīng)碼,可見(jiàn)此時(shí)該方法返回空,其他情況會(huì)返回非空的響應(yīng)對(duì)象。下面再回到CallServerInterceptor中的代碼
當(dāng)responseBuilder為空時(shí)繼續(xù)執(zhí)行正常邏輯,即從第二步開(kāi)始執(zhí)行。當(dāng)responseBuilder不為空時(shí),就不可以寫(xiě)如請(qǐng)求體信息,下面的else if()語(yǔ)句時(shí)針對(duì)Http2協(xié)議時(shí)關(guān)閉當(dāng)前連接,這里我么暫時(shí)不考慮,Http1.1協(xié)議下,代碼會(huì)跳過(guò)寫(xiě)請(qǐng)求體的步驟,繼續(xù)執(zhí)行,并且因?yàn)閞esponseBuilder不為空也會(huì)跳過(guò)讀取響應(yīng)頭的步驟,因?yàn)橹白x過(guò)一次,但是響應(yīng)碼不是100而已,可見(jiàn)當(dāng)響應(yīng)碼為100時(shí)會(huì)讀取兩次響應(yīng)頭,當(dāng)然也執(zhí)行了兩次請(qǐng)求(注意httpCodec.finishRequest()方法的調(diào)用就是刷新輸出流,也就相當(dāng)于執(zhí)行了一次請(qǐng)求)。
c省略部分是針對(duì)websocket所做的處理,由于對(duì)H5以及websocket不了解,這里就跳過(guò)該部分,有興趣的同學(xué)可以自行查看。
d省略部分就是收尾工作,對(duì)一些特殊情況的處理,下面為代碼:

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

代碼邏輯很簡(jiǎn)單,根據(jù)響應(yīng)信息在必要時(shí)關(guān)閉連接,在上一篇關(guān)于連接的文章中我們介紹了streamAllocation的作用,對(duì)應(yīng)關(guān)閉和回收資源的問(wèn)題沒(méi)有考慮清除,本文會(huì)在介紹完流的概念以后,再具體分析關(guān)于連接的關(guān)閉以及資源回收的問(wèn)題。最后為檢查響應(yīng)碼204和205兩種響應(yīng),這兩種響應(yīng)沒(méi)有響應(yīng)體。
至此我們分析完了CallServerInterceptor的代碼,可以看出由它實(shí)現(xiàn)的網(wǎng)絡(luò)請(qǐng)求,而在完成這一功能的若干步驟中都是依賴HttpCodec提供的功能來(lái)完成的,我們只考慮Http1.1協(xié)議,所以下面開(kāi)始分析Http1Codec的代碼。

2. OkHttp中的流 HttpCodec

在分析以上所提到的五個(gè)步驟之前,需要說(shuō)明在Http1Codec中使用了狀態(tài)模式,其實(shí)就是對(duì)象維護(hù)它所處的狀態(tài),在不同的狀態(tài)下執(zhí)行對(duì)應(yīng)的邏輯,并更新?tīng)顟B(tài),在執(zhí)行邏輯之前通過(guò)檢查對(duì)象的狀態(tài)避免網(wǎng)絡(luò)請(qǐng)求的若干執(zhí)行步驟發(fā)生錯(cuò)亂。首先來(lái)看狀態(tài)的定義:

  private static final int STATE_IDLE = 0; // Idle connections are ready to write request headers.
  private static final int STATE_OPEN_REQUEST_BODY = 1;
  private static final int STATE_WRITING_REQUEST_BODY = 2;
  private static final int STATE_READ_RESPONSE_HEADERS = 3;
  private static final int STATE_OPEN_RESPONSE_BODY = 4;
  private static final int STATE_READING_RESPONSE_BODY = 5;
  private static final int STATE_CLOSED = 6;

下面是Http1Codec的屬性域:

  /** The client that configures this stream. May be null for HTTPS proxy tunnels. */
  final OkHttpClient client;
  /** The stream allocation that owns this stream. May be null for HTTPS proxy tunnels. */
  final StreamAllocation streamAllocation;

  final BufferedSource source;
  final BufferedSink sink;
  int state = STATE_IDLE;

這些屬性域很容易理解,首先持有client,可以使用它所提供的功能,通常是獲取一些用戶所設(shè)置的屬性,其次是streamAllocation,它是連接與流的橋梁,所以很容易理解需要它獲取關(guān)于連接的功能。然后就是該流對(duì)象封裝的輸出流和輸入流,兩個(gè)流內(nèi)部封裝的自然就是socket的了。最后就是對(duì)象所處的狀態(tài)。介紹完屬性域以后我們就可以分步驟分析Http1Codec提供的功能了,在這些步驟中,邏輯很明確,首先檢查狀態(tài),然后執(zhí)行邏輯,最后更新?tīng)顟B(tài),當(dāng)然執(zhí)行邏輯和更新?tīng)顟B(tài)是可以交換的,不會(huì)造成影響,這步驟分析中我們不再考慮狀態(tài)的問(wèn)題重點(diǎn)只是關(guān)系邏輯的執(zhí)行。
-1. 首先第一步,寫(xiě)請(qǐng)求頭:

  @Override public void writeRequestHeaders(Request request) throws IOException {
    String requestLine = RequestLine.get(
        request, streamAllocation.connection().route().proxy().type());
    writeRequest(request.headers(), requestLine);
  }

  public void writeRequest(Headers headers, String requestLine) throws IOException {
    if (state != STATE_IDLE) throw new IllegalStateException("state: " + state);
    sink.writeUtf8(requestLine).writeUtf8("\r\n");
    for (int i = 0, size = headers.size(); i < size; i++) {
      sink.writeUtf8(headers.name(i))
          .writeUtf8(": ")
          .writeUtf8(headers.value(i))
          .writeUtf8("\r\n");
    }
    sink.writeUtf8("\r\n");
    state = STATE_OPEN_REQUEST_BODY;
  }

執(zhí)行邏輯很清晰,可以分成兩部分,對(duì)應(yīng)Http協(xié)議,即寫(xiě)入請(qǐng)求行和請(qǐng)求頭,至于請(qǐng)求行的獲取有興趣的同學(xué)可以自行查看源碼。
-2. 接著,第二步,寫(xiě)請(qǐng)求體,這一步中Http1Codec提供一個(gè)包裝了sink的輸出流,也是一個(gè)Sink, 這里我們看是如何封裝sink的:

 @Override public Sink createRequestBody(Request request, long contentLength) {
    if ("chunked".equalsIgnoreCase(request.header("Transfer-Encoding"))) {
      // Stream a request body of unknown length.
      return newChunkedSink();
    }

    if (contentLength != -1) {
      // Stream a request body of a known length.
      return newFixedLengthSink(contentLength);
    }

    throw new IllegalStateException(
        "Cannot stream a request body without chunked encoding or a known content length!");
  }

屬性Http協(xié)議的同學(xué)都知道其實(shí)請(qǐng)求體和響應(yīng)體可以分成固定長(zhǎng)度和非固定長(zhǎng)度兩種,其中非固定長(zhǎng)度由頭部信息中Transfer-Encoding=chunked來(lái)標(biāo)識(shí),固定長(zhǎng)度則有對(duì)應(yīng)的頭部信息標(biāo)識(shí)實(shí)體信息的對(duì)應(yīng)長(zhǎng)度。這里我們以非固定長(zhǎng)度為例分析Http1Codec是如何封裝sink的,對(duì)于固定長(zhǎng)度的也是類似的邏輯。

  public Sink newChunkedSink() {
    if (state != STATE_OPEN_REQUEST_BODY) throw new IllegalStateException("state: " + state);
    state = STATE_WRITING_REQUEST_BODY;
    return new ChunkedSink();
  }

   private final class ChunkedSink implements Sink {
    ...

    @Override public void write(Buffer source, long byteCount) throws IOException {
      if (closed) throw new IllegalStateException("closed");
      if (byteCount == 0) return;

      sink.writeHexadecimalUnsignedLong(byteCount);
      sink.writeUtf8("\r\n");
      sink.write(source, byteCount);
      sink.writeUtf8("\r\n");
    }

    @Override public synchronized void flush() throws IOException {
      if (closed) return; // Don't throw; this stream might have been closed on the caller's behalf.
      sink.flush();
    }

    @Override public synchronized void close() throws IOException {
      if (closed) return;
      closed = true;
      sink.writeUtf8("0\r\n\r\n");
      detachTimeout(timeout);
      state = STATE_READ_RESPONSE_HEADERS;
    }
  }

這里使用一個(gè)內(nèi)部類來(lái)封裝sink, 這里我們只看其中的三個(gè)重要的方法,即write() flush() close()方法,邏輯都很清晰,非固定長(zhǎng)度的請(qǐng)求體,都是在第一行寫(xiě)入一段數(shù)據(jù)的長(zhǎng)度,然后在之后寫(xiě)入該段數(shù)據(jù),從write()方法中可以看出是講buffer中的數(shù)據(jù)寫(xiě)入到sink對(duì)象中,如果熟悉okio的執(zhí)行邏輯,對(duì)此應(yīng)該很容易理解。然后刷新和關(guān)閉邏輯則很簡(jiǎn)單,其中關(guān)閉時(shí)注意更新?tīng)顟B(tài)。
對(duì)于固定長(zhǎng)度的請(qǐng)求體,其封裝sink的邏輯是類似的,其中需要傳入一個(gè)RemainingLength, 保證寫(xiě)數(shù)據(jù)結(jié)束時(shí)保證數(shù)據(jù)長(zhǎng)度是正確的即可,有興趣的可以查看代碼。
-3. 第三步是完成請(qǐng)求的寫(xiě)入,其實(shí)這一步其實(shí)很簡(jiǎn)單,只有一行代碼,就是執(zhí)行流的刷新:

  @Override public void finishRequest() throws IOException {
    sink.flush();
  }

注意這一步是不需要檢查狀態(tài)的,因?yàn)榇藭r(shí)的狀態(tài)有可能是STATE_OPEN_REQUEST_BODY(沒(méi)有請(qǐng)求體的情況)或者STATE_READ_RESPONSE_HEADERS(已經(jīng)完成請(qǐng)求體寫(xiě)入的情況)。這一步只是刷新流,所以什么情況下都不會(huì)造成影響,所以沒(méi)有必要檢查狀態(tài),也沒(méi)有更新?tīng)顟B(tài),保持之前的狀態(tài)即可。
-4. 第四步讀取請(qǐng)求頭,這一步的代碼我們?cè)谇懊媸且?jiàn)到過(guò)的,這里再次貼出,方便查閱,并且沒(méi)有省略:

@Override public Response.Builder readResponseHeaders(boolean expectContinue) throws IOException {
    if (state != STATE_OPEN_REQUEST_BODY && state != STATE_READ_RESPONSE_HEADERS) {
      throw new IllegalStateException("state: " + state);
    }
    try {
      StatusLine statusLine = StatusLine.parse(source.readUtf8LineStrict());
      Response.Builder responseBuilder = new Response.Builder()
          .protocol(statusLine.protocol)
          .code(statusLine.code)
          .message(statusLine.message)
          .headers(readHeaders());

      if (expectContinue && statusLine.code == HTTP_CONTINUE) {
        return null;
      }

      state = STATE_OPEN_RESPONSE_BODY;
      return responseBuilder;
    } catch (EOFException e) {
      // Provide more context if the server ends the stream before sending a response.
      IOException exception = new IOException("unexpected end of stream on " + streamAllocation);
      exception.initCause(e);
      throw exception;
    }
  }

可以此時(shí)所處的狀態(tài)有可能為STATE_OPEN_REQUEST_BODY和STATE_READ_RESPONSE_HEADERS兩種,然后讀取請(qǐng)求行和請(qǐng)求頭部信息,并返回響應(yīng)的Builder。
-5. 第五步為獲取響應(yīng)體:

  @Override public ResponseBody openResponseBody(Response response) throws IOException {
    Source source = getTransferStream(response);
    return new RealResponseBody(response.headers(), Okio.buffer(source));
  }

在之前的介紹中,我們知道響應(yīng)Response對(duì)象中是封裝一個(gè)source對(duì)象,用于讀取響應(yīng)數(shù)據(jù)。所以ResponseBody的構(gòu)建就是需要響應(yīng)頭和響應(yīng)體兩部分即可,響應(yīng)頭在上一部分中已經(jīng)添加到response對(duì)象中了,headers()獲取響應(yīng)頭即可。下面分析,如何封裝source對(duì)象,獲取一個(gè)對(duì)應(yīng)的source對(duì)象,可能有些拗口,如果你熟悉裝飾模式,以及okio的結(jié)構(gòu)應(yīng)該很容易明白。下面看getTransferStream的代碼:

private Source getTransferStream(Response response) throws IOException {
    if (!HttpHeaders.hasBody(response)) {
      return newFixedLengthSource(0);
    }

    if ("chunked".equalsIgnoreCase(response.header("Transfer-Encoding"))) {
      return newChunkedSource(response.request().url());
    }

    long contentLength = HttpHeaders.contentLength(response);
    if (contentLength != -1) {
      return newFixedLengthSource(contentLength);
    }

    // Wrap the input stream from the connection (rather than just returning
    // "socketIn" directly here), so that we can control its use after the
    // reference escapes.
    return newUnknownLengthSource();
  }

這里和寫(xiě)入請(qǐng)求體的地方十分類似,響應(yīng)體也是分為固定長(zhǎng)度和非固定長(zhǎng)度兩種,除此以外,為了代碼的健壯性okhttp還定義了UnknownLengthSource,這里我們不對(duì)該意外情況分析,下面我們以固定長(zhǎng)度為例分析source的封裝。

  private class FixedLengthSource extends AbstractSource {
    private long bytesRemaining;

    public FixedLengthSource(long length) throws IOException {
      bytesRemaining = length;
      if (bytesRemaining == 0) {
        endOfInput(true);
      }
    }

    @Override public long read(Buffer sink, long byteCount) throws IOException {
      if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
      if (closed) throw new IllegalStateException("closed");
      if (bytesRemaining == 0) return -1;

      long read = source.read(sink, Math.min(bytesRemaining, byteCount));
      if (read == -1) {
        endOfInput(false); // The server didn't supply the promised content length.
        throw new ProtocolException("unexpected end of stream");
      }

      bytesRemaining -= read;
      if (bytesRemaining == 0) {
        endOfInput(true);
      }
      return read;
    }

    @Override public void close() throws IOException {
      if (closed) return;

      if (bytesRemaining != 0 && !Util.discard(this, DISCARD_STREAM_TIMEOUT_MILLIS, MILLISECONDS)) {
        endOfInput(false);
      }

      closed = true;
    }
  }

這里可以看到有一個(gè)成員變量bytesRemaining標(biāo)識(shí)剩余的字節(jié)數(shù),保證讀取到的字節(jié)長(zhǎng)度與頭部信息中的長(zhǎng)度保證一致。read()中的代碼可以看到就是將該source對(duì)象的數(shù)據(jù)讀取到封裝的source中,用于構(gòu)建ResponseBody。
這里需要注意在本篇文章中,我們不斷地提到sink,source對(duì)象以及封裝的sink和source對(duì)象,這里解釋一下,前面兩個(gè)代表http1Codec對(duì)象中的sink和source對(duì)象,即封裝了socket的輸出和輸入流,而封裝的sink和source對(duì)象則是構(gòu)建的固定長(zhǎng)度和非固定長(zhǎng)度的輸出輸入流,其實(shí)它們只是對(duì)http1Codec成員變量中的sink和source的一種封裝,其實(shí)就是裝飾模式,封裝以后的sink和source對(duì)象可以用于在外部寫(xiě)請(qǐng)求體和構(gòu)建ResponseBody。
這里代碼邏輯很清晰,不再詳細(xì)介紹,相信通過(guò)閱讀代碼都可以明白,不過(guò)這里需要提及一下endOfInput()方法:

protected final void endOfInput(boolean reuseConnection) throws IOException {
      if (state == STATE_CLOSED) return;
      if (state != STATE_READING_RESPONSE_BODY) throw new IllegalStateException("state: " + state);

      detachTimeout(timeout);

      state = STATE_CLOSED;
      if (streamAllocation != null) {
        streamAllocation.streamFinished(!reuseConnection, Http1Codec.this);
      }
    }

這里的執(zhí)行邏輯也不多,除去檢查狀態(tài)和更新?tīng)顟B(tài)之外,就是接觸超時(shí)機(jī)制,最后需要注意就是調(diào)用streamAllocation的streamFinished()方法,該方法的參數(shù)包括連接是否可以繼續(xù)使用,以及流對(duì)象本身,該方法用于連接和流的清理以及資源回收工作。在上一篇介紹連接的文章中,由于對(duì)流不熟悉,所以對(duì)這一部分介紹的不清楚,下一小節(jié)中則對(duì)該部分內(nèi)容整體學(xué)習(xí)一下。
至此再去看Http1Codec的代碼,基本上已經(jīng)全部覆蓋,沒(méi)有分析到的其實(shí)就剩下幾個(gè)關(guān)于Sink和Source的內(nèi)部類,都分為固定長(zhǎng)度和非固定長(zhǎng)度,有興趣的可自行查看源碼。除此以外還有一個(gè)cancel()方法,如下:

  @Override public void cancel() {
    RealConnection connection = streamAllocation.connection();
    if (connection != null) connection.cancel();
  }

這里可以看出就是調(diào)用該流對(duì)應(yīng)的cancel方法,關(guān)于這一點(diǎn)在下一小節(jié)中統(tǒng)一分析講述。

3. Okhttp中連接與流的資源回收與清理

經(jīng)過(guò)前面的學(xué)習(xí),我們知道在okhttp中關(guān)于連接和流,有三個(gè)重要的類,即RealConnection, StreamAllocation和Http1Codec,這里我們只是分析Http1.1協(xié)議,所以這三個(gè)類是一一對(duì)應(yīng)的,即在一個(gè)連接上分配一個(gè)流對(duì)象,通過(guò)該流對(duì)象完成最終的網(wǎng)絡(luò)請(qǐng)求。而完成一次網(wǎng)絡(luò)請(qǐng)求可能會(huì)有成功和失敗兩種可能,同時(shí)也還會(huì)有在中途中斷的可能,針對(duì)以上情況,當(dāng)我們建立連接獲取到socket對(duì)象,并在該連接之上建立請(qǐng)求之后,無(wú)論那一種情況發(fā)生,都需要資源的回收和清理工作,而StreamAllocation作為連接和流的橋梁,自然承擔(dān)該項(xiàng)工作的主要責(zé)任。
我們知道一次請(qǐng)求包括成功,失敗和中斷等幾種情況,在StreamAllocation中對(duì)資源回收和清理時(shí),最終都會(huì)調(diào)用deallocate()方法,這里我們首先看該方法,然后自底向上分析:

private Socket deallocate(boolean noNewStreams, boolean released, boolean streamFinished) {
    assert (Thread.holdsLock(connectionPool));

    //1. 修改屬性,置狀態(tài)
    if (streamFinished) {
      this.codec = null;
    }
    if (released) {
      this.released = true;
    }
    Socket socket = null;
    if (connection != null) {
      if (noNewStreams) {
        connection.noNewStreams = true;
      }
      
      if (this.codec == null && (this.released || connection.noNewStreams)) {
        //2. 清理該StreamAllocation對(duì)象在Connection中的引用
        release(connection);
        //3. 執(zhí)行清理邏輯
        if (connection.allocations.isEmpty()) {
          connection.idleAtNanos = System.nanoTime();
          if (Internal.instance.connectionBecameIdle(connectionPool, connection)) {
            socket = connection.socket();
          }
        }
        connection = null;
      }
    }
    //4. 返回需要關(guān)閉的socket對(duì)象
    return socket;
  }

這里首先介紹三個(gè)參數(shù)的含義,noNewStreams代表該對(duì)象對(duì)應(yīng)的連接對(duì)象不再可用了,released表示可以將該streamAllocation對(duì)象置為可以釋放的狀態(tài)了,streamFinished表示該對(duì)象對(duì)應(yīng)的流已經(jīng)完成了任務(wù),或成功或失敗,就是可以將codec置為空了。
下面開(kāi)始分析它的邏輯,分成四步,已在注釋中說(shuō)明。首先是根據(jù)三個(gè)參數(shù)分別置不同的屬性狀態(tài),其中released是代表streamAllocation的狀態(tài),noNewStreams代表連接對(duì)象的狀態(tài)。然后是根據(jù)不同的狀態(tài)執(zhí)行清理工作,這里需要注意在判斷條件時(shí)是使用的成員變量而不是參數(shù),也就是各對(duì)象目前所處的狀態(tài),如果流對(duì)象被置空(說(shuō)明完成網(wǎng)絡(luò)請(qǐng)求或者請(qǐng)求失敗),并且該對(duì)象處于可以釋放的狀態(tài)或者連接不可用了(可能是因?yàn)橹袛鄷r(shí)設(shè)置的狀態(tài),流成功或失敗結(jié)束時(shí)也會(huì)設(shè)置狀態(tài),具體后面分析),此時(shí)就將連接置為空閑狀態(tài),并在必要時(shí)返回可以關(guān)閉的socket對(duì)象(這里所說(shuō)的必要時(shí),是有ConnectionPool對(duì)連接維護(hù)的邏輯決定的,可以參考上一篇對(duì)連接描述的文章)。在熟悉了這個(gè)流程以后,我們就可以分成四種情況(即成功,失敗,中斷,取消),分別分析它們對(duì)應(yīng)的過(guò)程。至于第三步清理引用,即release()方法,可以自行查看代碼,較為簡(jiǎn)單,其實(shí)就是將自己從connection中的streamAllocation列表中移除。

1. 成功的網(wǎng)絡(luò)請(qǐng)求

在上一小節(jié)的最后我們介紹了endOfInput()方法,即在讀取響應(yīng)體結(jié)束以后調(diào)用,此時(shí)是完成了網(wǎng)絡(luò)請(qǐng)求,執(zhí)行清理工作,此時(shí)調(diào)用了StreamAllocation的finishStreamed()方法,代碼如下:

  public void streamFinished(boolean noNewStreams, HttpCodec codec) {
    Socket socket;
    synchronized (connectionPool) {
      if (codec == null || codec != this.codec) {
        throw new IllegalStateException("expected " + this.codec + " but was " + codec);
      }
      if (!noNewStreams) {
        connection.successCount++;
      }
      socket = deallocate(noNewStreams, false, true);
    }
    closeQuietly(socket);
  }

這里的noNewStreams依然標(biāo)識(shí)對(duì)應(yīng)的該連接是否可以繼續(xù)使用,然后調(diào)用上面的deallocate()方法,表示網(wǎng)絡(luò)請(qǐng)求過(guò)程已經(jīng)完成,會(huì)置空codec對(duì)象,但是并沒(méi)有設(shè)置released狀態(tài),如果連接依然可用,并且此時(shí)streamAllocation并不是released的狀態(tài),此時(shí)并不會(huì)將連接置為空閑狀態(tài),還可以在該連接上繼續(xù)分配新的流對(duì)象,完成新的網(wǎng)絡(luò)請(qǐng)求,只不過(guò)請(qǐng)求的地址需要是完全相同的。如果是讀取響應(yīng)實(shí)體時(shí)發(fā)生的錯(cuò)誤,此時(shí)endOfInput方法傳入false,也就是對(duì)應(yīng)noNewStreams參數(shù),此時(shí)就會(huì)將連接置為空閑狀態(tài),并在必要時(shí)關(guān)閉socket(這里將請(qǐng)求成功,但是讀取失敗歸到了成功完成網(wǎng)絡(luò)請(qǐng)求的類別中)。除此以外,網(wǎng)絡(luò)請(qǐng)求成功時(shí),還有可能在響應(yīng)頭部信息中connection設(shè)置為close屬性,此時(shí)需要關(guān)閉連接,此時(shí)也是調(diào)用的該方法,noNewStreams參數(shù)也為true,在CallServerInterceptor分析的最后提到過(guò)該邏輯。

2. 失敗的網(wǎng)絡(luò)請(qǐng)求

在網(wǎng)絡(luò)請(qǐng)求失敗后,即在RetryAndFollowUpInterceptor中會(huì)調(diào)用StreamAllocation的streamFailed()方法,該方法的代碼如下:

  public void streamFailed(IOException e) {
    Socket socket;
    boolean noNewStreams = false;

    synchronized (connectionPool) {
      if (e instanceof StreamResetException) {
        ...
      } else if (connection != null
          && (!connection.isMultiplexed() || e instanceof ConnectionShutdownException)) {
        noNewStreams = true;

        // If this route hasn't completed a call, avoid it for new connections.
        if (connection.successCount == 0) {
          if (route != null && e != null) {
            routeSelector.connectFailed(route, e);
          }
          route = null;
        }
      }
      socket = deallocate(noNewStreams, false, true);
    }

    closeQuietly(socket);
  }

在RetryAndFollowUpInterceptor中調(diào)用該方法時(shí),參數(shù)為null, 所以這里我們先看參數(shù)為null的情況,此時(shí)連接不可用,所以noNewStreams參數(shù)為true,這里還有一個(gè)邏輯就是此時(shí)檢查一下該連接的成功次數(shù),這個(gè)值在完成一次網(wǎng)絡(luò)請(qǐng)求時(shí)并且該鏈接不關(guān)閉時(shí)會(huì)加一,如果這個(gè)連接從來(lái)都沒(méi)有成功過(guò),那么就要將它加入到黑名單中,這個(gè)黑名單在ConnectionPool的維護(hù)連接的邏輯中會(huì)用到,具體可以參考上一篇文章。接著調(diào)用deallocation()方法,并在必要時(shí)關(guān)閉socket。對(duì)于該方法的參數(shù)不為空時(shí),并且異常是StreamResetException的子類時(shí),這里暫且不分析,不明白其中的原理,也沒(méi)有找到在哪里調(diào)用的該方法,有清楚的可以在評(píng)論中告知,不勝感激。

3. 中斷的網(wǎng)絡(luò)請(qǐng)求

在RetryAndFollowUpInterceptor中,網(wǎng)絡(luò)請(qǐng)求不斷重試或重定向,如果不可重試或重定向,此時(shí)都需要將StreamAllocation置為可以釋放的狀態(tài),此時(shí)會(huì)調(diào)用StreamAllocation的方法release()方法,該方法的代碼如下:

public void release() {
    Socket socket;
    synchronized (connectionPool) {
      socket = deallocate(false, true, false);
    }
    closeQuietly(socket);
  }

可以看到該方法很簡(jiǎn)單,只是調(diào)用deallocate方法,release為true,它設(shè)置StreamAllocation為可釋放狀態(tài),如果沒(méi)有流對(duì)象或者連接不可用時(shí),就開(kāi)始回收資源,必要時(shí)關(guān)閉socket。

4. 取消的網(wǎng)絡(luò)請(qǐng)求

我們都知道在okhttp中,對(duì)網(wǎng)絡(luò)請(qǐng)求的封裝是使用的Callu對(duì)象,該對(duì)象有cancel()方法,也就是可以在任意時(shí)候取消一次網(wǎng)絡(luò)請(qǐng)求,下面我們就從Call的cancel()方法開(kāi)始學(xué)習(xí)如何取消一次網(wǎng)絡(luò)請(qǐng)求
Call.cancel():

 @Override public void cancel() {
    retryAndFollowUpInterceptor.cancel();
  }

可見(jiàn)是調(diào)用了RetryAndFollowUpInterceptor的cancel()方法,該方法的代碼為:

  public void cancel() {
    canceled = true;
    StreamAllocation streamAllocation = this.streamAllocation;
    if (streamAllocation != null) streamAllocation.cancel();
  }

修改成員變量,并調(diào)用StreamAllocation()的cancel()方法(關(guān)于這里的賦值,使用局部變量的意圖還不太明白),這里終于調(diào)用到了StreamAllocationd 取消方法,下面看其cancel()方法:

public void cancel() {
    HttpCodec codecToCancel;
    RealConnection connectionToCancel;
    synchronized (connectionPool) {
      canceled = true;
      codecToCancel = codec;
      connectionToCancel = connection;
    }
    if (codecToCancel != null) {
      codecToCancel.cancel();
    } else if (connectionToCancel != null) {
      connectionToCancel.cancel();
    }
  }

邏輯可以總結(jié)為如果有可以取消的流則取消流,沒(méi)有則取消連接,其實(shí)我們?cè)谇懊娼榻BHttp1Codec的cancel()方法中也看到了它也是調(diào)用的對(duì)應(yīng)的連接對(duì)象的cancel()方法,下面我們來(lái)看connection的cancel()方法:

  public void cancel() {
    // Close the raw socket so we don't end up doing synchronous I/O.
    closeQuietly(rawSocket);
  }

關(guān)閉原始的socket對(duì)象,簡(jiǎn)單直接。關(guān)閉socket對(duì)象以后,所有的I/O邏輯都會(huì)拋出一場(chǎng),因此取消了一次網(wǎng)絡(luò)請(qǐng)求,此時(shí)需要手動(dòng)關(guān)閉一些建立的流對(duì)象。

到這里我們就分析完了StreamAllocation在回收和清理資源方面所做的工作。總結(jié)起來(lái)有四個(gè)可以調(diào)用的方法,其中前三個(gè)最后對(duì)會(huì)調(diào)用deallocate方法去處理連接和流對(duì)象,而cancel()方法則是直接關(guān)閉了socket對(duì)象。如果在這里還不太明白,比較難理解的是中間的兩個(gè),其實(shí)可以結(jié)合RetryAndFollowUpIntercepter理解,中間的兩個(gè)方法中在重試攔截器中被多次調(diào)用,這與具體的網(wǎng)絡(luò)請(qǐng)求邏輯相關(guān),關(guān)于RetryAndFollowUpInterceptor在前一篇文章中分析較少,因此下一小節(jié)會(huì)對(duì)其做一次全面的分析學(xué)習(xí),從而也可以更好地輔助理解StreamAllocation對(duì)清理和回收所做的工作。

4. 再談RetryAndFollowUpInterceptor

對(duì)于攔截器, 我們還是看其intercept()方法:

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

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

    int followUpCount = 0;
    Response priorResponse = null;
    while (true) {
      //1. 判斷是否為取消狀態(tài)
      if (canceled) {
        streamAllocation.release();
        throw new IOException("Canceled");
      }

      Response response = null;
      boolean releaseConnection = true;
      try {
        response = ((RealInterceptorChain) chain).proceed(request, streamAllocation, null, null);
        releaseConnection = false;
      } catch (RouteException e) {
        //2. 出現(xiàn)異常狀況
        // The attempt to connect via a route failed. The request will not have been sent.
        if (!recover(e.getLastConnectException(), false, request)) {
          throw e.getLastConnectException();
        }
        releaseConnection = false;
        continue;
      } catch (IOException e) {
        // An attempt to communicate with a server failed. The request may have been sent.
        boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
        if (!recover(e, requestSendStarted, request)) throw e;
        releaseConnection = false;
        continue;
      } finally {
        //3. 最后判斷是否需要執(zhí)行清理
        // We're throwing an unchecked exception. Release any resources.
        if (releaseConnection) {
          streamAllocation.streamFailed(null);
          streamAllocation.release();
        }
      }
      ...
    }
  }

這里由于該方法較長(zhǎng),我將他們分成了兩個(gè)部分,前半部分是針對(duì)重試請(qǐng)求的處理,后半部分是針對(duì)重定向請(qǐng)求的處理。我們還是首先看該方法的前半部分,這里我將前半部分劃分成了三步,首先是判斷該請(qǐng)求是否被取消了,如果被取消了,此時(shí)有兩種可能,一是還沒(méi)有流對(duì)象,此時(shí)調(diào)用release()方法,置為可釋放狀態(tài),終止網(wǎng)絡(luò)請(qǐng)求,并將在條件滿足時(shí)將連接設(shè)置為空閑狀態(tài)。第二種可能是已經(jīng)有流對(duì)象,此次網(wǎng)絡(luò)請(qǐng)求就沒(méi)有辦法終止,此時(shí)只是設(shè)置可釋放狀態(tài),等網(wǎng)絡(luò)請(qǐng)求結(jié)束以后將流對(duì)象置空,此時(shí)可以立刻在條件滿足時(shí)將連接置為空閑狀態(tài)。
如果沒(méi)有取消,則開(kāi)始執(zhí)行網(wǎng)絡(luò)請(qǐng)求,這里也分成兩種可能,一是網(wǎng)絡(luò)請(qǐng)求出現(xiàn)意外狀態(tài),即拋出異常,此時(shí)需要判斷是否可以重試,判斷條件可以自行查看,這里不詳細(xì)說(shuō)明,但是在判斷方法中,調(diào)用了streamAllocation的streamFailed()方法,并傳遞了Exception。如果可以重試,此時(shí)的releaseConnection為false, finnally中不執(zhí)行任何邏輯,此時(shí)會(huì)執(zhí)行下一次循環(huán),如果不可以重試,則執(zhí)行第三步,及finnally中的邏輯,即調(diào)用StreamAllocation的失敗方法,此時(shí)會(huì)將流對(duì)象置空,同時(shí)調(diào)用release()方法,由于codec已經(jīng)為空,release()方法調(diào)用完以后則會(huì)在條件滿足時(shí)將connection置為空閑狀態(tài),然后由于拋出異常,此時(shí)會(huì)跳出while循環(huán)。
上面說(shuō)如果沒(méi)有取消,還會(huì)有另外一種情況,即正確完成網(wǎng)絡(luò)請(qǐng)求,此時(shí)處理重定向情況,直接去執(zhí)行該方法的第二部分,下面是第二部分代碼,注意這里都是在while()循環(huán)之中:

...
      Request followUp = followUpRequest(response);

      //沒(méi)有重定向或重試情況
      if (followUp == null) {
        if (!forWebSocket) {
          streamAllocation.release();
        }
        return response;
      }

      //有重定向情況
      closeQuietly(response.body());

      //兩種不可以重定向的情況
      if (++followUpCount > MAX_FOLLOW_UPS) {
        streamAllocation.release();
        throw new ProtocolException("Too many follow-up requests: " + followUpCount);
      }

      if (followUp.body() instanceof UnrepeatableRequestBody) {
        streamAllocation.release();
        throw new HttpRetryException("Cannot retry streamed HTTP body", response.code());
      }

      //對(duì)于不同的連接,釋放原來(lái)的streamAllocation, 建立新的streamAllocation
      if (!sameConnection(response, followUp.url())) {
        streamAllocation.release();
        streamAllocation = new StreamAllocation(
            client.connectionPool(), createAddress(followUp.url()), callStackTrace);
      } else if (streamAllocation.codec() != null) {
        throw new IllegalStateException("Closing the body of " + response
            + " didn't close its backing stream. Bad interceptor?");
      }

      //設(shè)置屬性,開(kāi)始重試,執(zhí)行下一次循環(huán)
      request = followUp;
      priorResponse = response;
    }
  }

首先獲取重定向請(qǐng)求對(duì)象,對(duì)于不需要重定向的請(qǐng)求,直接返回相應(yīng)對(duì)象,結(jié)束循環(huán)。如果需要重定向則需要關(guān)閉上一個(gè)響應(yīng)對(duì)象的響應(yīng)體,此時(shí)調(diào)用body的close()方法,最終對(duì)調(diào)用streamAllocation的streamFinished()方法,將其上的codec對(duì)象置空。然后判斷兩種不可以重定向的情況,拋出異常結(jié)束循環(huán)。如果可以執(zhí)行重定向的話,對(duì)于不同連接則需要釋放原來(lái)的streamAllocation,并建立新的streamAllocation,而對(duì)于相同連接直接使用即可,即在相同連接上建立新的流,這里判斷了原來(lái)的codec對(duì)象一定置為空了。最后設(shè)置新的請(qǐng)求對(duì)象,再次執(zhí)行循環(huán),完成重定向的請(qǐng)求。

至此較為完整地分析了RetryAndFollowUpInterceptor的代碼,這里需要注意重試和重定向沒(méi)有任何關(guān)系,這里只是寫(xiě)到一個(gè)攔截器中,放在一個(gè)無(wú)限循環(huán)中而已。通過(guò)這一段代碼的分析也可以更好地了解到StreamAllocation在資源管理中,回收和清理工作的幾個(gè)函數(shù)的作用。不過(guò)由于水平有限,對(duì)于recover()函數(shù),以及拋出的各種異常類型沒(méi)有深入分析,StreamAllocation的streamFailed()方法也沒(méi)有分析透徹,有了解的歡迎一起探討。不過(guò)這里已經(jīng)不影響對(duì)整體流程的分析。

5. 總結(jié)

本篇文章主要是介紹了Okhttp流的概念,以及CallServerInterceptor是如果使用HttpCodec提供的功能完成網(wǎng)絡(luò)請(qǐng)求的數(shù)據(jù)通信。在介紹完流的概念以后,結(jié)合之前的連接,重新對(duì)StreamAllocation中資源的回收和清理做了學(xué)習(xí)和分析,主要介紹了四個(gè)可以調(diào)用的方法,分別處理不同的情況,最后重新介紹了RetryAndFollowUpInterceptor的執(zhí)行邏輯,以及在執(zhí)行重試或重定向時(shí)是如何調(diào)用StreamAllocation的清理方法的。
通過(guò)三篇文章,我們已經(jīng)大概分析了okhttp中一次網(wǎng)絡(luò)請(qǐng)求的大致過(guò)程。從Call對(duì)象對(duì)請(qǐng)求的封裝,到使用dispatcher對(duì)請(qǐng)求的分發(fā),再到執(zhí)行請(qǐng)求時(shí)調(diào)用getResponseWithInterceptors()方法獲取請(qǐng)求,最后說(shuō)明這個(gè)攔截器鏈的遞歸調(diào)用結(jié)構(gòu)。在這個(gè)攔截器鏈中,RetryAndFollowUpInterceptor負(fù)責(zé)重試和重定向,ConnectionInterceptor負(fù)責(zé)建立連接和流對(duì)象,CallServerInterceptor負(fù)責(zé)完成最終的網(wǎng)絡(luò)請(qǐng)求,以上就是幾乎整個(gè)的網(wǎng)絡(luò)請(qǐng)求過(guò)程。在攔截器鏈中還有兩個(gè)攔截器沒(méi)有介紹,其中較為簡(jiǎn)單的是BridgeInterceptor,它主要負(fù)責(zé)okhttp中請(qǐng)求和響應(yīng)對(duì)象與實(shí)際Http協(xié)議中定義的請(qǐng)求和響應(yīng)之間的轉(zhuǎn)換,以及處理cookie相關(guān)的內(nèi)容,另一個(gè)是個(gè)CacheInterceptor,顧名思義也就是用來(lái)處理緩存的攔截器,接下來(lái)將會(huì)分成兩篇文章分別介紹它們的功能。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,546評(píng)論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,570評(píng)論 3 418
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 176,505評(píng)論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 63,017評(píng)論 1 313
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,786評(píng)論 6 410
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 55,219評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,287評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,438評(píng)論 0 288
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,971評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,796評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,995評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,540評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,230評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,662評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 35,918評(píng)論 1 286
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,697評(píng)論 3 392
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,991評(píng)論 2 374

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

  • 國(guó)家電網(wǎng)公司企業(yè)標(biāo)準(zhǔn)(Q/GDW)- 面向?qū)ο蟮挠秒娦畔?shù)據(jù)交換協(xié)議 - 報(bào)批稿:20170802 前言: 排版 ...
    庭說(shuō)閱讀 11,056評(píng)論 6 13
  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,785評(píng)論 18 139
  • 關(guān)于okhttp是一款優(yōu)秀的網(wǎng)絡(luò)請(qǐng)求框架,關(guān)于它的源碼分析文章有很多,這里分享我在學(xué)習(xí)過(guò)程中讀到的感覺(jué)比較好的文章...
    蕉下孤客閱讀 3,613評(píng)論 2 38
  • 簡(jiǎn)介 目前在HTTP協(xié)議請(qǐng)求庫(kù)中,OKHttp應(yīng)當(dāng)是非常火的,使用也非常的簡(jiǎn)單。網(wǎng)上有很多文章寫(xiě)了關(guān)于OkHttp...
    第八區(qū)閱讀 1,391評(píng)論 1 5
  • 本篇文章為okhttp源碼學(xué)習(xí)筆記系列的第二篇文章,本篇文章的主要內(nèi)容為okhttp中的連接與連接的管理,因此需要...
    蕉下孤客閱讀 2,734評(píng)論 6 24