OKHttp攔截器-橋攔截器

經(jīng)過上一篇的解析,我們已經(jīng)對OKHttp的同步請求和異步請求了然于胸隧土,還有五大攔截器可以說是它的畫龍點(diǎn)睛之筆行楞,今天我們就來看看,它們是怎么運(yùn)作的海雪。

RetryAndFollowUpInterceptor锦爵,顧名思義,用來處理請求失敗后重連和重定向的奥裸,上一篇我們知道了責(zé)任鏈調(diào)用的是intercept()方法:

@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
  val realChain = chain as RealInterceptorChain
  var request = chain.request
  val call = realChain.call
  var followUpCount = 0
  var priorResponse: Response? = null
  var newExchangeFinder = true
  var recoveredFailures = listOf<IOException>()
  while (true) {
    call.enterNetworkInterceptorExchange(request, newExchangeFinder)

    var response: Response
    var closeActiveExchange = true
    try {
      if (call.isCanceled()) {
        throw IOException("Canceled")
      }

      try {
        // 
        response = realChain.proceed(request)
        newExchangeFinder = true
      } catch (e: RouteException) {
        // The attempt to connect via a route failed. The request will not have been sent.
        if (!recover(e.lastConnectException, call, request, requestSendStarted = false)) {
          throw e.firstConnectException.withSuppressed(recoveredFailures)
        } else {
          recoveredFailures += e.firstConnectException
        }
        newExchangeFinder = false
        continue
      } catch (e: IOException) {
        // An attempt to communicate with a server failed. The request may have been sent.
        if (!recover(e, call, request, requestSendStarted = e !is ConnectionShutdownException)) {
          throw e.withSuppressed(recoveredFailures)
        } else {
          recoveredFailures += e
        }
        newExchangeFinder = false
        continue
      }

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

      val exchange = call.interceptorScopedExchange
      val followUp = followUpRequest(response, exchange)

      if (followUp == null) {
        if (exchange != null && exchange.isDuplex) {
          call.timeoutEarlyExit()
        }
        closeActiveExchange = false
        return response
      }

      val followUpBody = followUp.body
      if (followUpBody != null && followUpBody.isOneShot()) {
        closeActiveExchange = false
        return response
      }

      response.body?.closeQuietly()

      if (++followUpCount > MAX_FOLLOW_UPS) {
        throw ProtocolException("Too many follow-up requests: $followUpCount")
      }

      request = followUp
      priorResponse = response
    } finally {
      call.exitNetworkInterceptorExchange(closeActiveExchange)
    }
  }
}

攔截器代碼有點(diǎn)多险掀,我們分步驟來看,首先是一個while死循環(huán)湾宙,因?yàn)槲覀兂霈F(xiàn)異常后可能需要重試第二次迷郑、第三次...,所以這里用了一個死循環(huán)创倔,將請求進(jìn)行try catch捕獲嗡害,如果沒有異常,判斷是否需要重定向畦攘,如果不需要霸妹,直接返回response,否則重新創(chuàng)建一個Request進(jìn)行請求知押,并返回response叹螟;如果出現(xiàn)了異常鹃骂,進(jìn)入catch模塊。

重試

RouteException

catch (e: RouteException) {
    if (!recover(e.lastConnectException, call, request, requestSendStarted = false)) {
        throw e.firstConnectException.withSuppressed(recoveredFailures)
    } else {
        recoveredFailures += e.firstConnectException
    }
    newExchangeFinder = false
    continue
}

判斷recover()罢绽,如果返回false畏线,直接拋出異常,否則直接continue進(jìn)入下一次循環(huán)良价,循環(huán)后還是走的try語句塊寝殴,這樣就實(shí)現(xiàn)了重連機(jī)制,不用想明垢,recover()肯定就是判斷是否可以重試了蚣常。

