【Android】OkHttp系列(二):重試/重定向攔截器RetryAndFollowUpInterceptor

該系列OkHttp源碼分析基於OkHttp3.14.0版本

概述

用於對連接失敗時重新連接以及對需要重定向的響應進行重定向。

源碼分析

對於所有的攔截器而言,關鍵邏輯都在其intercept()方法中。

重試

@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) {
    ...省略部分代碼

    Response response;
    boolean success = false;
    try {
        //調用後續的攔截器
      response = realChain.proceed(request, transmitter, null);
      success = true;
    } catch (RouteException e) {
      // The attempt to connect via a route failed. The request will not have been sent.
      // 嘗試通過路由連接失敗。 該請求將不會被髮送。
        //這裏調用了recover()進行判斷
      if (!recover(e.getLastConnectException(), transmitter, false, request)) {
        throw e.getFirstConnectException();
      }
      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, transmitter, requestSendStarted, request)) throw e;
      continue;//重試
    } finally {
      // The network call threw an exception. Release any resources.
      // 網絡通話引發異常。 釋放所有資源。
      if (!success) {
        transmitter.exchangeDoneDueToException();
      }
    }

   ...省略部分重定向的代碼
  }
}

根據上面的代碼我們可以看到,調用後續攔截器的代碼chain.proced()是在一個while(true)循環中的。如果在發生了一些異常的情況下,將會繼續該循環。

那麼哪些情況下才會進行重試呢,主要關注的是下面的幾個方法:

  1. recover

  2. isRecoverable

recover

/**
 * Report and attempt to recover from a failure to communicate with a server. Returns true if
 * {@code e} is recoverable, or false if the failure is permanent. Requests with a body can only
 * be recovered if the body is buffered or if the failure occurred before the request has been
 * sent.
 * 報告並嘗試從與服務器通信的故障中恢復。
 * 如果{@code e}是可恢復的,則返回true;如果失敗是永久的,則返回false。
 * 僅在緩衝正文或在發送請求之前發生故障時,才能恢復帶有正文的請求。
 * Dong:
 * 該方法用於指示是否繼續重試
 */
private boolean recover(IOException e, Transmitter transmitter,
    boolean requestSendStarted, Request userRequest) {
  // The application layer has forbidden retries.
  // 1.client不允許重試
  if (!client.retryOnConnectionFailure()) return false;

  // We can't send the request body again.
  // 2.請求已經開始並且發生FileNotFindException,不允許重試
  // 3.請求已經開始並且請求體不爲空且請求體只允許讀寫一次,不允許重試
  if (requestSendStarted && requestIsOneShot(e, userRequest)) return false;

  // This exception is fatal.
  // 4.ProtocolException 不允許重試
  // 5.InterruptedIOException但是不爲SocketTimeoutException,不允許重試
  // 6.CertificateException 不允許重試
  // 7.SSLPeerUnverifiedException 不允許重試
  if (!isRecoverable(e, requestSendStarted)) return false;

  // No more routes to attempt.
  // 8.沒有更多的路由嘗試時,不允許重試
  if (!transmitter.canRetry()) return false;

  // For failure recovery, use the same route selector with a new connection.
  // 爲了進行故障恢復,請使用具有新連接的相同路由選擇器。
  return true;
}

isRecoverable

/**
 * 判斷該異常是否允許重試
 * @param e 異常
 * @param requestSendStarted 請求是否已經開始
 * @return true/false
 */
