经过上一篇的解析,我们已经对OKHttp的同步请求和异步请求了然于胸,还有五大拦截器可以说是它的画龙点睛之笔,今天我们就来看看,它们是怎么运作的。
RetryAndFollowUpInterceptor
,顾名思义,用来处理请求失败后重连和重定向的,上一篇我们知道了责任链调用的是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)
}
}
}
拦截器代码有点多,我们分步骤来看,首先是一个while死循环,因为我们出现异常后可能需要重试第二次、第三次...,所以这里用了一个死循环,将请求进行try catch捕获,如果没有异常,判断是否需要重定向,如果不需要,直接返回response,否则重新创建一个Request进行请求,并返回response;如果出现了异常,进入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进入下一次循环,循环后还是走的try语句块,这样就实现了重连机制,不用想,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, 那么不允许重试, 这个是我们创建OKHttpClient的时候进行的配置,默认为true,如果我们设置了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, 或者异常类型为文件未找到,就不会进行重试了,如果请求为post请求时,需要我们传递一个RequestBody对象,它是一个抽象类,isOneShot()默认返回false,如果我们需要某一个接口特殊处理,就可以重写此方法:class MyRequestBody : RequestBody() { override fun contentType(): MediaType? { return null } override fun writeTo(sink: BufferedSink) { } // 覆盖此方法,返回true,代表不要进行重试 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(协议异常)时,不允许重试;
- InterruptedIOException(IO中断异常),如果是因为连接超时那么就允许重试,反之不可以;
- SSLHandshakeException(SSL握手异常时),鉴权失败了就不可以重试;
- SSLPeerUnverifiedException(证书过期 or 失效),不可以重试;
if (!call.retryAfterFailure()) return false
,判断有没有可以用来连接的路由路线,如果没有就返回false,如果存在更多的线路,那么就会尝试换条线路进行重试。
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
}
同样是调用recover()
方法进行判断,这里就不多讲了。
重定向
如果请求的过程中没有抛出异常,那么就要判断是否可以重定向。
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")
}
调用followUpRequest()
方法获取重定向之后的Request。
如果不允许重定向,就返回null,这时候直接把response返回即可;
如果允许重定向,获取新的请求体,判断followUpBody.isOneShot()
为true,代表不可以重定向,直接返回response;
否则使用新的Request进行请求。
@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
}
}
根据服务器响应的code判断是否进行重定向
HTTP_PROXY_AUTH:407 客户端使用了HTTP代理服务器,如果在请求头中添加了
Proxy-Authorization
,让代理服务器授权进行重定向HTTP_UNAUTHORIZED:401 需要身份验证,有些服务器接口需要验证使用者身份 在请求头中添加
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之间切换,需要检查用户是不是允许(默认允许) 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 的请求,把请求头中关于请求体的数据删掉 if (!maintainBody) { requestBuilder.removeHeader("Transfer-Encoding") requestBuilder.removeHeader("Content-Length") requestBuilder.removeHeader("Content-Type") } } // 7. 在跨主机重定向时,删除身份验证请求头 if (!userResponse.request.url.canReuseConnectionFor(url)) { requestBuilder.removeHeader("Authorization") } // 返回Request对象 return requestBuilder.url(url).build() }
如果是以上几种状态,会走的这里的代码,并返回Request对象,其中每一步都有注释,这里就不一一赘述了。
-
HTTP_CLIENT_TIMEOUT:408,客户端请求超时,算是请求失败了,这里其实是走重试逻辑了
-
if (!client.retryOnConnectionFailure)
:先判断用户是否允许重试 -
if (requestBody != null && requestBody.isOneShot())
:判断本次请求是否可以重试 -
if (priorResponse != null && priorResponse.code == HTTP_CLIENT_TIMEOUT)
:如果是本身这次的响应就是重新请求的产物,也就是说上一次请求也是408,那我们这次不再重请求了 -
if (retryAfter(userResponse, 0) > 0)
:如果服务器告诉我们了 Retry-After 多久后重试,那框架不管了。
-
HTTP_UNAVAILABLE(503):服务不可用,和408差不多,但是只在服务器告诉你 Retry-After:0(意思就是立即重试) 才重新请求 。
HTTP_MISDIRECTED_REQUEST(421):这个是OKHttp4.x以后新加的,即使域名不同,OkHttp也可以合并HTTP/2连接,如果服务器返回了421,会进行重试。
总结
需要注意是,在重定向的时候,还有这样一段代码:
// MAX_FOLLOW_UPS = 20
if (++followUpCount > MAX_FOLLOW_UPS) {
throw ProtocolException("Too many follow-up requests: $followUpCount")
}
也就是说,重定向最大发生次数为20次,超过20次就会抛出异常。
这个拦截器是责任链中的第一个,根据上一篇我们分析的,相当于是最后一个处理响应结果的,在这个拦截器中的主要功能就是进行重试和重定向。
重试的前提是发生了RouteException
和IOException
,只要请求的过程中出现了这连个异常,就会通过record()
方法进行判断是否重试。
从定向是不需要重试的情况下,根据followUpRequest()
方法,判断各种响应码才决定是否重定向,重定向的发生次数最大20次。