OkHttp攔截器詳解

OkHttp攔截器詳解

攔截器責(zé)任鏈

OkHttp最核心的工作是在getResponseWithInterceptorChain()中進(jìn)行,在進(jìn)入這個(gè)方法分析之前任内,我們先來(lái)了解什么是責(zé)任鏈模式,因?yàn)榇朔椒ň褪抢玫呢?zé)任鏈模式完成一步步的請(qǐng)求官册。

責(zé)任鏈顧名思義就是由一系列的負(fù)責(zé)者構(gòu)成的一個(gè)鏈條挣惰,類(lèi)似于工廠(chǎng)流水線(xiàn)哆窿,你們懂的农曲,很多同學(xué)的男朋友/女朋友就是這么來(lái)的社搅。

責(zé)任鏈模式

為請(qǐng)求創(chuàng)建了一個(gè)接收者對(duì)象的鏈。這種模式給予請(qǐng)求的類(lèi)型乳规,對(duì)請(qǐng)求的發(fā)送者和接收者進(jìn)行解耦形葬。在這種模式中,通常每個(gè)接收者都包含對(duì)另一個(gè)接收者的引用暮的。如果一個(gè)對(duì)象不能處理該請(qǐng)求笙以,那么它會(huì)把相同的請(qǐng)求傳給下一個(gè)接收者,依此類(lèi)推冻辩。

攔截器流程

而OkHttp中的getResponseWithInterceptorChain()中經(jīng)歷的流程為

攔截器責(zé)任鏈

請(qǐng)求會(huì)被交給責(zé)任鏈中的一個(gè)個(gè)攔截器猖腕。默認(rèn)情況下有五大攔截器

  1. RetryAndFollowUpInterceptor

    第一個(gè)接觸到請(qǐng)求,最后接觸到響應(yīng)(U型調(diào)用)微猖;負(fù)責(zé)判斷是否需要重新發(fā)起整個(gè)請(qǐng)求

  2. BridgeInterceptor

    補(bǔ)全請(qǐng)求谈息,并對(duì)響應(yīng)進(jìn)行額外處理

  3. CacheInterceptor

    請(qǐng)求前查詢(xún)緩存缘屹,獲得響應(yīng)并判斷是否需要緩存

  4. ConnectInterceptor

    與服務(wù)器完成TCP連接

  5. CallServerInterceptor

    與服務(wù)器通信凛剥;封裝請(qǐng)求數(shù)據(jù)與解析響應(yīng)數(shù)據(jù)(如:HTTP報(bào)文)

攔截器詳情

一、重試及重定向攔截器

第一個(gè)攔截器:RetryAndFollowUpInterceptor轻姿,主要就是完成兩件事情:重試與重定向犁珠。

重試

請(qǐng)求階段發(fā)生了 RouteException 或者 IOException會(huì)進(jìn)行判斷是否重新發(fā)起請(qǐng)求逻炊。

RouteException

catch (RouteException e) {
    //todo 路由異常,連接未成功犁享,請(qǐng)求還沒(méi)發(fā)出去
    if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
        throw e.getLastConnectException();
    }
    releaseConnection = false;
    continue;
} 

IOException

catch (IOException e) {
    //todo 請(qǐng)求發(fā)出去了余素,但是和服務(wù)器通信失敗了。(socket流正在讀寫(xiě)數(shù)據(jù)的時(shí)候斷開(kāi)連接)
    // ConnectionShutdownException只對(duì)HTTP2存在炊昆。假定它就是false
    boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
    if (!recover(e, streamAllocation, requestSendStarted, request)) throw e;
        releaseConnection = false;
        continue;
} 

兩個(gè)異常都是根據(jù)recover 方法判斷是否能夠進(jìn)行重試桨吊,如果返回true,則表示允許重試凤巨。

private boolean recover(IOException e, StreamAllocation streamAllocation,
                            boolean requestSendStarted, Request userRequest) {
    streamAllocation.streamFailed(e);
    //todo 1视乐、在配置OkhttpClient是設(shè)置了不允許重試(默認(rèn)允許),則一旦發(fā)生請(qǐng)求失敗就不再重試
    if (!client.retryOnConnectionFailure()) return false;
    //todo 2敢茁、由于requestSendStarted只在http2的io異常中為true佑淀,先不管http2
    if (requestSendStarted && userRequest.body() instanceof UnrepeatableRequestBody)
        return false;

    //todo 3、判斷是不是屬于重試的異常
    if (!isRecoverable(e, requestSendStarted)) return false;

    //todo 4彰檬、有沒(méi)有可以用來(lái)連接的路由路線(xiàn)
    if (!streamAllocation.hasMoreRoutes()) return false;

    // For failure recovery, use the same route selector with a new connection.
    return true;
}

所以首先使用者在不禁止重試的前提下伸刃,如果出現(xiàn)了某些異常,并且存在更多的路由線(xiàn)路逢倍,則會(huì)嘗試換條線(xiàn)路進(jìn)行請(qǐng)求的重試捧颅。其中某些異常是在isRecoverable中進(jìn)行判斷:

private boolean isRecoverable(IOException e, boolean requestSendStarted) {
    // 出現(xiàn)協(xié)議異常,不能重試
    if (e instanceof ProtocolException) {
      return false;
    }

    // requestSendStarted認(rèn)為它一直為false(不管http2),異常屬于socket超時(shí)異常,直接判定可以重試
    if (e instanceof InterruptedIOException) {
      return e instanceof SocketTimeoutException && !requestSendStarted;
    }

    // SSL握手異常中较雕,證書(shū)出現(xiàn)問(wèn)題隘道,不能重試
    if (e instanceof SSLHandshakeException) {
      if (e.getCause() instanceof CertificateException) {
        return false;
      }
    }
    // SSL握手未授權(quán)異常 不能重試
    if (e instanceof SSLPeerUnverifiedException) {
      return false;
    }
    return true;
}

1、協(xié)議異常郎笆,如果是那么直接判定不能重試;(你的請(qǐng)求或者服務(wù)器的響應(yīng)本身就存在問(wèn)題谭梗,沒(méi)有按照http協(xié)議來(lái)定義數(shù)據(jù),再重試也沒(méi)用)

2宛蚓、超時(shí)異常激捏,可能由于網(wǎng)絡(luò)波動(dòng)造成了Socket管道的超時(shí),那有什么理由不重試凄吏?(后續(xù)還會(huì)涉及到路由)

3远舅、SSL證書(shū)異常/SSL驗(yàn)證失敗異常,前者是證書(shū)驗(yàn)證失敗痕钢,后者可能就是壓根就沒(méi)證書(shū)图柏,或者證書(shū)數(shù)據(jù)不正確,那還怎么重試

經(jīng)過(guò)了異常的判定之后任连,如果仍然允許進(jìn)行重試蚤吹,就會(huì)再檢查當(dāng)前有沒(méi)有可用路由路線(xiàn)來(lái)進(jìn)行連接。簡(jiǎn)單來(lái)說(shuō),比如 DNS 對(duì)域名解析后可能會(huì)返回多個(gè) IP裁着,在一個(gè)IP失敗后繁涂,嘗試另一個(gè)IP進(jìn)行重試。

重定向

如果請(qǐng)求結(jié)束后沒(méi)有發(fā)生異常并不代表當(dāng)前獲得的響應(yīng)就是最終需要交給用戶(hù)的二驰,還需要進(jìn)一步來(lái)判斷是否需要重定向的判斷扔罪。重定向的判斷位于followUpRequest方法

