原始碼分析三:OkHttp—CallServerInterceptor

楊昆發表於2018-03-13
CallServerInterceptor的主要功能就是—向伺服器傳送請求,並最終返回Response物件供客戶端使用。

上原始碼:

 public Response intercept(Chain chain) throws IOException {
    // 省略部分程式碼
    // 獲取HttpCodec 
    HttpCodec httpCodec = realChain.httpStream();
    // 省略部分程式碼
    Request request = realChain.request();
    //向伺服器傳送請求
    httpCodec.writeRequestHeaders(request);

    Response.Builder responseBuilder = null;
    // 檢測是否有請求body
    if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {

      if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
        httpCodec.flushRequest();
        //構建responseBuilder物件
        responseBuilder = httpCodec.readResponseHeaders(true);
      }

       //如果伺服器允許傳送請求body傳送
      if (responseBuilder == null) {
        Sink requestBodyOut = httpCodec.createRequestBody(request, request.body().contentLength());
        BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
        request.body().writeTo(bufferedRequestBody);
        bufferedRequestBody.close();
      } else if (!connection.isMultiplexed()) {
         //省略部分程式碼
      }
    }

    //結束請求
    httpCodec.finishRequest();

    //構建請求buidder物件
    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) {
      //省略部分程式碼
    } else {
      response = response.newBuilder()
          .body(httpCodec.openResponseBody(response))
          .build();
    }

    //省略部分程式碼

    return response;
  }
複製程式碼

該方法首先是獲取了httpCodec物件,該物件的主要功能就是對不同http協議(http1.1和http/2)的請求和響應做處理,該物件的初始化是在ConnectIntercepor的intercept裡面:

 HttpCodec httpCodec = streamAllocation.newStream(client, doExtensiveHealthChecks);複製程式碼

最終httpCodec的初始化又是在StreamAllocation的newStream方法(詳見《 原始碼分析三:OkHttp—ConnectInterceptor》):

ublic HttpCodec newStream(OkHttpClient client, boolean doExtensiveHealthChecks) {
     //:省略部分程式碼;

      HttpCodec resultCodec = resultConnection.newCodec(client, this);

  }
    public HttpCodec newCodec(
      OkHttpClient client, StreamAllocation streamAllocation) throws SocketException {
    if (http2Connection != null) {
      return new Http2Codec(client, streamAllocation, http2Connection);
    } else {
      //設定socket的讀超時時間
      socket.setSoTimeout(client.readTimeoutMillis());
      //InputStream的超時時間
      source.timeout().timeout(client.readTimeoutMillis(), MILLISECONDS);
      //OutputStream的超時時間
      sink.timeout().timeout(client.writeTimeoutMillis(), MILLISECONDS);
      return new Http1Codec(client, streamAllocation, source, sink);
    }
  }
複製程式碼

可以發現Okhttp的提供了兩種HttpCodec的實現類,如果使用了http2協議則返回Http2Codec,否則返回Http1Codec!並且設定了超時時間,本篇就以Http1Codec物件來進行分析。

我們知道Http傳送網路請求前兩個步驟是:
1、建立TCP連結
2、客戶端向web伺服器傳送請求命令:形如GET /login/login.jsp?username=android&password=123 HTTP/1.1的資訊

在Okhttp中ConnectInterceptor負責第一個步驟,那麼第二個步驟是如何實現的呢?答案就是httpCodec物件的writeRequestHeaders方法。(該方法在CallserverInterceptor的intercept裡面呼叫,見上面程式碼)

 public void writeRequestHeaders(Request request) throws IOException {
  //RequestLine.get用來構建形如GET xx HTTP/1.1的字串
    String requestLine = RequestLine.get(
        request, streamAllocation.connection().route().proxy().type());
  //像伺服器傳送請求,形如GET xxx HTTP/1.1
    writeRequest(request.headers(), requestLine);
  }  
複製程式碼

可以發現Okhttp通過OkIO的Sink物件(該物件可以看做Socket的OutputStream物件)的writeRequest來向伺服器傳送請求的。

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

我們知道HTTP支援post,delete,get,put等方法,而post,put等方法是需要請求體的(在Okhttp中用RequestBody來表示)。所以接著writeRequestHeaders之後Okhttp對請求體也做了響應的處理:

    //如果當前request請求需要請求體
    if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {

      //詢問Server使用願意接受資料 
      if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
        httpCodec.flushRequest();
        //構建responseBuilder物件
        responseBuilder = httpCodec.readResponseHeaders(true);
      }

       //向伺服器傳送請求體
      if (responseBuilder == null) {

         //傳送請求體,詳見下文描述
      } else if (!connection.isMultiplexed()) {
        //省略部分程式碼
      }
    }123456789101112131415161718複製程式碼

