OkHttp源碼學(xué)習(xí)系列二:攔截鏈分析

本文為本人原創(chuàng),轉(zhuǎn)載請注明作者和出處诱咏。

在上一章我們分析了Okhttp分發(fā)器對同步/異步請求的處理禽车,本章將和大家一起分析Okhttp的最核心模塊--攔截鏈的代碼。在這里你將會了解到Okhttp究竟如何處理請求的朝群。

系列文章索引:
OkHttp源碼學(xué)習(xí)系列一:總流程和Dispatcher分析
OkHttp源碼學(xué)習(xí)系列二:攔截鏈分析
OkHttp源碼學(xué)習(xí)系列三:緩存總結(jié)

零、攔截鏈總流程

上一章我們分析到了RealCall的getResponseWithInterceptorChain方法中符,現(xiàn)在我們繼續(xù),先來看下這個(gè)方法:

Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    interceptors.addAll(client.interceptors());
    interceptors.add(retryAndFollowUpInterceptor);
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    interceptors.add(new CacheInterceptor(client.internalCache()));
    interceptors.add(new ConnectInterceptor(client));
    if (!forWebSocket) {
      interceptors.addAll(client.networkInterceptors());
    }
    interceptors.add(new CallServerInterceptor(forWebSocket));
    
    Interceptor.Chain chain = new RealInterceptorChain(
        interceptors, null, null, null, 0, originalRequest);
    return chain.proceed(originalRequest);
}

可以看到這個(gè)方法前面一大堆都很簡單誉帅,new了一個(gè)存放攔截器的數(shù)組淀散。這個(gè)Interceptor是接口,后面添加的各個(gè)元素都是它的實(shí)現(xiàn)類蚜锨。這里先添加了okhttpClient對象里的攔截器档插,這里是用戶自定義的攔截器,全都會在這里添加進(jìn)去亚再。然后一次添加retryAndFollowUpInterceptor郭膛,BridgeInterceptor,CacheInterceptor氛悬,ConnectInterceptor则剃,CallServerInterceptor五個(gè)攔截器,其中在倒數(shù)第二的位置上如捅,如果上websocket連接棍现,還會額外添加networkInterceptor攔截器組。這里的添加順序要注意镜遣,后面我們會看到己肮,攔截器的攔截順序也是和添加順序是一致的。

添加完成后悲关,執(zhí)行了RealInterceptorChain也就是攔截器鏈的proceed方法谎僻,傳入原始request。其實(shí)攔截器鏈我們看名字大致能猜到寓辱,它會將各個(gè)攔截器串起來艘绍,像鏈條一樣依次去攔截或者說加工request,最終得到response后讶舰,依次再向上返回鞍盗,各個(gè)攔截器再依次處理response需了,最終返回給用戶。現(xiàn)在我們來看下proceed方法是如何串起各個(gè)攔截器的:

// Call the next interceptor in the chain.
RealInterceptorChain next = new RealInterceptorChain(
    interceptors, streamAllocation, httpCodec, connection, index + 1, request);
Interceptor interceptor = interceptors.get(index);
Response response = interceptor.intercept(next);

源代碼有點(diǎn)長般甲,這里我們略去各種異常判斷肋乍,只看核心的這段代碼》蟠妫看源碼的時(shí)候一定要記得抓住主脈絡(luò)墓造,因?yàn)槲覀儾豢赡芤淮闻靼谆蛴涀∷械脑创a細(xì)節(jié)。抓住重點(diǎn)锚烦,才不會只見樹木觅闽,不見森林。
在這里它又重復(fù)創(chuàng)建了一個(gè)RealInterceptorChain對象涮俄,只是對index做了+1處理蛉拙。這個(gè)index就是記錄interceptors攔截器數(shù)組的角標(biāo)的。之后會從數(shù)組中取出當(dāng)前的攔截器彻亲,調(diào)用intercept方法并傳入這個(gè)新new出來的RealInterceptorChain孕锄,得到response并返回。