private boolean isRecoverable(IOException e, boolean requestSendStarted) {
  // If there was a protocol problem, don't recover.
  // 如果存在協議問題,請不要恢復。
  if (e instanceof ProtocolException) {
    return false;
  }

  // If there was an interruption don't recover, but if there was a timeout connecting to a route
  // we should try the next route (if there is one).
  // 如果發生中斷,則無法恢復,但是如果連接到一條路由的超時,我們應該嘗試下一條路由(如果有一條路由)。
  if (e instanceof InterruptedIOException) {
    return e instanceof SocketTimeoutException && !requestSendStarted;
  }

  // Look for known client-side or negotiation errors that are unlikely to be fixed by trying
  // again with a different route.
  // 查找不太可能通過使用其他路由重試的已知客戶端或協商錯誤。
  if (e instanceof SSLHandshakeException) {
    // If the problem was a CertificateException from the X509TrustManager,
    // do not retry.
    // 如果問題是來自X509TrustManager的CertificateException,不要重試。
    if (e.getCause() instanceof CertificateException) {
      return false;
    }
  }
  if (e instanceof SSLPeerUnverifiedException) {
    // e.g. a certificate pinning error.
    // 例如 證書固定錯誤。
    return false;
  }

  // An example of one we might want to retry with a different route is a problem connecting to a
  // proxy and would manifest as a standard IOException. Unless it is one we know we should not
  // retry, we return true and try a new route.
  // 我們可能想使用其他路由重試的一個示例是連接到代理時出現問題,並表現爲標準IOException。
  // 除非它是一個我們不應該重試的方法,否則我們返回true並嘗試一條新路線。
  return true;
}

總結所有不允許進行重試的情況

1.配置OkHttpClient時不允許重試

2.請求已經開始並且發生FileNotFindException

3.請求已經開始並且請求體不爲空且請求體只允許讀寫一次

4.發生ProtocolException異常

5.發生InterruptedIOException但是不爲SocketTimeoutException

6.發生CertificateException異常

7.發生SSLPeerUnverifiedException異常

8.沒有更多的路由(線路)

重定向

由於代碼比較多,因此我刪除了部分和重定向無關的代碼。

@Override public Response intercept(Chain chain) throws IOException {
  ...省略部分代碼

  int followUpCount = 0;//重定向次數
  Response priorResponse = null;
  while (true) {
    transmitter.prepareToConnect(request);

    if (transmitter.isCanceled()) {
      throw new IOException("Canceled");
    }

    Response response;
    boolean success = false;
    try {
      response = realChain.proceed(request, transmitter, null);
      success = true;
    }
      ...省略了部分重試的代碼

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

    Exchange exchange = Internal.instance.exchange(response);
    Route route = exchange != null ? exchange.connection().route() : null;
    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();
    }

    //最多允許重定向20次
    if (++followUpCount > MAX_FOLLOW_UPS) {
      throw new ProtocolException("Too many follow-up requests: " + followUpCount);
    }

    request = followUp;
    priorResponse = response;
  }
}

根據源碼可以看到,重定向的操作是在一個死循環while(true)中的,而退出這個死循環即完成重定向的條件只有下面幾個:

  1. followUp爲null

  2. followUpBody不爲null且followUpBody只允許傳輸一次

  3. 超過最大允許重定向的次數MAX_FOLLOW_UPS,20次

條件有了,那麼我們就來具體分析一下,什麼時候才能滿足這些條件,由於超過最大重定向次數會拋出異常,因此按照正常業務邏輯,我們比較關注的是前兩個條件。

followUp什麼時候爲null

從代碼中可以看到,followUp是從followUpRequest()這個方法返回的,那麼我們進入這個方法看看裏面有什麼。

根據源碼可以看到,整體邏輯主要是根據之前的響應的狀態碼進行不同的邏輯。主要涉及到這幾個狀態碼:

  1. 407(HTTP_PROXY_AUTH)
  2. 401(HTTP_UNAUTHORIZED)
  3. 308(HTTP_PERM_REDIRECT)
  4. 307(HTTP_TEMP_REDIRECT)
  5. 300(HTTP_MULT_CHOICE)
  6. 301(HTTP_MOVED_PERM)
  7. 302(HTTP_MOVED_TEMP)
  8. 303(HTTP_SEE_OTHER)
  9. 408(HTTP_CLIENT_TIMEOUT)
  10. 503(HTTP_UNAVAILABLE)

比較特殊的是407和408這兩個狀態碼,407代表了需要進行代理服務器的認證,408代表了需要進行認證。這兩個的認證完成都需要我們自己進行處理,否則的話將會返回null。因爲根據我們前面提到的OkHttpClient.Builder的默認參數中可以看到,authenticatorproxyAuthenticator默認都是返回null的。

然後就是3xx系列的狀態碼了,熟悉http協議的可能知道,3xx系列的狀態碼就是和重定向相關的,不同的狀態碼錶示不同的重定向要求以及時效。關於它們之間的區別,可以去看看這篇文章《HTTP中的301、302、303、307、308》

