四、深入理解OkHttp:CallServerIntercepter

一移迫、前言

【1.1】OkHttp系列其他篇章:

  1. 同步請求的實現(xiàn)流程旺嬉。
  2. 異步請求的實現(xiàn)流程
  3. 重要攔截器:CacheInterceptor 的解析
  4. 重要攔截器:ConnectInterceptor 的解析厨埋。
  5. 重要攔截器:CallServerInterceptor 的解析邪媳。

【1.2】陳述

經(jīng)過前面幾章的準(zhǔn)備工作,我們終于可以和服務(wù)器進行正式的交流了荡陷。而與服務(wù)器進行正式的數(shù)據(jù)通信就發(fā)生在最后一個攔截器:ServerIntercepter.java雨效。在解析這個類之前,需要先看一下其他的類:ExchangeCodec废赞,這個類在上篇文章中就有涉及到徽龟,不過沒有仔細講下去。在這章中會講一下唉地。

二据悔、ExchangeCodec

【2.1】簡介

ExchageCodec是一個接口传透,他是來規(guī)范網(wǎng)絡(luò)請求的編碼行為和網(wǎng)絡(luò)回復(fù)的解碼行為。他有2個子類 Http1ExchangeCodec 和 Http2ExchangeCodec极颓。從名字上一看就知道朱盐,他們分別對應(yīng)了Http1協(xié)議和Http2協(xié)議。下面是ExchageCodec主要的接口規(guī)范:

public interface ExchangeCodec {
  ...
  
  /** 將請求體轉(zhuǎn)化為輸出流*/
  Sink createRequestBody(Request request, long contentLength) throws IOException;

  /** 寫請求頭*/
  void writeRequestHeaders(Request request) throws IOException;

  /** 將在緩存區(qū)的請求刷新到輸出流 */
  void flushRequest() throws IOException;

  /** 通知已經(jīng)完成請求動作 */
  void finishRequest() throws IOException;

    
 /** 讀取響應(yīng)體 */
  Source openResponseBodySource(Response response) throws IOException;

  /** 讀取響應(yīng)頭 */
  @Nullable Response.Builder readResponseHeaders(boolean expectContinue) throws IOException;
  
  ...

  /** 取消請求 */
  void cancel();
 
}
  

總結(jié): 總的來說菠隆,ExchageCodec.java規(guī)范了網(wǎng)絡(luò)交互過程中的寫請求和讀響應(yīng)的動作兵琳。具體的如下:

  1. 將請求體轉(zhuǎn)化為輸出流。
  2. 寫請求頭浸赫。
  3. 將在請求刷新到底層的Socket。
  4. 通知完成請求動作赃绊。
  5. 讀響應(yīng)頭既峡。
  6. 讀響應(yīng)體。
  7. 取消請求碧查。

三运敢、Http1ExchangeCodec

【3.1】createRequestBody()