private fun recover(
  e: IOException,
  call: RealCall,
  userRequest: Request,
  requestSendStarted: Boolean
): Boolean {
  // The application layer has forbidden retries.
  if (!client.retryOnConnectionFailure) return false

  // We can't send the request body again.
  if (requestSendStarted && requestIsOneShot(e, userRequest)) return false

  // This exception is fatal.
  if (!isRecoverable(e, requestSendStarted)) return false

  // No more routes to attempt.
  if (!call.retryAfterFailure()) return false

  // For failure recovery, use the same route selector with a new connection.
  return true
}
  1. !client.retryOnConnectionFailure為false, 那么不允許重試痊银, 這個是我們創(chuàng)建OKHttpClient的時候進(jìn)行的配置抵蚊,默認(rèn)為true,如果我們設(shè)置了false溯革,就不會重試了

  2. if (requestSendStarted && requestIsOneShot(e, userRequest)) return false 
    
    private fun requestIsOneShot(e: IOException, userRequest: Request): Boolean {
        val requestBody = userRequest.body
        return (requestBody != null && requestBody.isOneShot()) ||
            e is FileNotFoundException
      }
    

    如果requestBody.isOneShot()為true贞绳, 或者異常類型為文件未找到,就不會進(jìn)行重試了致稀,如果請求為post請求時熔酷,需要我們傳遞一個RequestBody對象,它是一個抽象類豺裆,isOneShot()默認(rèn)返回false拒秘,如果我們需要某一個接口特殊處理,就可以重寫此方法:

    class MyRequestBody : RequestBody() {
        override fun contentType(): MediaType? {
            return null
        }
    
        override fun writeTo(sink: BufferedSink) {
        }
     // 覆蓋此方法臭猜,返回true躺酒,代表不要進(jìn)行重試
        override fun isOneShot(): Boolean {
            return true
        }
    }
    
  3. if (!isRecoverable(e, requestSendStarted)) return false,這個方法判斷一些異常類型蔑歌,某些異常時不可以重試:

    private fun isRecoverable(e: IOException, requestSendStarted: Boolean): Boolean {
      // If there was a protocol problem, don't recover.
      if (e is 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 is InterruptedIOException) {
        return e is 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 is SSLHandshakeException) {
        // If the problem was a CertificateException from the X509TrustManager,
        // do not retry.
        if (e.cause is CertificateException) {
          return false
        }
      }
      if (e is 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
    }
    
    • ProtocolException(協(xié)議異常)時羹应,不允許重試;
    • InterruptedIOException(IO中斷異常)次屠,如果是因?yàn)檫B接超時那么就允許重試园匹,反之不可以;
    • SSLHandshakeException(SSL握手異常時)劫灶,鑒權(quán)失敗了就不可以重試裸违;
    • SSLPeerUnverifiedException(證書過期 or 失效),不可以重試本昏;
  4. if (!call.retryAfterFailure()) return false供汛,判斷有沒有可以用來連接的路由路線,如果沒有就返回false,如果存在更多的線路怔昨,那么就會嘗試換條線路進(jìn)行重試雀久。

IOException

catch (e: IOException) {
  // An attempt to communicate with a server failed. The request may have been sent.
  if (!recover(e, call, request, requestSendStarted = e !is ConnectionShutdownException)) {
    throw e.withSuppressed(recoveredFailures)
  } else {
    recoveredFailures += e
  }
  newExchangeFinder = false
  continue
}

同樣是調(diào)用recover()方法進(jìn)行判斷,這里就不多講了趁舀。

重定向

如果請求的過程中沒有拋出異常赖捌,那么就要判斷是否可以重定向。

val followUp = followUpRequest(response, exchange)

if (followUp == null) {
  if (exchange != null && exchange.isDuplex) {
    call.timeoutEarlyExit()
  }
  closeActiveExchange = false
  return response
}

val followUpBody = followUp.body
if (followUpBody != null && followUpBody.isOneShot()) {
  closeActiveExchange = false
  return response
}

response.body?.closeQuietly()

if (++followUpCount > MAX_FOLLOW_UPS) {
  throw ProtocolException("Too many follow-up requests: $followUpCount")
}

調(diào)用followUpRequest()方法獲取重定向之后的Request矮烹。

如果不允許重定向越庇,就返回null,這時候直接把response返回即可擂送;

如果允許重定向,獲取新的請求體唯欣,判斷followUpBody.isOneShot()為true嘹吨,代表不可以重定向,直接返回response境氢;

否則使用新的Request進(jìn)行請求蟀拷。

