OkHttp-CallServerInterceptor源碼解析

本文基於okhttp3.10.0,並且只介紹http1相關的內容不涉及http2.0

終於到最後一個攔截器了,前面我們說到在ConnectInterceptor中會建立連接創建RealConnection和HttpCodec,然後傳遞給下一個攔截器也就是CallServerInterceptor進行網絡讀寫操作,那今天的博客就從這裏開始。

1. 源碼解析

講之前簡單介紹下HttpCodec,它是對客戶端與服務器進行io操作的封裝,默認提供了兩個實現Http1Codec、Http2Codec,我們只講http1.0所以只關注Http1Codec即可

public final class Http1Codec implements HttpCodec {
  final BufferedSource source;
  final BufferedSink sink;
}

內部有source和sink兩個成員變量用來進行讀寫操作,這兩個流是在ConnectInterceptor建立連接的時候通過socket創建,所以通過他們就可以與遠端進行通信了。

	//RealConnection.java
	private void connectSocket(int connectTimeout, int readTimeout, Call call,
      EventListener eventListener) throws IOException {//建立連接
			source = Okio.buffer(Okio.source(rawSocket));
      sink = Okio.buffer(Okio.sink(rawSocket));
  }

  public HttpCodec newCodec(OkHttpClient client, Interceptor.Chain chain,,
      StreamAllocation streamAllocation) throws SocketException {//創建HttpCodec
      socket.setSoTimeout(chain.readTimeoutMillis());
      source.timeout().timeout(chain.readTimeoutMillis(), MILLISECONDS);
      sink.timeout().timeout(chain.writeTimeoutMillis(), MILLISECONDS);
      return new Http1Codec(client, streamAllocation, source, sink);
  }

ok回到主題,對於CallServerInterceptor中的操作,我們大體可以分爲四步

  1. 寫請求頭
  2. 寫請求體
  3. 讀響應頭
  4. 讀響應體

那源碼講解的時候會將CallServerInterceptor源碼做一個拆解,從這四步來分析。

1.1 寫請求頭

	//CallServerInterceptor
	@Override public Response intercept(Chain chain) throws IOException {
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    HttpCodec httpCodec = realChain.httpStream();//獲取ConnectInterceptor創建的HttpCodec
    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);
		
    ...
    return response;
  }

在CallServerInterceptor中第一步是先拿到HttpCodec,這個類是我們與server進行io操作的封裝類,內部是通過Okio與遠端進行讀寫的。

然後調用到httpCodec#writeRequestHeaders(request)進行請求頭的寫入

	@Override public void writeRequestHeaders(Request request) throws IOException {
    String requestLine = RequestLine.get(
        request, streamAllocation.connection().route().proxy().type());//獲取請求行
    writeRequest(request.headers(), requestLine);//寫入請求頭
  }

先獲取請求行,然後寫入請起頭

 	//RequestLine#get
	public static String get(Request request, Proxy.Type proxyType) {
    StringBuilder result = new StringBuilder();
    result.append(request.method());
    result.append(' ');

    if (includeAuthorityInRequestLine(request, proxyType)) {
      result.append(request.url());
    } else {
      result.append(requestPath(request.url()));
    }

    result.append(" HTTP/1.1");
    return result.toString();
  }

請求行的獲取其實就是method url 協議三段拼接起來的,像這樣POST /app/a/update HTTP/1.1

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

先寫入請求行,然後在遍歷寫入請求頭,最後寫完大概像這樣

POST /app/a/update HTTP/1.1
Host: XXX.XXX.XXX.XXX:9999
Content-Type: application/x-www-form-urlencoded

1.2 寫請求體

  @Override public Response intercept(Chain chain) throws IOException {
    //...
    
		Response.Builder responseBuilder = null;
    if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {//如果method不爲get和head,並且有請求體
      if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {//100-continue屬於特殊的請求頭我們可以忽略
        httpCodec.flushRequest();
        realChain.eventListener().responseHeadersStart(realChain.call());
        responseBuilder = httpCodec.readResponseHeaders(true);
      }

      if (responseBuilder == null) {
        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()) {
        streamAllocation.noNewStreams();
      }
    }

    httpCodec.finishRequest();//完成請求寫入
    
    //...
  }

如果非get、head請求並且有請求體,創建輸出流對象,寫入請求體。