private Request followUpRequest(Response userResponse) throws IOException {
    if (userResponse == null) throw new IllegalStateException();
    Connection connection = streamAllocation.connection();
    Route route = connection != null
        ? connection.route()
        : null;
    int responseCode = userResponse.code();

    final String method = userResponse.request().method();
    switch (responseCode) {
      // 407 客戶(hù)端使用了HTTP代理服務(wù)器,在請(qǐng)求頭中添加 “Proxy-Authorization”桶雀,讓代理服務(wù)器授權(quán)
      case HTTP_PROXY_AUTH:
        Proxy selectedProxy = route != null
            ? route.proxy()
            : client.proxy();
        if (selectedProxy.type() != Proxy.Type.HTTP) {
          throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy");
        }
        return client.proxyAuthenticator().authenticate(route, userResponse);
      // 401 需要身份驗(yàn)證 有些服務(wù)器接口需要驗(yàn)證使用者身份 在請(qǐng)求頭中添加 “Authorization” 
      case HTTP_UNAUTHORIZED:
        return client.authenticator().authenticate(route, userResponse);
      // 308 永久重定向 
      // 307 臨時(shí)重定向
      case HTTP_PERM_REDIRECT:
      case HTTP_TEMP_REDIRECT:
        // 如果請(qǐng)求方式不是GET或者HEAD矿酵,框架不會(huì)自動(dòng)重定向請(qǐng)求
        if (!method.equals("GET") && !method.equals("HEAD")) {
          return null;
        }
      // 300 301 302 303 
      case HTTP_MULT_CHOICE:
      case HTTP_MOVED_PERM:
      case HTTP_MOVED_TEMP:
      case HTTP_SEE_OTHER:
        // 如果用戶(hù)不允許重定向,那就返回null
        if (!client.followRedirects()) return null;
        // 從響應(yīng)頭取出location 
        String location = userResponse.header("Location");
        if (location == null) return null;
        // 根據(jù)location 配置新的請(qǐng)求 url
        HttpUrl url = userResponse.request().url().resolve(location);
        // 如果為null矗积,說(shuō)明協(xié)議有問(wèn)題坏瘩,取不出來(lái)HttpUrl,那就返回null漠魏,不進(jìn)行重定向
        if (url == null) return null;
        // 如果重定向在http到https之間切換倔矾,需要檢查用戶(hù)是不是允許(默認(rèn)允許)
        boolean sameScheme = url.scheme().equals(userResponse.request().url().scheme());
        if (!sameScheme && !client.followSslRedirects()) return null;

        Request.Builder requestBuilder = userResponse.request().newBuilder();
        /**
         *  重定向請(qǐng)求中 只要不是 PROPFIND 請(qǐng)求,無(wú)論是POST還是其他的方法都要改為GET請(qǐng)求方式柱锹,
         *  即只有 PROPFIND 請(qǐng)求才能有請(qǐng)求體
         */
        //請(qǐng)求不是get與head
        if (HttpMethod.permitsRequestBody(method)) {
          final boolean maintainBody = HttpMethod.redirectsWithBody(method);
           // 除了 PROPFIND 請(qǐng)求之外都改成GET請(qǐng)求
          if (HttpMethod.redirectsToGet(method)) {
            requestBuilder.method("GET", null);
          } else {
            RequestBody requestBody = maintainBody ? userResponse.request().body() : null;
            requestBuilder.method(method, requestBody);
          }
          // 不是 PROPFIND 的請(qǐng)求哪自,把請(qǐng)求頭中關(guān)于請(qǐng)求體的數(shù)據(jù)刪掉
          if (!maintainBody) {
            requestBuilder.removeHeader("Transfer-Encoding");
            requestBuilder.removeHeader("Content-Length");
            requestBuilder.removeHeader("Content-Type");
          }
        }

        // 在跨主機(jī)重定向時(shí),刪除身份驗(yàn)證請(qǐng)求頭
        if (!sameConnection(userResponse, url)) {
          requestBuilder.removeHeader("Authorization");
        }

        return requestBuilder.url(url).build();

      // 408 客戶(hù)端請(qǐng)求超時(shí) 
      case HTTP_CLIENT_TIMEOUT:
        // 408 算是連接失敗了禁熏,所以判斷用戶(hù)是不是允許重試
        if (!client.retryOnConnectionFailure()) {
            return null;
        }
        // UnrepeatableRequestBody實(shí)際并沒(méi)發(fā)現(xiàn)有其他地方用到
        if (userResponse.request().body() instanceof UnrepeatableRequestBody) {
            return null;
        }
        // 如果是本身這次的響應(yīng)就是重新請(qǐng)求的產(chǎn)物同時(shí)上一次之所以重請(qǐng)求還是因?yàn)?08壤巷,那我們這次不再重請(qǐng)求了
        if (userResponse.priorResponse() != null
                        && userResponse.priorResponse().code() == HTTP_CLIENT_TIMEOUT) {
            return null;
        }
        // 如果服務(wù)器告訴我們了 Retry-After 多久后重試,那框架不管了瞧毙。
        if (retryAfter(userResponse, 0) > 0) {
            return null;
        }
        return userResponse.request();
       // 503 服務(wù)不可用 和408差不多胧华,但是只在服務(wù)器告訴你 Retry-After:0(意思就是立即重試) 才重請(qǐng)求
       case HTTP_UNAVAILABLE:
        if (userResponse.priorResponse() != null
                        && userResponse.priorResponse().code() == HTTP_UNAVAILABLE) {
            return null;
         }

         if (retryAfter(userResponse, Integer.MAX_VALUE) == 0) {
            return userResponse.request();
         }

         return null;
      default:
        return null;
    }
}

整個(gè)是否需要重定向的判斷內(nèi)容很多,記不住宙彪,這很正常矩动,關(guān)鍵在于理解他們的意思。如果此方法返回空释漆,那就表示不需要再重定向了悲没,直接返回響應(yīng);但是如果返回非空男图,那就要重新請(qǐng)求返回的Request示姿,但是需要注意的是,我們的followup在攔截器中定義的最大次數(shù)為20次逊笆。

總結(jié)

本攔截器是整個(gè)責(zé)任鏈中的第一個(gè)栈戳,這意味著它會(huì)是首次接觸到Request與最后接收到Response的角色,在這個(gè)攔截器中主要功能就是判斷是否需要重試與重定向难裆。

重試的前提是出現(xiàn)了RouteException或者IOException子檀。一但在后續(xù)的攔截器執(zhí)行過(guò)程中出現(xiàn)這兩個(gè)異常,就會(huì)通過(guò)recover方法進(jìn)行判斷是否進(jìn)行連接重試。

重定向發(fā)生在重試的判定之后命锄,如果不滿(mǎn)足重試的條件,還需要進(jìn)一步調(diào)用followUpRequest根據(jù)Response 的響應(yīng)碼(當(dāng)然偏化,如果直接請(qǐng)求失敗脐恩,Response都不存在就會(huì)拋出異常)。followup最大發(fā)生20次侦讨。

二驶冒、橋接攔截器

BridgeInterceptor,連接應(yīng)用程序和服務(wù)器的橋梁韵卤,我們發(fā)出的請(qǐng)求將會(huì)經(jīng)過(guò)它的處理才能發(fā)給服務(wù)器骗污,比如設(shè)置請(qǐng)求內(nèi)容長(zhǎng)度,編碼沈条,gzip壓縮需忿,cookie等,獲取響應(yīng)后保存Cookie等操作蜡歹。這個(gè)攔截器相對(duì)比較簡(jiǎn)單屋厘。

補(bǔ)全請(qǐng)求頭:

