前言
在上一篇Android开源框架系列-OkHttp4.11.0-请求转发详细分析中我们分析了okhttp的请求转发逻辑,今天进入拦截器的源码分析,okhttp有五大拦截器,今天要分析的是重试重定向拦截器-RetryAndFollowUpInterceptor。
intercept
okhttp的五大拦截器通过责任链层层调用,首先来到的是RetryAndFollowUpInterceptor的intercept方法。
kotlin
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
//一个while无限循环,所以它只能由方法内部主动退出,否则会无限进行下去,这也是重试和重定向的基础。
while (true) {
//这一行比较关键,它是在为后边的拦截器执行做准备,查看1
call.enterNetworkInterceptorExchange(request, newExchangeFinder)
var response: Response
var closeActiveExchange = true
try {
try {
//看到这里可以知道,RetryAndFollowUpInterceptor只是做了一个ExchangeFinder对象的创建之后
//就进入其他拦截器去执行了,RetryAndFollowUpInterceptor既然是重试重定向的功能,那么就表示他
//是在响应回来之后才发挥作用的。我们假设realChain.proceed已经执行完成,可以进行下一步了
response = realChain.proceed(request)
newExchangeFinder = true
} catch (e: RouteException) {
//代码执行到这里表示由于路由问题,请求并没有发送出去,此时就要根据具体的情况来判断有没有重试的必要,查看2
if (!recover(e.lastConnectException, call, request, requestSendStarted = false)) {
}
continue
} catch (e: IOException) {
// 代码执行到这里,表示发生了io异常,有可能请求已经发送出去了,但是由于服务器没有正常响应导致异常,查看2
if (!recover(e, call, request, requestSendStarted = e !is ConnectionShutdownException)) {
}
continue
}
// 代码第一次执行到这里,一定是空的,先不看
if (priorResponse != null) {
response = response.newBuilder()
.priorResponse(priorResponse.newBuilder()
.body(null)
.build())
.build()
}
val exchange = call.interceptorScopedExchange
//检查是否需要重定向,如果需要则返回一个新的request,查看5
val followUp = followUpRequest(response, exchange)
//返回null表示无法重定向(或重试)
if (followUp == null) {
if (exchange != null && exchange.isDuplex) {
call.timeoutEarlyExit()
}
closeActiveExchange = false
return response
}
...
request = followUp
priorResponse = response
} finally {
call.exitNetworkInterceptorExchange(closeActiveExchange)
}
}
}
1.enterNetworkInterceptorExchange
ExchangeFinder对象创建之后并不会在RetryAndFollowUpInterceptor拦截器中使用,我们先了解有这么一个对象存在即可。
kotlin
fun enterNetworkInterceptorExchange(request: Request, newExchangeFinder: Boolean) {
//传入的newExchangeFinder为true,所以这里new了一个ExchangeFinder对象出来
if (newExchangeFinder) {
this.exchangeFinder = ExchangeFinder(
//连接池,此时连接池对象RealConnectionPool已经创建存在了
connectionPool,
createAddress(request.url),
this,
eventListener
)
}
}
2.recover
recover方法负责判断是否满足重新请求的条件。
kotlin
private fun recover(
e: IOException,
call: RealCall,
userRequest: Request,
requestSendStarted: Boolean
): Boolean {
// 如果client已经明确的设置了不允许重试(全局性设置),则返回false
if (!client.retryOnConnectionFailure) return false
// requestSendStarted表示请求是否已经发送过了,当e为RouteException的时候,requestSendStarted为false,表示没有发出,就跳过这个判断,当e为IOException并且不是ConnectionShutdownException
//(ConnectionShutdownException为okhttp内定义的一种异常,只有http2协议会抛出)
//的时候,requestSendStarted为true,requestIsOneShot查看3
if (requestSendStarted && requestIsOneShot(e, userRequest)) return false
// isRecoverable查看4
if (!isRecoverable(e, requestSendStarted)) return false
// 如果此请求设置了失败后不允许重试(单个请求设置),则返回false
if (!call.retryAfterFailure()) return false
//走到这里说明满足重试的条件,返回true
return true
}
3.requestIsOneShot
kotlin
private fun requestIsOneShot(e: IOException, userRequest: Request): Boolean {
val requestBody = userRequest.body
//如果请求体设置了只请求一次,或者请求一次之后响应的是文件未找到,则没有必要继续重试
return (requestBody != null && requestBody.isOneShot()) ||
e is FileNotFoundException
}
4.isRecoverable
kotlin
private fun isRecoverable(e: IOException, requestSendStarted: Boolean): Boolean {
// 协议异常,重试没有用,不必重试
if (e is ProtocolException) {
return false
}
// 超时类型的异常可以重试
if (e is InterruptedIOException) {
return e is SocketTimeoutException && !requestSendStarted
}
// 如果是因为证书问题导致握手失败,没有重试的必要
if (e is SSLHandshakeException) {
if (e.cause is CertificateException) {
return false
}
}
if (e is SSLPeerUnverifiedException) {
//证书校验失败
return false
}
// 其他情况则尝试重试
return true
}
5.followUpRequest
followUpRequest主要是通过响应码来判断是否需要进行重定向
kotlin
@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 -> {
//查看6
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)
}
//查看7
HTTP_UNAUTHORIZED -> return client.authenticator.authenticate(route, userResponse)
//查看7,重定向相应码,300、301、302、303、307、308
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这个响应码较少见,但还是有一部分服务器使用这个响应码,当服务器返回408时,我们
//可以不做任何修改,再次发起请求
if (!client.retryOnConnectionFailure) {
//设置了失败后不重试,返回
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) {
// 加入已经进行过一次408失败后的重试,不再继续重试,返回
return null
}
//服务端通过响应头Retry-After提醒客户端多久之后再重试,则直接返回,等待下次
if (retryAfter(userResponse, 0) > 0) {
return null
}
return userResponse.request
}
HTTP_UNAVAILABLE -> {
//503是服务端出错,可以尝试重试,但如果尝试一次又失败了,则直接返回
val priorResponse = userResponse.priorResponse
if (priorResponse != null && priorResponse.code == HTTP_UNAVAILABLE) {
return null
}
//假如服务端通过响应头Retry-After给了一个0,则表示可以立即重试
if (retryAfter(userResponse, Integer.MAX_VALUE) == 0) {
return userResponse.request
}
//都不满足不重试
return null
}
HTTP_MISDIRECTED_REQUEST -> {
// OkHttp可以合并HTTP/2连接,即使域名不同。见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
}
}
6.HTTP_PROXY_AUTH(407)
尝试使用代理服务器时,代理服务器会向你请求进行身份验证,此时服务器会返回407,我们需要构建一个新的request去请求,并添加上代理服务器需要的身份验证的凭证,这个身份凭证就是通过 Proxy-Authorization请求头的方式添加,其格式为:
typescript
Proxy-Authorization: <type> <credentials>
其中type表示身份验证类型,常见的身份验证类型就是基本验证:Basic,其他身份验证机制见这里。 credentials表示将用户名和密码用冒号拼接(aladdin:opensesame),然后将拼接生成的字符串使用 base64编码方式进行编码。 如:
javascript
Proxy-Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l
我们通过okhttp怎么设置这个请求头?
less
.proxyAuthenticator(new Authenticator() {
@Nullable
@Override
public Request authenticate(@Nullable Route route, @NonNull Response response) throws IOException {
String credential = Credentials.basic("代理服务用户名", "代理服务密码");
return response.request().newBuilder()
.header("Proxy-Authorization", credential)
.build();
}
}
})
7.HTTP_UNAUTHORIZED(401)
和HTTP_PROXY_AUTH类似,401也是一种需要验证的机制。状态码401 Unauthorized代表客户端错误,指的是由于缺乏目标资源要求的身份验证凭证,发送的请求未得到满足。此时需要在请求头中添加Authorization,Authorization请求头的添加方式和Proxy-Authorization类似,Authorization请求头用于提供服务器验证用户代理身份的凭据,允许访问受保护的资源。
Basic 身份验证格式如下,YWxhZGRpbjpvcGVuc2VzYW1l是将用户名和密码通过一个冒号(aladdin:opensesame
)相结合,然后将生成的字符串编码为base64后的结果。
makefile
Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l
我们通过okhttp怎么设置这个请求头?
less
.authenticator(new Authenticator() {
@Nullable
@Override
public Request authenticate(@Nullable Route route, @NonNull Response response) throws IOException {
String credential = Credentials.basic("jesse", "password1");
return response.request().newBuilder()
.header("Authorization", credential)
.build();
}
})
Proxy-Authorization和Authorization的区别?
他们之间的区别在于Proxy-Authorization用于客户端与代理服务器之间的认证,而Authorization是用于客户端和服务器之间的认证。
8.HTTP_PERM_REDIRECT, HTTP_TEMP_REDIRECT, HTTP_MULT_CHOICE, HTTP_MOVED_PERM, HTTP_MOVED_TEMP, HTTP_SEE_OTHER
3xx 重定向状态码告诉客户端使用替代位置访问客户端感兴趣的资源。当资源被移动后,服务器可发送一个重定向状态码和一个可选的 Location 首部告诉客户端资源已被移走,以及现在哪里可以找到该资源,这样客户端就可以在不打扰使用者的情况在新的位置获取资源了。
看看这个重定向请求是如何构建的?
kotlin
private fun buildRedirectRequest(userResponse: Response, method: String): Request? {
// 不允许重定向,直接返回
if (!client.followRedirects) return null
//从响应头中取出Location,表示的就是资源被转移后的位置
val location = userResponse.header("Location") ?: return null
// resolve是将取出的url解析成HttpUrl对象,这样host、scheme等就都拿到了
val url = userResponse.request.url.resolve(location) ?: return null
// 如果重定向url的协议和原url的协议不同,并且设置了协议变化不能重定向,则返回空
//followSslRedirects:是否遵循从HTTPS到HTTP和从HTTP到HTTPS的重定向,默认可以。
val sameScheme = url.scheme == userResponse.request.url.scheme
if (!sameScheme && !client.followSslRedirects) return null
// 大部分重定向请求是不带body的,但是也存在非get类型的请求,含有body,修改请求头
val requestBuilder = userResponse.request.newBuilder()
//permitsRequestBody见9,不是get也不是head的情况下,带有body
if (HttpMethod.permitsRequestBody(method)) {
val responseCode = userResponse.code
val maintainBody = HttpMethod.redirectsWithBody(method) ||
responseCode == HTTP_PERM_REDIRECT ||
responseCode == HTTP_TEMP_REDIRECT
if (HttpMethod.redirectsToGet(method) && responseCode != HTTP_PERM_REDIRECT && responseCode != HTTP_TEMP_REDIRECT) {
//除了PROPFIND类型的请求外,都转为get请求
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")
}
}
// 加入重定向url的host、port、scheme都相同,就不用带Authorization进行验证了
if (!userResponse.request.url.canReuseConnectionFor(url)) {
requestBuilder.removeHeader("Authorization")
}
//将请求构建出来
return requestBuilder.url(url).build()
}
9.permitsRequestBody
kotlin
@JvmStatic
fun permitsRequestBody(method: String): Boolean = !(method == "GET" || method == "HEAD")
总结
至此我们就分析完了okhttp重试重定向拦截器的逻辑,下一篇会继续进行BridgeInterceptor的分析。