@Throws(IOException::class)
private fun followUpRequest(userResponse: Response, exchange: Exchange?): Request? {
  val route = exchange?.connection?.route()
  val responseCode = userResponse.code

  val method = userResponse.request.method
  when (responseCode) {
    HTTP_PROXY_AUTH -> {
      val selectedProxy = route!!.proxy
      if (selectedProxy.type() != Proxy.Type.HTTP) {
        throw ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy")
      }
      return client.proxyAuthenticator.authenticate(route, userResponse)
    }

    HTTP_UNAUTHORIZED -> return client.authenticator.authenticate(route, userResponse)

    HTTP_PERM_REDIRECT, HTTP_TEMP_REDIRECT, HTTP_MULT_CHOICE, HTTP_MOVED_PERM, HTTP_MOVED_TEMP, HTTP_SEE_OTHER -> {
      return buildRedirectRequest(userResponse, method)
    }

    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) {
        // The application layer has directed us not to retry the request.
        return null
      }

      val requestBody = userResponse.request.body
      if (requestBody != null && requestBody.isOneShot()) {
        return null
      }
      val priorResponse = userResponse.priorResponse
      if (priorResponse != null && 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
    }

    HTTP_UNAVAILABLE -> {
      val priorResponse = userResponse.priorResponse
      if (priorResponse != null && 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
    }

    HTTP_MISDIRECTED_REQUEST -> {
      // OkHttp can coalesce HTTP/2 connections even if the domain names are different. See
      // RealConnection.isEligible(). If we attempted this and the server returned HTTP 421, then
      // we can retry on a different connection.
      val requestBody = userResponse.request.body
      if (requestBody != null && requestBody.isOneShot()) {
        return null
      }

      if (exchange == null || !exchange.isCoalescedConnection) {
        return null
      }

      exchange.connection.noCoalescedConnections()
      return userResponse.request
    }

    else -> return null
  }
}

根據(jù)服務(wù)器響應(yīng)的code判斷是否進(jìn)行重定向

  • HTTP_PROXY_AUTH:407 客戶端使用了HTTP代理服務(wù)器,如果在請求頭中添加了Proxy-Authorization萍聊,讓代理服務(wù)器授權(quán)進(jìn)行重定向

  • HTTP_UNAUTHORIZED:401 需要身份驗(yàn)證问芬,有些服務(wù)器接口需要驗(yàn)證使用者身份 在請求頭中添加Authorization

  • **HTTP_PERM_REDIRECT(308), **永久重定向

    **HTTP_TEMP_REDIRECT(307), **臨時重定向

    HTTP_MULT_CHOICE(300),

    **HTTP_MOVED_PERM(301), **

    HTTP_MOVED_TEMP(302),

    HTTP_SEE_OTHER(303)

    private fun buildRedirectRequest(userResponse: Response, method: String): Request? {
       
        if (!client.followRedirects) return null
      // 1. 如果請求頭中沒有Location , 那么沒辦法重定向
        val location = userResponse.header("Location") ?: return null
        // 2. 解析Location請求頭中的url寿桨,如果不是正確的url此衅,返回null
        val url = userResponse.request.url.resolve(location) ?: return null
    
        // 3. 如果重定向在http到https之間切換,需要檢查用戶是不是允許(默認(rèn)允許)
        val sameScheme = url.scheme == userResponse.request.url.scheme
        if (!sameScheme && !client.followSslRedirects) return null
     
        val requestBuilder = userResponse.request.newBuilder()
        // 4.判斷請求是不是get或head
        if (HttpMethod.permitsRequestBody(method)) {
          val responseCode = userResponse.code
          val maintainBody = HttpMethod.redirectsWithBody(method) ||
              responseCode == HTTP_PERM_REDIRECT ||
              responseCode == HTTP_TEMP_REDIRECT
           // 5. 重定向請求中 只要不是 PROPFIND 請求亭螟,無論是POST還是其他的方法都要改為GET請求方式挡鞍,即只有 PROPFIND 請求才能有請求體
          // HttpMethod.redirectsToGet(method) 判斷是否是PROPFIND,不是返回true
          if (HttpMethod.redirectsToGet(method) && responseCode != HTTP_PERM_REDIRECT && responseCode != HTTP_TEMP_REDIRECT) {
            requestBuilder.method("GET", null)
          } else {
            // 如果是PROPFIND請求预烙,添加請求體
            val requestBody = if (maintainBody) userResponse.request.body else null
            requestBuilder.method(method, requestBody)
          }
          // 6. 不是 PROPFIND 的請求墨微,把請求頭中關(guān)于請求體的數(shù)據(jù)刪掉
          if (!maintainBody) {
            requestBuilder.removeHeader("Transfer-Encoding")
            requestBuilder.removeHeader("Content-Length")
            requestBuilder.removeHeader("Content-Type")
          }
        }
        // 7. 在跨主機(jī)重定向時,刪除身份驗(yàn)證請求頭
        if (!userResponse.request.url.canReuseConnectionFor(url)) {
          requestBuilder.removeHeader("Authorization")
        }
      // 返回Request對象
        return requestBuilder.url(url).build()
    }
    

    如果是以上幾種狀態(tài)扁掸,會走的這里的代碼翘县,并返回Request對象,其中每一步都有注釋谴分,這里就不一一贅述了锈麸。

  • HTTP_CLIENT_TIMEOUT:408,客戶端請求超時牺蹄,算是請求失敗了掐隐,這里其實(shí)是走重試邏輯了

    • if (!client.retryOnConnectionFailure):先判斷用戶是否允許重試
    • if (requestBody != null && requestBody.isOneShot()):判斷本次請求是否可以重試
    • if (priorResponse != null && priorResponse.code == HTTP_CLIENT_TIMEOUT):如果是本身這次的響應(yīng)就是重新請求的產(chǎn)物,也就是說上一次請求也是408,那我們這次不再重請求了
    • if (retryAfter(userResponse, 0) > 0):如果服務(wù)器告訴我們了 Retry-After 多久后重試虑省,那框架不管了匿刮。
  • HTTP_UNAVAILABLE(503):服務(wù)不可用,和408差不多探颈,但是只在服務(wù)器告訴你 Retry-After:0(意思就是立即重試) 才重新請求 熟丸。

  • HTTP_MISDIRECTED_REQUEST(421):這個是OKHttp4.x以后新加的,即使域名不同伪节,OkHttp也可以合并HTTP/2連接光羞,如果服務(wù)器返回了421,會進(jìn)行重試怀大。

