OkHttp原理解析2(攔截器篇)(更新中...)

Hello小夥伴們,現在公司已經恢復了正常辦公,但是疫情依舊還在繼續。最近工作實在是有點小忙,導致更新有點緩慢,實在抱歉,本文是OkHttp原理解析的第二篇, 主要針對OkHttp中幾個 默認攔截器 的具體實現邏輯進行分析。
因爲OkHttp的很大一部分邏輯都在攔截器中,因此本文會比較長,同時採用連載更新的方式進行描述,每完成一個攔截器的邏輯分析都會進行更新。
如有對OkHttp的框架流程不太瞭解的可優先閱讀網我上篇博客 OkHttp原理解析1(框架流程篇)

我又要開始表演了~~~
但爲了方便後續描述,我還是簡單對上文做了個總結。大概分爲以下幾步。

  1. 創建OkHttpClient(可通過OkHttpClient.Builder創建,內部設置一些基礎參數)

  2. 創建 Request 對象設置 url,請求類型等參數

  3. 通過OkHttpClient.newCall()創建RealCall對象,同時創建了Transmitter對象。

  4. 通過RealCall.enqueue()或者RealCall.execute()觸發 異步或同步 請求。

  5. 調用OkHttpClient.dispatcher().enqueue(new AsyncCall(responseCallback))做請求準備,循環runningAsyncCalls和readyAsyncCalls 隊列找出host相同的AsyncCall進行重用,並將readyAsyncCallsAsyncCall轉移到runningAsyncCalls中,如果runningAsyncCalls超過64則終止轉移,如相同主機計數器>5則終止轉移本AsyncCall。

  6. 循環runningAsyncCalls調用AsyncCall.executeOn(executorService())

    6.1. AsyncCall爲Runnable,執行run()方法,調用AsyncCall.execute()連帶調用AsyncCall.getResponseWithInterceptorChain()設置攔截器List,首先設置用戶自定義的攔截器。最後通過RealInterceptorChain.proceed()啓動攔截器。

而攔截器的啓動與運行依賴 責任鏈 模式,大概分爲以下3步。

  1. 首先創建RealInterceptorChain對象,通過procee()判斷各種異常,並獲取當前Interceptor對象

  2. 然後 通過Interceptor.intercept(RealInterceptorChain)啓動當前攔截器邏輯,並且觸發下一個攔截器啓動

  3. 如果當前攔截器出現異常等錯誤,則終止責任鏈

我們從添加攔截的的位置開始本文介紹,其實上文已經做了描述,源碼如下所示。

//RealCall.getResponseWithInterceptorChain();
  Response getResponseWithInterceptorChain() throws IOException {
    // 建立一個完整的攔截器堆棧
    List<Interceptor> interceptors = new ArrayList<>();
     //將創建okhttpclient時的攔截器添加到interceptors
    interceptors.addAll(client.interceptors());
    //重試攔截器,負責處理失敗後的重試與重定向
    interceptors.add(new RetryAndFollowUpInterceptor(client));
    //請求轉化攔截器(用戶請求轉爲服務器請求,服務器響應轉爲用戶響應)
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    //緩存攔截器。負責
    //1.根據條件,緩存配置,有效期等返回緩存響應,也可增加到緩存。
    //2.設置請求頭(If-None-Match、If-Modified-Since等) 服務器可能返回304(未修改)
    //3.可配置自定義的緩存攔截器。
    interceptors.add(new CacheInterceptor(client.internalCache()));
    //網絡連接攔截器,主要負責和服務器建立連接。
    interceptors.add(new ConnectInterceptor(client));
    if (!forWebSocket) {
      //創建okhttpclient時設置的networkInterceptors
      interceptors.addAll(client.networkInterceptors());
    }
    //數據流攔截器,主要負責像服務器發送和讀取數據,請求報文封裝和解析。
    interceptors.add(new CallServerInterceptor(forWebSocket));
    //責任鏈模式的創建。
    Interceptor.Chain chain = new RealInterceptorChain(interceptors, transmitter, null, 0,
        originalRequest, this, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis());

    boolean calledNoMoreExchanges = false;
    try {
      //啓動責任鏈
      Response response = chain.proceed(originalRequest);
      if (transmitter.isCanceled()) {
        closeQuietly(response);
        throw new IOException("Canceled");
      }
      return response;
    } catch (IOException e) {
      calledNoMoreExchanges = true;
      throw transmitter.noMoreExchanges(e);
    } finally {
      if (!calledNoMoreExchanges) {
        transmitter.noMoreExchanges(null);
      }
    }
  }

