okhttp之五個攔截器的介紹

轉載請以鏈接形式標明出處:
本文出自:103style的博客


base on 3.12.0


目錄

  • 前言
  • 重試及重定向攔截器 RetryAndFollowUpInterceptor
  • 橋接攔截器 BridgeInterceptor
  • 緩存攔截器 CacheInterceptor
  • 連接攔截器 ConnectInterceptor
  • 讀寫攔截器 CallServerInterceptor
  • 小結

前言

前面我們對 okhttp的RealCall.execute()流程做了介紹,在文末有提到五個自帶的攔截器,本文就來介紹五個攔截器主要的工作。

還沒看過 RealCall.execute()流程 的小夥伴可以先去看看。

這裏先簡單介紹下五個攔截器的作用

  • RetryAndFollowUpInterceptor:負責請求的重試和重定向
  • BridgeInterceptor:給請求添加對應的 header 信息,處理響應結果的 header 信息
  • CacheInterceptor:根據當前獲取的狀態選擇 網絡請求 、讀取緩存、更新緩存。
  • ConnectInterceptor:建立 http 連接。
  • CallServerInterceptor:讀寫網絡數據。

話不多說,我們接下來直接看這寫攔截器的 intercept(chain) 主要做了什麼事情。


重試及重定向攔截器 RetryAndFollowUpInterceptor

首先介紹下RetryAndFollowUpInterceptor 主要做了哪些邏輯:

  • 首先獲取以及初始化相關的實例.

  • 獲取下層攔截器返回的結果,出現異常 則根據是否可以恢復來判斷中斷 還是 重新開始循環.

  • 根據返回的信息 判斷是否需要重定向?
    當重定向次數大於 MAX_FOLLOW_UPS = 20 時則拋出異常.

  • 然後判斷重定向返回的信息是否出現異常。

    • 出現則拋出異常並釋放資源.
    • 不出現則用重定向返回的信息構建 request重新傳給下層攔截器.

下面我們來結合具體代碼看看.

  • 獲取以及初始化相關的實例
    我們可以看到這裏主要是獲取 RealInterceptorChainRealCall對象,然後構建了一個StreamAllocation,然後進入死循環執行下面的邏輯。
    StreamAllocation主要是 用來建立執行HTTP請求所需網絡設施的組件,後面我們會詳細介紹。

    Request request = chain.request();
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Call call = realChain.call();
    EventListener eventListener = realChain.eventListener();
    
    StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
            createAddress(request.url()), call, eventListener, callStackTrace);
    this.streamAllocation =streamAllocation;
    //然後開始進入死循環
    while (true) {...}
    
  • 獲取並處理下層攔截器返回的結果
    這裏主要是獲取下層攔截器返回的結果,然後判斷是否可以重試。

    Response response;
    boolean releaseConnection = true;
    try {
        response = realChain.proceed(request, streamAllocation, null, null);
        releaseConnection = false;
    } catch (RouteException e) {
        if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
            throw e.getFirstConnectException();
        }
        releaseConnection = false;
        continue;
    } catch (IOException e) {
        boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
        if (!recover(e, streamAllocation, requestSendStarted, request)) throw e;
        releaseConnection = false;
        continue;
    } finally {
        if (releaseConnection) {
            //不能重試則釋放資源
            streamAllocation.streamFailed(null);
            streamAllocation.release();
        }
    }
    

    判斷是否可以重試的邏輯:

    private boolean recover(IOException e, StreamAllocation streamAllocation,
                            boolean requestSendStarted, Request userRequest) {
        streamAllocation.streamFailed(e);
        //client.retryOnConnectionFailure(false) 配置了不允許重試
        if (!client.retryOnConnectionFailure()) return false;
        //無法發送請求內容
        if (requestSendStarted && userRequest.body() instanceof UnrepeatableRequestBody) return false;
        //異常是致命的
        if (!isRecoverable(e, requestSendStarted)) return false;
        //沒有可重試的路徑
        if (!streamAllocation.hasMoreRoutes()) return false;
    
        //上面都不滿足 則可以重試
        return true;
    }
    
  • 根據返回的信息判斷是否需要重定向

    Request followUp;
    try {
        followUp = followUpRequest(response, streamAllocation.route());
    } catch (IOException e) {
        streamAllocation.release();
        throw e;
    }
    if (followUp == null) {
        streamAllocation.release();
        return response;
    }
    

    followUpRequest
    主要是根據返回信息的響應碼做對應的操作。

    private Request followUpRequest(Response userResponse, 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  需要代理驗證
                return client.proxyAuthenticator().authenticate(route, userResponse);
            case HTTP_UNAUTHORIZED://401  沒有認證
                return client.authenticator().authenticate(route, userResponse);
            case HTTP_PERM_REDIRECT://308
            case HTTP_TEMP_REDIRECT://307
                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
                //client不允許重定向
                if (!client.followRedirects()) return null;
                boolean sameScheme = url.scheme().equals(userResponse.request().url().scheme());
                if (!sameScheme && !client.followSslRedirects()) return null;
    
                //根據相應的狀態修改request的header信息
                //...
                return requestBuilder.url(url).build();
            case HTTP_CLIENT_TIMEOUT://408
                ...
            case HTTP_UNAVAILABLE://503
                ...
            default:
                return null;
        }
    }
    
  • 然後判斷重定向返回的信息是否出現異常。

    //關閉之前響應數據的流信息
    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()), call, eventListener, callStackTrace);
        this.streamAllocation = streamAllocation;
    } else if (streamAllocation.codec() != null) {
        throw new IllegalStateException("Closing the body of " + response
                + " didn't close its backing stream. Bad interceptor?");
    }
    //將重定向的請求賦值給request 
    request = followUp;
    priorResponse = response;
    

