OkHttp4.3源码解析之 - 重试和重定向

OkHttp4.3源码解析之 - 重试和重定向

回顾

上一篇文章:发起请求

大家还记得OkHttp是如何发起一条请求的吗?上面这篇文章里介绍了OkHttp是在什么时候把多个拦截器加入到责任链中的。如果大家没看过的可以先去了解一下,因为这个流程和本文息息相关。

如果是忘了的话我们再简单的回顾一遍:

  • 构建OkHttpClient对象
  • 构建Request对象
  • 使用enqueue()发起请求,并处理回调

在第一步里,我们可以通过addInterceptor(xxx)来添加自定义的拦截器,在第三步里,我们通过源码可以看到在RealCall中通过getResponseWithInterceptorChain()方法来处理这些拦截器。

一个简单的例子

现在有个需求,希望每个请求都能把请求时间给打印出来,该怎么做呢?

OkHttpTest类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class OkHttpTest {    
internal fun test() {
Request.Builder()
.url("https://www.baidu.com").build().let { request ->
OkHttpClient.Builder()
.addInterceptor(LoggingInterceptor()) // 添加自定义的拦截器
.build()
.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
println("bluelzy --- ${e.message}")
}

override fun onResponse(call: Call, response: Response) {
println("bluelzy --- ${response.body.toString()}")
}

})
}

}
}

LoggingInterceptor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class LoggingInterceptor : Interceptor {

override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()

val startTime = System.nanoTime()
logger.info(String.format("Bluelzy --- Sending request %s", request.url)

val response = chain.proceed(request)

val endTime = System.nanoTime()
logger.info(String.format("Bluelzy --- Received response from %s in %.1fms%n%s",
response.request.url, (endTime-startTime)/1e6, response.headers))

return response

}
}

运行程序然后打开Logcat

1
2
3
4
5
6
7
8
9
10
11
com.bluelzy.kotlinlearning I/TaskRunner: Bluelzy --- Sending request https://www.baidu.com/ 
com.bluelzy.kotlinlearning I/TaskRunner: Bluelzy --- Received response for https://www.baidu.com/ in 172.9ms
Cache-Control: private, no-cache, no-store, proxy-revalidate, no-transform
Connection: keep-alive
Content-Type: text/html
Date: Sat, 15 Feb 2020 12:41:37 GMT
Last-Modified: Mon, 23 Jan 2017 13:24:32 GMT
Pragma: no-cache
Server: bfe/1.0.8.18
Set-Cookie: BDORZ=27315; max-age=86400; domain=.baidu.com; path=/
Transfer-Encoding: chunked

可以看到这里把我们需要的信息都打印出来了。

思考一下:

  • 这个logger拦截器是怎么添加到OkHttp的呢?
  • OkHttp如何运用责任链模式进行多个不同网络层之间的责任划分?
  • 我们在实际开发中还能运用责任链模式做其他什么操作吗?

OkHttp中的责任链模式

自定义的拦截器是如何添加到OkHttp中的?

还记得上一篇文章说的吗,我们构建一个OkHttpClient对象,使用的就是Builder模式,通过addInterceptor(interceptor: Interceptor) 这个方法,把拦截器加入到队列中,这个拦截器队列就是OkHttpClient中的一个全局变量,不仅用于存放我们自定义的拦截器,也用于存放OkHttp默认实现的拦截器。

1
2
3
fun addInterceptor(interceptor: Interceptor) = apply {
interceptors += interceptor
}

OkHttp的责任链模式是如何起作用的?

我们看RealCall里的这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Throws(IOException::class)
fun getResponseWithInterceptorChain(): Response {
// Build a full stack of interceptors.
val interceptors = mutableListOf<Interceptor>()
interceptors += client.interceptors
interceptors += RetryAndFollowUpInterceptor(client)
interceptors += BridgeInterceptor(client.cookieJar)
interceptors += CacheInterceptor(client.cache)
interceptors += ConnectInterceptor
if (!forWebSocket) {
interceptors += client.networkInterceptors
}
interceptors += CallServerInterceptor(forWebSocket)

// 省略代码
}

这里做了几件事:

  • 把OkHttpClient里面的Interceptor加入到列表中
  • 把默认的几个Interceptor加入到列表中
  • 判断是不是Socket请求,不是的话把networkInterceptor加入到列表中
  • 最后加入CallServerInterceptor,这个拦截器是用来向服务器发起请求的,因此要加在最后

RetryAndFollowUpInterceptor

大家如果看了上一篇文章的话,应该还记得真正处理责任链的是RetryAndFollowUpInterceptor.proceed()方法,通过index取出对应的拦截器并执行interceptor.intercept() 方法。而无论是我们自定义的Interceptor,还是OkHttp中默认实现的,都会继承Interceptor这个接口,因此都会实现intercept()方法。

