OKHttp重試機(jī)制剖析及常見異常分析

OKHttp重試機(jī)制剖析

OKHttp擁有網(wǎng)絡(luò)連接失敗時(shí)的重試功能:

OkHttp perseveres when the network is troublesome: it will silently recover from common connection problems. If your service has multiple IP addresses OkHttp will attempt alternate addresses if the first connect fails. This is necessary for IPv4+IPv6 and for services hosted in redundant data centers. OkHttp initiates new connections with modern TLS features (SNI, ALPN), and falls back to TLS 1.0 if the handshake fails.

要了解OKHttp的重試機(jī)制,我們最關(guān)心的就是RetryAndFollowUpInterceptor, 在遭遇網(wǎng)絡(luò)異常時(shí),OKHttp的網(wǎng)絡(luò)異常相關(guān)的重試都在RetryAndFollowUpInterceptor完成。具體我們先從RetryAndFollowUpInterceptor的#intercept(Chain chian)方法開始入手,下面的代碼片段已經(jīng)去掉了非核心邏輯:

  //StreamAllocation init...
  Response priorResponse = null;
    while (true) {
      if (canceled) {
        streamAllocation.release();
        throw new IOException("Canceled");
      }

      Response response;
      boolean releaseConnection = true;
      try {
        response = realChain.proceed(request, streamAllocation, null, null);
        releaseConnection = false;
      } catch (RouteException e) {
        //socket連接階段,如果發(fā)生連接失敗,會(huì)統(tǒng)一封裝成該異常并拋出
        `RouteException`:通過路由的嘗試失敗了,請(qǐng)求將不會(huì)被發(fā)送,此時(shí)會(huì)嘗試通過調(diào)用`#recover`來恢復(fù);
        // The attempt to connect via a route failed. The request will not have been sent.
        if (!recover(e.getLastConnectException(), false, request)) {
          throw e.getLastConnectException();
        }
        releaseConnection = false;
        continue;
      } catch (IOException e) {
        //socket連接成功后,發(fā)生請(qǐng)求階段時(shí)拋出的各類網(wǎng)絡(luò)異常
        // An attempt to communicate with a server failed. The request may have been sent.
        boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
        if (!recover(e, requestSendStarted, request)) throw e;
        releaseConnection = false;
        continue;
      } finally {
        // We're throwing an unchecked exception. Release any resources.
        if (releaseConnection) {
          streamAllocation.streamFailed(null);
          streamAllocation.release();
        }
      }

接下來看核心的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.
   */
  private boolean recover(IOException e, boolean requestSendStarted, Request userRequest) {
    streamAllocation.streamFailed(e);

    // The application layer has forbidden retries. 應(yīng)用層禁止重試則不再重試
    if (!client.retryOnConnectionFailure()) return false;

    // We can't send the request body again. 如果請(qǐng)求已經(jīng)發(fā)出,并且請(qǐng)求的body不支持重試則不再重試
    if (requestSendStarted && userRequest.body() instanceof UnrepeatableRequestBody) return false;

    // This exception is fatal. //致命錯(cuò)誤
    if (!isRecoverable(e, requestSendStarted)) return false;

    // No more routes to attempt. 沒有更多route發(fā)起重試
    if (!streamAllocation.hasMoreRoutes()) return false;

    // For failure recovery, use the same route selector with a new connection.
    return true;
  }

在該方法中,首先是通過調(diào)用streamAllocation.streamFailed(e)來記錄該次異常,進(jìn)而在RouteDatabase中記錄錯(cuò)誤的route以降低優(yōu)先級(jí),避免下次相同address的請(qǐng)求依然使用這個(gè)失敗過的route。如果沒有更多可用的連接線路則不能重試連接

public final class RouteDatabase {
  private final Set<Route> failedRoutes = new LinkedHashSet<>();

  /** Records a failure connecting to {@code failedRoute}. */
  public synchronized void failed(Route failedRoute) {
    failedRoutes.add(failedRoute);
  }

  /** Records success connecting to {@code route}. */
  public synchronized void connected(Route route) {
    failedRoutes.remove(route);
  }

  /** Returns true if {@code route} has failed recently and should be avoided. */
  public synchronized boolean shouldPostpone(Route route) {
    return failedRoutes.contains(route);
  }
}

接著我們重點(diǎn)再關(guān)注isRecoverable方法:

  private boolean isRecoverable(IOException e, boolean requestSendStarted) {
    // If there was a protocol problem, don't recover.  協(xié)議錯(cuò)誤不再重試
    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.
      if (e.getCause() instanceof CertificateException) {
        return false;
      }
    }
//使用 HostnameVerifier 來驗(yàn)證 host 是否合法,如果不合法會(huì)拋出 SSLPeerUnverifiedException
 // 握手HandShake#getSeesion 拋出的異常,屬于握手過程中的一環(huán)
    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.
    return true;
  }

常見網(wǎng)絡(luò)異常分析:

UnknowHostException

產(chǎn)生原因:
  • 網(wǎng)絡(luò)中斷
  • DNS 服務(wù)器故障
  • 域名解析劫持
解決辦法:
  • HttpDNS
  • 合理的兜底策略

![Uploading image_079055.png . . .]

InterruptedIOException

產(chǎn)生原因:
  • 請(qǐng)求讀寫階段,請(qǐng)求線程被中斷
解決辦法:
  • 檢查是否符合業(yè)務(wù)邏輯

SocketTimeoutException

產(chǎn)生原因:
  • 帶寬低、延遲高
  • 路徑擁堵、服務(wù)端負(fù)載吃緊
  • 路由節(jié)點(diǎn)臨時(shí)異常
解決辦法:
  • 合理設(shè)置重試
  • 切換ip重試

要特別注意: 請(qǐng)求時(shí)因?yàn)樽x寫超時(shí)等原因產(chǎn)生的SocketTimeoutException,OkHttp內(nèi)部是不會(huì)重試的

sockettiemout.jpg

因此如果app層特別關(guān)心該異常,則應(yīng)該自定義intercetors,對(duì)該異常進(jìn)行特殊處理。

SSLHandshakeException

產(chǎn)生原因:
  • Tls協(xié)議協(xié)商失敗/握手格式不兼容
  • 辦法服務(wù)器證書的CA未知
  • 服務(wù)器證書不是由CA簽名的,而是自簽名
  • 服務(wù)器配置缺少中間CA(不完整的證書鏈)
  • 服務(wù)器主機(jī)名不匹配(SNI);
  • 遭遇了中間人攻擊。
解決辦法:
  • 指定SNI
  • 證書鎖定
  • 降級(jí)Http。。。
  • 聯(lián)系SA

SSLPeerUnverifiedException

產(chǎn)生原因:
  • 證書域名校驗(yàn)錯(cuò)誤
解決辦法:
  • 指定SNI
  • 證書鎖定
  • 降級(jí)Http。。。
  • 聯(lián)系SA
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容