橋接攔截器 BridgeInterceptor

BridgeInterceptor 的主要做了以下工作:

  • http請求 添加對應的 header 信息.
  • 如果下層攔截器返回的數據的 Content-Encodinggzip,則通過 GzipSource 獲取返回的數據。

廢話不多說,看代碼:

  • 給http請求添加對應的header信息.
    • RequestBody不爲空則根據內容添加Content-TypeContent-LengthTransfer-Encoding.
    • 給沒有HostConnectionUser-Agent頭信息的 request 補上.
    • 根據對應條件給 request 補上Accept-EncodingCookie.
    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");
    }
    
    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());
    }
    
  • 處理gzip格式數據
    • 主要就是通過GzipSource 包裝 gzip 格式的數據,具體可以看GzipSource.read(...)
    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);
        String contentType = networkResponse.header("Content-Type");
        responseBuilder.body(new RealResponseBody(contentType, -1L, Okio.buffer(responseBody)));
    }
    return responseBuilder.build();
    

緩存攔截器 CacheInterceptor

CacheInterceptor 的作用主要是:

  • 根據當前時間獲取當前request的緩存
  • 根據緩存中緩存的request和response做對應處理
  • 有緩存時,則根據條件判斷是否緩存到本地

接下來看代碼實現:

  • 根據當前時間獲取當前request的緩存

    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;
    if (cache != null) {
        cache.trackResponse(strategy);
    }
    if (cacheCandidate != null && cacheResponse == null) {
        closeQuietly(cacheCandidate.body());
    }
    
  • 根據緩存中緩存的request和 response做對應處理

    • 網絡不可用,又沒有緩存則返回 504錯誤.
    • 網絡不可用,緩存可用,則直接返回緩存。
    • 網絡可用,緩存不可用,則通過下層攔截器獲取網絡數據。
    //網絡不可用,又沒有緩存則返回 `504`錯誤.
    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 {
        //請求網絡數據
        networkResponse = chain.proceed(networkRequest);
    } finally {
        if (networkResponse == null && cacheCandidate != null) {
            closeQuietly(cacheCandidate.body());
        }
    }
    
  • 緩存不爲空時,根據條件是否緩存到本地

    • 如果請求返回碼是HTTP_NOT_MODIFIED: 304,則更新緩存。
    • HEAD 類型的請求,則更新緩存。
    • POSTPATCHPUTDELETEMOVE請求則刪除緩存中的request
    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();
            cache.trackConditionalCacheHit();
            cache.update(cacheResponse, response);
            return response;
        } else {
            closeQuietly(cacheResponse.body());
        }
    }
    Response response = networkResponse.newBuilder()
            .cacheResponse(stripBody(cacheResponse))
            .networkResponse(stripBody(networkResponse))
            .build();
    if (cache != null) {
        if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
            // HEAD 請求
            CacheRequest cacheRequest = cache.put(response);
            return cacheWritingResponse(cacheRequest, response);
        }
        if (HttpMethod.invalidatesCache(networkRequest.method())) {
            //POST、PATCH、PUT、DELETE、MOVE
            try {
                cache.remove(networkRequest);
            } catch (IOException ignored) {
            }
        }
    }
    
    return response;
    