看到這里可能有點(diǎn)懵苞尝,直覺告訴我們應(yīng)該只要一個(gè)RealInterceptorChain對象去管理所有攔截器畸肆,然而這里似乎沒攔截一次都會產(chǎn)生一個(gè)新的RealInterceptorChain,只不過把攔截器數(shù)組的角標(biāo)index往后移了一位宙址。不要著急轴脐,我們看了這里關(guān)鍵的攔截方法intercept就可以知道是怎么一回事了。這是個(gè)抽象方法抡砂,它的所有實(shí)現(xiàn)大咱,都會調(diào)用chain.proceed(request)這句代碼,包括我們自己自定義的攔截器舀患,在intercept方法中我們也必須調(diào)用這句方法才能生效徽级。在后面的貼出來的代碼中大家也都會看到這句代碼。這個(gè)方法仿佛之前見過聊浅?是的沒錯(cuò)餐抢,在getResponseWithInterceptorChain中我們第一次new攔截器鏈對象就調(diào)用了該方法。這里又調(diào)用了該方法低匙,只是這里的攔截器對象是我們新new出來的旷痕,index+1的攔截器鏈。于是乎在這次調(diào)用中顽冶,又會new出一個(gè)新的攔截器鏈對象欺抗,并會根據(jù)index取出下一個(gè)攔截器,執(zhí)行該攔截器的intercept方法强重。如此遞歸調(diào)用绞呈,直至攔截器數(shù)組中沒有下一個(gè)攔截器贸人,得到response并向上傳遞。

可以看到這里的攔截鏈?zhǔn)褂玫姆浅G擅畹枭悬c(diǎn)像棧的數(shù)據(jù)結(jié)構(gòu)艺智。依次將各個(gè)攔截器的方法入棧,最后得到response圾亏,再依次彈棧十拣。如果是我來寫的話,可能就直接一個(gè)for循環(huán)依次調(diào)用每個(gè)攔截器的攔截方法志鹃。但是這樣的話我還得再來一遍反循環(huán)夭问,再來依次處理加工response。很明顯這里棧的結(jié)構(gòu)更符合我們的業(yè)務(wù)場景曹铃。

一缰趋、RetryAndFollowUpInterceptor

下面我們來看第一個(gè)攔截器RetryAndFollowUpInterceptor,看名字可以猜到這個(gè)攔截器是負(fù)責(zé)失敗重連和重定向的攔截器陕见。

直接來看它的intercept方法:

@Override public Response intercept(Chain chain) throws IOException {
    Request request = chain.request();
    
    streamAllocation = new StreamAllocation(
        client.connectionPool(), createAddress(request.url()), callStackTrace);
    
    int followUpCount = 0;
    Response priorResponse = null;
    while (true) {
      if (canceled) {
        streamAllocation.release();
        throw new IOException("Canceled");
      }
    
      Response response = null;
      boolean releaseConnection = true;
      try {
        response = ((RealInterceptorChain) chain).proceed(request, streamAllocation, null, null);
        releaseConnection = false;
      } catch (RouteException e) {
        // The attempt to connect via a route failed. The request will not have been sent.
        if (!recover(e.getLastConnectException(), false, request)) {
          throw e.getLastConnectException();
        }
        releaseConnection = false;
        continue;
      } catch (IOException e) {
        // An attempt to communicate with a server failed. The request may have been sent.
        boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
        if (!recover(e, requestSendStarted, request)) throw e;
        releaseConnection = false;
        continue;
      } finally {
        // We're throwing an unchecked exception. Release any resources.
        if (releaseConnection) {
          streamAllocation.streamFailed(null);
          streamAllocation.release();
        }
      }
    
      // Attach the prior response if it exists. Such responses never have a body.
      if (priorResponse != null) {
        response = response.newBuilder()
            .priorResponse(priorResponse.newBuilder()
                    .body(null)
                    .build())
            .build();
      }
    
      Request followUp = followUpRequest(response);
    
      if (followUp == null) {
        if (!forWebSocket) {
          streamAllocation.release();
        }
        return response;
      }
    
      closeQuietly(response.body());
    
      if (++followUpCount > MAX_FOLLOW_UPS) {
        streamAllocation.release();
        throw new ProtocolException("Too many follow-up requests: " + followUpCount);
      }
    
      if (followUp.body() instanceof UnrepeatableRequestBody) {
        streamAllocation.release();
        throw new HttpRetryException("Cannot retry streamed HTTP body", response.code());
      }
    
      if (!sameConnection(response, followUp.url())) {
        streamAllocation.release();
        streamAllocation = new StreamAllocation(
            client.connectionPool(), createAddress(followUp.url()), callStackTrace);
      } else if (streamAllocation.codec() != null) {
        throw new IllegalStateException("Closing the body of " + response
            + " didn't close its backing stream. Bad interceptor?");
      }
    
      request = followUp;
      priorResponse = response;
    }
}

