OkHttp 原始碼剖析系列(二)——攔截器整體流程分析

N0tExpectErr0r發表於2020-01-03

你好,我是 N0tExpectErr0r,一名熱愛技術的 Android 開發

我的個人部落格:blog.N0tExpectErr0r.cn

OkHttp 原始碼剖析系列文章目錄:

OkHttp 原始碼剖析系列(一)——請求的發起及攔截器機制概述

OkHttp 原始碼剖析系列(二)——攔截器整體流程分析

OkHttp 原始碼剖析系列(三)——快取機制

OkHttp 原始碼剖析系列(四)——連線建立概述

OkHttp 原始碼剖析系列(五)——代理路由選擇

OkHttp 原始碼剖析系列(六)——連線複用機制及連線的建立

OkHttp 原始碼剖析系列(七)——請求的發起及響應的讀取

之前的文章介紹到了 OkHttp 的攔截器機制的整體概述,現在讓我們依次研究一下其攔截器的實現。

RetryAndFollowUpInterceptor

前面提到,RetryAndFollowUpInerceptor 負責了 HTTP 請求的重定向功能,那讓我們先了解一下 HTTP 協議中的重定向。

HTTP 中的重定向

HTTP 協議提供了一種重定向的功能,它通過由伺服器返回特定格式的響應從而觸發客戶端的重定向。其對應的 Response Code 格式為 3XX,並且會在 Response Header 的 Location 欄位中放入新的 URL,這樣我們客戶端就可以根據該 Location 欄位所指定的 URL 重新請求從而得到需要的資料。

其過程如下圖所示:

img

其中重定向對應的狀態碼及含義如下表所示(摘自維基百科):

image-20190730211626735

重定向與伺服器轉發的區別

可以發現,重定向和伺服器轉發請求是有些相似的,它們有什麼不同呢?

  1. 重定向是客戶端行為,而伺服器轉發則是服務端行為

  2. 重定向我們的客戶端發出了多次請求,而轉發我們的客戶端只發出了一次請求。

  3. 重定向的控制權在客戶端,轉發的控制權在服務端。

###程式碼分析

接下來讓我們研究一下 RetryAndFollowUpInterceptor 的實現原理,我們看到 RetryAndFollowUpInterceptor.intercept 方法:

@Override
public Response intercept(Chain chain) throws IOException {
    Request request = chain.request();
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    // 獲取transmitter
    Transmitter transmitter = realChain.transmitter();
    int followUpCount = 0;
    Response priorResponse = null;
    while (true) {
    	// 進行一些連線前的準備工作
        transmitter.prepareToConnect(request);
        // 處理取消事件
        if (transmitter.isCanceled()) {
            throw new IOException("Canceled");
        }
        Response response;
        boolean success = false;
        try {
        	// 呼叫chain的proceed方法獲取下層得到的結果
            response = realChain.proceed(request, transmitter, null);
            success = true;
        } catch (RouteException e) {
            // 若不滿足重定向的條件,丟擲異常
            if (!recover(e.getLastConnectException(), transmitter, false, request)) {
                throw e.getFirstConnectException();
            }
            // 滿足重定向條件,重試
            continue;
        } catch (IOException e) {
            boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
            // 不滿足重定向條件,丟擲異常
            if (!recover(e, transmitter, requestSendStarted, request)) throw e;
            // 滿足重定向條件,重試
            continue;
        } finally {
            if (!success) {
            	// 若丟擲了異常,釋放資源
                transmitter.exchangeDoneDueToException();
            }
        }
        // 在本次response中設定上一次的response,其body為空
        if (priorResponse != null) {
            response = response.newBuilder()
                    .priorResponse(priorResponse.newBuilder()
                            .body(null)
                            .build())
                    .build();
        }
        Exchange exchange = Internal.instance.exchange(response);
        Route route = exchange != null ? exchange.connection().route() : null;
        // 根據response code獲取重定向後的request
        Request followUp = followUpRequest(response, route);
        if (followUp == null) {
        	// 不再需要重定向,停止timeout計時並返回response
            if (exchange != null && exchange.isDuplex()) {
                transmitter.timeoutEarlyExit();
            }
            return response;
        }
        RequestBody followUpBody = followUp.body();
        if (followUpBody != null && followUpBody.isOneShot()) {
            return response;
        }
        closeQuietly(response.body());
        if (transmitter.hasExchange()) {
            exchange.detachWithViolence();
        }
        // 重定向不超過20次,否則丟擲異常
        if (++followUpCount > MAX_FOLLOW_UPS) {
            throw new ProtocolException("Too many follow-up requests: " + followUpCount);
        }
        // 修改下次重定向的request
        request = followUp;
        // 記錄上一次的response
        priorResponse = response;
    }
}
複製程式碼

