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

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)方法開(kāi)始入手丹皱,下面的代碼片段已經(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`:通過(guò)路由的嘗試失敗了摊崭,請(qǐng)求將不會(huì)被發(fā)送讼油,此時(shí)會(huì)嘗試通過(guò)調(diào)用`#recover`來(lái)恢復(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í)拋出的各類(lèi)網(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();
        }
      }

接下來(lái)看核心的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. 沒(méi)有更多route發(fā)起重試
    if (!streamAllocation.hasMoreRoutes()) return false;

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

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

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 來(lái)驗(yàn)證 host 是否合法,如果不合法會(huì)拋出 SSLPeerUnverifiedException
 // 握手HandShake#getSeesion 拋出的異常耸彪,屬于握手過(guò)程中的一環(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;
  }

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

UnknowHostException

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

![Uploading image_079055.png . . .]

InterruptedIOException

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

SocketTimeoutException

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

要特別注意: 請(qǐng)求時(shí)因?yàn)樽x寫(xiě)超時(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ù)器證書(shū)的CA未知
  • 服務(wù)器證書(shū)不是由CA簽名的,而是自簽名
  • 服務(wù)器配置缺少中間CA(不完整的證書(shū)鏈)
  • 服務(wù)器主機(jī)名不匹配(SNI)荧呐;
  • 遭遇了中間人攻擊汉形。
解決辦法:
  • 指定SNI
  • 證書(shū)鎖定
  • 降級(jí)Http。倍阐。概疆。
  • 聯(lián)系SA

SSLPeerUnverifiedException

產(chǎn)生原因:
  • 證書(shū)域名校驗(yàn)錯(cuò)誤
解決辦法:
  • 指定SNI
  • 證書(shū)鎖定
  • 降級(jí)Http。峰搪。岔冀。
  • 聯(lián)系SA
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市概耻,隨后出現(xiàn)的幾起案子使套,更是在濱河造成了極大的恐慌,老刑警劉巖鞠柄,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件侦高,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡厌杜,警方通過(guò)查閱死者的電腦和手機(jī)奉呛,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)夯尽,“玉大人侧馅,你說(shuō)我怎么就攤上這事∧琶龋” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵谊娇,是天一觀的道長(zhǎng)肺孤。 經(jīng)常有香客問(wèn)我罗晕,道長(zhǎng),這世上最難降的妖魔是什么赠堵? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任小渊,我火速辦了婚禮,結(jié)果婚禮上茫叭,老公的妹妹穿的比我還像新娘酬屉。我一直安慰自己,他們只是感情好揍愁,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布呐萨。 她就那樣靜靜地躺著,像睡著了一般莽囤。 火紅的嫁衣襯著肌膚如雪谬擦。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,125評(píng)論 1 297
  • 那天朽缎,我揣著相機(jī)與錄音惨远,去河邊找鬼。 笑死话肖,一個(gè)胖子當(dāng)著我的面吹牛北秽,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播最筒,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼贺氓,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了是钥?” 一聲冷哼從身側(cè)響起掠归,我...
    開(kāi)封第一講書(shū)人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎悄泥,沒(méi)想到半個(gè)月后虏冻,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡弹囚,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年厨相,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鸥鹉。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡蛮穿,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出毁渗,到底是詐尸還是另有隱情践磅,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布灸异,位于F島的核電站府适,受9級(jí)特大地震影響羔飞,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜檐春,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一逻淌、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧疟暖,春花似錦卡儒、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至窜骄,卻和暖如春锦募,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背邻遏。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工糠亩, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人准验。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓赎线,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親糊饱。 傳聞我的和親對(duì)象是個(gè)殘疾皇子垂寥,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容