OkHttp 源码阅读笔记(三)
第一篇文章中介绍了 OkHttp
的同步调用和异步调用,Dispatcher
的任务调度器工作方式和 RealInterceptorChain
拦截器链的工作方式:Android OkHttp 源码阅读笔记(一)。
第二篇文章中介绍了 OkHttp
如何从缓存中获取链接,如何创建链接以及 ConnectionPool
的工作原理:Android OkHttp 源码阅读笔记(二)。
本篇文章是系列文章的第三篇,主要介绍几种系统拦截器的工作原理,在理解他们的工作原理前最好是对 HTTP
协议有一些理解,当然一边看源码,一边去网上查 HTTP
协议的相关功能也是没有问题的😄,这取决于你自己。
RetryAndFollowUpInterceptor
它的名字已经很直白了,它主要做两件事,链接错误的重试和重定向处理。
-
链接错误的重试
我们在前一篇文章中有了解到一个域名可能是有多个Proxy
和 多个IP Address
来完成链接,一种链接的方式在OkHttp
中称为Route
,ExchangeFinder
每获取到一个Route
后,会通过RealConnection#isHealthy()
方法来判断链接是否健康,如果不健康,就自动去获取下一个Route
了。是啊,ExchangeFinder
会自动去获取,但是这里有一个前提,那就是不抛出异常的条件下。如果抛出了链接过程相关的异常就会被RetryAndFlowUpInterceptor
所捕获到(比如DNS
,TCP
或者TLS
相关的错误),RetryAndFlowUpInterceptor
会判断是否有还有其他的Route
,如果有的话,会触发ExchangeFinder
再次去查找或者创建可用的链接,直到试过所有的Route
后,最后抛出异常(如果只有一个Route
就直接抛出异常,我们的大部分情况都是这样)。 -
重定向处理
这个感觉没有什么好说的,就是HTTP
协议中的重定向处理。
链接错误的重试
以下的代码我只保留了和链接错误重试的相关逻辑:
@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
// 是否需要构建一个新的 ExchangeFinder
var newExchangeFinder = true
var recoveredFailures = listOf<IOException>()
while (true) {
// 触发 RealInterceptor 创建 ExchangeFinder(需要 newExchangeFinder 为 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) {
// 在创建链接过程中发生的错误都是 RouteException
// The attempt to connect via a route failed. The request will not have been sent.
// recover 方法来判断当前的错误是否可以重试
if (!recover(e.lastConnectException, call, request, requestSendStarted = false)) {
// 不可重试
throw e.firstConnectException.withSuppressed(recoveredFailures)
} else {
// 可以重试
recoveredFailures += e.firstConnectException
}
// 重试时不会创建新的 ExchangeFinder
newExchangeFinder = false
continue
} catch (e: IOException) {
// IOException 和上面的 RouteException 是类似的,只是某些参数不同。
// 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
}
// ...
} finally {
call.exitNetworkInterceptorExchange(closeActiveExchange)
}
}
}
ExchangeFinder#find()
方法中抛出的异常全部都是 RouteException
,然后会通过 recover()
方法来判断是否要继续重试下一个 Route
,如果不重试就直接抛出异常,如果需要重试就进入下次循环,注意这里把 newExchangeFinder
设置成了 false
,这样就不会创建一个新的 ExchangeFinder
了,然后 ExchangeFinder
就可以尝试用下一个 Route
来创建可用的链接。
我们再来看看 recover()
的实现:
private fun recover(
e: IOException,
call: RealCall,
userRequest: Request,
requestSendStarted: Boolean
): Boolean {
// The application layer has forbidden retries.
// 设置是否允许重试
if (!client.retryOnConnectionFailure) return false
// 判断是否已经开始发送 Request 中的内容了,如果已经开始就必须是 OneShot
// 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
// 判断是否还有 Route 可以尝试
// No more routes to attempt.
if (!call.retryAfterFailure()) return false
// For failure recovery, use the same route selector with a new connection.
return true
}
必须要同时满足以下四个条件才允许重试:
-
配置中必须支持重试
默认值是true
,可以在构建OkHttpClient
的时候自定义。 -
没有发送过
Request
相关内容,如果已经发送过必须是OneShot
这个貌似和Http 2
有关。 -
不是致命错误
致命错误的判断后面再看。 -
还有可以重试的
Route
是否有重试Route
的判断后面看。
这里看看 isRecoverable()
方法的实现:
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
}
致命错误的判断比较简单,大家自己看看源码就好了。
继续看看 RealCall#retryAfterFailure()
方法的实现:
fun retryAfterFailure() = exchangeFinder!!.retryAfterFailure()
然后继续调用了 ExchangeFinder#retryAfterFailure()
方法:
fun retryAfterFailure(): Boolean {
// 不同种类的失败记录都是 0,就表示没有失败过,直接返回 false
if (refusedStreamCount == 0 && connectionShutdownCount == 0 && otherFailureCount == 0) {
return false // Nothing to recover from.
}
// 如果有重试的 Route
if (nextRouteToTry != null) {
return true
}
// 重新查找重试的 Route
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.
// 如果 Section 中还有 Route,返回 true
if (routeSelection?.hasNext() == true) return true
// If we haven't initialized the route selector yet, assume it'll have at least one route.
// 如果没有初始化 RouteSelector,返回 true
val localRouteSelector = routeSelector ?: return true
// If we do have a route selector, use its routes.
// 判断 RouteSelector 中是否还有 Selection
return localRouteSelector.hasNext()
}
如果有读我的上一篇文章,就能理解上面提到的 RouteSelector
和 Selection
,他们都是用来管理 Route
的。
重定向处理
我们忽略掉链接重试的逻辑,只看和重定向相关的逻辑:
@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) {
// ...
} catch (e: IOException) {
// ...
}
// Attach the prior response if it exists. Such responses never have a body.
// 如果这已经是第二次请求了(或者 2 次以上),会把上次的重定向 Response 存放在新的 Response 对象中
if (priorResponse != null) {
response = response.newBuilder()
.priorResponse(priorResponse.newBuilder()
.body(null)
.build())
.build()
}
// 获取 Exchange 对象
val exchange = call.interceptorScopedExchange
// 获取重定向的 Request,如果为空就表示不需要重定向
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()) {
// 这部分逻辑和 Http 2相关,跳过。
closeActiveExchange = false
return response
}
// 如果是重定向的 Reponse,需要把它的 body 流关闭,提前释放对应的链接,避免泄漏。
response.body?.closeQuietly()
if (++followUpCount > MAX_FOLLOW_UPS) {
// 达到最大的重定向次数,直接抛出异常,最大为 20 次
throw ProtocolException("Too many follow-up requests: $followUpCount")
}
// 替换成重定向的 Request
request = followUp
// 将当前的 Response 保存下来,下次再请求时保存到后面的 Response 中
priorResponse = response
} finally {
call.exitNetworkInterceptorExchange(closeActiveExchange)
}
}
}
是否执行重定向的关键方法是 followUpRequest()
,它的返回值是后续重定向请求的新的 Request
,如果返回值为空就表示不需要重定向,反之就需要。最大的重定向次数是 20 次,超过次数就会直接抛出异常,这里注意和链接异常重试过程做一下比较,重定向是需要重新创建 ExchangeFinder
对象的,因为重定向后的地址可能会改变域名,所以原来的网络链接的相关 Route
就可能变为不可用。
我们继续看看 followUpRequest()
方法中是如何判断是否需要执行重定向的吧。
@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 -> {
val selectedProxy = route!!.proxy
if (selectedProxy.type() != Proxy.Type.HTTP) {
throw ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy")
}
// 通过 `proxyAuthenticator` 获取认证后的 Request
return client.proxyAuthenticator.authenticate(route, userResponse)
}
// HTTP 认证,通过 `authenticator` 获取认证后的 Request
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 -> {
// 构建重定向的 Request
return buildRedirectRequest(userResponse, method)
}
// HTTP 的超时
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
}
// 直接返回之前同样的 Request 去重试
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
}
// 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.
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
}
}
上面的重定向逻辑看似代码挺多,其实很简单,我们来整理一下:
-
HTTP_PROXY_AUTH
(407)
代理认证,有的代理是需要认证信息的,看源码是只支持HTTP
类型的代理,需要通过proxyAuthenticator
(需要自定义) 来获取写入了认证信息的Request
。 -
HTTP_UNAUTHORIZED
(401)
Http
的请求需要认证,和上面的代理认证类似,不过是通过authenticator
(需要自定义)来获取认证后的Request
。 -
HTTP_PERM_REDIRECT
(308),HTTP_TEMP_REDIRECT
(307),HTTP_MULT_CHOICE
(300),HTTP_MOVED_PERM
(301),HTTP_MOVED_TEMP
(302),HTTP_SEE_OTHER
(303)
以上都是普通的重定向,他们都会通过buildRedirectRequest()
方法来构建新的Request
(后续再看源码)。 -
HTTP_CLIENT_TIMEOUT
(408)
超时错误,首先判断是否允许重试(可以配置,默认允许),然后判断之前是否已经重试过超时的这种场景(为true
跳过),判断Response
中是否有返回限制重试的时间(大于0)(有限制跳过),最后执行重试 -
HTTP_UNAVAILABLE
(503)
处理逻辑和HTTP_CLITEN_TIMEOUT
差不多,但是必须指定重试时间为 0 才会重试。
我们再来看看通用的重定向 Request
创建方法 buildRedirectRequest()
:
private fun buildRedirectRequest(userResponse: Response, method: String): Request? {
// Does the client allow redirects?
// 是否允许重定向
if (!client.followRedirects) return null
// 从 Response 中获取新连接的字符串
val location = userResponse.header("Location") ?: return null
// Don't follow redirects to unsupported protocols.
// 将字符串转换成 HttpUrl 对象
val url = userResponse.request.url.resolve(location) ?: return null
// If configured, don't follow redirects between SSL and non-SSL.
val sameScheme = url.scheme == userResponse.request.url.scheme
// 判断配置 http 协议和 https 协议之间是否允许重定向
if (!sameScheme && !client.followSslRedirects) return null
// Most redirects don't include a request body.
val requestBuilder = userResponse.request.newBuilder()
// 以下是 RequestBody 的处理,我省略了
if (HttpMethod.permitsRequestBody(method)) {
// ...
}
// When redirecting across hosts, drop all authentication headers. This
// is potentially annoying to the application layer since they have no
// way to retain them.
// 如果前后不是同一个请求,移除认证的 Header。
if (!userResponse.request.url.canReuseConnectionFor(url)) {
requestBuilder.removeHeader("Authorization")
}
// 替换原有的 Request 的 url 然后构建一个新的 Request。
return requestBuilder.url(url).build()
}
首先判断配置是否支持重定向,默认为支持(不支持直接返回);判断 Reaponse Header
中是否有重定向的 Location
(没有直接返回);判断重定向前后的协议是否发生改变,如果发生改变了,通过配置判断是否支持协议改变后的重定向,默认支持(不支持直接返回),所谓的协议改变就是,前面是 http
协议,重定向后是 https
协议,或者前面是 https
协议,重定向后是 http
协议;如果前后的请求 url
不一致,就移除认证的 Header
;最后替换旧的 Request
中的 url
为重定向的 url
,最后构建一个 Request
返回。
BridgeInterceptor
BridgeInterceptor
负责对原有的某些 Request Header
和 Response Header
作出一些修正和添加一些默认的值,还负责对 Cookie
的处理,Cookie
的保存和获取的相关类是 CookieJar
,默认 OkHttp
是没有实现的,我们可以在代码中可以自己实现。如果有人不理解 Cookie
,可以去网上找找别的资料,我这里就不介绍了。
直接看 BridgeInterceptor
源码:
class BridgeInterceptor(private val cookieJar: CookieJar) : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val userRequest = chain.request()
val requestBuilder = userRequest.newBuilder()
val body = userRequest.body
if (body != null) {
// 设置 ContentType
val contentType = body.contentType()
if (contentType != null) {
requestBuilder.header("Content-Type", contentType.toString())
}
// 设置 ContentLength
val contentLength = body.contentLength()
if (contentLength != -1L) {
requestBuilder.header("Content-Length", contentLength.toString())
requestBuilder.removeHeader("Transfer-Encoding")
} else {
requestBuilder.header("Transfer-Encoding", "chunked")
requestBuilder.removeHeader("Content-Length")
}
}
// 如果没有 Host,设置 Host
if (userRequest.header("Host") == null) {
requestBuilder.header("Host", userRequest.url.toHostHeader())
}
// 如果没有 Connection,设置 Connection,默认是 Keep-Alive
if (userRequest.header("Connection") == null) {
requestBuilder.header("Connection", "Keep-Alive")
}
// If we add an "Accept-Encoding: gzip" header field we're responsible for also decompressing
// the transfer stream.
// 如果没有 Accept-Encoding,设置默认支持的传输编码方式 `gzip`
var transparentGzip = false
if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
transparentGzip = true
requestBuilder.header("Accept-Encoding", "gzip")
}
// 通过 CookieJar 获取当前 url,需要的 Cookies
val cookies = cookieJar.loadForRequest(userRequest.url)
if (cookies.isNotEmpty()) {
requestBuilder.header("Cookie", cookieHeader(cookies))
}
// 如果没有设置 User-Agent,添加默认 User-Agent。
if (userRequest.header("User-Agent") == null) {
requestBuilder.header("User-Agent", userAgent)
}
val networkResponse = chain.proceed(requestBuilder.build())
// 将 ReponseHeader 中的 Cookie 相关的数据写入到 `CookieJar` 中
cookieJar.receiveHeaders(userRequest.url, networkResponse.headers)
val responseBuilder = networkResponse.newBuilder()
.request(userRequest)
// 如果 ResponseBody 中是使用 gzip 编码
if (transparentGzip &&
"gzip".equals(networkResponse.header("Content-Encoding"), ignoreCase = true) &&
networkResponse.promisesBody()) {
val responseBody = networkResponse.body
if (responseBody != null) {
// 将原来的 ResponseBody 使用 GzipSource 来封装,通过它就可以直接解码 gzip 格式的数据
val gzipSource = GzipSource(responseBody.source())
// 移除 Response Header 中的 Content-Encoding 和 Content-Length
val strippedHeaders = networkResponse.headers.newBuilder()
.removeAll("Content-Encoding")
.removeAll("Content-Length")
.build()
responseBuilder.headers(strippedHeaders)
val contentType = networkResponse.header("Content-Type")
// 将 ResponseBody 替换成上面的带有 GzipSource 的 RealResponseBody
responseBuilder.body(RealResponseBody(contentType, -1L, gzipSource.buffer()))
}
}
return responseBuilder.build()
}
/** Returns a 'Cookie' HTTP request header with all cookies, like `a=b; c=d`. */
private fun cookieHeader(cookies: List<Cookie>): String = buildString {
cookies.forEachIndexed { index, cookie ->
if (index > 0) append("; ")
append(cookie.name).append('=').append(cookie.value)
}
}
}
BridgeInterceptor
的代码可以说非常简单,代码非常清晰。可以分为两部分,对 Request Header
的处理和对 Response Header
与 Response Body
的处理。
如果 Request Body
不为空的话,通过 Request Body
获取 Content Type
和 Content Length
,然后把他们写入到 Request Header
中;如果 Request Header
中没有设置 Host
的话,将当前请求的 url
格式化后写入到 Request Header
,具体格式化的代码感兴趣自己可以看看;如果 Request Header
没有设置 Connection
,设置为 Keep-Alive
,这个参数是 Http 1.1
中定义的,也就是请求完成后不要关闭链接,后续的请求还可以复用这个链接;如果 Request Header
中没有设置 Accept-Encoding
和 Range
,设置为 gzip
,也就是表明客户端支持的编码格式;从 CookieJar
中获取要对该次请求 url
需要设置的 Cookies
,然后把它们写入到 Request Header
中;如果 Request Header
中没有设置 User-Agent
,设置默认的 OkHttp
的 User-Agent
;到这里 Reqeust Header
的处理就完成了,然后就是发起请求,等待 Response
。
获取到 Response
后,首先将 Response Header
中和 Cookies
相关的内容解析后放入 CookieJar
中(具体如何解析,我就没有贴代码了,大家可以自己去看看。);然后检查它的 Response Body
的编码方式,如果是 gzip
,然后会把原来的 Response Body
的 Source
使用 GzipSource
来封装,GzipSource
是 OkIo
中的对象,通过它可以将原来 gzip
编码的 Source
解码成明文。然后移除 Resonse Header
中的 Content-Encoding
和 Content-Length
,最后构建一个新的 Response
。
最后
本来想的是一口气把所有的系统 Interceptor
内容全部介绍完,然后发现写的东西越写越多,如果一次的内容太多你阅读累,我写的也累,所以剩下的内容下篇文章再介绍。
网友评论