根據源碼可以看到,當狀態碼爲308\308時,如果請求Method不爲”GET“也不爲"HEAD",將直接返回null。

case HTTP_PERM_REDIRECT://308
case HTTP_TEMP_REDIRECT://307
  // "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"
  // 如果響應GET或HEAD以外的請求而接收到307或308狀態代碼,則用戶代理務不必自動重定向該請求
  if (!method.equals("GET") && !method.equals("HEAD")) {
    return null;
  }

其他的3xx系列狀態碼,會先進行一些必要的參數校驗。

// Does the client allow redirects?
// 判斷當前客戶端是否允許重定向 默認爲true
if (!client.followRedirects()) return null;

String location = userResponse.header("Location");
if (location == null) return null;//響應頭中沒有聲明重定向的地址
HttpUrl url = userResponse.request().url().resolve(location);//驗證url是否合法

// Don't follow redirects to unsupported protocols.
// 不要遵循重定向到不支持的協議
if (url == null) return null;

// If configured, don't follow redirects between SSL and non-SSL.
// 如果已配置,請不要遵循SSL和非SSL之間的重定向。即http和https之間重定向
boolean sameScheme = url.scheme().equals(userResponse.request().url().scheme());

/**如果重定向的協議和之前請求的協議不一致並且client配置時不允許https重定向,則返回null*/
if (!sameScheme && !client.followSslRedirects()) return null;

然後就是構建一個新的Request並返回了。

對於狀態碼408來說比較特殊,因爲它表示請求超時了,超時一般來說其實不應該叫重定向了,而應該是重試纔對。不太清楚爲何OkHttp會將408放到重定向裏面來。既然是重試,那麼這裏的檢測邏輯和重試其實是有挺相似的,都是檢測配置OkHttpClient時是否允許重試、RequestBody是否不爲空且只允許傳輸一次。跟重試不同的是,這裏會檢查上一次響應的狀態碼是否也是408,如果是的話那麼將不會再進行重試了。另外如果響應頭中聲明瞭"Retry-After",並且大於0的話,也不再進行重定向了,也返回null。

case HTTP_CLIENT_TIMEOUT://408
  // 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.)
  // 408在實踐中很少見,但是像HAProxy這樣的某些服務器使用此響應代碼。
  // 規範說,我們可以重複請求而無需進行修改。
  // 現代瀏覽器還會重複請求(甚至是非冪等的請求)。
  if (!client.retryOnConnectionFailure()) {
    // The application layer has directed us not to retry the request.
    // 應用層不允許進行重試
    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) {
    //如果要求延遲一段時間,即響應頭中的"Retry-After">0,返回null
    return null;
  }

  return userResponse.request();

最後是狀態碼503,這個狀態碼的邏輯就很簡單了,除非服務端要求立即重試,即響應頭中的"Retry-After"爲0,否則的話結束重定向,返回null。

followUpBody什麼時候不爲null

根據前面的分析,我們可以知道,followUpBody不爲null的情況只有在狀態碼爲3xx系列的時候。

case HTTP_PERM_REDIRECT://308
case HTTP_TEMP_REDIRECT://307
  ...
  // fall-through
case HTTP_MULT_CHOICE://300
case HTTP_MOVED_PERM://301
case HTTP_MOVED_TEMP://302
case HTTP_SEE_OTHER://303
  ...
  // Most redirects don't include a request body.
  // 大多數重定向不包含請求正文。
  Request.Builder requestBuilder = userResponse.request().newBuilder();
  if (HttpMethod.permitsRequestBody(method)) {//排除"GET"和"HEAD"請求
    final boolean maintainBody = HttpMethod.redirectsWithBody(method);//是否是"PROPFIND"請求
    if (HttpMethod.redirectsToGet(method)) {
      //method不是"PROPFIND"
      requestBuilder.method("GET", null);
    } else {
      //method是"PROPFIND",這裏設置了requestBody
      RequestBody requestBody = maintainBody ? userResponse.request().body() : null;
      requestBuilder.method(method, requestBody);
    }

根據源碼中可以看到,如果爲3xx系列請求,並且請求Method爲"PROPFIND"時,followUpBody是不爲null的。

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