經(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
}
!client.retryOnConnectionFailure
為false, 那么不允許重試痊银, 這個是我們創(chuàng)建OKHttpClient的時候進(jìn)行的配置抵蚊,默認(rèn)為true,如果我們設(shè)置了false溯革,就不會重試了-
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 } }
-
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 失效),不可以重試本昏;
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ā)生了RouteException
和IOException
倒信,只要請求的過程中出現(xiàn)了這連個異常,就會通過record()
方法進(jìn)行判斷是否重試泳梆。
從定向是不需要重試的情況下鳖悠,根據(jù)followUpRequest()
方法,判斷各種響應(yīng)碼才決定是否重定向优妙,重定向的發(fā)生次數(shù)最大20次竞穷。