OkHttp 源碼剖析系列(二)——攔截器大體流程分析

系列索引

本系列文章基于 OkHttp3.14

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-Length、Content-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之旅系列

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。