總結(jié)

需要注意是纱兑,在重定向的時候,還有這樣一段代碼:

// MAX_FOLLOW_UPS = 20
if (++followUpCount > MAX_FOLLOW_UPS) {
  throw ProtocolException("Too many follow-up requests: $followUpCount")
}

也就是說化借,重定向最大發(fā)生次數(shù)為20次潜慎,超過20次就會拋出異常。

這個攔截器是責(zé)任鏈中的第一個蓖康,根據(jù)上一篇我們分析的铐炫,相當(dāng)于是最后一個處理響應(yīng)結(jié)果的,在這個攔截器中的主要功能就是進(jìn)行重試和重定向蒜焊。

重試的前提是發(fā)生了RouteExceptionIOException倒信,只要請求的過程中出現(xiàn)了這連個異常,就會通過record()方法進(jìn)行判斷是否重試泳梆。

從定向是不需要重試的情況下鳖悠,根據(jù)followUpRequest()方法,判斷各種響應(yīng)碼才決定是否重定向优妙,重定向的發(fā)生次數(shù)最大20次竞穷。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市鳞溉,隨后出現(xiàn)的幾起案子瘾带,更是在濱河造成了極大的恐慌,老刑警劉巖熟菲,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件看政,死亡現(xiàn)場離奇詭異,居然都是意外死亡抄罕,警方通過查閱死者的電腦和手機(jī)允蚣,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來呆贿,“玉大人嚷兔,你說我怎么就攤上這事森渐。” “怎么了冒晰?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵同衣,是天一觀的道長。 經(jīng)常有香客問我壶运,道長耐齐,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任蒋情,我火速辦了婚禮埠况,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘棵癣。我一直安慰自己辕翰,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布狈谊。 她就那樣靜靜地躺著喜命,像睡著了一般。 火紅的嫁衣襯著肌膚如雪的畴。 梳的紋絲不亂的頭發(fā)上渊抄,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天尝胆,我揣著相機(jī)與錄音丧裁,去河邊找鬼。 笑死含衔,一個胖子當(dāng)著我的面吹牛煎娇,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播贪染,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼缓呛,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了杭隙?” 一聲冷哼從身側(cè)響起哟绊,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎痰憎,沒想到半個月后票髓,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡铣耘,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年洽沟,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蜗细。...
    茶點(diǎn)故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡裆操,死狀恐怖怒详,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情踪区,我是刑警寧澤昆烁,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站朽缴,受9級特大地震影響善玫,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜密强,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一茅郎、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧或渤,春花似錦系冗、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至池磁,卻和暖如春奔害,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背地熄。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工华临, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人端考。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓雅潭,卻偏偏與公主長得像,于是被迫代替她去往敵國和親却特。 傳聞我的和親對象是個殘疾皇子扶供,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評論 2 345

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