Http1ExchageCodec.java
@Override public Sink createRequestBody(Request request, long contentLength) throws IOException {
    if (request.body() != null && request.body().isDuplex()) {
      throw new ProtocolException("Duplex connections are not supported for HTTP/1");
    }

    //創(chuàng)建一個不知長度的輸出流。
    if ("chunked".equalsIgnoreCase(request.header("Transfer-Encoding"))) {
      return newChunkedSink();
    }

    //創(chuàng)建一個知道長度的輸出流忠售。
    if (contentLength != -1L) {
      return newKnownLengthSink();
    }

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

總結(jié): 該方法總的來說就是根據(jù)請求的長度的確定性生成響應(yīng)的流類型

【3.2】 writeRequestHeaders():寫入請求頭

  @Override public void writeRequestHeaders(Request request) throws IOException {
    String requestLine = RequestLine.get(
        request, realConnection.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;
  }

【3.3】flushRequest()/finishRequest()

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

總結(jié): 他們調(diào)用的都是flush方法传惠。所以做的都是同一件是,把緩存區(qū)的數(shù)據(jù)刷新到底層Socket稻扬。

【3.4】openResponseBodySource():讀取響應(yīng)體

@Override public Source openResponseBodySource(Response response) {
    //1. 如果沒有響應(yīng)體卦方,那么構(gòu)建一個讀取長度為0的輸入流
    if (!HttpHeaders.hasBody(response)) {
      return newFixedLengthSource(0);
    }

    //2. 不確定長度的輸入流
    if ("chunked".equalsIgnoreCase(response.header("Transfer-Encoding"))) {
      return newChunkedSource(response.request().url());
    }

    //3. 確定長度的輸入流
    long contentLength = HttpHeaders.contentLength(response);
    if (contentLength != -1) {
      return newFixedLengthSource(contentLength);
    }

    return newUnknownLengthSource();
  }

【3.5】readResponseHeaders():讀取響應(yīng)頭

 @Override public Response.Builder readResponseHeaders(boolean expectContinue) throws IOException {
    ...

    try {
    //解析響應(yīng)頭的String。
      StatusLine statusLine = StatusLine.parse(readHeaderLine());

      //構(gòu)建響應(yīng)體
      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;
      } else if (statusLine.code == HTTP_CONTINUE) {
        state = STATE_READ_RESPONSE_HEADERS;
        return responseBuilder;
      }

      state = STATE_OPEN_RESPONSE_BODY;
      return responseBuilder;
    } catch (EOFException e) {
      ...
    }
  }
  
  ///讀取響應(yīng)頭輸入流
 private String readHeaderLine() throws IOException {
    String line = source.readUtf8LineStrict(headerLimit);
    headerLimit -= line.length();
    return line;
  }

三泰佳、CallServerIntercepter.java: 最后一個攔截器


  @Override public Response intercept(Chain chain) throws IOException {
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Exchange exchange = realChain.exchange();
    Request request = realChain.request();
    long sentRequestMillis = System.currentTimeMillis();

    //1. 寫入請求頭
    exchange.writeRequestHeaders(request);

    boolean responseHeadersStarted = false;
    Response.Builder responseBuilder = null;
    
    //2. 是否為有請求體的請求
    if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
      //3. 若請求頭里有"100-continue"悔橄,代表先只有請求頭的向服務(wù)器請求蔚万。
      // 需要等待服務(wù)器的響應(yīng)頭再進一步請求。
      if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
        exchange.flushRequest();
        responseHeadersStarted = true;
        exchange.responseHeadersStart();
        responseBuilder = exchange.readResponseHeaders(true);
      }
      
      //4. 可以繼續(xù)發(fā)出請求數(shù)據(jù)
      if (responseBuilder == null) {
        //5. 將請求體寫入socket
        if (request.body().isDuplex()) {
          exchange.flushRequest();
          BufferedSink bufferedRequestBody = Okio.buffer(
              exchange.createRequestBody(request, true));
          request.body().writeTo(bufferedRequestBody);
        } else {
          BufferedSink bufferedRequestBody = Okio.buffer(
              exchange.createRequestBody(request, false));
          request.body().writeTo(bufferedRequestBody);
          bufferedRequestBody.close();
        }
      } else {
        exchange.noRequestBody();
        if (!exchange.connection().isMultiplexed()) {
          
          exchange.noNewExchangesOnConnection();
        }
      }
    } else {
      exchange.noRequestBody();
    }

    //6. 通知結(jié)束請求。
    if (request.body() == null || !request.body().isDuplex()) {
      exchange.finishRequest();
    }

    if (!responseHeadersStarted) {
      exchange.responseHeadersStart();
    }

    //7. 讀取響應(yīng)頭
    if (responseBuilder == null) {
      responseBuilder = exchange.readResponseHeaders(false);
    }

    //8. 構(gòu)建響應(yīng)體
    Response response = responseBuilder
        .request(request)
        .handshake(exchange.connection().handshake())
        .sentRequestAtMillis(sentRequestMillis)
        .receivedResponseAtMillis(System.currentTimeMillis())
        .build();

    //9. 如果響應(yīng)碼=100侧到,需要再請求一次。
    int code = response.code();
    if (code == 100) {
      response = exchange.readResponseHeaders(false)
          .request(request)
          .handshake(exchange.connection().handshake())
          .sentRequestAtMillis(sentRequestMillis)
          .receivedResponseAtMillis(System.currentTimeMillis())
          .build();

      code = response.code();
    }

    exchange.responseHeadersEnd(response);

    if (forWebSocket && code == 101) {
      //空連接
      response = response.newBuilder()
          .body(Util.EMPTY_RESPONSE)
          .build();
    } else {
      //10.讀取響應(yīng)體詳細
      response = response.newBuilder()
          .body(exchange.openResponseBody(response))
          .build();
    }

    //11. 如果有close頭榔组,那么關(guān)閉連接雳殊。
    if ("close".equalsIgnoreCase(response.request().header("Connection"))
        || "close".equalsIgnoreCase(response.header("Connection"))) {
      exchange.noNewExchangesOnConnection();
    }
    ...
    
    //12. 返回請求
    return response;
  }