可以看到,這裡外部通過一個迴圈,實現不斷重定向,可以看一下迴圈內主要做了什麼:

  1. 進行一些預處理
  2. 呼叫 chain.proceed 方法進行請求獲取 Response
  3. 過程中若下層丟擲異常,則嘗試重定向
  4. 若不滿足重定向條件,則丟擲異常
  5. 若出現其他未知的異常,則通過丟擲異常釋放資源
  6. 在本次 Response 中設定上一次的 Response priorResponse,且body為空
  7. 根據 Response 中的 response code 進行重定向,呼叫 followUpRequest 方法獲取重定向後的 request followUp
  8. 若重定向後的 followUp 為 null,說明不再需要重定向,停止 timeout 計時並返回 Response
  9. 若重定向超過指定次數(預設 20 次),則丟擲異常。
  10. 若仍未返回,則需要下一次重定向,對下一次的 request 等變數進行賦值。

讓我們看看 followUpRequest 方法做了什麼:

private Request followUpRequest(Response userResponse, @Nullable Route route) throws IOException {
    if (userResponse == null) throw new IllegalStateException();
    int responseCode = userResponse.code();
    final String method = userResponse.request().method();
    switch (responseCode) {
        case HTTP_PROXY_AUTH:		// 407
          	// ...
          	// 代理身份認證
        case HTTP_UNAUTHORIZED:		// 401
            // ...
            // 身份認證
        case HTTP_PERM_REDIRECT:	// 308
        case HTTP_TEMP_REDIRECT:	// 307
            // 307、308 兩種狀態碼不對 GET、HEAD 以外的請求重定向
            if (!method.equals("GET") && !method.equals("HEAD")) {
                return null;
            }
        case HTTP_MULT_CHOICE:		// 300
        case HTTP_MOVED_PERM:		// 301
        case HTTP_MOVED_TEMP:		// 302
        case HTTP_SEE_OTHER:		// 303
            // 若客戶端關閉了重定向,則直接返回 null
            if (!client.followRedirects()) return null;
            // 獲取LocationHeader以獲取重定向目標
            String location = userResponse.header("Location");
            if (location == null) return null;
            HttpUrl url = userResponse.request().url().resolve(location);
			// ...
            Request.Builder requestBuilder = userResponse.request().newBuilder();
            // 處理重定向使用的method
            if (HttpMethod.permitsRequestBody(method)) {
                final boolean maintainBody = HttpMethod.redirectsWithBody(method);
                if (HttpMethod.redirectsToGet(method)) {
                    requestBuilder.method("GET", null);
                } else {
                    RequestBody requestBody = maintainBody ? userResponse.request().body() : null;
                    requestBuilder.method(method, requestBody);
                }
                if (!maintainBody) {
                    requestBuilder.removeHeader("Transfer-Encoding");
                    requestBuilder.removeHeader("Content-Length");
                    requestBuilder.removeHeader("Content-Type");
                }
            }
            // 重新構建request
            return requestBuilder.url(url).build();
        case HTTP_CLIENT_TIMEOUT:	// 408
            // 408 說明需要重新傳送一次相同的請求
			// ...
			return userResponse.request();
        case HTTP_UNAVAILABLE:		// 503
            // ...
            return null;
        default:
            return null;
    }
}
複製程式碼

可以看到,主要是針對重定向的幾個狀態碼進行特殊處理,從中取出 Location 欄位,構造重定向後的 request

BridgeInterceptor

BridgeInterceptor 的名字取的非常形象,它就像一座橋樑,連線了使用者與伺服器。在使用者向伺服器傳送請求時,它會把使用者所構建的請求轉換為向伺服器請求的真正的 Request,而在伺服器返回了響應後,它又會將伺服器所返回的響應轉換為使用者所能夠使用的 Response

讓我們看到 BridgeInterceptor.intercept 方法:

@Override
public Response intercept(Chain chain) throws IOException {
    Request userRequest = chain.request();
    Request.Builder requestBuilder = userRequest.newBuilder();
    RequestBody body = userRequest.body();
    // 將一些userRequest中的屬性設定進builder中
    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");
    }
    boolean transparentGzip = false;
    // 若未設定Accept-Encoding,自動設定gzip
    if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
        transparentGzip = true;
        requestBuilder.header("Accept-Encoding", "gzip");
    }
    // 將userRequest中的cookies設定進builder
    List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());
    if (!cookies.isEmpty()) {
        requestBuilder.header("Cookie", cookieHeader(cookies));
    }
    // 設定user-agent
    if (userRequest.header("User-Agent") == null) {
        requestBuilder.header("User-Agent", Version.userAgent());
    }
    // 讀取服務端響應
    Response networkResponse = chain.proceed(requestBuilder.build());
    // 對響應的header進行處理
    HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());
   	// 根據服務端的響應構建新的Response,並將userRequest設定為其request
   	Response.Builder responseBuilder = networkResponse.newBuilder()
            .request(userRequest);
    // 若之前設定了gzip壓縮且response中也包含了gzip壓縮,則進行gzip解壓
    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);
        String contentType = networkResponse.header("Content-Type");
        responseBuilder.body(new RealResponseBody(contentType, -1L, Okio.buffer(responseBody)));
    }
    return responseBuilder.build();
}
複製程式碼

可以看到,這裡主要對 Header 進行處理,將一些原來 request 中的 Header 進行處理後設定進了新 request,並用其進行請求。其中若呼叫者未設定 Accept-Encoding,則它會預設設定 gzip

而在對 response 處理時,若之前設定了 gzip,則進行 gzip 解壓。這種自動解壓會自動將 Content-LengthContent-Encoding 欄位從 Header 中移除,因此上層可能會獲取到 -1。

而這裡關於 Cookie 的處理我們暫時不關心,後續文章中再對其作介紹。

CacheInterceptor

CacheInterceptor 主要負責了對快取的讀取以及更新,讓我們看看其 intercept 方法:

@Override
public Response intercept(Chain chain) throws IOException {
	// 嘗試獲取快取的cache
    Response cacheCandidate = cache != null
            ? cache.get(chain.request())
            : null;
    long now = System.currentTimeMillis();
    // 傳入當前時間、request以及從快取中取出的cache,構建快取策略
    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
    // 通過快取策略獲取新的request
    Request networkRequest = strategy.networkRequest;
	// 通過快取策略獲取快取中取出的response
	Response cacheResponse = strategy.cacheResponse;
    if (cache != null) {
        cache.trackResponse(strategy);
    }
    if (cacheCandidate != null && cacheResponse == null) {
        closeQuietly(cacheCandidate.body()); // The cache candidate wasn t applicable. Close it.
    }
    // 根據快取策略若不能使用網路且沒有快取,則請求失敗,構建一個請求失敗的Response並返回
    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 (networkRequest == null) {
        return cacheResponse.newBuilder()
                .cacheResponse(stripBody(cacheResponse))
                .build();
    }
    Response networkResponse = null;
    try {
    	// 網路請求獲取response
        networkResponse = chain.proceed(networkRequest);
    } finally {
        // 如果IO的過程中出現了crash,回收資源
        if (networkResponse == null && cacheCandidate != null) {
            closeQuietly(cacheCandidate.body());
        }
    }
    // 如果快取中有快取,並且請求的code為304,則結合快取及網路請求結果後返回,並且更新快取中的內容
    if (cacheResponse != null) {
        if (networkResponse.code() == HTTP_NOT_MODIFIED) {	// 304
            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());
        }
    }
    // 構建response
    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;
}
複製程式碼

可以看到,這裡主要是以下步驟

  1. 嘗試從快取中獲取了快取的 response
  2. 根據 當前時間、request、快取的response 構建快取策略。
  3. 若快取策略不能使用網路(networkRequest == null),且無快取(cacheResponse == null),則直接請求失敗。
  4. 若快取策略不能使用網路,由於前面有判斷所以可以確定有快取,直接構建快取的 response 並返回。
  5. 呼叫 chain.proceed 網路請求獲取 response
  6. 對 code 304 作出處理,結合本地及網路返回資料構建 response 並返回
  7. 構建網路請求的所獲得的 response ,並且由於該網路請求並未進行過快取,進行快取並返回結果

而關於快取相關的具體實現這裡先不過多做介紹,後面會專門開一篇文章進行分析,這裡主要以流程為主。