這里的代碼非常長埠胖,但是又不太好精簡我就都貼上來了。不要害怕淳玩,還是比較容易理順的。首先它new了一個(gè)StreamAllocation對象非竿,這個(gè)對象封裝了RealConnection蜕着、RouteSelector、HttpCodec等http連接要用到的重要對象红柱,有興趣的同學(xué)可以看看這部分源碼承匣,這里不展開講它,只要知道這是個(gè)http連接必須要用到的重要資源對象锤悄。

之后定義了followUpCount也就是重連次數(shù)韧骗,以及一個(gè)空的response。緊接著就是一個(gè)while(true)死循環(huán)零聚。這個(gè)死循環(huán)不用看也能想到袍暴,肯定是不斷的進(jìn)行重連/重定向的。核心代碼是這句response = ((RealInterceptorChain) chain).proceed(request, streamAllocation, null, null)隶症,是的政模,所謂的重連就是重復(fù)調(diào)用下一個(gè)攔截器繼續(xù)攔截。這里和第一次在RealCall中的調(diào)用不一樣的地方是傳入了streamAllocation給下一個(gè)攔截器鏈蚂会,之前傳的是null淋样。

那么這個(gè)死循環(huán)如何結(jié)束呢?這里除了獲得response后的且沒有重定向得return胁住,好像沒找到其他退出循環(huán)的地方趁猴。是不是沒有response之前會一直重復(fù)請求直到有結(jié)果呢刊咳?這明顯不肯能,這里是通過拋異常的方式來結(jié)束重連儡司。我們來看拋異常的地方有哪些:

    1. 當(dāng)前請求被cancel的時(shí)候
    1. 重連次數(shù)超過上限(默認(rèn)是20娱挨,用戶可修改)
    1. 重定向的請求體屬于不能重復(fù)的請求體
    1. 沒有正確釋放streamAllocation內(nèi)的codec

除了最后一條,其他時(shí)候都會調(diào)用streamAllocation.release()來釋放資源枫慷。另外让蕾,這里還有一處要點(diǎn)是調(diào)用了followUpRequest方法進(jìn)行重定向。這個(gè)方法非常長或听,這里簡單講下是干嘛的:先獲得傳入的響應(yīng)體的code探孝,根據(jù)不同的code去構(gòu)建不同的request進(jìn)行重定向。如果不需要重定向誉裆,直接返回null顿颅,如果是301、302這樣的重定向響應(yīng)碼足丢,使用重定向的url重新構(gòu)建request粱腻,在上面的死循環(huán)中重新進(jìn)行請求。

到這里RetryAndFollowUpInterceptor就講的差不多了斩跌,它主要負(fù)責(zé)的任務(wù)就是失敗重連绍些,和重定向重新構(gòu)建request重連,是不是還挺簡單的耀鸦?

二柬批、BridgeInterceptor

BridgeInterceptor是五個(gè)攔截器中最簡單的一個(gè)了,它做的事情很簡單袖订,正如其名字氮帐,它是一個(gè)連接網(wǎng)絡(luò)數(shù)據(jù)流和我們程序能用的request、response對象的橋梁洛姑。

 @Override public Response intercept(Chain chain) throws IOException {
    Request userRequest = chain.request();
    Request.Builder requestBuilder = userRequest.newBuilder();

    RequestBody body = userRequest.body();
    if (body != null) {
      MediaType contentType = body.contentType();
      if (contentType != null) {
        requestBuilder.header("Content-Type", contentType.toString());
      }

      long contentLength = body.contentLength();
      if (contentLength != -1) {
        requestBuilder.header("Content-Length", Long.toString(contentLength));
        requestBuilder.removeHeader("Transfer-Encoding");
      } else {
        requestBuilder.header("Transfer-Encoding", "chunked");
        requestBuilder.removeHeader("Content-Length");
      }
    }

    if (userRequest.header("Host") == null) {
      requestBuilder.header("Host", hostHeader(userRequest.url(), false));
    }

    if (userRequest.header("Connection") == null) {
      requestBuilder.header("Connection", "Keep-Alive");
    }

    // If we add an "Accept-Encoding: gzip" header field we're responsible for also decompressing
    // the transfer stream.
    boolean transparentGzip = false;
    if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
      transparentGzip = true;
      requestBuilder.header("Accept-Encoding", "gzip");
    }

    List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());
    if (!cookies.isEmpty()) {
      requestBuilder.header("Cookie", cookieHeader(cookies));
    }

    if (userRequest.header("User-Agent") == null) {
      requestBuilder.header("User-Agent", Version.userAgent());
    }

    Response networkResponse = chain.proceed(requestBuilder.build());

    HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());

    Response.Builder responseBuilder = networkResponse.newBuilder()
        .request(userRequest);

    if (transparentGzip
        && "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding"))
        && HttpHeaders.hasBody(networkResponse)) {
      GzipSource responseBody = new GzipSource(networkResponse.body().source());
      Headers strippedHeaders = networkResponse.headers().newBuilder()
          .removeAll("Content-Encoding")
          .removeAll("Content-Length")
          .build();
      responseBuilder.headers(strippedHeaders);
      responseBuilder.body(new RealResponseBody(strippedHeaders, Okio.buffer(responseBody)));
    }

    return responseBuilder.build();
  }