總結(jié): CallServerIntercepter的攔截邏輯很簡單,總的來說就是將請求頭臀晃,請求體寫入Socket觉渴,然后讀取Socket的響應(yīng)頭和響應(yīng)體。而具體的IO操作徽惋,OkHttp是采用的okio疆拘,這是個優(yōu)秀的IO庫,具體的邏輯這里就不深挖了寂曹。具體的流程如下:

  1. 寫入請求頭哎迄。
  2. 如果請求頭里有"100-continue", 代表先將請求頭發(fā)送給服務(wù)器回右,看服務(wù)器的響應(yīng)決定是否進行下一步請求體的發(fā)送。
  3. 寫入請求體漱挚,并發(fā)送請求翔烁。
  4. 讀取響應(yīng)體,并構(gòu)建一個Resonse
  5. 如果響應(yīng)碼為100旨涝,需要再請求一次蹬屹。
  6. 讀取詳細的響應(yīng)體。
  7. 如果響應(yīng)頭有“close”白华,那么關(guān)閉這條連接慨默。
  8. 返回響應(yīng)。

OkHttp的幾個重要部分講解就到這里全部結(jié)束了弧腥∠萌。回顧一下,我們從網(wǎng)絡(luò)的同步/異步請求管搪,降到它的攔截鏈模式虾攻。然后著重講了幾個重要的攔截器:cacheIntercepter、ConnectInterpcet和CallServerIntercepter更鲁。這幾篇文章是本人在自學(xué)中霎箍,總結(jié)記錄。有不對的地方歡迎指出澡为。最后漂坏,放上一張總體架構(gòu)圖,有助于整體理解:

image

(圖片來源感謝:https://yq.aliyun.com/articles/78105?spm=5176.100239.blogcont78104.10.FlPFWr)

最后媒至,在這里需要鳴謝以下博文:
http://www.reibang.com/p/82f74db14a18
http://www.reibang.com/p/7624b45fbdc1
http://www.reibang.com/p/227cee9c8d15
本文引用的圖片如有涉權(quán)樊拓,請聯(lián)系本人刪除,謝謝塘慕!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末筋夏,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子图呢,更是在濱河造成了極大的恐慌条篷,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蛤织,死亡現(xiàn)場離奇詭異赴叹,居然都是意外死亡,警方通過查閱死者的電腦和手機指蚜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進店門乞巧,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人摊鸡,你說我怎么就攤上這事绽媒〔隙” “怎么了?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵是辕,是天一觀的道長囤热。 經(jīng)常有香客問我,道長获三,這世上最難降的妖魔是什么旁蔼? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮疙教,結(jié)果婚禮上棺聊,老公的妹妹穿的比我還像新娘。我一直安慰自己贞谓,他們只是感情好限佩,可當(dāng)我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著经宏,像睡著了一般犀暑。 火紅的嫁衣襯著肌膚如雪驯击。 梳的紋絲不亂的頭發(fā)上烁兰,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天,我揣著相機與錄音徊都,去河邊找鬼沪斟。 笑死,一個胖子當(dāng)著我的面吹牛暇矫,可吹牛的內(nèi)容都是我干的主之。 我是一名探鬼主播,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼李根,長吁一口氣:“原來是場噩夢啊……” “哼槽奕!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起房轿,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤粤攒,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后囱持,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體夯接,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年纷妆,在試婚紗的時候發(fā)現(xiàn)自己被綠了盔几。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡掩幢,死狀恐怖逊拍,靈堂內(nèi)的尸體忽然破棺而出上鞠,到底是詐尸還是另有隱情,我是刑警寧澤顺献,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布旗国,位于F島的核電站,受9級特大地震影響注整,放射性物質(zhì)發(fā)生泄漏能曾。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一肿轨、第九天 我趴在偏房一處隱蔽的房頂上張望寿冕。 院中可真熱鬧,春花似錦椒袍、人聲如沸驼唱。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽玫恳。三九已至,卻和暖如春优俘,著一層夾襖步出監(jiān)牢的瞬間京办,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工帆焕, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留惭婿,地道東北人。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓叶雹,卻偏偏與公主長得像财饥,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子折晦,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,592評論 2 353