OKHttp原始碼解析(6)----攔截器CallServerInterceptor

王大帝發表於2018-12-09

系列文章

OKio原始碼解析

OKHttp原始碼解析(1)----整體流程

OKHttp原始碼解析(2)----攔截器RetryAndFollowUpInterceptor

OKHttp原始碼解析(3)----攔截器BridgeInterceptor

OKHttp原始碼解析(4)----攔截器CacheInterceptor

OKHttp原始碼解析(5)----攔截器ConnectInterceptor

OKHttp原始碼解析(6)----攔截器CallServerInterceptor

1.簡介

This is the last interceptor in the chain. It makes a network call to the server. 這是鏈中最後一個攔截器,它向伺服器發起了一次網路訪問

請求服務攔截器,負責向伺服器傳送請求資料、從伺服器讀取響應資料

2.原始碼解析

2.1流程

@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();
     // 整理請求頭並寫入
    httpCodec.writeRequestHeaders(request);

    Response.Builder responseBuilder = null;
     // 檢查是否為有 body 的請求方法
    if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
      // If there is a "Expect: 100-continue" header on the request, wait for a "HTTP/1.1 100
      // Continue" response before transmitting the request body. If we do not get that, return what
      // we did get (such as a 4xx response) without ever transmitting the request body.
      // 如果有 Expect: 100-continue在請求頭中,那麼要等伺服器的響應。100-continue協議需要在post資料前,
      徵詢伺服器情況,看伺服器是否處理POST的資料
      if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
      //傳送100-continue的請求
        httpCodec.flushRequest();
        //如果有中間級的響應,返回null
        responseBuilder = httpCodec.readResponseHeaders(true);
      }

      if (responseBuilder == null) {
        // Write the request body if the "Expect: 100-continue" expectation was met.
        // 寫入請求體
        Sink requestBodyOut = httpCodec.createRequestBody(request, request.body().contentLength());
        BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
        request.body().writeTo(bufferedRequestBody);
        bufferedRequestBody.close();
      } 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) {
      responseBuilder = httpCodec.readResponseHeaders(false);
    }

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

    int code = response.code();
     // 如果為 web socket 且狀態碼是 101 ,那麼 body 為空
    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 {
     //設定響應body,此時並沒有讀取socket中的輸入流
      response = response.newBuilder()
          .body(httpCodec.openResponseBody(response))
          .build();
    }
     // 如果請求頭中有 close 那麼斷開連線
    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;
  }

複製程式碼

首先我們要明確Source和Sink是什麼: 在JAVA IO中有輸入流和輸出流,在OKio的IO體系中,Sourc就表示輸入流,Sink表示輸出流。OKio的IO體系我們將在新的一章中進行講解,本章不進行展開。

  • Source: 輸入流,這是一個介面類,其子類有:BufferedSource、PeekSource、ForwardingSource、GzipSource、InputStreamSource、InflaterSource
  • Sink: 輸出流 ,介面類,其子類有:BufferedSink、ForwardingSink、GzipSink、OutputStreamSink、DeflaterSink、BlackholeSink

在本流程中,主幹邏輯有以下4步:

  1. httpCodec.writeRequestHeaders(request)
  2. httpCodec.createRequestBody(request, contentLength)
  3. httpCodec.readResponseHeaders(true)
  4. httpCodec.openResponseBody(response)

以上4步分別完成了請求頭的寫入,請求體的寫入,響應頭的讀取,響應體的讀取。

2.2流的讀取

當時看這裡的時候,有個疑惑點,網路請求的資料及輸入流中的資料在哪裡,其實在第4步中獲取到響應的body資訊,只是獲取一個流物件,只有在應用程式碼中呼叫流物件的讀方法或者response.body().string()方法等,才會從socket的輸入流中讀取資訊到應用的記憶體中使用。下面我們來分析一下流的讀取過程。

response.body().string()

複製程式碼

ResponseBody:


public final String string() throws IOException {
  BufferedSource source = source();
    try {
      Charset charset = Util.bomAwareCharset(source, charset());
      return source.readString(charset);
    } finally {
      Util.closeQuietly(source);
    }
  }
複製程式碼

從原始碼可以看到source.readString(charset);將流轉換為String進行輸出。下面我們分兩步來討論:

  • source的來源
  • source.readString()之後的過程

2.2.1 source的來源

source來自於方法source(),ResponseBody中source()是個抽象方法:

  public abstract BufferedSource source();
複製程式碼

其實現類為RealResponseBody:

  @Override public BufferedSource source() {
    return source;
  }
複製程式碼