請(qǐng)求頭 說(shuō)明
Content-Type 請(qǐng)求體類(lèi)型,如:application/x-www-form-urlencoded
Content-Length/Transfer-Encoding 請(qǐng)求體解析方式
Host 請(qǐng)求的主機(jī)站點(diǎn)
Connection: Keep-Alive 保持長(zhǎng)連接
Accept-Encoding: gzip 接受響應(yīng)支持gzip壓縮
Cookie cookie身份辨別
User-Agent 請(qǐng)求的用戶(hù)信息,如:操作系統(tǒng)月而、瀏覽器等

在補(bǔ)全了請(qǐng)求頭后交給下一個(gè)攔截器處理汗洒,得到響應(yīng)后,主要干兩件事情:

1父款、保存cookie溢谤,在下次請(qǐng)求則會(huì)讀取對(duì)應(yīng)的數(shù)據(jù)設(shè)置進(jìn)入請(qǐng)求頭,默認(rèn)的CookieJar不提供實(shí)現(xiàn)

2憨攒、如果使用gzip返回的數(shù)據(jù)世杀,則使用GzipSource包裝便于解析。

總結(jié)

橋接攔截器的執(zhí)行邏輯主要就是以下幾點(diǎn)

對(duì)用戶(hù)構(gòu)建的Request進(jìn)行添加或者刪除相關(guān)頭部信息肝集,以轉(zhuǎn)化成能夠真正進(jìn)行網(wǎng)絡(luò)請(qǐng)求的Request
將符合網(wǎng)絡(luò)請(qǐng)求規(guī)范的Request交給下一個(gè)攔截器處理玫坛,并獲取Response
如果響應(yīng)體經(jīng)過(guò)了GZIP壓縮,那就需要解壓包晰,再構(gòu)建成用戶(hù)可用的Response并返回

三湿镀、緩存攔截器

CacheInterceptor,在發(fā)出請(qǐng)求前伐憾,判斷是否命中緩存勉痴。如果命中則可以不請(qǐng)求,直接使用緩存的響應(yīng)树肃。 (只會(huì)存在Get請(qǐng)求的緩存)

步驟為:

1蒸矛、從緩存中獲得對(duì)應(yīng)請(qǐng)求的響應(yīng)緩存

2、創(chuàng)建CacheStrategy ,創(chuàng)建時(shí)會(huì)判斷是否能夠使用緩存,在CacheStrategy 中存在兩個(gè)成員:networkRequestcacheResponse雏掠。他們的組合如下:

networkRequest cacheResponse 說(shuō)明
Null Not Null 直接使用緩存
Not Null Null 向服務(wù)器發(fā)起請(qǐng)求
Null Null 直接gg斩祭,okhttp直接返回504
Not Null Not Null 發(fā)起請(qǐng)求,若得到響應(yīng)為304(無(wú)修改)乡话,則更新緩存響應(yīng)并返回

3摧玫、交給下一個(gè)責(zé)任鏈繼續(xù)處理

4、后續(xù)工作绑青,返回304則用緩存的響應(yīng)诬像;否則使用網(wǎng)絡(luò)響應(yīng)并緩存本次響應(yīng)(只緩存Get請(qǐng)求的響應(yīng))

緩存攔截器的工作說(shuō)起來(lái)比較簡(jiǎn)單,但是具體的實(shí)現(xiàn)闸婴,需要處理的內(nèi)容很多坏挠。在緩存攔截器中判斷是否可以使用緩存,或是請(qǐng)求服務(wù)器都是通過(guò)CacheStrategy判斷邪乍。

緩存策略

CacheStrategy降狠。首先需要認(rèn)識(shí)幾個(gè)請(qǐng)求頭與響應(yīng)頭

響應(yīng)頭 說(shuō)明 例子
Date 消息發(fā)送的時(shí)間 Date: Sat, 18 Nov 2028 06:17:41 GMT
Expires 資源過(guò)期的時(shí)間 Expires: Sat, 18 Nov 2028 06:17:41 GMT
Last-Modified 資源最后修改時(shí)間 Last-Modified: Fri, 22 Jul 2016 02:57:17 GMT
ETag 資源在服務(wù)器的唯一標(biāo)識(shí) ETag: "16df0-5383097a03d40"
Age 服務(wù)器用緩存響應(yīng)請(qǐng)求,該緩存從產(chǎn)生到現(xiàn)在經(jīng)過(guò)多長(zhǎng)時(shí)間(秒) Age: 3825683
Cache-Control - -
請(qǐng)求頭 說(shuō)明 例子
If-Modified-Since 服務(wù)器沒(méi)有在指定的時(shí)間后修改請(qǐng)求對(duì)應(yīng)資源,返回304(無(wú)修改) If-Modified-Since: Fri, 22 Jul 2016 02:57:17 GMT
If-None-Match 服務(wù)器將其與請(qǐng)求對(duì)應(yīng)資源的Etag值進(jìn)行比較庇楞,匹配返回304 If-None-Match: "16df0-5383097a03d40"
Cache-Control - -

其中Cache-Control可以在請(qǐng)求頭存在喊熟,也能在響應(yīng)頭存在,對(duì)應(yīng)的value可以設(shè)置多種組合:

  1. max-age=[秒] :資源最大有效時(shí)間;
  2. public :表明該資源可以被任何用戶(hù)緩存姐刁,比如客戶(hù)端芥牌,代理服務(wù)器等都可以緩存資源;
  3. private:表明該資源只能被單個(gè)用戶(hù)緩存,默認(rèn)是private聂使。
  4. no-store:資源不允許被緩存
  5. no-cache:(請(qǐng)求)不使用緩存
  6. immutable:(響應(yīng))資源不會(huì)改變
  7. min-fresh=[秒]:(請(qǐng)求)緩存最小新鮮度(用戶(hù)認(rèn)為這個(gè)緩存有效的時(shí)長(zhǎng))
  8. must-revalidate:(響應(yīng))不允許使用過(guò)期緩存
  9. max-stale=[秒]:(請(qǐng)求)緩存過(guò)期后多久內(nèi)仍然有效

假設(shè)存在max-age=100壁拉,min-fresh=20。這代表了用戶(hù)認(rèn)為這個(gè)緩存的響應(yīng)柏靶,從服務(wù)器創(chuàng)建響應(yīng) 到 能夠緩存使用的時(shí)間為100-20=80s弃理。但是如果max-stale=100。這代表了緩存有效時(shí)間80s過(guò)后屎蜓,仍然允許使用100s痘昌,可以看成緩存有效時(shí)長(zhǎng)為180s。

緩存控制
詳細(xì)流程

如果從緩存中獲得了本次請(qǐng)求URL對(duì)應(yīng)的Response炬转,首先會(huì)從響應(yīng)中獲得以上數(shù)據(jù)備用辆苔。

public Factory(long nowMillis, Request request, Response cacheResponse) {
            this.nowMillis = nowMillis;
            this.request = request;
            this.cacheResponse = cacheResponse;

            if (cacheResponse != null) {
                //對(duì)應(yīng)響應(yīng)的請(qǐng)求發(fā)出的本地時(shí)間 和 接收到響應(yīng)的本地時(shí)間
                this.sentRequestMillis = cacheResponse.sentRequestAtMillis();
                this.receivedResponseMillis = cacheResponse.receivedResponseAtMillis();
                Headers headers = cacheResponse.headers();
                for (int i = 0, size = headers.size(); i < size; i++) {
                    String fieldName = headers.name(i);
                    String value = headers.value(i);
                    if ("Date".equalsIgnoreCase(fieldName)) {
                        servedDate = HttpDate.parse(value);
                        servedDateString = value;
                    } else if ("Expires".equalsIgnoreCase(fieldName)) {
                        expires = HttpDate.parse(value);
                    } else if ("Last-Modified".equalsIgnoreCase(fieldName)) {
                        lastModified = HttpDate.parse(value);
                        lastModifiedString = value;
                    } else if ("ETag".equalsIgnoreCase(fieldName)) {
                        etag = value;
                    } else if ("Age".equalsIgnoreCase(fieldName)) {
                        ageSeconds = HttpHeaders.parseSeconds(value, -1);
                    }
                }
            }
        }