通過上面的程式碼可以發現Okhttp對Expect頭部也做了支援,上面程式碼對客戶端是否使用該頭部做了判斷,“100 continue”的作用就是:客戶端有一個RequestBody(比如post或者PUT方法)要發給伺服器,但是客戶端希望在傳送RequestBody之前檢視伺服器是否接受這個body,服務端在接受到這個請求後必須進行響應。客戶端通過Expect首部來傳送這個訊息,當然如果客戶端沒有實體傳送,就不應該傳送100 continue 首部,因為這樣會使伺服器誤以為客戶端有body要傳送。所以okhttp在傳送這個之前要permitsRequestBody來判斷。當然常規的get請求是不會走這個方法的。

如果伺服器允許傳送ReqeustBody,那麼就通過下面這三行程式碼來傳送請求體:

  //構建請求體物件組成的輸入流
  Sink requestBodyOut = httpCodec.createRequestBody(request, request.body().contentLength());
        BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
        //傳送請求體
        request.body().writeTo(bufferedRequestBody);12345複製程式碼

最終呼叫ReqeustBody的writeTo方法來傳送請求體,實際上是呼叫bufferedRequestBody物件的write方法,簡單例項如下(當然實際可能是FormBody或者是自定義的ReqeustBody):

ublic static RequestBody create(
      final @Nullable MediaType contentType, final ByteString content) {
    return new RequestBody() {
      //省略部分程式碼
      @Override public void writeTo(BufferedSink sink) throws IOException {
        sink.write(content);
      }
    };
  }123456789複製程式碼

到現在為止,客戶端向服務端傳送請求的部分已經講解完畢,下面就剩下讀取伺服器響應然後構建Response物件了:

//構建請求buider物件
    if (responseBuilder == null) {
      responseBuilder = httpCodec.readResponseHeaders(false);
    }

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

    int code = response.code();
    if (forWebSocket && code == 101) {
      //返回空的即無效的響應
      response = response.newBuilder()
          .body(Util.EMPTY_RESPONSE)
          .build();
    } else {
      response = response.newBuilder()
          .body(httpCodec.openResponseBody(response))
          .build();
    }
12345678910111213141516171819202122232425複製程式碼

上面的程式碼做了三個工作:
1、呼叫HttpCodec的readResponseHeaders方法讀取伺服器響應的資料,構建Response.Builder物件(以Hppt1Codec分析):

 public Response.Builder readResponseHeaders(boolean expectContinue) throws IOException {
      //省略部分程式碼
      //讀取伺服器
      StatusLine statusLine = StatusLine.parse(source.readUtf8LineStrict());

      Response.Builder responseBuilder = new Response.Builder()
          .protocol(statusLine.protocol)//http協議版本
          .code(statusLine.code)//http響應狀態碼
          //http的message :like "OK" or "Not Modified"
          .message(statusLine.message)
          .headers(readHeaders());//http響應header
      //省略部分程式碼

      return responseBuilder;

  }12345678910111213141516複製程式碼

2、通過ResopnseBuilder物件來最終建立Response物件,並返回。
最關鍵的是伺服器的響應體或者響應內容是如果傳給Response的,程式碼如下:

  response = response.newBuilder()
          .body(httpCodec.openResponseBody(response))
          .build();123複製程式碼

Response的body通過httpCodec物件的openResponseBody傳進來,進入Http1Codec物件的openResponseBody方法看看都做了些神馬:

public ResponseBody openResponseBody(Response response) throws IOException {
    Source source = getTransferStream(response);
    return new RealResponseBody(response.headers(), Okio.buffer(source));
  }1234複製程式碼

很簡單,openResponseBody將Socket的輸入流InputStream物件交給OkIo的Source物件(在本篇博文中只需簡單的將Sink作為Socket的輸入流,Source作為Socket的輸入流看待即可,詳細的分析可參考OKIO),然後封裝成RealResponseBody(該類是ResponseBody的子類)作為Response的body.

那麼我們怎麼通過這個body來獲取伺服器傳送過來的字串呢?ResponseBody提供了string()方法:

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

string()方法也很簡單,就是通過一些處理然後讓呼叫source.readString來讀取伺服器的資料。需要注意的是該方法最後呼叫closeQuietly來關閉了當前請求的InputStream輸入流,所以string()方法只能呼叫一次,再次呼叫的話會報錯,畢竟輸入流已經關閉了,你還怎麼讀取資料呢?

到此為止CallServerInterceptor簡單分析完畢,總結下主要做了如下工作:
1、獲取HttpCodec物件,對<=Http1.1之前的或者http/2不同協議的http請求處理。
2、傳送http請求資料,構建Resposne.Builder物件,然後構建Response並返回。

到此為止Okhttp從發起請求到響應請求生成Response物件的流程已經分析完畢。


相關文章