接下来,我们来看看RetryAndFollowUpInterceptor里面做了什么?

1
2
3
4
5
> This interceptor recovers from failures and follows redirects as necessary. It may throw an [IOException] if the call was canceled.
>
> 这个拦截器主要是请求失败时尝试恢复连接,还有处理重定向的问题。
> 如果请求被取消了,可能会抛出IOException异常
>

既然主要是处理这两个问题,那么我们就重点关注看看,这个拦截器是如何处理的。

失败时尝试重连
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
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
val realChain = chain as RealInterceptorChain
val transmitter = realChain.transmitter()
var followUpCount = 0
var priorResponse: Response? = null
while (true) {
transmitter.prepareToConnect(request) // 创建Stream

if (transmitter.isCanceled) {
throw IOException("Canceled") // 请求被取消时抛出异常
}

var response: Response
var success = false
try {
response = realChain.proceed(request, transmitter, null)
success = true
} catch (e: RouteException) {
// 路由异常,调用recover()
if (!recover(e.lastConnectException, transmitter, false, request)) {
throw e.firstConnectException
}
continue
} catch (e: IOException) {
// 服务端异常,调用recover()
val requestSendStarted = e !is ConnectionShutdownException
if (!recover(e, transmitter, requestSendStarted, request)) throw e
continue
} finally {
// The network call threw an exception. Release any resources.
if (!success) {
transmitter.exchangeDoneDueToException()
}
}
// 省略代码
}
}

在发起请求的时候,如果出现了异常,根据不同异常会调用recover()方法,我们看看这个方法做了什么

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,
transmitter: Transmitter,
requestSendStarted: Boolean,
userRequest: Request
): Boolean {
// 判断客户端是否禁止重连,是的话直接返回false
if (!client.retryOnConnectionFailure) return false

// 如果已经发送了请求,那么也直接返回false
if (requestSendStarted && requestIsOneShot(e, userRequest)) return false

// 如果是Fatal类异常,返回false
if (!isRecoverable(e, requestSendStarted)) return false

// 如果没有其他路由可以尝试重连,返回false
if (!transmitter.canRetry()) return false

return true
}
  • 如果我们在OkHttpClient设置了不能重连,game over
  • 这个重连只能发生在连接失败的时候,如果是请求已经发送了,game over
  • 如果这个异常是协议相关的问题,那么同样game over
  • OkHttp会在连接池里面尝试不同的IP,如果没有其他可用的IP,game over

如果这个异常是在连接的时候发生的,而且还有可用的IP,我们也设置了可以重试(默认为true),那么就会再构造一个Request,用于重试。

这里OkHttp用了一个很巧妙的方法来实现,那就是递归。

  1. 首先在intercept()方法开头加入了while(true),只要没有return或者throw excpetion,就会一直执行下去

  2. 1
    response = realChain.proceed(request, transmitter, null)
  3. 通过上面这一句代码,每次构建一个Request,然后调用proceed进行请求

  4. 每一个请求的结果都会返回到上一个Response中

  5. 每次请求完毕followUpCount 加一,在OkHttp中,这个参数用于控制请求最大数,默认是20,一旦超过这个数,也会抛出异常

如何进行重定向

重定向和重试其实都在intercept()方法中,主要是这句代码:

1
val followUp = followUpRequest(response, route)