判斷緩存的命中會(huì)使用get()方法

public CacheStrategy get() {
    CacheStrategy candidate = getCandidate();
    //todo 如果可以使用緩存,那networkRequest必定為null扼劈;指定了只使用緩存但是networkRequest又不為null驻啤,沖突。那就gg(攔截器返回504)
    if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
        // We're forbidden from using the network and the cache is insufficient.
        return new CacheStrategy(null, null);
    }
    return candidate;
}

方法中調(diào)用getCandidate()方法來(lái)完成真正的緩存判斷荐吵。

1骑冗、緩存是否存在

整個(gè)方法中的第一個(gè)判斷是緩存是不是存在:

if (cacheResponse == null) {
    return new CacheStrategy(request, null);
}

cacheResponse是從緩存中找到的響應(yīng)赊瞬,如果為null,那就表示沒(méi)有找到對(duì)應(yīng)的緩存贼涩,創(chuàng)建的CacheStrategy實(shí)例對(duì)象只存在networkRequest巧涧,這代表了需要發(fā)起網(wǎng)絡(luò)請(qǐng)求。

2遥倦、https請(qǐng)求的緩存

繼續(xù)往下走意味著cacheResponse必定存在谤绳,但是它不一定能用。后續(xù)進(jìn)行有效性的一系列判斷

if (request.isHttps() && cacheResponse.handshake() == null) {
    return new CacheStrategy(request, null);
}

如果本次請(qǐng)求是HTTPS谊迄,但是緩存中沒(méi)有對(duì)應(yīng)的握手信息闷供,那么緩存無(wú)效烟央。

3统诺、響應(yīng)碼以及響應(yīng)頭
if (!isCacheable(cacheResponse, request)) {
    return new CacheStrategy(request, null);
}

整個(gè)邏輯都在isCacheable中,他的內(nèi)容是:

public static boolean isCacheable(Response response, Request request) {
        // Always go to network for uncacheable response codes (RFC 7231 section 6.1),
        // This implementation doesn't support caching partial content.
        switch (response.code()) {
            case HTTP_OK:
            case HTTP_NOT_AUTHORITATIVE:
            case HTTP_NO_CONTENT:
            case HTTP_MULT_CHOICE:
            case HTTP_MOVED_PERM:
            case HTTP_NOT_FOUND:
            case HTTP_BAD_METHOD:
            case HTTP_GONE:
            case HTTP_REQ_TOO_LONG:
            case HTTP_NOT_IMPLEMENTED:
            case StatusLine.HTTP_PERM_REDIRECT:
                // These codes can be cached unless headers forbid it.
                break;

            case HTTP_MOVED_TEMP:
            case StatusLine.HTTP_TEMP_REDIRECT:
                // These codes can only be cached with the right response headers.
                // http://tools.ietf.org/html/rfc7234#section-3
                // s-maxage is not checked because OkHttp is a private cache that should ignore
                // s-maxage.
                if (response.header("Expires") != null
                        || response.cacheControl().maxAgeSeconds() != -1
                        || response.cacheControl().isPublic()
                        || response.cacheControl().isPrivate()) {
                    break;
                }
                // Fall-through.
            default:
                // All other codes cannot be cached.
                return false;
        }

        // A 'no-store' directive on request or response prevents the response from being cached.
        return !response.cacheControl().noStore() && !request.cacheControl().noStore();
}

緩存響應(yīng)中的響應(yīng)碼為 200, 203, 204, 300, 301, 404, 405, 410, 414, 501, 308 的情況下,只判斷服務(wù)器是不是給了 Cache-Control: no-store (資源不能被緩存)疑俭,所以如果服務(wù)器給到了這個(gè)響應(yīng)頭粮呢,那就和前面兩個(gè)判定一致(緩存不可用)。否則繼續(xù)進(jìn)一步判斷緩存是否可用

而如果響應(yīng)碼是302/307(重定向)钞艇,則需要進(jìn)一步判斷是不是存在一些允許緩存的響應(yīng)頭啄寡。根據(jù)注解中的給到的文檔http://tools.ietf.org/html/rfc7234#section-3中的描述,如果存在Expires或者Cache-Control的值為:

  1. max-age=[秒] :資源最大有效時(shí)間;

  2. public :表明該資源可以被任何用戶(hù)緩存哩照,比如客戶(hù)端挺物,代理服務(wù)器等都可以緩存資源;

  3. private:表明該資源只能被單個(gè)用戶(hù)緩存,默認(rèn)是private飘弧。

同時(shí)不存在Cache-Control: no-store识藤,那就可以繼續(xù)進(jìn)一步判斷緩存是否可用。

所以綜合來(lái)看判定優(yōu)先級(jí)如下:

1次伶、響應(yīng)碼不為 200, 203, 204, 300, 301, 404, 405, 410, 414, 501, 308痴昧,302307 緩存不可用;

2冠王、當(dāng)響應(yīng)碼為302或者307時(shí)赶撰,未包含某些響應(yīng)頭,則緩存不可用;

3柱彻、當(dāng)存在Cache-Control: no-store響應(yīng)頭則緩存不可用豪娜。

如果響應(yīng)緩存可用,進(jìn)一步再判斷緩存有效性

4哟楷、用戶(hù)的請(qǐng)求配置
CacheControl requestCaching = request.cacheControl();
if (requestCaching.noCache() || hasConditions(request)) {
    return new CacheStrategy(request, null);
}
private static boolean hasConditions(Request request) {
    return request.header("If-Modified-Since") != null || request.header("If-None-Match") != null;
}

走到這一步侵歇,OkHttp需要先對(duì)用戶(hù)本次發(fā)起的Request進(jìn)行判定,如果用戶(hù)指定了Cache-Control: no-cache(不使用緩存)的請(qǐng)求頭或者請(qǐng)求頭包含 If-Modified-SinceIf-None-Match(請(qǐng)求驗(yàn)證)吓蘑,那么就不允許使用緩存惕虑。

請(qǐng)求頭 說(shuō)明
Cache-Control: no-cache 忽略緩存
If-Modified-Since: 時(shí)間 值一般為DatalastModified坟冲,服務(wù)器沒(méi)有在指定的時(shí)間后修改請(qǐng)求對(duì)應(yīng)資源,返回304(無(wú)修改)
If-None-Match:標(biāo)記 值一般為Etag,將其與請(qǐng)求對(duì)應(yīng)資源的Etag值進(jìn)行比較;如果匹配溃蔫,返回304

這意味著如果用戶(hù)請(qǐng)求頭中包含了這些內(nèi)容健提,那就必須向服務(wù)器發(fā)起請(qǐng)求。但是需要注意的是伟叛,OkHttp并不會(huì)緩存304的響應(yīng)私痹,如果是此種情況,即用戶(hù)主動(dòng)要求與服務(wù)器發(fā)起請(qǐng)求统刮,服務(wù)器返回的304(無(wú)響應(yīng)體)紊遵,則直接把304的響應(yīng)返回給用戶(hù):“既然你主動(dòng)要求,我就只告知你本次請(qǐng)求結(jié)果”侥蒙。而如果不包含這些請(qǐng)求頭暗膜,那繼續(xù)判定緩存有效性。