主要看我註釋的部分,OkHttp主要涉及了以下幾個默認攔截器

  1. client.interceptors();//用戶自定義攔截器

  2. RetryAndFollowUpInterceptor(client)//失敗重試攔截器

  3. BridgeInterceptor(client.cookieJar())//請求轉化攔截器

  4. CacheInterceptor(client.internalCache())//緩存攔截器

  5. ConnectInterceptor(client)//網絡連接攔截器

  6. CallServerInterceptor(forWebSocket)//數據流攔截器

OkHttp會把用戶自定義的攔截器默認放到攔截器列表的頭部,以方面優先執行,然後通過創建RealInterceptorChain對象,並調用RealInterceptorChain.proceed()啓動第一個攔截器,然後調用攔截器的interceptor.intercept(next)執行第一個攔截器的邏輯並將下一個攔截器RealInterceptorChain對象傳入依此類推。接下來我們就一個個進行分析。

1. RetryAndFollowUpInterceptor(client) 連接失敗重試攔截器

首先開始的是RetryAndFollowUpInterceptor失敗重試攔截器,這個攔截器是可以在OkHttpClient.Builder對象中通過retryOnConnectionFailure(boolean retryOnConnectionFailure)設置是否開啓,默認構建的OkHttpClient對象是開啓的。
該攔截器主要的職責官方註釋解釋了這麼幾點。

  1. 無法訪問ip地址,如果URL的主機有多個IP地址,則無法訪問任何單個IP地址不會使整個請求失敗。這可以提高多宿服務的可用性。

  2. 過時的池連接,通過ConnectionPool重用套接字以減少請求延遲,但這些連接偶爾會超時。

  3. 無法訪問的代理服務器,可以使用ProxySelector,最終返回到直接連接。

這描述略顯抽象,咱們還是通過源碼看下真像吧。

//我們應該嘗試多少重定向和驗證挑戰?Chrome遵循21個重定向;Firefox、curl和wget遵循20;Safari遵循16;HTTP/1.0建議5。
private static final int MAX_FOLLOW_UPS = 20;