可以看到它將我們傳入的request新增了好幾個(gè)請求頭茬暇,這就是為什么我們平時(shí)使用的時(shí)候即使完全不加請求頭揩瞪,抓出來的包依然有好多請求頭坯苹。這里要注意一下“Keep-Alive”犀农,這個(gè)請求頭是告訴服務(wù)器,在請求完成后不要斷開連接产徊。我們知道http是短鏈接昂勒,為什么okhttp還要默認(rèn)加這么個(gè)請求頭呢?這是為了復(fù)用連接舟铜,當(dāng)再次發(fā)起相同請求的時(shí)候戈盈,可以節(jié)省再次開啟連接的消耗。

當(dāng)?shù)玫絩esponse的時(shí)候,如果http連接使用了gzip壓縮塘娶,會對響應(yīng)流進(jìn)行g(shù)zip解壓縮归斤,因此我們自己在使用的時(shí)候完全不需要再做這些操作。

三刁岸、CacheInterceptor

CacheInterceptor顧名思義脏里,是負(fù)責(zé)緩存相關(guān)處理的一個(gè)攔截器。由于在下一章我會專門講okhttp的緩存機(jī)制虹曙,這里有些細(xì)節(jié)我會先跳過迫横。它的intercept代碼非常長,而且每一段都要分析酝碳,所以我分開來一段一段地分析矾踱。

Response cacheCandidate = cache != null
    ? cache.get(chain.request())
    : null;

long now = System.currentTimeMillis();

CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;

首先從cache對象根據(jù)當(dāng)前的request取出緩存的response命名為cacheCandidate,也就是候選的緩存疏哗,不一定會使用呛讲。之后使用當(dāng)前時(shí)間、request和候選緩存構(gòu)建出了一個(gè)CacheStrategy對象返奉,這個(gè)對象是負(fù)責(zé)緩存策略實(shí)施的贝搁。它會根據(jù)不同的請求碼、響應(yīng)碼返回不同的networkRequest和cacheResponse芽偏。這個(gè)類會放到下一章展開討論雷逆,目前我們只要知道,它會根據(jù)request和cacheCandidate返回networkRequest(實(shí)際要進(jìn)行網(wǎng)絡(luò)請求的request)以及cacheResponse(緩存的response)污尉。如果networkRequest為null(比如requset中含有only-if-cached請求頭的時(shí)候)关面,表示不進(jìn)行網(wǎng)絡(luò)請求。當(dāng)cacheResponse為null的時(shí)候十厢,說明沒有緩存,或者當(dāng)前緩存過期或者當(dāng)前緩存策略不允許使用緩存等捂齐,總之就代表無緩存可用蛮放。

if (cache != null) {
  cache.trackResponse(strategy);
}

if (cacheCandidate != null && cacheResponse == null) {
  closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
}

Cache的trackResponse是起計(jì)數(shù)作用的,不是重點(diǎn)奠宜。下面一個(gè)判斷如果cacheCandidate不為null代表我們?nèi)〉搅司彺姘洌琧acheResponse為null代表這個(gè)緩存過期或者由于緩存策略等原因用不了,那么這個(gè)cacheCandidate沒有用了压真,我門需要關(guān)閉它的流娩嚼。

// If we're forbidden from using the network and the cache is insufficient, fail.
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();
}

// If we don't need the network, we're done.
if (networkRequest == null) {
  return cacheResponse.newBuilder()
      .cacheResponse(stripBody(cacheResponse))
      .build();
}