5鞭衩、資源是否不變
CacheControl responseCaching = cacheResponse.cacheControl();
if (responseCaching.immutable()) {
    return new CacheStrategy(null, cacheResponse);
}

如果緩存的響應(yīng)中包含Cache-Control: immutable学搜,這意味著對(duì)應(yīng)請(qǐng)求的響應(yīng)內(nèi)容將一直不會(huì)改變。此時(shí)就可以直接使用緩存论衍。否則繼續(xù)判斷緩存是否可用

6瑞佩、響應(yīng)的緩存有效期

這一步為進(jìn)一步根據(jù)緩存響應(yīng)中的一些信息判定緩存是否處于有效期內(nèi)。如果滿(mǎn)足:

緩存存活時(shí)間 < 緩存新鮮度 - 緩存最小新鮮度 + 過(guò)期后繼續(xù)使用時(shí)長(zhǎng)

代表可以使用緩存坯台。其中新鮮度可以理解為有效時(shí)間炬丸,而這里的 "緩存新鮮度-緩存最小新鮮度" 就代表了緩存真正有效的時(shí)間。

// 6.1蜒蕾、獲得緩存的響應(yīng)從創(chuàng)建到現(xiàn)在的時(shí)間
long ageMillis = cacheResponseAge();
//todo
// 6.2稠炬、獲取這個(gè)響應(yīng)有效緩存的時(shí)長(zhǎng)
long freshMillis = computeFreshnessLifetime();
if (requestCaching.maxAgeSeconds() != -1) {
//todo 如果請(qǐng)求中指定了 max-age 表示指定了能拿的緩存有效時(shí)長(zhǎng),就需要綜合響應(yīng)有效緩存時(shí)長(zhǎng)與請(qǐng)求能拿緩存的時(shí)長(zhǎng)滥搭,獲得最小的能夠使用響應(yīng)緩存的時(shí)長(zhǎng)
        freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
}
// 6.3 請(qǐng)求包含  Cache-Control:min-fresh=[秒]  能夠使用還未過(guò)指定時(shí)間的緩存 (請(qǐng)求認(rèn)為的緩存有效時(shí)間)
long minFreshMillis = 0;
if (requestCaching.minFreshSeconds() != -1) {
    minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
}
// 6.4
//  6.4.1酸纲、Cache-Control:must-revalidate 可緩存但必須再向源服務(wù)器進(jìn)行確認(rèn)
//  6.4.2、Cache-Control:max-stale=[秒] 緩存過(guò)期后還能使用指定的時(shí)長(zhǎng)  如果未指定多少秒瑟匆,則表示無(wú)論過(guò)期多長(zhǎng)時(shí)間都可以闽坡;如果指定了,則只要是指定時(shí)間內(nèi)就能使用緩存
    // 前者會(huì)忽略后者愁溜,所以判斷了不必須向服務(wù)器確認(rèn)疾嗅,再獲得請(qǐng)求頭中的max-stale
long maxStaleMillis = 0;
if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
    maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
}

// 6.5 不需要與服務(wù)器驗(yàn)證有效性 && 響應(yīng)存在的時(shí)間+請(qǐng)求認(rèn)為的緩存有效時(shí)間 小于 緩存有效時(shí)長(zhǎng)+過(guò)期后還可以使用的時(shí)間
// 允許使用緩存
if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
    Response.Builder builder = cacheResponse.newBuilder();
    //todo 如果已過(guò)期,但未超過(guò) 過(guò)期后繼續(xù)使用時(shí)長(zhǎng)冕象,那還可以繼續(xù)使用代承,只用添加相應(yīng)的頭部字段
    if (ageMillis + minFreshMillis >= freshMillis) {
        builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"");
    }
    //todo 如果緩存已超過(guò)一天并且響應(yīng)中沒(méi)有設(shè)置過(guò)期時(shí)間也需要添加警告
    long oneDayMillis = 24 * 60 * 60 * 1000L;
    if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
        builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
    }
    return new CacheStrategy(null, builder.build());
}

6.1、緩存到現(xiàn)在存活的時(shí)間:ageMillis

首先cacheResponseAge()方法獲得了響應(yīng)大概存在了多久:

long ageMillis = cacheResponseAge();

private long cacheResponseAge() {
    long apparentReceivedAge = servedDate != null
                    ? Math.max(0, receivedResponseMillis - servedDate.getTime())
                    : 0;
    long receivedAge = ageSeconds != -1
                    ? Math.max(apparentReceivedAge, SECONDS.toMillis(ageSeconds))
                    : apparentReceivedAge;
    long responseDuration = receivedResponseMillis - sentRequestMillis;
    long residentDuration = nowMillis - receivedResponseMillis;
    return receivedAge + responseDuration + residentDuration;
}

1渐扮、apparentReceivedAge代表了客戶(hù)端收到響應(yīng)到服務(wù)器發(fā)出響應(yīng)的一個(gè)時(shí)間差

seredData是從緩存中獲得的Data響應(yīng)頭對(duì)應(yīng)的時(shí)間(服務(wù)器發(fā)出本響應(yīng)的時(shí)間)论悴;
receivedResponseMillis為本次響應(yīng)對(duì)應(yīng)的客戶(hù)端發(fā)出請(qǐng)求的時(shí)間

2掖棉、receivedAge是代表了客戶(hù)端的緩存,在收到時(shí)就已經(jīng)存在多久了

ageSeconds是從緩存中獲得的Age響應(yīng)頭對(duì)應(yīng)的秒數(shù) (本地緩存的響應(yīng)是由服務(wù)器的緩存返回膀估,這個(gè)緩存在服務(wù)器存在的時(shí)間)

ageSeconds與上一步計(jì)算結(jié)果apparentReceivedAge的最大值為收到響應(yīng)時(shí)幔亥,這個(gè)響應(yīng)數(shù)據(jù)已經(jīng)存在多久。

假設(shè)我們發(fā)出請(qǐng)求時(shí)察纯,服務(wù)器存在一個(gè)緩存帕棉,其中 Data: 0點(diǎn)
此時(shí)饼记,客戶(hù)端在1小時(shí)候發(fā)起請(qǐng)求香伴,此時(shí)由服務(wù)器在緩存中插入Age: 1小時(shí)并返回給客戶(hù)端,此時(shí)客戶(hù)端計(jì)算的receivedAge就是1小時(shí)具则,這就代表了客戶(hù)端的緩存在收到時(shí)就已經(jīng)存在多久了即纲。(不代表到本次請(qǐng)求時(shí)存在多久了)

3、responseDuration是緩存對(duì)應(yīng)的請(qǐng)求乡洼,在發(fā)送請(qǐng)求與接收請(qǐng)求之間的時(shí)間差

4崇裁、residentDuration是這個(gè)緩存接收到的時(shí)間到現(xiàn)在的一個(gè)時(shí)間差

receivedAge + responseDuration + residentDuration所代表的意義就是:

緩存在客戶(hù)端收到時(shí)就已經(jīng)存在的時(shí)間 + 請(qǐng)求過(guò)程中花費(fèi)的時(shí)間 + 本次請(qǐng)求距離緩存獲得的時(shí)間匕坯,就是緩存真正存在了多久束昵。

6.2、緩存新鮮度(有效時(shí)間):freshMillis

long freshMillis = computeFreshnessLifetime();