@Override public Response intercept(Chain chain) throws IOException {
    Request request = chain.request();
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Transmitter transmitter = realChain.transmitter();

    int followUpCount = 0;
    Response priorResponse = null;
    while (true) {
      //通過transmitter創建ExchangeFinder,Address,RouteSelector三個對象
      transmitter.prepareToConnect(request);
      //判斷如果當前請求結束了則拋出異常,可通過transmitter終止請求。
      if (transmitter.isCanceled()) {
        throw new IOException("Canceled");
      }

      Response response;
      boolean success = false;
      try {
        //啓動下一個攔截器
        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();
        }
      }

      // 如果body不爲空
      if (priorResponse != null) {
        //獲得新的response
        response = response.newBuilder()
            .priorResponse(priorResponse.newBuilder()
                    .body(null)
                    .build())
            .build();
      }

      Exchange exchange = Internal.instance.exchange(response);
      Route route = exchange != null ? exchange.connection().route() : null;
      // 調用followUpRequest()查看響應是否需要重定向,不需要就返回當前請求,如果需要返回新的請求
      Request followUp = followUpRequest(response, route);
      // 不需要重定向或無法重定向
      if (followUp == null) {
        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();
      }
      //如果重定向次數超過最大次數拋出異常
      if (++followUpCount > MAX_FOLLOW_UPS) {
        throw new ProtocolException("Too many follow-up requests: " + followUpCount);
      }

      request = followUp;
      priorResponse = response;
    }
  }

  //RetryAndFollowUpInterceptor.recover()
  //報告並嘗試從與服務器通信失敗中恢復。如果{@code e}可恢復,則返回true;
  //如果失敗是永久性的,則返回false。只有在緩衝了正文或在發送請求之前發生故障時,纔可以恢復具有正文的請求。
 private boolean recover(IOException e, Transmitter transmitter,
      boolean requestSendStarted, Request userRequest) {
    // 用戶設置的禁止重試
    if (!client.retryOnConnectionFailure()) return false;

    // 不能再發送請求體
    if (requestSendStarted && requestIsOneShot(e, userRequest)) return false;

    // 異常是致命的
    if (!isRecoverable(e, requestSendStarted)) return false;

    // 沒有更多的路線可以嘗試。
    if (!transmitter.canRetry()) return false;

    //對於故障恢復,請對新連接使用同一路由選擇器
    return true;
  }

  /**
   * 查看響應是否需要重定向,不需要就返回當前請求,如果需要返回新的請求。
   * 找出接收{@code userResponse}時要發出的HTTP請求。這將添加身份驗證頭、遵循重定向或處理客戶端請求超時。
   * 如果後續操作不必要或不適用,則返回null。
   */
  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) {
      //407 代理需要身份認證
      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 需要身份認證
      case HTTP_UNAUTHORIZED:
        return client.authenticator().authenticate(route, userResponse);

      case HTTP_PERM_REDIRECT:
      case HTTP_TEMP_REDIRECT:
        // "If the 307 or 308 status code is received in response to a request other than GET
        // or HEAD, the user agent MUST NOT automatically redirect the request"
        if (!method.equals("GET") && !method.equals("HEAD")) {
          return null;
        }
        // fall-through
       // 300多種選擇。請求的資源可包括多個位置,相應可返回一個資源特徵與地址的列表用於用戶終端(例如:瀏覽器)選擇
      case HTTP_MULT_CHOICE:
      // 301永久移動。請求的資源已被永久的移動到新URI,返回信息會包括新的URI,瀏覽器會自動定向到新URI。
      case HTTP_MOVED_PERM:
      // 302臨時移動。與301類似。但資源只是臨時被移動。
      case HTTP_MOVED_TEMP:
      // 303查看其它地址。與301類似。使用GET和POST請求查看
      case HTTP_SEE_OTHER:
        // Does the client allow redirects?
        if (!client.followRedirects()) return null;

        String location = userResponse.header("Location");
        if (location == null) return null;
        HttpUrl url = userResponse.request().url().resolve(location);

        // 不要遵循重定向到不支持的協議。
        if (url == null) return null;

        //如果已配置,請不要遵循SSL和非SSL之間的重定向。
        boolean sameScheme = url.scheme().equals(userResponse.request().url().scheme());
        if (!sameScheme && !client.followSslRedirects()) return null;

        // 大多數重定向不包括請求正文。
        Request.Builder requestBuilder = userResponse.request().newBuilder();
        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");
          }
        }

        // 跨主機重定向時,刪除所有身份驗證頭。這對應用程序層來說可能很煩人,因爲它們無法保留它們。
        if (!sameConnection(userResponse.request().url(), url)) {
          requestBuilder.removeHeader("Authorization");
        }

        return requestBuilder.url(url).build();
      //408 超時
      case HTTP_CLIENT_TIMEOUT:
        // 408's are rare in practice, but some servers like HAProxy use this response code. The
        // spec says that we may repeat the request without modifications. Modern browsers also
        // repeat the request (even non-idempotent ones.)
        if (!client.retryOnConnectionFailure()) {
          // 應用層指示我們不要重試請求
          return null;
        }

        RequestBody requestBody = userResponse.request().body();
        if (requestBody != null && requestBody.isOneShot()) {
          return null;
        }

        if (userResponse.priorResponse() != null
            && userResponse.priorResponse().code() == HTTP_CLIENT_TIMEOUT) {
          // We attempted to retry and got another timeout. Give up.
          return null;
        }

        if (retryAfter(userResponse, 0) > 0) {
          return null;
        }

        return userResponse.request();
      // 503 由於超載或系統維護,服務器暫時的無法處理客戶端的請求。延時的長度可包含在服務器的Retry-After頭信息中
      case HTTP_UNAVAILABLE:
        if (userResponse.priorResponse() != null
            && userResponse.priorResponse().code() == HTTP_UNAVAILABLE) {
          // We attempted to retry and got another timeout. Give up.
          return null;
        }

        if (retryAfter(userResponse, Integer.MAX_VALUE) == 0) {
          // specifically received an instruction to retry without delay
          return userResponse.request();
        }

        return null;

      default:
        return null;
    }
  }