前面說過了networkRequest為null代表我們將不進(jìn)行網(wǎng)絡(luò)請求,這時(shí)候分為兩種情況滴肿。如果cacheResponse也為null說明我們沒有有效的緩存response岳悟,而我們又不會進(jìn)行網(wǎng)絡(luò)請求,因此給上層構(gòu)建了一個(gè)響應(yīng)碼味504的response。如果cacheResponse不為null贵少,說明我們有可用緩存呵俏,而此次請求又不會再請求網(wǎng)絡(luò),因此直接將緩存response返回滔灶。

Response networkResponse = null;
try {
  networkResponse = chain.proceed(networkRequest);
} finally {
  // If we're crashing on I/O or otherwise, don't leak the cache body.
  if (networkResponse == null && cacheCandidate != null) {
    closeQuietly(cacheCandidate.body());
  }
}

// If we have a cache response too, then we're doing a conditional get.
if (cacheResponse != null) {
  if (networkResponse.code() == HTTP_NOT_MODIFIED) {
    Response response = cacheResponse.newBuilder()
        .headers(combine(cacheResponse.headers(), networkResponse.headers()))
        .sentRequestAtMillis(networkResponse.sentRequestAtMillis())
        .receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build();
    networkResponse.body().close();

    // Update the cache after combining headers but before stripping the
    // Content-Encoding header (as performed by initContentStream()).
    cache.trackConditionalCacheHit();
    cache.update(cacheResponse, response);
    return response;
  } else {
    closeQuietly(cacheResponse.body());
  }
}

上面networkRequest為null也就是不進(jìn)行網(wǎng)絡(luò)請求的情況我們已經(jīng)全部處理了普碎,下面必須要進(jìn)行網(wǎng)絡(luò)請求了,調(diào)用攔截鏈的proceed方法讓后面的攔截器去請求網(wǎng)絡(luò)數(shù)據(jù)得到response录平。如果cacheResponse不為null麻车,那么此時(shí)我們有兩個(gè)response:一個(gè)是緩存的,一個(gè)是網(wǎng)絡(luò)請求返回的动猬,此時(shí)我們需要處理到底使用哪個(gè)涝影。此時(shí)如果服務(wù)器返回的響應(yīng)碼為HTTP_NOT_MODIFIED,也就是我們常見的304燃逻,代表服務(wù)器的資源沒有變化序目,客戶端去取本地緩存即可伯襟,此時(shí)服務(wù)器不會返回響應(yīng)體。那么這個(gè)時(shí)候我們會使用緩存的cacheResponse構(gòu)建一個(gè)新的response并返回姆怪。同時(shí)我們會記錄這次我們的cache命中了叛赚,因?yàn)閛khttp默認(rèn)使用了LruCache算法,Least Recently Used最近最少使用原則俺附,需要我們在使用的時(shí)候計(jì)數(shù)溪掀。同時(shí)把新構(gòu)建的response刷新到緩存中去揪胃。當(dāng)然了,如果沒有返回304喊递,說明緩存已經(jīng)過期骚勘,我們需要將它的流關(guān)閉。

Response response = networkResponse.newBuilder()
    .cacheResponse(stripBody(cacheResponse))
    .networkResponse(stripBody(networkResponse))
    .build();

if (cache != null) {
  if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
    // Offer this request to the cache.
    CacheRequest cacheRequest = cache.put(response);
    return cacheWritingResponse(cacheRequest, response);
  }

  if (HttpMethod.invalidatesCache(networkRequest.method())) {
    try {
      cache.remove(networkRequest);
    } catch (IOException ignored) {
      // The cache cannot be written.
    }
  }
}

return response;

最后剩下來的情況就是緩存過期盛杰,我們需要使用網(wǎng)絡(luò)返回的response這種情況。直接使用networkResponse構(gòu)建response并返回定拟。此時(shí)我們還需要做一件事青自,就是更新我們的緩存驱证,將最終response寫入到cache對象中去。但此時(shí)如果我們的請求方式不支持緩存(常見的兩種請求方式逆瑞,get請求支持緩存伙单,post不支持)吻育,我們不但不能更新緩存,還要將緩存刪掉摊趾。

至此游两,緩存攔截器也講的差不多了,在下一章我將會進(jìn)一步講解okhttp的緩存策略實(shí)現(xiàn)梢为。