private long computeFreshnessLifetime() {
    CacheControl responseCaching = cacheResponse.cacheControl();
            
    if (responseCaching.maxAgeSeconds() != -1) {
        return SECONDS.toMillis(responseCaching.maxAgeSeconds());
    } else if (expires != null) {
        long servedMillis = servedDate != null ? servedDate.getTime() : receivedResponseMillis;
        long delta = expires.getTime() - servedMillis;
        return delta > 0 ? delta : 0;
    } else if (lastModified != null && cacheResponse.request().url().query() == null) {
        // As recommended by the HTTP RFC and implemented in Firefox, the
        // max age of a document should be defaulted to 10% of the
        // document's age at the time it was served. Default expiration
        // dates aren't used for URIs containing a query.
        long servedMillis = servedDate != null ? servedDate.getTime() : sentRequestMillis;
        long delta = servedMillis - lastModified.getTime();
        return delta > 0 ? (delta / 10) : 0;
    }
    return 0;
}

緩存新鮮度(有效時(shí)長(zhǎng))的判定會(huì)有幾種情況葛峻,按優(yōu)先級(jí)排列如下:

1锹雏、緩存響應(yīng)包含Cache-Control: max-age=[秒]資源最大有效時(shí)間

2、緩存響應(yīng)包含Expires: 時(shí)間术奖,則通過(guò)Data或接收該響應(yīng)時(shí)間計(jì)算資源有效時(shí)間

3礁遵、緩存響應(yīng)包含Last-Modified: 時(shí)間,則通過(guò)Data或發(fā)送該響應(yīng)對(duì)應(yīng)請(qǐng)求的時(shí)間計(jì)算資源有效時(shí)間采记;并且根據(jù)建議以及在Firefox瀏覽器的實(shí)現(xiàn)佣耐,使用得到結(jié)果的10%來(lái)作為資源的有效時(shí)間。

6.3唧龄、緩存最小新鮮度:minFreshMillis

long minFreshMillis = 0;
if (requestCaching.minFreshSeconds() != -1) {
    minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
}

如果用戶(hù)的請(qǐng)求頭中包含Cache-Control: min-fresh=[秒]兼砖,代表用戶(hù)認(rèn)為這個(gè)緩存有效的時(shí)長(zhǎng)。假設(shè)本身緩存新鮮度為: 100毫秒既棺,而緩存最小新鮮度為:10毫秒讽挟,那么緩存真正有效時(shí)間為90毫秒。

6.4丸冕、緩存過(guò)期后仍有效時(shí)長(zhǎng):maxStaleMillis

long maxStaleMillis = 0;
if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
    maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
}

這個(gè)判斷中第一個(gè)條件為緩存的響應(yīng)中沒(méi)有包含Cache-Control: must-revalidate(不可用過(guò)期資源)耽梅,獲得用戶(hù)請(qǐng)求頭中包含Cache-Control: max-stale=[秒]緩存過(guò)期后仍有效的時(shí)長(zhǎng)。

6.5胖烛、判定緩存是否有效

if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
    Response.Builder builder = cacheResponse.newBuilder();
    //todo 如果已過(guò)期眼姐,但未超過(guò) 過(guò)期后繼續(xù)使用時(shí)長(zhǎng)诅迷,那還可以繼續(xù)使用,只用添加相應(yīng)的頭部字段
    if (ageMillis + minFreshMillis >= freshMillis) {
        builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"");
    }
    //todo 如果緩存已超過(guò)一天并且響應(yīng)中沒(méi)有設(shè)置過(guò)期時(shí)間也需要添加警告
    long oneDayMillis = 24 * 60 * 60 * 1000L;
    if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
        builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
    }
    return new CacheStrategy(null, builder.build());
}

最后利用上4步產(chǎn)生的值众旗,只要緩存的響應(yīng)未指定no-cache忽略緩存竟贯,如果:

緩存存活時(shí)間+緩存最小新鮮度 < 緩存新鮮度+過(guò)期后繼續(xù)使用時(shí)長(zhǎng),代表可以使用緩存逝钥。

假設(shè) 緩存到現(xiàn)在存活了:100 毫秒;
用戶(hù)認(rèn)為緩存有效時(shí)間(緩存最小新鮮度)為:10 毫秒;
緩存新鮮度為: 100 毫秒;
緩存過(guò)期后仍能使用: 0 毫秒;
這些條件下屑那,首先緩存的真實(shí)有效時(shí)間為: 90毫秒,而緩存已經(jīng)過(guò)了這個(gè)時(shí)間艘款,所以無(wú)法使用緩存持际。

不等式可以轉(zhuǎn)換為: 緩存存活時(shí)間 < 緩存新鮮度 - 緩存最小新鮮度 + 過(guò)期后繼續(xù)使用時(shí)長(zhǎng),即
存活時(shí)間 < 緩存有效時(shí)間 + 過(guò)期后繼續(xù)使用時(shí)間

總體來(lái)說(shuō)哗咆,只要不忽略緩存并且緩存未過(guò)期蜘欲,則使用緩存。

7晌柬、緩存過(guò)期處理

String conditionName;
String conditionValue;
if (etag != null) {
    conditionName = "If-None-Match";
    conditionValue = etag;
} else if (lastModified != null) {
    conditionName = "If-Modified-Since";
    conditionValue = lastModifiedString;
} else if (servedDate != null) {
    conditionName = "If-Modified-Since";
    conditionValue = servedDateString;
} else {
    //意味著無(wú)法與服務(wù)器發(fā)起比較姥份,只能重新請(qǐng)求
    return new CacheStrategy(request, null); // No condition! Make a regular request.
}

//添加請(qǐng)求頭
Headers.Builder conditionalRequestHeaders = request.headers().newBuilder();
Internal.instance.addLenient(conditionalRequestHeaders, conditionName, conditionValue);
Request conditionalRequest = request.newBuilder()
    .headers(conditionalRequestHeaders.build())
    .build();
return new CacheStrategy(conditionalRequest, cacheResponse);

如果繼續(xù)執(zhí)行,表示緩存已經(jīng)過(guò)期無(wú)法使用年碘。此時(shí)我們判定緩存的響應(yīng)中如果存在Etag澈歉,則使用If-None-Match交給服務(wù)器進(jìn)行驗(yàn)證;如果存在Last-Modified或者Data屿衅,則使用If-Modified-Since交給服務(wù)器驗(yàn)證埃难。服務(wù)器如果無(wú)修改則會(huì)返回304,這時(shí)候注意:

由于是緩存過(guò)期而發(fā)起的請(qǐng)求(與第4個(gè)判斷用戶(hù)的主動(dòng)設(shè)置不同)涤久,如果服務(wù)器返回304涡尘,那框架會(huì)自動(dòng)更新緩存,所以此時(shí)CacheStrategy既包含networkRequest也包含cacheResponse

8响迂、收尾

至此考抄,緩存的判定結(jié)束,攔截器中只需要判斷CacheStrategynetworkRequestcacheResponse的不同組合就能夠判斷是否允許使用緩存蔗彤。

但是需要注意的是川梅,如果用戶(hù)在創(chuàng)建請(qǐng)求時(shí),配置了onlyIfCached這意味著用戶(hù)這次希望這個(gè)請(qǐng)求只從緩存獲得幕与,不需要發(fā)起請(qǐng)求挑势。那如果生成的CacheStrategy存在networkRequest這意味著肯定會(huì)發(fā)起請(qǐng)求,此時(shí)出現(xiàn)沖突啦鸣!那會(huì)直接給到攔截器一個(gè)既沒(méi)有networkRequest又沒(méi)有cacheResponse的對(duì)象潮饱。攔截器直接返回用戶(hù)504

