上一篇文章中,我们了解学习了OkHttp的网络请求的主流程,本文主要分析OkHttp的不同拦截器的源码以及涉及到的功能作用。
目录
一、整体代码流程
直接看RetryAndFollowUpInterceptor的核心方法
@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)
}
}
}
整体流程:
1)创建一个死循环,如果直接得到服务器的返回,就不做重试流程,去看是否需要重定向;需要重定向的话,通过followUpRequest()获得一个新的Request,获得Response
2)如果没有得到服务器返回,通过异常捕获,去尝试重连,2次,3次,不断continue
注意,在最后有这样一行代码,这里是重定向的限制,最大值默认是20次,超过会抛出异常
if (++followUpCount > MAX_FOLLOW_UPS) {
throw ProtocolException("Too many follow-up requests: $followUpCount")
}
另外,在流程开始,enterNetworkInterceptorExchange()初始化了ExchangeFinder对象,ExchangeFinder在ConnectInterceptor中会被用到,用来获取新的连接RealInterceptorChain,不是用之前重试失败留下的连接
二、重连
看重试的核心方法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
}
通过上面的intercept方法我们知道,recover()方法如果返回false,会抛出异常,否则,进入continue流程,继续尝试重连。
retryOnConnectionFailure 如果是false,直接返回false,那样就不会重连了,retryOnConnectionFailure 是OkHttpclient的属性变量,可以在初始化的时候设置,不去做重连
private fun requestIsOneShot(e: IOException, userRequest: Request): Boolean {
val requestBody = userRequest.body
return (requestBody != null && requestBody.isOneShot()) ||
e is FileNotFoundException
}
这里的关键是requsetIsOneShot方法,如果request.isOneShot方法返回true,或者异常是文件没有找到,这个时候就不会重拾了,isOneShot是RequestBody的方法,默认返回false,如果不需要重试,自定义一个RequestBody方法,重写这个isOneShot方法返回true即可
继续向下看该方法
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 证书过期或失效,不做重连
all.retryAfterFailure()
这里主要是判断有没有可用的线路来尝试重连,没有的话返回false,如果有的话,就尝试换线路重连
主流程中的另外一个catch中的recover方法流程和上面的是一个方法,不做分析了,接下来看重试部分
三、重定向
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
}
}
如果不允许重定向,直接返回null,看followUpRequest如何获取新的request,可以看到里面有很多responseCode,我们逐个看下里面的内容
-
HTTP_PROXY_AUTH = 407
客户端使用了Http代理服务器,在请求头中加上Proxy-Authorization,让代理服务器进行重定向
-
HTTP_UNAUTHORIZED = 401
身份认证,需要在请求头中加Authorization
-
HTTP_PERM_REDIRECT = 308, HTTP_TEMP_REDIRECT = 307, HTTP_MULT_CHOICE, HTTP_MOVED_PERM, HTTP_MOVED_TEMP, HTTP_SEE_OTHER
private fun buildRedirectRequest(userResponse: Response, method: String): Request? {
// Does the client allow redirects?
if (!client.followRedirects) return null
// 如果请求头中没有Location , 不去重定向
val location = userResponse.header("Location") ?: return null
// Don't follow redirects to unsupported protocols.
val url = userResponse.request.url.resolve(location) ?: return null
// If configured, don't follow redirects between SSL and non-SSL.
//这里英文已经注释了,其实就是Scheme就是http或https,如果涉及Scheme的切换,要看是否配置允
//许,默认允许
val sameScheme = url.scheme == userResponse.request.url.scheme
if (!sameScheme && !client.followSslRedirects) return null
// Most redirects don't include a request body.
val requestBuilder = userResponse.request.newBuilder()
//判断方法是不是(method == "GET" || method == "HEAD")
if (HttpMethod.permitsRequestBody(method)) {
val responseCode = userResponse.code
val maintainBody = HttpMethod.redirectsWithBody(method) ||
responseCode == HTTP_PERM_REDIRECT ||
responseCode == HTTP_TEMP_REDIRECT
//判断方法是不是PROPFIND,不是PROPFIND方法请求,请求方法都要改成是get,并且不能有请求体
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)
}
// 不是PROPFIND 的请求,请求头中关于请求体的数据删掉
if (!maintainBody) {
requestBuilder.removeHeader("Transfer-Encoding")
requestBuilder.removeHeader("Content-Length")
requestBuilder.removeHeader("Content-Type")
}
}
// When redirecting across hosts, drop all authentication headers. This
// is potentially annoying to the application layer since they have no
// way to retain them.
//跨主机重定向时,删除身份验证请求头
if (!userResponse.request.url.canReuseConnectionFor(url)) {
requestBuilder.removeHeader("Authorization")
}
return requestBuilder.url(url).build()
}
上面buildRedirectRequest()的主要部分已经注释,代码流程比较简单
HTTP_CLIENT_TIMEOUT = 408
客户端请求超时,前面两行代码在重试逻辑中分析过了,直接在代码上注释
val priorResponse = userResponse.priorResponse
// 上次响应码如果已经是超时,直接返回,不需要走重定向
if (priorResponse != null && priorResponse.code == HTTP_CLIENT_TIMEOUT) {
// We attempted to retry and got another timeout. Give up.
return null
}
// 如果服务器告诉我们了 Retry-After 多久后重试,也不需要走重定向
if (retryAfter(userResponse, 0) > 0) {
return null
}
-
HTTP_UNAVAILABLE = 503
服务不可用,和408类似,服务器如果告诉了Retry-After,则立即返回Request
-
HTTP_MISDIRECTED_REQUEST = 421
// 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.
直接看代码注释,大意就是:“即使域名不同,OkHttp 也可以合并 HTTP/2 连接。请参阅 RealConnection.isEligible()。 如果我们尝试这样做并且服务器返回 HTTP 421,那么我们可以在不同的连接上重试”。总结成一句话,421响应码可以在一个不同的连接上重试请求。
四、总结
- 重连主要是在catch异常中完成,异常是RouteException和IOException
- 重连涉及判断协议异常、IO中断异常,SSL握手异常,证书过期或失效,重试判断的是HTTP响应码,比如代理服务器、请求超时、身份认证失效等,其中307和308请求头不是GET或者HEAD不做重定向
- 重定向的最大次数默认是20次