方法中的source為構造方法中傳遞過來的,建立RealResponseBody的是CallServerInterceptor原始碼中的httpCodec.openResponseBody(response),看一下httpCodec.openResponseBody()的原始碼:

  @Override public ResponseBody openResponseBody(Response response) throws IOException {
      ......
    if (!HttpHeaders.hasBody(response)) {
      Source source = newFixedLengthSource(0);
      return new RealResponseBody(contentType, 0, Okio.buffer(source));
    }
     ......
  }
複製程式碼

檢視newFixedLengthSource(0)方法:

  public Source newFixedLengthSource(long length) throws IOException {
    if (state != STATE_OPEN_RESPONSE_BODY) throw new IllegalStateException("state: " + state);
    state = STATE_READING_RESPONSE_BODY;
    return new FixedLengthSource(length);
  }
複製程式碼

檢視FixedLengthSource()類的read方法:

    @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 = super.read(sink, Math.min(bytesRemaining, byteCount));
      if (read == -1) {
        ProtocolException e = new ProtocolException("unexpected end of stream");
        endOfInput(false, e); // The server did not supply the promised content length.
        throw e;
      }

      bytesRemaining -= read;
      if (bytesRemaining == 0) {
        endOfInput(true, null);
      }
      return read;
    }
複製程式碼

read方法是將source資料寫入Buffer中,我們來看source的來源,繼續檢視父類AbstractSource中的super.read():

    @Override public long read(Buffer sink, long byteCount) throws IOException {
      try {
        long read = source.read(sink, byteCount);
        if (read > 0) {
          bytesRead += read;
        }
        return read;
      } catch (IOException e) {
        endOfInput(false, e);
        throw e;
      }
    }
複製程式碼

查詢source.read(sink, byteCount)中的source的來源,這個source是Http1Codec構造方法傳遞進行的:

  public Http1Codec(OkHttpClient client, StreamAllocation streamAllocation, BufferedSource source,
      BufferedSink sink) {
    this.client = client;
    this.streamAllocation = streamAllocation;
    this.source = source;
    this.sink = sink;
  }
複製程式碼

經過上溯程式碼,我們發現建立Http1Codec是在ConnectInterceptor中開始的,流程如下:

  @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);
  }
複製程式碼

streamAllocation.newStream(client, chain, doExtensiveHealthChecks);

  public HttpCodec newStream(
      OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
     ....
    try {
     //建立連線connection,根據socket建立輸入流sink和輸出流source
      RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
          writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
     //建立HttpCodec,有Http1Codec和Http2Codec兩個子類
      HttpCodec resultCodec = resultConnection.newCodec(client, chain, this);
     ...
    }
  }
複製程式碼

其中findHealthyConnection建立了source,resultConnection.newCodec建立了Http1Codec:

  • findHealthyConnection-->findConnection-->result.connect(RealConnection)-->connectSocket()--> source = Okio.buffer(Okio.source(rawSocket))

  • newCodec

  public HttpCodec newCodec(OkHttpClient client, Interceptor.Chain chain,
      StreamAllocation streamAllocation) throws SocketException {
    if (http2Connection != null) {
      return new Http2Codec(client, chain, streamAllocation, http2Connection);
    } else {
      socket.setSoTimeout(chain.readTimeoutMillis());
      source.timeout().timeout(chain.readTimeoutMillis(), MILLISECONDS);
      sink.timeout().timeout(chain.writeTimeoutMillis(), MILLISECONDS);
      return new Http1Codec(client, streamAllocation, source, sink);
    }
  }
複製程式碼

小結:

okhttp在ConnectInterceptor連線攔截器中,根據socket建立了網路設施connection,輸出流sink和輸入流source。並在CallServerInterceptor請求伺服器攔截器中完成了請求的傳送和響應頭的讀取,此時並沒有讀取響應體即我們關心的網路請求結果。響應體的讀取是使用者拿到response後,使用response.body().string()從socket中讀取的。

2.2.2 source.readString()之後的過程:

source我們找到了,是RealBufferedSource類包含的引用了socket的source,那麼我們在RealBufferedSource()中檢視方法readString():

  override fun readString(charset: Charset): String {
    buffer.writeAll(source)
    return buffer.readString(charset)
  }
複製程式碼

流的讀取分為兩步:

  1. 將soucre寫入快取buffer
  2. 從快取中把資料讀取出來,返回。

本章在追原始碼,寫的有點亂,開發者在看的時候,還是需要去讀一下原始碼,自己跟蹤一下。下一章將學習一下OKio的IO體系,結合OKio將對okhttp有更深的理解。OKio原始碼解析

References

http之100-continue(轉)

OkHttp 原始碼學習筆記(三) 資料交換的流 HTTPCodec

相關文章