//緩存策略 get 方法
if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
    // We're forbidden from using the network and the cache is insufficient.
    return new CacheStrategy(null, null);
}

//緩存攔截器
if (networkRequest == null && cacheResponse == null) {
    return new Response.Builder()
                    .request(chain.request())
                    .protocol(Protocol.HTTP_1_1)
                    .code(504)
                    .message("Unsatisfiable Request (only-if-cached)")
                    .body(Util.EMPTY_RESPONSE)
                    .sentRequestAtMillis(-1L)
                    .receivedResponseAtMillis(System.currentTimeMillis())
                    .build();
}
9诫给、總結(jié)

1香拉、如果從緩存獲取的Response是null啦扬,那就需要使用網(wǎng)絡(luò)請(qǐng)求獲取響應(yīng);
2凫碌、如果是Https請(qǐng)求扑毡,但是又丟失了握手信息,那也不能使用緩存盛险,需要進(jìn)行網(wǎng)絡(luò)請(qǐng)求瞄摊;
3籽懦、如果判斷響應(yīng)碼不能緩存且響應(yīng)頭有no-store標(biāo)識(shí)纬傲,那就需要進(jìn)行網(wǎng)絡(luò)請(qǐng)求;
4涯塔、如果請(qǐng)求頭有no-cache標(biāo)識(shí)或者有If-Modified-Since/If-None-Match鹤啡,那么需要進(jìn)行網(wǎng)絡(luò)請(qǐng)求惯驼;
5、如果響應(yīng)頭沒(méi)有no-cache標(biāo)識(shí)递瑰,且緩存時(shí)間沒(méi)有超過(guò)極限時(shí)間祟牲,那么可以使用緩存,不需要進(jìn)行網(wǎng)絡(luò)請(qǐng)求抖部;
6说贝、如果緩存過(guò)期了,判斷響應(yīng)頭是否設(shè)置Etag/Last-Modified/Date您朽,沒(méi)有那就直接使用網(wǎng)絡(luò)請(qǐng)求否則需要考慮服務(wù)器返回304狂丝;

并且换淆,只要需要進(jìn)行網(wǎng)絡(luò)請(qǐng)求哗总,請(qǐng)求頭中就不能包含only-if-cached,否則框架直接返回504倍试!

緩存攔截器本身主要邏輯其實(shí)都在緩存策略中讯屈,攔截器本身邏輯非常簡(jiǎn)單,如果確定需要發(fā)起網(wǎng)絡(luò)請(qǐng)求县习,則下一個(gè)攔截器為ConnectInterceptor

四涮母、連接攔截器

ConnectInterceptor,打開(kāi)與目標(biāo)服務(wù)器的連接躁愿,并執(zhí)行下一個(gè)攔截器叛本。它簡(jiǎn)短的可以直接完整貼在這里:

public final class ConnectInterceptor implements Interceptor {
  public final OkHttpClient client;