四轰坊、ConnectInterceptor

經(jīng)歷過前面三個(gè)攔截器的處理肴沫,我們發(fā)現(xiàn)目前為止我們還沒有進(jìn)行真正的請求蕴忆。別急,ConnectInterceptor就是一個(gè)負(fù)責(zé)建立http連接的攔截器站蝠。它的intercept代碼不多,趕緊來看下:

@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, doExtensiveHealthChecks);
    RealConnection connection = streamAllocation.connection();

    return realChain.proceed(request, streamAllocation, httpCodec, connection);
}

非常簡短是不是,它只做了兩件事澜倦,一是生成HttpCodec對象藻治,該對象是給http請求/響應(yīng)編碼、解碼的验靡。另外還調(diào)用了streamAllocation.connection方法取出RealConnection連接對象雏节。但是取出connection對象后矾屯,我找了半天沒找到在哪建立連接的,最后發(fā)現(xiàn)其實(shí)是在streamAllocation.newStream()方法中建立的孙技,來看看這個(gè)方法:

public HttpCodec newStream(OkHttpClient client, boolean doExtensiveHealthChecks) {
    int connectTimeout = client.connectTimeoutMillis();
    int readTimeout = client.readTimeoutMillis();
    int writeTimeout = client.writeTimeoutMillis();
    boolean connectionRetryEnabled = client.retryOnConnectionFailure();

    try {
      RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
          writeTimeout, connectionRetryEnabled, doExtensiveHealthChecks);
      HttpCodec resultCodec = resultConnection.newCodec(client, this);

      synchronized (connectionPool) {
        codec = resultCodec;
        return resultCodec;
      }
    } catch (IOException e) {
      throw new RouteException(e);
    }
}

這里從okhttpclient中取出了一些連接參數(shù)牵啦,調(diào)用了findHealthyConnection方法獲得連接妄痪,并且通過這個(gè)RealConnection對象創(chuàng)建了此次請求的HttpCodec對象衫生。點(diǎn)進(jìn)來看看findHealthyConnection方法,該方法是個(gè)無限循環(huán),直到findConnection方法找到健康的連接彭羹,直接看findConnection方法:

  private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
      boolean connectionRetryEnabled) throws IOException {
    Route selectedRoute;
    synchronized (connectionPool) {
      if (released) throw new IllegalStateException("released");
      if (codec != null) throw new IllegalStateException("codec != null");
      if (canceled) throw new IOException("Canceled");

      // Attempt to use an already-allocated connection.
      RealConnection allocatedConnection = this.connection;
      if (allocatedConnection != null && !allocatedConnection.noNewStreams) {
        return allocatedConnection;
      }

      // Attempt to get a connection from the pool.
      Internal.instance.get(connectionPool, address, this, null);
      if (connection != null) {
        return connection;
      }

      selectedRoute = route;
    }

    // If we need a route, make one. This is a blocking operation.
    if (selectedRoute == null) {
      selectedRoute = routeSelector.next();
    }

    RealConnection result;
    synchronized (connectionPool) {
      if (canceled) throw new IOException("Canceled");

      // Now that we have an IP address, make another attempt at getting a connection from the pool.
      // This could match due to connection coalescing.
      Internal.instance.get(connectionPool, address, this, selectedRoute);
      if (connection != null) {
        route = selectedRoute;
        return connection;
      }

      // Create a connection and assign it to this allocation immediately. This makes it possible
      // for an asynchronous cancel() to interrupt the handshake we're about to do.
      route = selectedRoute;
      refusedStreamCount = 0;
      result = new RealConnection(connectionPool, selectedRoute);
      acquire(result);
    }

    // Do TCP + TLS handshakes. This is a blocking operation.
    result.connect(connectTimeout, readTimeout, writeTimeout, connectionRetryEnabled);
    routeDatabase().connected(result.route());

    Socket socket = null;
    synchronized (connectionPool) {
      // Pool the connection.
      Internal.instance.put(connectionPool, result);

      // If another multiplexed connection to the same address was created concurrently, then
      // release this connection and acquire that one.
      if (result.isMultiplexed()) {
        socket = Internal.instance.deduplicate(connectionPool, address, this);
        result = connection;
      }
    }
    closeQuietly(socket);

    return result;
  }