接下來輸出流的創建httpCodec#createRequestBody()

	@Override public Sink createRequestBody(Request request, long contentLength) {
    if ("chunked".equalsIgnoreCase(request.header("Transfer-Encoding"))) {//分塊傳輸的則創建newChunkedSink
      return newChunkedSink();
    }

    if (contentLength != -1) {//普通固定長度的則創建newFixedLengthSink
      return newFixedLengthSink(contentLength);
    }

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

  public Sink newFixedLengthSink(long contentLength) {
    if (state != STATE_OPEN_REQUEST_BODY) throw new IllegalStateException("state: " + state);
    state = STATE_WRITING_REQUEST_BODY;
    return new FixedLengthSink(contentLength);//創建一個FixedLengthSink對象
  }

根據當前請求是否需要分塊傳輸做區分,一般情況下都是固定長度的創建一個FixedLengthSink

  private final class FixedLengthSink implements Sink {
    private final ForwardingTimeout timeout = new ForwardingTimeout(sink.timeout());
    private boolean closed;
    private long bytesRemaining;

    FixedLengthSink(long bytesRemaining) {
      this.bytesRemaining = bytesRemaining;//記錄要傳輸的數據量
    }

    @Override public Timeout timeout() {
      return timeout;
    }

    @Override public void write(Buffer source, long byteCount) throws IOException {
      if (closed) throw new IllegalStateException("closed");
      checkOffsetAndCount(source.size(), 0, byteCount);
      if (byteCount > bytesRemaining) {//超出傳輸數據量拋異常
        throw new ProtocolException("expected " + bytesRemaining
            + " bytes but received " + byteCount);
      }
      sink.write(source, byteCount);
      bytesRemaining -= byteCount;
    }

    @Override public 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 void close() throws IOException {
      if (closed) return;
      closed = true;
      if (bytesRemaining > 0) throw new ProtocolException("unexpected end of stream");
      detachTimeout(timeout);
      state = STATE_READ_RESPONSE_HEADERS;
    }
  }

內部其實就是對sink的包裝,加上了對長度的判斷,如果write的數據長度超過了content-lenght則拋出異常,而new CountingSink()也是包裝了一層記錄了下傳輸成功的數據量,這裏就不細說了。

再看到請求的寫入request.body().writeTo(bufferedRequestBody),有多種實現,我們直接看錶單的吧

	//FormBody.java
	@Override public void writeTo(BufferedSink sink) throws IOException {
    writeOrCountBytes(sink, false);
  }

 private long writeOrCountBytes(@Nullable BufferedSink sink, boolean countBytes) {
    long byteCount = 0L;

    Buffer buffer;
    if (countBytes) {
      buffer = new Buffer();
    } else {
      buffer = sink.buffer();//獲取輸出流的buffer
    }

    for (int i = 0, size = encodedNames.size(); i < size; i++) {//將表單數據寫入
      if (i > 0) buffer.writeByte('&');
      buffer.writeUtf8(encodedNames.get(i));
      buffer.writeByte('=');
      buffer.writeUtf8(encodedValues.get(i));
    }

    if (countBytes) {
      byteCount = buffer.size();
      buffer.clear();
    }

    return byteCount;
  }

for循環寫入請求體,最後CallServerInterceptor中調用了bufferedRequestBody.close()將請求體寫入。

1.3 讀響應頭

  @Override public Response intercept(Chain chain) throws IOException {
    //...
		if (responseBuilder == null) {
      realChain.eventListener().responseHeadersStart(realChain.call());
      responseBuilder = httpCodec.readResponseHeaders(false);//獲取響應頭
    }

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

通過httpCodec#readResponseHeaders(false)獲取響應頭

  @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(readHeaderLine());//獲取響應中第一行數據解析成StatusLine

      Response.Builder responseBuilder = new Response.Builder()//創建Response.Builder對象
          .protocol(statusLine.protocol)
          .code(statusLine.code)
          .message(statusLine.message)
          .headers(readHeaders());//讀取響應頭

      if (expectContinue && statusLine.code == HTTP_CONTINUE) {
        return null;
      } else if (statusLine.code == HTTP_CONTINUE) {
        state = STATE_READ_RESPONSE_HEADERS;
        return responseBuilder;
      }

      state = STATE_OPEN_RESPONSE_BODY;
      return responseBuilder;
    } catch (EOFException e) {
      IOException exception = new IOException("unexpected end of stream on " + streamAllocation);
      exception.initCause(e);
      throw exception;
    }
  }

