經(jīng)過(guò)上一篇的解析,我們已經(jīng)對(duì)OKHttp的同步請(qǐng)求和異步請(qǐng)求了然于胸,還有五大攔截器可以說(shuō)是它的畫龍點(diǎn)睛之筆,今天我們就來(lái)看看,它們是怎么運(yùn)作的。
RetryAndFollowUpInterceptor,顧名思義,用來(lái)處理請(qǐng)求失敗后重連和重定向的,上一篇我們知道了責(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)多,我們分步驟來(lái)看,首先是一個(gè)while死循環(huán),因?yàn)槲覀兂霈F(xiàn)異常后可能需要重試第二次、第三次...,所以這里用了一個(gè)死循環(huán),將請(qǐng)求進(jìn)行try catch捕獲,如果沒(méi)有異常,判斷是否需要重定向,如果不需要,直接返回response,否則重新創(chuàng)建一個(gè)Request進(jìn)行請(qǐng)求,并返回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語(yǔ)句塊,這樣就實(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
}
!client.retryOnConnectionFailure為false, 那么不允許重試, 這個(gè)是我們創(chuàng)建OKHttpClient的時(shí)候進(jìn)行的配置,默認(rèn)為true,如果我們?cè)O(shè)置了false,就不會(huì)重試了-
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, 或者異常類型為文件未找到,就不會(huì)進(jìn)行重試了,如果請(qǐng)求為post請(qǐng)求時(shí),需要我們傳遞一個(gè)RequestBody對(duì)象,它是一個(gè)抽象類,isOneShot()默認(rèn)返回false,如果我們需要某一個(gè)接口特殊處理,就可以重寫此方法:class MyRequestBody : RequestBody() { override fun contentType(): MediaType? { return null } override fun writeTo(sink: BufferedSink) { } // 覆蓋此方法,返回true,代表不要進(jìn)行重試 override fun isOneShot(): Boolean { return true } } -
if (!isRecoverable(e, requestSendStarted)) return false,這個(gè)方法判斷一些異常類型,某些異常時(shí)不可以重試: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é)議異常)時(shí),不允許重試;
- InterruptedIOException(IO中斷異常),如果是因?yàn)檫B接超時(shí)那么就允許重試,反之不可以;
- SSLHandshakeException(SSL握手異常時(shí)),鑒權(quán)失敗了就不可以重試;
- SSLPeerUnverifiedException(證書過(guò)期 or 失效),不可以重試;
if (!call.retryAfterFailure()) return false,判斷有沒(méi)有可以用來(lái)連接的路由路線,如果沒(méi)有就返回false,如果存在更多的線路,那么就會(huì)嘗試換條線路進(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)行判斷,這里就不多講了。
重定向
如果請(qǐng)求的過(guò)程中沒(méi)有拋出異常,那么就要判斷是否可以重定向。
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,這時(shí)候直接把response返回即可;
如果允許重定向,獲取新的請(qǐng)求體,判斷followUpBody.isOneShot()為true,代表不可以重定向,直接返回response;
否則使用新的Request進(jìn)行請(qǐng)求。
@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ù)器,如果在請(qǐng)求頭中添加了
Proxy-Authorization,讓代理服務(wù)器授權(quán)進(jìn)行重定向HTTP_UNAUTHORIZED:401 需要身份驗(yàn)證,有些服務(wù)器接口需要驗(yàn)證使用者身份 在請(qǐng)求頭中添加
Authorization-
**HTTP_PERM_REDIRECT(308), **永久重定向
**HTTP_TEMP_REDIRECT(307), **臨時(shí)重定向
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. 如果請(qǐng)求頭中沒(méi)有Location , 那么沒(méi)辦法重定向 val location = userResponse.header("Location") ?: return null // 2. 解析Location請(qǐng)求頭中的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.判斷請(qǐng)求是不是get或head if (HttpMethod.permitsRequestBody(method)) { val responseCode = userResponse.code val maintainBody = HttpMethod.redirectsWithBody(method) || responseCode == HTTP_PERM_REDIRECT || responseCode == HTTP_TEMP_REDIRECT // 5. 重定向請(qǐng)求中 只要不是 PROPFIND 請(qǐng)求,無(wú)論是POST還是其他的方法都要改為GET請(qǐng)求方式,即只有 PROPFIND 請(qǐng)求才能有請(qǐng)求體 // HttpMethod.redirectsToGet(method) 判斷是否是PROPFIND,不是返回true if (HttpMethod.redirectsToGet(method) && responseCode != HTTP_PERM_REDIRECT && responseCode != HTTP_TEMP_REDIRECT) { requestBuilder.method("GET", null) } else { // 如果是PROPFIND請(qǐng)求,添加請(qǐng)求體 val requestBody = if (maintainBody) userResponse.request.body else null requestBuilder.method(method, requestBody) } // 6. 不是 PROPFIND 的請(qǐng)求,把請(qǐng)求頭中關(guān)于請(qǐng)求體的數(shù)據(jù)刪掉 if (!maintainBody) { requestBuilder.removeHeader("Transfer-Encoding") requestBuilder.removeHeader("Content-Length") requestBuilder.removeHeader("Content-Type") } } // 7. 在跨主機(jī)重定向時(shí),刪除身份驗(yàn)證請(qǐng)求頭 if (!userResponse.request.url.canReuseConnectionFor(url)) { requestBuilder.removeHeader("Authorization") } // 返回Request對(duì)象 return requestBuilder.url(url).build() }如果是以上幾種狀態(tài),會(huì)走的這里的代碼,并返回Request對(duì)象,其中每一步都有注釋,這里就不一一贅述了。
-
HTTP_CLIENT_TIMEOUT:408,客戶端請(qǐng)求超時(shí),算是請(qǐng)求失敗了,這里其實(shí)是走重試邏輯了
-
if (!client.retryOnConnectionFailure):先判斷用戶是否允許重試 -
if (requestBody != null && requestBody.isOneShot()):判斷本次請(qǐng)求是否可以重試 -
if (priorResponse != null && priorResponse.code == HTTP_CLIENT_TIMEOUT):如果是本身這次的響應(yīng)就是重新請(qǐng)求的產(chǎn)物,也就是說(shuō)上一次請(qǐng)求也是408,那我們這次不再重請(qǐng)求了 -
if (retryAfter(userResponse, 0) > 0):如果服務(wù)器告訴我們了 Retry-After 多久后重試,那框架不管了。
-
HTTP_UNAVAILABLE(503):服務(wù)不可用,和408差不多,但是只在服務(wù)器告訴你 Retry-After:0(意思就是立即重試) 才重新請(qǐng)求 。
HTTP_MISDIRECTED_REQUEST(421):這個(gè)是OKHttp4.x以后新加的,即使域名不同,OkHttp也可以合并HTTP/2連接,如果服務(wù)器返回了421,會(huì)進(jìn)行重試。
總結(jié)
需要注意是,在重定向的時(shí)候,還有這樣一段代碼:
// MAX_FOLLOW_UPS = 20
if (++followUpCount > MAX_FOLLOW_UPS) {
throw ProtocolException("Too many follow-up requests: $followUpCount")
}
也就是說(shuō),重定向最大發(fā)生次數(shù)為20次,超過(guò)20次就會(huì)拋出異常。
這個(gè)攔截器是責(zé)任鏈中的第一個(gè),根據(jù)上一篇我們分析的,相當(dāng)于是最后一個(gè)處理響應(yīng)結(jié)果的,在這個(gè)攔截器中的主要功能就是進(jìn)行重試和重定向。
重試的前提是發(fā)生了RouteException和IOException,只要請(qǐng)求的過(guò)程中出現(xiàn)了這連個(gè)異常,就會(huì)通過(guò)record()方法進(jìn)行判斷是否重試。
從定向是不需要重試的情況下,根據(jù)followUpRequest()方法,判斷各種響應(yīng)碼才決定是否重定向,重定向的發(fā)生次數(shù)最大20次。