又是一個(gè)比較長的方法还最。前半段是在嘗試尋找connection毡惜,最開始尋找是否有可以復(fù)用的已經(jīng)建立好連接并且空閑的connection经伙。沒有的話繼續(xù)從連接池里取,如果連接池里沒有辜梳,就需要new一個(gè)connection并加入連接池泳叠。有了connection對象后危纫,終于可以調(diào)用其connect方法真正進(jìn)行連接啦!看注釋可以知道契耿,這里的connect方法進(jìn)行了TCP + TLS握手操作螃征,是個(gè)阻塞操作盯滚。同時(shí)這里還會判斷下連接是否multiplexed,也就是和當(dāng)前某個(gè)已有的連接重復(fù)了内列,如果重復(fù)的話會釋放該連接背率。已經(jīng)到這步了寝姿,當(dāng)然要看看connect方法是怎樣建立連接的:

  public void connect(
      int connectTimeout, int readTimeout, int writeTimeout, boolean connectionRetryEnabled) {
    if (protocol != null) throw new IllegalStateException("already connected");

    RouteException routeException = null;
    List<ConnectionSpec> connectionSpecs = route.address().connectionSpecs();
    ConnectionSpecSelector connectionSpecSelector = new ConnectionSpecSelector(connectionSpecs);

    if (route.address().sslSocketFactory() == null) {
      if (!connectionSpecs.contains(ConnectionSpec.CLEARTEXT)) {
        throw new RouteException(new UnknownServiceException(
            "CLEARTEXT communication not enabled for client"));
      }
      String host = route.address().url().host();
      if (!Platform.get().isCleartextTrafficPermitted(host)) {
        throw new RouteException(new UnknownServiceException(
            "CLEARTEXT communication to " + host + " not permitted by network security policy"));
      }
    }

    while (true) {
      try {
        if (route.requiresTunnel()) {
          connectTunnel(connectTimeout, readTimeout, writeTimeout);
        } else {
          connectSocket(connectTimeout, readTimeout);
        }
        establishProtocol(connectionSpecSelector);
        break;
      } catch (IOException e) {
        closeQuietly(socket);
        closeQuietly(rawSocket);
        socket = null;
        rawSocket = null;
        source = null;
        sink = null;
        handshake = null;
        protocol = null;
        http2Connection = null;

        if (routeException == null) {
          routeException = new RouteException(e);
        } else {
          routeException.addConnectException(e);
        }

        if (!connectionRetryEnabled || !connectionSpecSelector.connectionFailed(e)) {
          throw routeException;
        }
      }
    }

    if (http2Connection != null) {
      synchronized (connectionPool) {
        allocationLimit = http2Connection.maxConcurrentStreams();
      }
    }
  }

這里的代碼比較接近底層了饵筑,本人看起來也是十分吃力翻翩。首先它會判斷Protocol對象是否為null,Protocol是封裝的通信協(xié)議對象胶征,如果已經(jīng)存在桨仿,則表明已連接服傍。然后會根據(jù)路由地址獲取connectionSpecs集合,ConnectionSpec點(diǎn)進(jìn)去看了下是更一步的有TLS協(xié)議的連接罩抗。之后判斷如果TLS協(xié)議不合格灿椅,會拋RouteException不進(jìn)行連接茫蛹。這也從一方面解釋了okhttp為什么強(qiáng)調(diào)它是一個(gè)很安全的http框架。

緊接著是一個(gè)無限循環(huán)進(jìn)行連接骨坑,調(diào)用establishProtocol建立真正的Client-Server通信柬采,這個(gè)方法會拋出IO異常警没。如果順利的話跳出循環(huán),有異常的話會根據(jù)情況是否拋異常放棄連接還是繼續(xù)嘗試下一次連接亡脸。最后還會對http流的數(shù)量做一個(gè)限制浅碾。

繼續(xù)往下看的話establishProtocol方法會調(diào)用connectTls方法進(jìn)行三次握手操作续语,與https有關(guān)的ssl認(rèn)證過程就在其中,限于篇幅就不仔細(xì)講了滥朱。最終會調(diào)用Http2Connection對象的start方法開啟連接徙邻。這個(gè)過程的細(xì)節(jié)非常多,由于我們重點(diǎn)是講攔截器的缰犁,這里就此打住淳地。以后有機(jī)會的再開一篇文章專門講這里的代碼帅容。

CallServerInterceptor