  public ConnectInterceptor(OkHttpClient client) {
    this.client = client;
  }

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

雖然代碼量很少,實(shí)際上大部分功能都封裝到其它類(lèi)去了彤钟,這里只是調(diào)用而已来候。

首先我們看到的StreamAllocation這個(gè)對(duì)象是在第一個(gè)攔截器:重定向攔截器創(chuàng)建的,但是真正使用的地方卻在這里逸雹。

"當(dāng)一個(gè)請(qǐng)求發(fā)出营搅,需要建立連接云挟,連接建立后需要使用流用來(lái)讀寫(xiě)數(shù)據(jù)";而這個(gè)StreamAllocation就是協(xié)調(diào)請(qǐng)求转质、連接與數(shù)據(jù)流三者之間的關(guān)系园欣,它負(fù)責(zé)為一次請(qǐng)求尋找連接,然后獲得流來(lái)實(shí)現(xiàn)網(wǎng)絡(luò)通信休蟹。

這里使用的newStream方法實(shí)際上就是去查找或者建立一個(gè)與請(qǐng)求主機(jī)有效的連接沸枯,返回的HttpCodec中包含了輸入輸出流,并且封裝了對(duì)HTTP請(qǐng)求報(bào)文的編碼與解碼赂弓,直接使用它就能夠與請(qǐng)求主機(jī)完成HTTP通信辉饱。

StreamAllocation中簡(jiǎn)單來(lái)說(shuō)就是維護(hù)連接:RealConnection——封裝了Socket與一個(gè)Socket連接池〖鹫梗可復(fù)用的RealConnection需要:

public boolean isEligible(Address address, @Nullable Route route) {
    // If this connection is not accepting new streams, we're done.
    if (allocations.size() >= allocationLimit || noNewStreams) return false;

    // If the non-host fields of the address don't overlap, we're done.
    if (!Internal.instance.equalsNonHost(this.route.address(), address)) return false;

    // If the host exactly matches, we're done: this connection can carry the address.
    if (address.url().host().equals(this.route().address().url().host())) {
      return true; // This connection is a perfect match.
    }

    // At this point we don't have a hostname match. But we still be able to carry the request if
    // our connection coalescing requirements are met. See also:
    // https://hpbn.co/optimizing-application-delivery/#eliminate-domain-sharding
    // https://daniel.haxx.se/blog/2016/08/18/http2-connection-coalescing/

    // 1. This connection must be HTTP/2.
    if (http2Connection == null) return false;

    // 2. The routes must share an IP address. This requires us to have a DNS address for both
    // hosts, which only happens after route planning. We can't coalesce connections that use a
    // proxy, since proxies don't tell us the origin server's IP address.
    if (route == null) return false;
    if (route.proxy().type() != Proxy.Type.DIRECT) return false;
    if (this.route.proxy().type() != Proxy.Type.DIRECT) return false;
    if (!this.route.socketAddress().equals(route.socketAddress())) return false;

    // 3. This connection's server certificate's must cover the new host.
    if (route.address().hostnameVerifier() != OkHostnameVerifier.INSTANCE) return false;
    if (!supportsUrl(address.url())) return false;

    // 4. Certificate pinning must match the host.
    try {
      address.certificatePinner().check(address.url().host(), handshake().peerCertificates());
    } catch (SSLPeerUnverifiedException e) {
      return false;
    }

    return true; // The caller's address can be carried by this connection.
  }

1彭沼、if (allocations.size() >= allocationLimit || noNewStreams) return false;

? 連接到達(dá)最大并發(fā)流或者連接不允許建立新的流;如http1.x正在使用的連接不能給其他人用(最大并發(fā)流為:1)或者連接被關(guān)閉备埃;那就不允許復(fù)用姓惑;

2、

if (!Internal.instance.equalsNonHost(this.route.address(), address)) return false;
if (address.url().host().equals(this.route().address().url().host())) {
      return true; // This connection is a perfect match.
}

DNS按脚、代理于毙、SSL證書(shū)、服務(wù)器域名辅搬、端口完全相同則可復(fù)用唯沮;

如果上述條件都不滿(mǎn)足,在HTTP/2的某些場(chǎng)景下可能仍可以復(fù)用(http2先不管)堪遂。

所以綜上介蛉,如果在連接池中找到個(gè)連接參數(shù)一致并且未被關(guān)閉沒(méi)被占用的連接,則可以復(fù)用溶褪。

總結(jié)

這個(gè)攔截器中的所有實(shí)現(xiàn)都是為了獲得一份與目標(biāo)服務(wù)器的連接币旧,在這個(gè)連接上進(jìn)行HTTP數(shù)據(jù)的收發(fā)。

五猿妈、請(qǐng)求服務(wù)器攔截器

CallServerInterceptor吹菱,利用HttpCodec發(fā)出請(qǐng)求到服務(wù)器并且解析生成Response

首先調(diào)用httpCodec.writeRequestHeaders(request); 將請(qǐng)求頭寫(xiě)入到緩存中(直到調(diào)用flushRequest()才真正發(fā)送給服務(wù)器)彭则。然后馬上進(jìn)行第一個(gè)邏輯判斷

Response.Builder responseBuilder = null;
if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
// 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();
        realChain.eventListener().responseHeadersStart(realChain.call());
        responseBuilder = httpCodec.readResponseHeaders(true);
    }
    if (responseBuilder == null) {
        // Write the request body if the "Expect: 100-continue" expectation was met.
        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()) { 
        //HTTP2多路復(fù)用鳍刷,不需要關(guān)閉socket,不管俯抖!
        // 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();

整個(gè)if都和一個(gè)請(qǐng)求頭有關(guān): Expect: 100-continue输瓜。這個(gè)請(qǐng)求頭代表了在發(fā)送請(qǐng)求體之前需要和服務(wù)器確定是否愿意接受客戶(hù)端發(fā)送的請(qǐng)求體。所以permitsRequestBody判斷為是否會(huì)攜帶請(qǐng)求體的方式(POST),如果命中if前痘,則會(huì)先給服務(wù)器發(fā)起一次查詢(xún)是否愿意接收請(qǐng)求體凛捏,這時(shí)候如果服務(wù)器愿意會(huì)響應(yīng)100(沒(méi)有響應(yīng)體,responseBuilder 即為nul)芹缔。這時(shí)候才能夠繼續(xù)發(fā)送剩余請(qǐng)求數(shù)據(jù)坯癣。

但是如果服務(wù)器不同意接受請(qǐng)求體,那么我們就需要標(biāo)記該連接不能再被復(fù)用最欠,調(diào)用noNewStreams()關(guān)閉相關(guān)的Socket示罗。

后續(xù)代碼為:

if (responseBuilder == null) {
    realChain.eventListener().responseHeadersStart(realChain.call());
    responseBuilder = httpCodec.readResponseHeaders(false);
}

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

這時(shí)responseBuilder的情況即為:

1、POST方式請(qǐng)求芝硬,請(qǐng)求頭中包含Expect蚜点,服務(wù)器允許接受請(qǐng)求體,并且已經(jīng)發(fā)出了請(qǐng)求體拌阴,responseBuilder為null;

2绍绘、POST方式請(qǐng)求,請(qǐng)求頭中包含Expect迟赃,服務(wù)器不允許接受請(qǐng)求體陪拘,responseBuilder不為null

3、POST方式請(qǐng)求纤壁,未包含Expect左刽,直接發(fā)出請(qǐng)求體,responseBuilder為null;

4酌媒、POST方式請(qǐng)求欠痴,沒(méi)有請(qǐng)求體,responseBuilder為null;

5秒咨、GET方式請(qǐng)求喇辽,responseBuilder為null;

對(duì)應(yīng)上面的5種情況,讀取響應(yīng)頭并且組成響應(yīng)Response拭荤,注意:此Response沒(méi)有響應(yīng)體茵臭。同時(shí)需要注意的是,如果服務(wù)器接受 Expect: 100-continue這是不是意味著我們發(fā)起了兩次Request舅世?那此時(shí)的響應(yīng)頭是第一次查詢(xún)服務(wù)器是否支持接受請(qǐng)求體的,而不是真正的請(qǐng)求對(duì)應(yīng)的結(jié)果響應(yīng)奇徒。所以緊接著:

int code = response.code();
if (code == 100) {
    // server sent a 100-continue even though we did not request one.
    // try again to read the actual response
    responseBuilder = httpCodec.readResponseHeaders(false);

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

    code = response.code();
}

如果響應(yīng)是100雏亚,這代表了是請(qǐng)求Expect: 100-continue成功的響應(yīng),需要馬上再次讀取一份響應(yīng)頭摩钙,這才是真正的請(qǐng)求對(duì)應(yīng)結(jié)果響應(yīng)頭罢低。

然后收尾

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 {
    response = response.newBuilder()
                    .body(httpCodec.openResponseBody(response))
                    .build();
}

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;

forWebSocket代表websocket的請(qǐng)求,我們直接進(jìn)入else网持,這里就是讀取響應(yīng)體數(shù)據(jù)宜岛。然后判斷請(qǐng)求和服務(wù)器是不是都希望長(zhǎng)連接,一旦有一方指明close功舀,那么就需要關(guān)閉socket萍倡。而如果服務(wù)器返回204/205,一般情況而言不會(huì)存在這些返回碼辟汰,但是一旦出現(xiàn)這意味著沒(méi)有響應(yīng)體列敲,但是解析到的響應(yīng)頭中包含Content-Lenght且不為0,這表響應(yīng)體的數(shù)據(jù)字節(jié)長(zhǎng)度帖汞。此時(shí)出現(xiàn)了沖突戴而,直接拋出協(xié)議異常!

總結(jié)

在這個(gè)攔截器中就是完成HTTP協(xié)議報(bào)文的封裝與解析翩蘸。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末所意,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子催首,更是在濱河造成了極大的恐慌扁眯,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,546評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件翅帜,死亡現(xiàn)場(chǎng)離奇詭異姻檀,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)涝滴,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,224評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)绣版,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人歼疮,你說(shuō)我怎么就攤上這事杂抽。” “怎么了韩脏?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,911評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵缩麸,是天一觀(guān)的道長(zhǎng)。 經(jīng)常有香客問(wèn)我赡矢,道長(zhǎng)杭朱,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,737評(píng)論 1 294
  • 正文 為了忘掉前任吹散,我火速辦了婚禮弧械,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘空民。我一直安慰自己刃唐,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,753評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著画饥,像睡著了一般衔瓮。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上抖甘,一...
    開(kāi)封第一講書(shū)人閱讀 51,598評(píng)論 1 305
  • 那天热鞍,我揣著相機(jī)與錄音,去河邊找鬼单山。 笑死碍现,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的米奸。 我是一名探鬼主播昼接,決...
    沈念sama閱讀 40,338評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼悴晰!你這毒婦竟也來(lái)了慢睡?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,249評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤铡溪,失蹤者是張志新(化名)和其女友劉穎漂辐,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體棕硫,經(jīng)...
    沈念sama閱讀 45,696評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡髓涯,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,888評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了纬纪。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片问畅。...
    茶點(diǎn)故事閱讀 40,013評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡铐料,死狀恐怖篓跛,靈堂內(nèi)的尸體忽然破棺而出林艘,到底是詐尸還是另有隱情,我是刑警寧澤微酬,帶...
    沈念sama閱讀 35,731評(píng)論 5 346
  • 正文 年R本政府宣布拷呆,位于F島的核電站,受9級(jí)特大地震影響疫粥,放射性物質(zhì)發(fā)生泄漏茬斧。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,348評(píng)論 3 330
  • 文/蒙蒙 一梗逮、第九天 我趴在偏房一處隱蔽的房頂上張望项秉。 院中可真熱鬧,春花似錦慷彤、人聲如沸娄蔼。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,929評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)岁诉。三九已至锚沸,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間涕癣,已是汗流浹背哗蜈。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,048評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留坠韩,地道東北人距潘。 一個(gè)月前我還...
    沈念sama閱讀 48,203評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像只搁,于是被迫代替她去往敵國(guó)和親音比。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,960評(píng)論 2 355

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