讀取第一行數據作爲響應行

  private String readHeaderLine() throws IOException {
    String line = source.readUtf8LineStrict(headerLimit);
    headerLimit -= line.length();
    return line;
  }

隨後解析爲StatusLine對象,然後在readHeaders()讀取響應頭

  public Headers readHeaders() throws IOException {
    Headers.Builder headers = new Headers.Builder();
    for (String line; (line = readHeaderLine()).length() != 0; ) {//解析到第一個空行,因爲response是用空行來分割響應頭和響應體的
      Internal.instance.addLenient(headers, line);//給Headers.Builder添加頭部屬性
    }
    return headers.build();
  }

    Builder addLenient(String line) {
      int index = line.indexOf(":", 1);
      if (index != -1) {
        return addLenient(line.substring(0, index), line.substring(index + 1));//獲取name和value
      } else if (line.startsWith(":")) {
        // Work around empty header names and header names that start with a
        // colon (created by old broken SPDY versions of the response cache).
        return addLenient("", line.substring(1)); // Empty header name.
      } else {
        return addLenient("", line); // No header name.
      }
    }

    Builder addLenient(String name, String value) {//添加到builder中
      namesAndValues.add(name);
      namesAndValues.add(value.trim());
      return this;
    }

也是一行一行讀,直到讀到第一個換行爲止,因爲響應頭和響應體之間是通過一個空行來區分。然後創建Header對象傳入responseBuilder,最後構建出response。

1.4 讀響應體

	@Override public Response intercept(Chain chain) throws IOException {
    //...
		response = response.newBuilder()
          .body(httpCodec.openResponseBody(response))//讀取響應體
          .build();
    //...
		}

直接看到httpCodec.openResponseBody(response)

  @Override public ResponseBody openResponseBody(Response response) throws IOException {
    streamAllocation.eventListener.responseBodyStart(streamAllocation.call);
    String contentType = response.header("Content-Type");

    if (!HttpHeaders.hasBody(response)) {//一般情況下都是走到這個分支
      Source source = newFixedLengthSource(0);
      return new RealResponseBody(contentType, 0, Okio.buffer(source));
    }

    if ("chunked".equalsIgnoreCase(response.header("Transfer-Encoding"))) {
      Source source = newChunkedSource(response.request().url());
      return new RealResponseBody(contentType, -1L, Okio.buffer(source));
    }

    long contentLength = HttpHeaders.contentLength(response);
    if (contentLength != -1) {
      Source source = newFixedLengthSource(contentLength);
      return new RealResponseBody(contentType, contentLength, Okio.buffer(source));
    }

    return new RealResponseBody(contentType, -1L, Okio.buffer(newUnknownLengthSource()));
  }

根據不同類型創建不同的流去讀請求體數據,一般情況走的一個分支,newFixedLengthSource(0),跟前面創建Sink差不多的流程,這裏是包裝了一下source加了點功能,然後傳入到了RealResponseBody對象中,這裏body對象就創建好了,那麼Response也就創建好了。

public final class RealResponseBody extends ResponseBody {
  private final @Nullable String contentTypeString;
  private final long contentLength;
  private final BufferedSource source;

  public RealResponseBody(
      @Nullable String contentTypeString, long contentLength, BufferedSource source) {
    this.contentTypeString = contentTypeString;
    this.contentLength = contentLength;
    this.source = source;
  }

  @Override public MediaType contentType() {
    return contentTypeString != null ? MediaType.parse(contentTypeString) : null;
  }

  @Override public long contentLength() {
    return contentLength;
  }

  @Override public BufferedSource source() {
    return source;
  }
}

而傳入的source則通過成員變量存儲了起來,直到我們調用ResponseBody#string()方法的時候纔將響應體數據讀出

  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中讀取數據,並且讀完後它調用了source#close將流關閉了,所以當我們多次執行ResponseBody#string()的時候會拋出異常因爲流已經關閉了。

2. 總結

CallServerInterceptor內部就是通過ConnectInterceptor創建的HttpCodec與遠端進行io操作,流程上就是四步

  1. 寫請求頭
  2. 寫請求體
  3. 讀響應頭
  4. 讀響應體

也沒啥可說的了就醬,okhttp全部攔截器終於看完了(^-^)V

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章