✨作者简介:00后,22年刚刚毕业,一枚在鹅厂搬砖的程序员。
✨前置任务:在阅读本篇文章之前希望读者已经阅读了上篇文章OkHttp原理第二篇—OkHttp的责任链模式,本篇文章详细对
RetryAndFollowUpInterceptor
进行解析,也希望读者在阅读的时候自己边研究源码边思考本篇文章,硬看是很难看懂的若有错误也希望大家指正。
✨学习目标:弄懂
RetryAndFollowUpInterceptor
如何处理重试和重定向。
✨创作初衷:学习
OkHttp
的原理,阅读Kotlin
框架源码,提高自己对Kotlin
代码的阅读能力。为了读代码而读代码,笔者知道这是不对的,但作为应届生,提高阅读源码的能力笔者认为还是很重要的。
主要处理请求的重试和重定向
下面进行验证:
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) {
// 创建ExchangeFinder,此类用于创建和寻找Exchange,Exchange主要处理此次请求中的IO的连接管理和事件请,会在后续的拦截器中进行分析
call.enterNetworkInterceptorExchange(request, newExchangeFinder)
var response: Response
var closeActiveExchange = true
try {
// 若call取消则扔出IO异常
if (call.isCanceled()) {
throw IOException("Canceled")
}
try {
// 将Request交给给下个节点,此处的下个节点为BridgeInterceptor,若下层节点处理过程中扔出错误会被下面的catch捕获
response = realChain.proceed(request)
newExchangeFinder = true
} catch (e: RouteException) {
// 重点方法,决定能否重试的关键方法,此方法返回为true才可重试,看重试小节中的分析
// 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) {
// 只有HTTP2才会抛出ConnectionShutdownException,此篇文章不对HTTP2的实现进行分析,在IO异常进入下述方法第四个参数为true
// An attempt to communicate with a server failed. The request may have been sent.
// 检测该IO异常是否能重新连接
if (!recover(e, call, request, requestSendStarted = e !is ConnectionShutdownException)) {
throw e.withSuppressed(recoveredFailures)
} else {
recoveredFailures += e
}
newExchangeFinder = false
continue
}
// 走到这里,则一定拿到了服务器的响应
// 若循环中的代码完整执行过,则priorResponse不为空,也就意味着需要重定向(不太严谨的说法),类似链表的结构保存下来整个响应路径
if (priorResponse != null) {
response = response.newBuilder()
.priorResponse(priorResponse.newBuilder()
.body(null) //清除body
.build())
.build()
}
// exchange为Exchange类,主要处理此次网络连接的IO操作
val exchange = call.interceptorScopedExchange
// 处理重定向的重点方法,此方法会根据返回的response和连接管理器判断是否需要重定向,看下重定向小节
val followUp = followUpRequest(response, exchange)
// 若为null,则意味着此次请求结束了
if (followUp == null) {
// 此if与websocket相关,此处不分析
if (exchange != null && exchange.isDuplex) {
call.timeoutEarlyExit()
}
closeActiveExchange = false
return response
}
// 拿到新请求的请求体
val followUpBody = followUp.body
// 请求体只可能是原始请求的请求体,若原始请求的请求体不允许重试则直接返回
if (followUpBody != null && followUpBody.isOneShot()) {
closeActiveExchange = false
return response
}
// 需要继续请求,关闭此次响应的body
response.body?.closeQuietly()
// 若重定向次数超过20,则抛出协议一场
if (++followUpCount > MAX_FOLLOW_UPS) {
throw ProtocolException("Too many follow-up requests: $followUpCount")
}
// 继续下一次请求
request = followUp
priorResponse = response
} finally {
// webSocket相关,不予分析
call.exitNetworkInterceptorExchange(closeActiveExchange)
}
}
}
1.RetryAndFollowUpInterceptor#recover
此方法返回为false
则不可重试
//This interceptor recovers from failures and follows redirects as necessary. It may throw an IOException if the call was canceled.
private fun recover(
e: IOException,
call: RealCall,
userRequest: Request,
requestSendStarted: Boolean
): Boolean {
// 在配置OkhttpClient时设置了不允许重试(默认允许),则一旦发生请求失败就不再重试,在构建Client时可以指定。
// The application layer has forbidden retries.
if (!client.retryOnConnectionFailure) return false
// 如果是RouteException或者HTTP2且异常的具体类型为ConnectionShutdownException的情况下,此if不会命中,因requestSendStarted为false
// 若请求时使用了请求体,且请求体继承自RequestBody并重写isOneShot()返回为true,则不允许重试,看下2.RetryAndFollowUpInterceptor#requestIsOneShot分析
// We can't send the request body again.
if (requestSendStarted && requestIsOneShot(e, userRequest)) return false
// 下述方法若返回false则也不允许重试,看下3分析
// This exception is fatal.
if (!isRecoverable(e, requestSendStarted)) return false
// 没有路线可以尝试了,也不允许重试
if (!call.retryAfterFailure()) return false
// For failure recovery, use the same route selector with a new connection.
return true
}
2.RetryAndFollowUpInterceptor#requestIsOneShot
private fun requestIsOneShot(e: IOException, userRequest: Request): Boolean {
val requestBody = userRequest.body
return (requestBody != null && requestBody.isOneShot()) || //isOneShot()是需要程序员重写RequestBody的方法,不重写的情况下默认返回为false
e is FileNotFoundException //若下层节点处理过程中抛出FileNotFoundException也不允许重试
}
3.RetryAndFollowUpInterceptor#isRecoverable
private fun isRecoverable(e: IOException, requestSendStarted: Boolean): Boolean {
// 协议异常则不许重试,举一个协议异常的例子,没有使用代理却返回响应码407需要代理验证,此类与协议规范冲突的错误大多数为ProtocolException
if (e is ProtocolException) {
return false
}
// 若产生中断异常,Socket超时或者在非IO异常(路由异常)时可能可以重试(requestSendStarted是catch中捕获的最初的异常决定的)
// 若捕获异常为RouteException,requestSendStarted为false
// 若捕获异常为IOException,且异常的具体类型为ConnectionShutdownException时,requestSendStarted也为false
if (e is InterruptedIOException) {
return e is SocketTimeoutException && !requestSendStarted
}
//ssl握手异常,且具体错误属于是证书异常,则不允许重试,因为重试也肯定失败
if (e is SSLHandshakeException) {
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.
// 翻译:如果产生连接代理出现问题,且是抛出IO异常,此时要返回true,尝试新的路由路线
return true
}
总结上述中不可重试的情况,分为两种情况一种是程序员不希望重试一种是下层抛出异常。
程序员控制:
OkhttpClient
时设置retryOnConnectionFailure
为false
,所有请求将不可重试。Body
继承自RequestBody
并重写isOneShot()
返回为true
,可以做到过滤某些请求。下层节点抛出异常:
ProtocolException
协议异常不允许重试,协议异常在近期不可修复,即使重试也无济于事。SSLHandshakeException
握手异常且异常的具体原因是CertificateException
证书异常时不允许重试,原因于上一条一样,近期无法修复。SocketTimeoutException
时若捕获的异常最初的为IO
异常,且此异常的真实类型不为ConnectionShutdownException
,也不允许重试,因此大多数IO
异常都不允许重试。SSLPeerUnverifiedException
异常证书校验失败也不允许重试。这里提出一个问题,重试不会死循环吗?
不会,首先重试的条件是苛刻的,绝大多数重试情况都只是网络波动,且在判断是否可重试时,有非常关键的一个条件,有没有可以重试的路线,一直重试必然会导致路线全部使用完,此时也一定会跳出循环。
回到上述RetryAndFollowUpInterceptor#intercept()方法,继续往下分析响应成功的情况。
重定向简单理解则是客户端请求服务端时,服务端让客户端请求别的网站,服务端会在响应中添加新的请求地址,客户端去请求这个新的地址。
在学习客户端如何处理重定向的之前,先学习HTTP
中协议的一些知识。
在响应码为308,307,300,301,302,303
时需要重定向,响应码作用如下:
URI
之一,和直接请求一个新的网站区别不大。URI
上。由于重定向是临时发生的,所以客户端在之后的请求中还应该使用原本的 URI。302
作用类似,301
,302
在最初的协议规范中不允许请求方法的改变,比如get
请求,重定向后应仍为get
,但是由于历史原因,客户端在实现时会改变方法,因此拓展出303
响应码,代表302
可改变方法。302
是一致的,唯一的区别在于,307
状态码不会改变请求方法。 301
是一致的,唯一的区别在于,308
状态码不会改变请求方法。由于协议的一些规定,需要重建Request
,下述方法则根据协议规则创建新的Request
。
1.RetryAndFollowUpInterceptor#followUpRequest
//Figures out the HTTP request to make in response to receiving userResponse. This will either add authentication headers, follow redirects or handle a client request timeout. If a follow-up is either unnecessary or not applicable, this returns null.
//翻译如下:计算出响应接收 userResponse 的 HTTP 请求。这将添加身份验证标头、遵循重定向或处理客户端请求超时。如果后续行动不必要或不适用,则返回 null,个人理解:说白了就是根据上次相应的响应码,判断是不是需要发起新的请求。
@Throws(IOException::class)
private fun followUpRequest(userResponse: Response, exchange: Exchange?): Request? {
// 拿到此次请求的Route,其内部有此次请求的代理,地址信息
val route = exchange?.connection?.route()
// 原始请求的响应码
val responseCode = userResponse.code
// 原始请求的方法get,post ...
val method = userResponse.request.method
when (responseCode) {
// 如果响应码为407,代理需要验证客户端身份
HTTP_PROXY_AUTH -> {
// 获取此次响应的代理
val selectedProxy = route!!.proxy
// 没有设置代理但是却返回407,需要抛出协议异常
if (selectedProxy.type() != Proxy.Type.HTTP) {
throw ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy")
}
// 回调下面方法获取到新的Request,此Request则一定包括了验证代理服务器的账号密码信息,看代理小节
return client.proxyAuthenticator.authenticate(route, userResponse)
}
// 响应码401,服务器本身需要验证客户端身份,看下面服务器鉴权小节
HTTP_UNAUTHORIZED -> return client.authenticator.authenticate(route, userResponse)
// 响应码308,307,300,301,302,303需要直接重定向
HTTP_PERM_REDIRECT, HTTP_TEMP_REDIRECT, HTTP_MULT_CHOICE, HTTP_MOVED_PERM, HTTP_MOVED_TEMP, HTTP_SEE_OTHER -> {
// 根据响应获取新的Request,本节下2.RetryAndFollowUpInterceptor#buildRedirectRequest分析
return buildRedirectRequest(userResponse, method)
}
// 响应码408,请求超时,服务器觉得客户端太慢了,此处应该归类于重试环节而不是重定向,此处的重试是收到了服务器的响应408,在重试小节中的重试是没有收到服务器的响应
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.)
//上述翻译:408 在实践中很少见,但一些服务器(如 HAProxy)使用此响应代码。规范说我们可以不加修改地重复请求。现代浏览器也会重复请求(甚至是非幂等的)。
// 在重试小节对此标志位进行过分析,表示是否可重试,false则直接返回null,返回null意味着结束此次请求,返回Response,最终业务层拿到的是响应码为408的Response
if (!client.retryOnConnectionFailure) {
// The application layer has directed us not to retry the request.
return null
}
val requestBody = userResponse.request.body
// 在重试小节也分析过,若请求体实现isOneShot()方法返回false,表示此请求不可重试
if (requestBody != null && requestBody.isOneShot()) {
return null
}
val priorResponse = userResponse.priorResponse // 之前的重试和重定向记录
// 若之前重试过且响应码为408,则不允许重试,
// 场景举例:客户端发起请求且响应为408
// 第一次请求时返回408,priorResponse为null,重试此次请求,执行下次循环会把408的这次响应赋值给priorResponse,再次走到这时priorResponse不为null,此时则会返回null,结束此次请求,因此上述只说priorResponse和重定向相关是不准确的
// 之所以这么做笔者理解应该是priorResponse本意是保存重定向路径,若命中408则意味着路径中会保存重试的408响应导致路径不准确,不如直接结束此次请求,直接返回
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
}
// 原样返回请求,对此次请求进行重试
return userResponse.request
}
// 503 服务器错误
HTTP_UNAVAILABLE -> {
val priorResponse = userResponse.priorResponse
// 若上次返回码也为503,则结束此次请求
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
}
//421请求码,只会在HTTP2中发生,此处不做讨论
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.
// 翻译:即使域名不同,OkHttp 也可以合并 HTTP2 连接。请参阅 RealConnection.isEligible()。如果我们尝试了这个并且服务器返回了 HTTP 421,那么我们可以在不同的连接上重试。
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
}
}
2.RetryAndFollowUpInterceptor#buildRedirectRequest
private fun buildRedirectRequest(userResponse: Response, method: String): Request? {
// 在创建client时是否设置可以重定向,若不允许重定向则返回null
if (!client.followRedirects) return null
// 获取上次返回头中的Location字段,此字段记录重定向的地址
val location = userResponse.header("Location") ?: return null
// 根据Location字段解析新的URL地址
val url = userResponse.request.url.resolve(location) ?: return null
// scheme为http or https,此处记录重定向的协议是否发生改变
val sameScheme = url.scheme == userResponse.request.url.scheme
// 若协议发生改变且client.followSslRedirects为false,则不允许重定向,此标志位默认为true在创建client时可以设置
if (!sameScheme && !client.followSslRedirects) return null
// 将上次的Request进行copy
val requestBuilder = userResponse.request.newBuilder()
// 若上次请求方法不为get和head则Request需要进一步封装处理
if (HttpMethod.permitsRequestBody(method)) {
val responseCode = userResponse.code
// 此标志位在在请求方法为PropFind,或响应码为307,308时为true
val maintainBody = HttpMethod.redirectsWithBody(method) ||
responseCode == HTTP_PERM_REDIRECT ||
responseCode == HTTP_TEMP_REDIRECT
// 原先的请求方法不为PROPFIND,且响应码非307,308,重定向后一律改为get
if (HttpMethod.redirectsToGet(method) && responseCode != HTTP_PERM_REDIRECT && responseCode != HTTP_TEMP_REDIRECT) {
requestBuilder.method("GET", null)
} else {
// 若maintainBody为true 则沿用上次请求的请求体
val requestBody = if (maintainBody) userResponse.request.body else null
// 设置新的request的方法和请求体
requestBuilder.method(method, requestBody)
}
// 若不维持请求体则将下述三个字段移除
if (!maintainBody) {
requestBuilder.removeHeader("Transfer-Encoding") //请求体传输编码
requestBuilder.removeHeader("Content-Length") //请求体长度
requestBuilder.removeHeader("Content-Type") //请求体传输类型
}
}
// 跨主机重定向时去除身份验证信息
if (!userResponse.request.url.canReuseConnectionFor(url)) {
requestBuilder.removeHeader("Authorization")
}
return requestBuilder.url(url).build()
}
新Request
的构建是比较复杂的,整体流程如下:
client
的followSslRedirects
决定,此标志位在创建Client
时可以设置,若此标志位为false
则不允许改变协议进行重定向。get
或者head
则不需要继续处理。get
或者head
则需要进一步处理,请求方法为PropFind
,或响应码为307
,308
时会保留请求体,若请求方法为PROPFIND
,且响应码非307
,308
,重定向后请求方法一律改为get
,get
请求不需要请求体则去掉相关的头信息Transfer-Encoding
,Content-Length
,Content-Type
。Authorization
是否需要改变请求方法为get
,请求体是否有效,(√,×)表示改变请求方法,请求体无效,表格如下:
请求方法\响应码 | 300,301,302,303 | 307,308 |
---|---|---|
get,head | (×,×) | (×,×) |
非get,非head,非propfind | (√,×) | (×,√) |
propfind | (×,√) | (×,√) |
get
和head
请求时不需要使用请求体则请求体一定是无效的。
代理分为两种:
VPN
算正向代理的一种。客户端设置的代理为正向代理。
在OkHttp
设置代理的方式如下:
val okHttpClient = OkHttpClient().newBuilder()
.proxy(Proxy(Proxy.Type.HTTP, InetSocketAddress("192.168.0.124", 8853))) //设置代理
.proxyAuthenticator { route, response ->
val info = Credentials.basic("zjm", "ljn")
response.request.newBuilder()
.header("Proxy-Authorization", info)
.build()
}
.build()
回到重定向小节继续分析其他情况。
响应码401需要服务端直接鉴权,和407的区别是407是代理服务端需要鉴权。
OkHttp
设置服务端鉴权的方式如下:
val okHttpClient = OkHttpClient().newBuilder()
.authenticator { route, response ->
val info = Credentials.basic("zjm", "ljn")
response.request.newBuilder()
.header("Authorization", info)
.build()
}
.build()
和响应码407
的区别是请求头的字段名不同。
回到重定向小节继续分析其他情况。
每个拦截器都是HTTP
协议的部分实现,内部有大量的协议内容,此小节对其进行总结
响应码
RetryAndFollowUpInterceptor
共涉及407,401,308,307,300,301,302,303,408,503,421
共11个响应码。
308,307,300,301,302,303
在重定向小节中已经叙述,此小节不再叙述。
407
代理服务器需要验证客户端身份401
服务器需要验证客户端身份408
请求超时,服务器认为客户端太慢了503
服务器不存在访问的资源421 ``HTTP2
情况下才会返回,一般是当前客户端所在的IP地址到服务器的连接数超过了服务器许可的最大范围响应头字段
Location
重定向服务端返回的新的地址Retry-After
标志此资源在多长时间内不可被重试请求头字段
Proxy-Authorization
代理服务器鉴权,包括客户端的身份信息Authorization
服务器鉴权,包括客户端的身份信息Transfer-Encoding
希望的传输方式,比如chunked
,整个报文就会分块传输,与Content-Length
冲突Content-Length
请求的内容长度Content-Type
请求的与实体对应的MIME信息请求方法
GET
大家都知道就不过多赘述了POST
大家都知道就不过多赘述了HEAD
与GET
类似,不同时HEAD
只获取头信息,不需要响应正文PROPFIND
WebDav
(HTTP1.1
的拓展)中的方法,用于获取某个属性的值一篇关于HTTP协议版本的区别的好文,点击这里
RetryAndFollowUpInterceptor
主要处理重试和重定向,在上述代码分析中已经得到验证。
重试是OkHttp
对网络请求的优化,重试的条件是苛刻的,大多数情况原因是网络波动。
重定向是HTTP
协议规范的内容,协议细节比较多,重点为30x
的响应码处理,以及OkHttp
实现重定向的思路。
✨ 原创不易,还希望各位大佬支持一下 \textcolor{blue}{原创不易,还希望各位大佬支持一下} 原创不易,还希望各位大佬支持一下
点赞,你的认可是我创作的动力! \textcolor{green}{点赞,你的认可是我创作的动力!} 点赞,你的认可是我创作的动力!
⭐️ 收藏,你的青睐是我努力的方向! \textcolor{green}{收藏,你的青睐是我努力的方向!} 收藏,你的青睐是我努力的方向!
✏️ 评论,你的意见是我进步的财富! \textcolor{green}{评论,你的意见是我进步的财富!} 评论,你的意见是我进步的财富!
下篇预告:分析第二个拦截器-BridgeInterceptor
,分析其如何处理请求头
下篇文章已更新OkHttp原理第四篇-BridgeInterceptor