followUpRequeset():

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
@Throws(IOException::class)
private fun followUpRequest(userResponse: Response, route: Route?): Request? {
val responseCode = userResponse.code

val method = userResponse.request.method
when (responseCode) {
HTTP_PROXY_AUTH -> {
// 省略代码

// 1.重定向 (307, 308)
HTTP_PERM_REDIRECT, HTTP_TEMP_REDIRECT -> {
if (method != "GET" && method != "HEAD") {
return null
}
return buildRedirectRequest(userResponse, method)
}

// 2.重定向(300,301,302,303)
HTTP_MULT_CHOICE, HTTP_MOVED_PERM, HTTP_MOVED_TEMP, HTTP_SEE_OTHER -> {
return buildRedirectRequest(userResponse, method)
}

HTTP_CLIENT_TIMEOUT -> {
// 省略代码

return userResponse.request
}

HTTP_UNAVAILABLE -> {
// 省略代码

return null
}
else -> return null
}
}

可以看到,这里通过判断responseCode来进行不同的处理,我们重点关注重定向,看看buildRedirectRequest()方法:

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
private fun buildRedirectRequest(userResponse: Response, method: String): Request? {
// 1.客户端是否支持重定向
if (!client.followRedirects) return null

val location = userResponse.header("Location") ?: return null
// 2.是不是http/https协议,不是的话抛异常,返回null
val url = userResponse.request.url.resolve(location) ?: return null

// 3.是否支持Ssl重定向
val sameScheme = url.scheme == userResponse.request.url.scheme
if (!sameScheme && !client.followSslRedirects) return null

// 4.构建请求体
val requestBuilder = userResponse.request.newBuilder()
if (HttpMethod.permitsRequestBody(method)) {
val maintainBody = HttpMethod.redirectsWithBody(method)
if (HttpMethod.redirectsToGet(method)) {
requestBuilder.method("GET", null)
} else {
val requestBody = if (maintainBody) userResponse.request.body else null
requestBuilder.method(method, requestBody)
}
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()
}

这里就是一个很标准的Builder模式的应用了,通过request.newBuilder()和后续的builder.xxx()构建一个Request.Builder对象。最终调用build()方法返回Request对象。

总结

在RetryAndFollowUpInterceptor中,主要做了两件事

  1. 重试
  2. 重定向

这两者都是通过followUpRequest()方法来构建一个新的Request,然后递归调用proceed()方法进行请求。中间有很多的错误/异常判断,一旦条件不满足就会停止请求并且释放资源。

我们在实际工作中如何使用?

例如,我们现在不想使用OkHttp默认的重试拦截器,希望自己定义重试次数,那么可以这样写:

RetryInterceptor:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* author : BlueLzy
* e-mail : bluehobert@gmail.com
* date : 2020/02/16 16:07
* desc : 重试拦截器
*/
class RetryInterceptor(private val maxRetryCount: Int = 2) : Interceptor {

private var retryNum = 0

override fun intercept(chain: Interceptor.Chain): Response =
chain.proceed(chain.request()).apply {
while (!isSuccessful && retryNum < maxRetryCount) {
retryNum++
logger.info("BlueLzy --- 重试次数:$retryNum")
chain.proceed(request)
}
}

}

为了能测试重试拦截器,我们定义一个测试请求,每次返回400

TestInterceptor:

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
/**
* author : BlueLzy
* e-mail : bluehobert@gmail.com
* date : 2020/02/16 16:18
* desc :
*/
class TestInterceptor : Interceptor {

override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
return if (request.url.toString() == TEST_URL) {
val responseString = "我是响应数据"
Response.Builder()
.code(400)
.request(request)
.protocol(Protocol.HTTP_1_1)
.message(responseString)
.body(responseString.toResponseBody("application/json".toMediaTypeOrNull()))
.addHeader("content-type", "application/json")
.build()
} else {
chain.proceed(request)
}
}
}

最后发起请求

OkHttpTest:

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
class OkHttpTest {

internal fun test() {
Request.Builder()
.url(TEST_URL).build().let { request ->
OkHttpClient.Builder()
.addInterceptor(RetryInterceptor()) // 添加重试拦截器,默认重试次数为2
.addInterceptor(TestInterceptor()) // 添加测试请求
.build()
.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
println("BlueLzy --- ${e.message}")
}

override fun onResponse(call: Call, response: Response) {
println("BlueLzy --- ${response.message}")
}

})
}
}

companion object {
const val TEST_URL = "https://www.baidu.com/"
}

}

我们可以看到logcat打印出来的信息:

1
2
3
4
5
6
2020-02-16 16:46:29.338 1914-2113/com.bluelzy.kotlinlearning I/TaskRunner: BlueLzy --- Sending request https://www.baidu.com/
2020-02-16 16:46:29.338 1914-2113/com.bluelzy.kotlinlearning I/TaskRunner: BlueLzy --- 重试次数:1
2020-02-16 16:46:29.339 1914-2113/com.bluelzy.kotlinlearning I/TaskRunner: BlueLzy --- 重试次数:2
2020-02-16 16:46:29.339 1914-2113/com.bluelzy.kotlinlearning I/TaskRunner: BlueLzy --- Received response for https://www.baidu.com/ in 1.0ms
content-type: application/json
2020-02-16 16:46:29.339 1914-2113/com.bluelzy.kotlinlearning I/System.out: BlueLzy --- 我是响应数据

发起了1次默认请求+2次重试的请求 = 3次请求。这样就实现了自由控制重试次数的需求了,当然我们也可以在每次重试中做其他的操作,例如更改请求头和请求体。

总结

本文我们主要说了OkHttp中的重试机制和重定向,分析了RetryAndFollowUpinterceptor的源码。最后举了个在实际工作中应用的小例子。

下一篇我们说一下OkHttp中的缓存机制 - CacheInterceptor

如有不对之处,欢迎大家多多交流。感谢阅读!

本文结束啦感谢您的阅读