//Transmitter.prepareToConnect()
public void prepareToConnect(Request request) {
    if (this.request != null) {
      //判斷是不是相同的連接,如果是則用之前的。
      if (sameConnection(this.request.url(), request.url()) && exchangeFinder.hasRouteToTry()) {
        return; // Already ready.
      }
      if (exchange != null) throw new IllegalStateException();
      //exchangeFinder
      if (exchangeFinder != null) {
        maybeReleaseConnection(null, true);
        exchangeFinder = null;
      }
    }

    this.request = request;
    this.exchangeFinder = new ExchangeFinder(this, connectionPool, createAddress(request.url()),
        call, eventListener);
  }

//Transmitter.createAddress()
 private Address createAddress(HttpUrl url) {
    SSLSocketFactory sslSocketFactory = null;
    HostnameVerifier hostnameVerifier = null;
    CertificatePinner certificatePinner = null;
    if (url.isHttps()) {
      //https的設置
      sslSocketFactory = client.sslSocketFactory();
      hostnameVerifier = client.hostnameVerifier();
      certificatePinner = client.certificatePinner();
    }

    return new Address(url.host(), url.port(), client.dns(), client.socketFactory(),
        sslSocketFactory, hostnameVerifier, certificatePinner, client.proxyAuthenticator(),
        client.proxy(), client.protocols(), client.connectionSpecs(), client.proxySelector());
  }

//ExchangeFinder.ExchangeFinder()
  ExchangeFinder(Transmitter transmitter, RealConnectionPool connectionPool,
      Address address, Call call, EventListener eventListener) {
    this.transmitter = transmitter;
    this.connectionPool = connectionPool;
    this.address = address;
    this.call = call;
    this.eventListener = eventListener;
    //創建路由選擇器
    this.routeSelector = new RouteSelector(
        address, connectionPool.routeDatabase, call, eventListener);
  }
//RouteSelector.RouteSelector()
  RouteSelector(Address address, RouteDatabase routeDatabase, Call call,
      EventListener eventListener) {
    this.address = address;
    this.routeDatabase = routeDatabase;
    this.call = call;
    this.eventListener = eventListener;

    resetNextProxy(address.url(), address.proxy());
  }

該攔截器的邏輯很多,但其實主要做兩件事,一個是重試,一個是重定向,邏輯流程大概是這樣的。

  1. 通過realChain.transmitter獲取了transmitter對象,並啓用一個while死循環,

  2. 然後通過transmitter.prepareToConnect(request)transmitter創建ExchangeFinder,Address,RouteSelector三個對象,並判斷了是否是相同的連接,是否需要maybeReleaseConnection(),重置ExchangeFinder。

    2.1 . ExchangeFinder此攔截器只是創建ExchangeFinder,但ExchangeFinder中有個find()方法主要通過內部的 findHealthyConnection() 從 connectionPool 中找到一個可用的連接,這個連接可能是複用的,並 connect(),從而得到 輸入/輸出 流 (source/sink) ,返回一個 Exchange 給 CallServerIntercepter , 通過這個 Exchange 就可以添加請求頭和請求體,並讀取響應頭和響應體,來交給上面的 Intercepter,層層向上傳遞。

    2.2 . Address爲請求參數的封裝類,包含url,端口,DNS,SSL,Proxy,ProxySelector,SocketFactory,主機名驗證,證書校驗等邏輯。

    2.3 . RouteSelector主要來選擇路由,主要做三件事。1.收集所有的可用路由。2.選擇可用路由。3.藉助RouteDatabase內的Set對象來維護連接失敗的路由信息,防止去連接失敗路由浪費時間。

  3. 啓動攔截器列表的下一個攔截器。

  4. 判斷全局變量priorResponse是否爲null,如果不爲空則代表請求成功了。

  5. 執行 followUpRequest()查看響應是否需要重定向,如果不需要重定向則返回當前請求

  6. 判斷重定向次數,如果超過最大值則退出。

  7. 重置request,並把當前的Response保存到priorResponse,進入下一次的while循環。

總結來說:

通過while死循環來獲取response,每次循環開始都根據條件獲取下一個request,如果沒有request,則返回response,退出循環。而獲取的request 的條件是根據上一次請求的response 狀態碼確定的,在循環體中同時創建了一些後續需要的對象

感謝觀看,覺得不錯可以點贊關注一下。

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