okhttp拦截器之RetryAndFollowUpInterceptor

okhttp源码版本: 4.9.0

承接上篇okhttp框架解析

第一个内置拦截器,RetryAndFollowUpInterceptor如它的名字所诉重试&重定向。它会对特定的网络错误类型和需要重定向的请求进行请求网络重试,那么要想做到重试估计要开启一个循环不停的处理,那它具体是如何工作的呢?

先上RetryAndFollowUpInterceptor:intercept方法代码:

RetryAndFollowUpInterceptor:intercept

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
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) {//开启轮询
//1、构建一个ExchangeFinder
call.enterNetworkInterceptorExchange(request, newExchangeFinder)//

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

try {
//2、运行下一个把拦截器,并拿到请求响应结果
response = realChain.proceed(request)//拿到响应结果
newExchangeFinder = true
} catch (e: RouteException) {
// The attempt to connect via a route failed. The request will not have been sent.
//3 判断发生的RouteException是否回复
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.
//4 判断发生的IOException是否回复
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.
//5 根据上一次的Response,构建一个新的response
if (priorResponse != null) {//之前的Response
response = response.newBuilder()
.priorResponse(priorResponse.newBuilder()
.body(null)
.build())
.build()
}

val exchange = call.interceptorScopedExchange
//6、根据响应码创建一个新的Request请求
val followUp = followUpRequest(response, exchange)//重定向
//7、上一步构建的Request为空,则不再继续下去,直接把的Response返回
if (followUp == null) {
if (exchange != null && exchange.isDuplex) {
call.timeoutEarlyExit()
}
closeActiveExchange = false
return response
}
//8. 如果构建的Request的body为一次性的请求,也同样直接返回Response
val followUpBody = followUp.body
if (followUpBody != null && followUpBody.isOneShot()) {
closeActiveExchange = false
return response
}

response.body?.closeQuietly()
//9、如果重定向次数大于20次,也直接抛出异常,否次进入下次轮询也就是重试
if (++followUpCount > MAX_FOLLOW_UPS) {
throw ProtocolException("Too many follow-up requests: $followUpCount")
}

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

在上面的代码中,在关键流程都做了注释。可以看intercept方法内部直接开启了一个while循环,和预期的一样。循环内部可以主要分为三块:

  1. 网络请求前的准备工作
  2. 把责任链往下传,运行下一个拦截器
  3. 根据响应结果判断是否要重试

第一步:网络请求前的准备工作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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) {//开启轮询
//1、构建一个ExchangeFinder
call.enterNetworkInterceptorExchange(request, newExchangeFinder)//
//...
}
}

调用RealCallenterNetworkInterceptorExchange方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fun enterNetworkInterceptorExchange(request: Request, newExchangeFinder: Boolean) {
check(interceptorScopedExchange == null)

//...
if (newExchangeFinder) {
this.exchangeFinder = ExchangeFinder(
connectionPool,
createAddress(request.url),
this,
eventListener
)
}
}

这里会给RealCall创建一个ExchangeFinder对象,这个ExchangeFinder的构造函数中包含两个重要对象connectionPoolAddressconnectionPool连接池在之后ConnectInterceptor拦截器中会重点讲到,Address的创建通过createAddress方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private fun createAddress(url: HttpUrl): Address {
var sslSocketFactory: SSLSocketFactory? = null
var hostnameVerifier: HostnameVerifier? = null
var certificatePinner: CertificatePinner? = null
if (url.isHttps) {
sslSocketFactory = client.sslSocketFactory
hostnameVerifier = client.hostnameVerifier
certificatePinner = client.certificatePinner
}

return Address(
uriHost = url.host,//主机名
uriPort = url.port,//端口号
dns = client.dns,//dns
socketFactory = client.socketFactory,// socket工厂
sslSocketFactory = sslSocketFactory,//TLS
hostnameVerifier = hostnameVerifier,//TLS 主机验证
certificatePinner = certificatePinner,//证书hash
proxyAuthenticator = client.proxyAuthenticator,//身份验证
proxy = client.proxy,//代理
protocols = client.protocols,//协议类型:HTTP_1_1、HTTP_2
connectionSpecs = client.connectionSpecs,//连接规则:加密和不加密
proxySelector = client.proxySelector
)
}

createAddress创建Address对象,其包含主机名、端口号、TSL等建立网络联系相关的内容、Authenticator、代理、协议等,Address又用于ExchangeFinder对象的创建。ExchangeFinder从字面含义解释为交换查找器,一次网络请求和响应作为一次数据交换,所以ExchangeFinder是作为一个发现数据交换的工具,也为建立网络连接做好前置的准备。

第二步:把责任链往下传

这里比简单:response = realChain.proceed(request)拿到response

第三部:判断是否重试&重定向