ConnectInterceptor

ConnectInterceptor 主要負責的是與伺服器的連線的建立,它的程式碼非常短:

@Override
public Response intercept(Chain chain) throws IOException {
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Request request = realChain.request();
    Transmitter transmitter = realChain.transmitter();
    boolean doExtensiveHealthChecks = !request.method().equals("GET");
    // 構建Exchange
    Exchange exchange = transmitter.newExchange(chain, doExtensiveHealthChecks);
    return realChain.proceed(request, transmitter, exchange);
}
複製程式碼

這裡主要是呼叫 transmitter.newExchange 構建一個 Exchange,之後呼叫了 realChain.proceed(request, transmitter, exchange) 方法。

這個 Exchange 類究竟是什麼呢?我們看到它的 JavaDoc:

Transmits a single HTTP request and a response pair. This layers connection management and events on {@link ExchangeCodec}, which handles the actual I/O.

也就是說 Exchange 類可以將 ExchangeCodec 這個類的連線管理及事件進行分層,而 ExchangeCodec 是一個真正執行 I/O 的類,看來這個類主要是進行一些連線管理的事務。在 newExchange 的過程中可能就建立/複用了客戶與伺服器的連線。

這裡具體的連線獲取過程我們暫時先不做介紹,在後續文章中會詳細進行介紹,此篇文章更偏向整體流程的講解。

CallServerInterceptor

CallServerInterceptor 是整個網路請求鏈的最後一個攔截器,它真正實現了對伺服器 Response 的讀取,讓我們看看它的實現:

@Override
public Response intercept(Chain chain) throws IOException {
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Exchange exchange = realChain.exchange();
    Request request = realChain.request();
    long sentRequestMillis = System.currentTimeMillis();
    // 寫入請求頭
    exchange.writeRequestHeaders(request);
    boolean responseHeadersStarted = false;
    Response.Builder responseBuilder = null;
    if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
        // 對 100-continue 這一 header 做特殊處理
        if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
            exchange.flushRequest();
            responseHeadersStarted = true;
            exchange.responseHeadersStart();
            responseBuilder = exchange.readResponseHeaders(true);
        }
        if (responseBuilder == null) {
        	// 寫入請求體
            if (request.body().isDuplex()) {
                // Prepare a duplex body so that the application can send a request body later.
                exchange.flushRequest();
                BufferedSink bufferedRequestBody = Okio.buffer(
                        exchange.createRequestBody(request, true));
                request.body().writeTo(bufferedRequestBody);
            } else {
                // Write the request body if the "Expect: 100-continue" expectation was met.
                BufferedSink bufferedRequestBody = Okio.buffer(
                        exchange.createRequestBody(request, false));
                request.body().writeTo(bufferedRequestBody);
                bufferedRequestBody.close();
            }
        } else {
            exchange.noRequestBody();
            if (!exchange.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.
                exchange.noNewExchangesOnConnection();
            }
        }
    } else {
        exchange.noRequestBody();
    }
    if (request.body() == null || !request.body().isDuplex()) {
        exchange.finishRequest();
    }
    if (!responseHeadersStarted) {
        exchange.responseHeadersStart();
    }
    if (responseBuilder == null) {
    	// 讀取響應頭
        responseBuilder = exchange.readResponseHeaders(false);
    }
    Response response = responseBuilder
            .request(request)
            .handshake(exchange.connection().handshake())
            .sentRequestAtMillis(sentRequestMillis)
            .receivedResponseAtMillis(System.currentTimeMillis())
            .build();
    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
        // 讀取響應頭
        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) {
        // 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(exchange.openResponseBody(response))
                .build();
    }
    if ("close".equalsIgnoreCase(response.request().header("Connection"))
            || "close".equalsIgnoreCase(response.header("Connection"))) {
        exchange.noNewExchangesOnConnection();
    }
    if ((code == 204 || code == 205) && response.body().contentLength() > 0) {
        throw new ProtocolException(
                "HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());
    }
    return response;
}
複製程式碼

這裡程式碼量非常多,但其實核心是下面幾步:

  1. 寫入Request Header
  2. 寫入Request Body
  3. 讀取Response Header
  4. 讀取Response Body

其具體實現我們後續文章再進行介紹,到了這裡整個責任鏈的大體流程我們就分析完了。

參考資料

HTTP狀態碼

OkHttp之旅系列

相關文章