連接攔截器 ConnectInterceptor

ConnectInterceptor主要就是通過StreamAllocationHttpCodecRealConnection 合理建立 http連接。

這三個類後面會單獨講解,主要就是通過 在連接池中尋找可以的連接,沒有則創建,並通過okio來操作數據流,然後由CallServerInterceptor繼續處理

public Response intercept(Chain chain) throws IOException {
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Request request = realChain.request();
    StreamAllocation streamAllocation = realChain.streamAllocation();

    boolean doExtensiveHealthChecks = !request.method().equals("GET");
    HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
    RealConnection connection = streamAllocation.connection();

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

讀寫攔截器 CallServerInterceptor

CallServerInterceptor即爲獲取請求的響應數據,並回傳給上一層攔截器。

  • 處理帶有 RequestBody 並符合條件的 request
  • 然後通過Response.Builder構建響應數據,並根據相應數據的返回碼做響應處理。

開始看代碼

  • 處理帶有RequestBody並符合條件的request
    處理帶有RequestBody的非 GETHEAD 請求。

    • headerExpect100-continue時,則爲Response.Builder添加頭信息
    • 如果服務器允許發送請求body發送,則通過okio寫入請求數據.
    Response.Builder responseBuilder = null;
    if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
        //等待HTTP/1.1 100響應
        if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
            httpCodec.flushRequest();
            realChain.eventListener().responseHeadersStart(realChain.call());
            responseBuilder = httpCodec.readResponseHeaders(true);
        }
        if (responseBuilder == null) {
            //如果響應滿足 HTTP/1.1 100
            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()) {
            //不滿足HTTP/1.1 100 則防止連接被重用
            streamAllocation.noNewStreams();
        }
    }
    //結束請求
    httpCodec.finishRequest();
    
  • 構建並處理響應數據

    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();
    
    int code = response.code();
    if (code == 100) {
        //服務器返回100 則重新請求
        responseBuilder = httpCodec.readResponseHeaders(false);
        response = responseBuilder
                .request(request)
                .handshake(streamAllocation.connection().handshake())
                .sentRequestAtMillis(sentRequestMillis)
                .receivedResponseAtMillis(System.currentTimeMillis())
                .build();
    
        code = response.code();
    }
    realChain.eventListener()
            .responseHeadersEnd(realChain.call(), response);
    if (forWebSocket && code == 101) {
        response = response.newBuilder()
                .body(Util.EMPTY_RESPONSE)
                .build();
    } else {
        response = response.newBuilder()
                .body(httpCodec.openResponseBody(response))
                .build();
    }
    //Connection 爲 close 則禁止創建新的流
    if ("close".equalsIgnoreCase(response.request().header("Connection"))
            || "close".equalsIgnoreCase(response.header("Connection"))) {
        streamAllocation.noNewStreams();
    }
     //204 205 的請求返回Content-Length 必須爲 0
    if ((code == 204 || code == 205) && response.body().contentLength() > 0) {
        throw new ProtocolException(
                "HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());
    }
    return response;
    

小結

通過上面的介紹我們基本瞭解了五個攔截器對應的作用,這裏再回顧下:

  • RetryAndFollowUpInterceptor:負責請求的重試和重定向
  • BridgeInterceptor:給請求添加對應的 header 信息,處理響應結果的 header 信息
  • CacheInterceptor:根據當前獲取的狀態選擇 網絡請求 、讀取緩存、更新緩存。
  • ConnectInterceptor:建立 http 連接。
  • CallServerInterceptor:讀寫網絡數據。

每個攔截器的都有自己負責的功能。


以上

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章