先看catch部分地内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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) {//开启轮询
//1、构建一个ExchangeFinder
call.enterNetworkInterceptorExchange(request, newExchangeFinder)//

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

try {
//2、运行下一个把拦截器,并拿到请求响应结果
response = realChain.proceed(request)//拿到响应结果
newExchangeFinder = true
} catch (e: RouteException) {
// The attempt to connect via a route failed. The request will not have been sent.
//3 判断发生的RouteException是否回复
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.
//4 判断发生的IOException是否回复
if (!recover(e, call, request, requestSendStarted = e !is ConnectionShutdownException)) {
throw e.withSuppressed(recoveredFailures)
} else {
recoveredFailures += e
}
newExchangeFinder = false
continue//再次轮询
}
//...
} finally {
call.exitNetworkInterceptorExchange(closeActiveExchange)
}
}
}

如果发生的异常是属于RouteException或者IOException,并且是可恢复的请求,则continue再次发生网络请求。
那我们先分析一下RouteException情况:

RealCall

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private fun recover(
e: IOException,
call: RealCall,
userRequest: Request,
requestSendStarted: Boolean
): Boolean {
// 1
if (!client.retryOnConnectionFailure) return false
// 2
if (requestSendStarted && requestIsOneShot(e, userRequest)) return false

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

// 4 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. 如果OkHttpClient在构建时设置retryOnConnectionFailure为false,则不重试,默认为true
  2. 如果请求最多只能被发送一次,则不会重试
  3. 如果是以下致命的错误不重试
    1. ProtocolException协议异常
    2. SocketTimeoutException Socket连接超时异常
    3. SSLHandshakeException&&CertificateException TSL握手异常
    4. SSLPeerUnverifiedException 异常
  4. 如果寻找到可用的路由,如果不存在也不允许重试

在第四个条件中什么叫可用路由呢? 需要先复习一下http相关的知识,访问某个确定的主机需要满足三个地址:IP地址、端口号、代理类型。比如 you个域名叫http://wds.com,一个域名可能有多个IP,如果给这个设置了http代理,就存在如下几种情况;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
http://wds.com 
1:ip:1.2.3.4
port:80

2:ip:5.6.7.8
port:80

假设代理的域名叫http://wdsproxy.com proxy:http

1:ip:11.22.33.44
port:80

2:ip:55.66.77.88
port:80
代理和不代理共就四中路由情况

再回到代码中,看Route这个类
router
Route包含:Address(包含端口号)、代理类型、IP地址。所以retryAfterFailure方法就是从RrouteSelection中遍历
代码如下很简单可自行阅读:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
fun retryAfterFailure(): Boolean {
if (refusedStreamCount == 0 && connectionShutdownCount == 0 && otherFailureCount == 0) {
return false // Nothing to recover from.
}

if (nextRouteToTry != null) {
return true
}

val retryRoute = retryRoute()
if (retryRoute != null) {
// Lock in the route because retryRoute() is racy and we don't want to call it twice.
nextRouteToTry = retryRoute
return true
}

// If we have a routes left, use 'em.
if (routeSelection?.hasNext() == true) return true

// If we haven't initialized the route selector yet, assume it'll have at least one route.
val localRouteSelector = routeSelector ?: return true

// If we do have a route selector, use its routes.
return localRouteSelector.hasNext()
}

分析完RouteExceptionIOException情况类似,这里就不分析了。

followUpRequest方法:
根据请求码创建一个新的请求,以供下一次重试请求使用:会根据上一次的请求结果添加认证头信息,跟踪重定向或处理客户端请求超时等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
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
}
}
  1. HTTP_PROXY_AUTH(407):代理认证,需要进行代理认证(默认实现返回null,可以进行重写覆盖实现);
  2. HTTP_UNAUTHORIZED(401):未授权,需要进行授权认证(默认实现返回null,可以进行重写覆盖实现);
  3. HTTP_PERM_REDIRECT(307) 或者 HTTP_TEMP_REDIRECT(308) :临时重定向或者永久重定向,如果请求方法不为GET且不为METHOD,返回空,表示不允许重定向;
  4. HTTP_MULT_CHOICE(300) 多选项
  5. HTTP_MOVED_PERM(301) 永久重定向,表示请求的资源已经分配了新的URI,以后应该使用新的URI 、
  6. HTTP_MOVED_TEMP(302)临时性重定向
  7. HTTP_MOVED_TEMP(303)表示由于请求对应的资源存在着另外一个URI,应该使用GET方法定向获取请求的资源
  8. HTTP_CLIENT_TIMEOUT(408):请求超时,
  9. HTTP_UNAVAILABLE(503):表明服务器暂时处于超负载或正在进行停机维护,现无法处理请求。
  10. 本次请求结果和上一次请求结果均返回503,则放弃重试
  11. 如果返回的Retry-After为0,没有任何延迟,则返回Request对象,否则返回空;

RetryAndFollowUpInterceptor主要内容就分析完了