這是okhttp網(wǎng)絡(luò)請求的最后一個(gè)攔截器并徘,ConnectInterceptor是負(fù)責(zé)建立http連接的,而它負(fù)責(zé)真正向服務(wù)器發(fā)起請求耍贾。直接看它的intercept代碼:

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

首先獲取HttpCodec和RealConnection連接對象荐开,通過HttpCodec編碼最終的請求頭简肴。

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

這段代碼則是編碼請求體的砰识,首先判斷下請求方法是否支持請求體(比如get請求就沒有請求體),然后通過HttpCodec編碼最終的請求體辫狼。

    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();
    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;

最終的requst完成后见秤,通過HttpCodec讀取響應(yīng)頭和響應(yīng)體并進(jìn)行解碼真椿,構(gòu)建最終的response。其中如果響應(yīng)碼為101(表明要升級協(xié)議)测摔,則返回一個(gè)空的response锋八。204或205(表明響應(yīng)體沒有內(nèi)容)但響應(yīng)體長度缺不為0則拋異常。如果Connection響應(yīng)頭為close路媚,表明請求完畢樊销,需要關(guān)閉流围苫。

最終將response逐層向上返回撤师,經(jīng)過所有攔截器的處理后返回給用戶剃盾。

終于寫完了,限于我的知識水平有限衰伯,有錯(cuò)誤的地方請大家及時(shí)糾正积蔚,感謝您的閱讀尽爆!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末漱贱,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子募强,更是在濱河造成了極大的恐慌彪笼,老刑警劉巖配猫,帶你破解...
    沈念sama閱讀 210,914評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件泵肄,死亡現(xiàn)場離奇詭異淑翼,居然都是意外死亡玄括,警方通過查閱死者的電腦和手機(jī)肉瓦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評論 2 383
  • 文/潘曉璐 我一進(jìn)店門泞莉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人斯嚎,你說我怎么就攤上這事堡僻∫咛辏” “怎么了巢价?”我有些...
    開封第一講書人閱讀 156,531評論 0 345
  • 文/不壞的土叔 我叫張陵蹄溉,是天一觀的道長柒爵。 經(jīng)常有香客問我,道長法瑟,這世上最難降的妖魔是什么唁奢? 我笑而不...
    開封第一講書人閱讀 56,309評論 1 282
  • 正文 為了忘掉前任麻掸,我火速辦了婚禮,結(jié)果婚禮上熬北,老公的妹妹穿的比我還像新娘讶隐。我一直安慰自己,他們只是感情好效五,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,381評論 5 384
  • 文/花漫 我一把揭開白布火俄。 她就那樣靜靜地躺著讲冠,像睡著了一般竿开。 火紅的嫁衣襯著肌膚如雪玻熙。 梳的紋絲不亂的頭發(fā)上嗦随,一...
    開封第一講書人閱讀 49,730評論 1 289
  • 那天枚尼,我揣著相機(jī)與錄音,去河邊找鬼崎溃。 笑死盯质,一個(gè)胖子當(dāng)著我的面吹牛呼巷,可吹牛的內(nèi)容都是我干的王悍。 我是一名探鬼主播,決...
    沈念sama閱讀 38,882評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼啤咽,長吁一口氣:“原來是場噩夢啊……” “哼宇整!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起霸饲,我...
    開封第一講書人閱讀 37,643評論 0 266
  • 序言:老撾萬榮一對情侶失蹤厚脉,失蹤者是張志新(化名)和其女友劉穎胶惰,沒想到半個(gè)月后孵滞,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,095評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡泄伪,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,448評論 2 325
  • 正文 我和宋清朗相戀三年蟋滴,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了津函。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片孤页。...
    茶點(diǎn)故事閱讀 38,566評論 1 339
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡蕉堰,死狀恐怖悲龟,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情皿渗,我是刑警寧澤乐疆,帶...
    沈念sama閱讀 34,253評論 4 328
  • 正文 年R本政府宣布挤土,位于F島的核電站,受9級特大地震影響迷殿,放射性物質(zhì)發(fā)生泄漏咖杂。R本人自食惡果不足惜诉字,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,829評論 3 312
  • 文/蒙蒙 一壤圃、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦墨叛、人聲如沸模蜡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽甥绿。三九已至共缕,卻和暖如春士复,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背便贵。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評論 1 264
  • 我被黑心中介騙來泰國打工承璃, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留绸硕,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,248評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像咬崔,于是被迫代替她去往敵國和親垮斯。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,440評論 2 348

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