这是 OkHttp 深度解析系列文章
OkHttp深度解析(一) : 从一次完整请求看 OkHttp整体架构
OkHttp深度解析(二) : OkHttpClient 你没见过的那些属性
OkHttp深度解析(三) : 拦截器?连接复用&合并?Http2?
OkHttp 网络请求全过程深度解构
okhttp拦截器运行原理
以下是一个请求过程中拦截器的运行机制,用时序图的方式解释拦截器原理,一目了然,过目不忘!
图片较大,网页上可能看不清,建议下载后放大查看
拦截器运行原理详解
-
RealInterceptorChain
它是管理所有拦截器的的类,链式调用就是由它实现的,
getResponseWithInterceptorChain是拦截器运行的核心方法,内部将所有的拦截器都放在了一个 list 中,然后初始化了RealInterceptorChain的一个实例,并将拦截器列表添加进去,最后通过chain.proceed(originalRequest)方法传入原始请求,启动这个链条,然后经过拦截器对这个originalRequest的层层处理,最终得到一个发往服务器的targetRequest@Throws(IOException::class) internal fun getResponseWithInterceptorChain(): Response { // 构建完整的拦截器堆栈 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) val chain = RealInterceptorChain( call = this, interceptors = interceptors,//将拦截器列表添加到拦截器链 index = 0, exchange = null, request = originalRequest, connectTimeoutMillis = client.connectTimeoutMillis, readTimeoutMillis = client.readTimeoutMillis, writeTimeoutMillis = client.writeTimeoutMillis ) var calledNoMoreExchanges = false try { //启动拦截器链 val response = chain.proceed(originalRequest) if (isCanceled()) { response.closeQuietly() throw IOException("Canceled") } return response } catch (e: IOException) { calledNoMoreExchanges = true throw noMoreExchanges(e) as Throwable } finally { if (!calledNoMoreExchanges) { noMoreExchanges(null) } } }首次执行proceed 方法时,从传入的 interceptors 中拿到 index = 0的 Interceptor,然后调用它的
intercept方法执行第一个拦截器,同时将index + 1 ,copy 出一个新的 RealInterceptorChain,传入 intercept 方法中,以此类推,推动整个链条连续执行。@Throws(IOException::class) override fun proceed(request: Request): Response { check(index < interceptors.size) calls++ if (exchange != null) { check(exchange.finder.sameHostAndPort(request.url)) { "network interceptor ${interceptors[index - 1]} must retain the same host and port" } check(calls == 1) { "network interceptor ${interceptors[index - 1]} must call proceed() exactly once" } } //将 index+1 并拷贝一个新的 RealInterceptorChain 对象 val next = copy(index = index + 1, request = request) //找到当前需要执行的拦截器对象 val interceptor = interceptors[index] @Suppress("USELESS_ELVIS") val response = interceptor.intercept(next) ?: throw NullPointerException( "interceptor $interceptor returned null") if (exchange != null) { check(index + 1 >= interceptors.size || next.calls == 1) { "network interceptor $interceptor must call proceed() exactly once" } } check(response.body != null) { "interceptor $interceptor returned a response with no body" } return response } -
拦截器实例:每个拦截器实例可以分为三部分工作
- Before Proceed:这是拦截器对请求进行处理的阶段。每个拦截器在调用
chain.proceed(request)方法将请求传递给下一个拦截器之前,会执行自己专属的任务。这通常包括对请求的修改或增强 - Call Proceed:这是责任链的推动环节。通过调用
chain.proceed(request),当前拦截器将请求控制权交给下一个拦截器,并等待其返回响应。这个调用是拦截器链执行的枢纽 。 - After Proceed:当下一个拦截器返回响应后,当前拦截器对响应进行处理的阶段。拦截器可以检查、修改甚至完全替换返回的响应数据,然后再将其返回给上一个拦截器。
- Before Proceed:这是拦截器对请求进行处理的阶段。每个拦截器在调用
拦截器拆解分析
-
RetryAndFollowupInterceptor
顾名思义,这是一个用来重试和重定向的拦截器,前置工作主要是初始化一个
ExchangeFinder实例,用于后续查找可用的 TCP 连接,后置工作是判断是否有错误或者是否需要重定向 -
BridgeInterceptor
很简单,Bridge 是桥,表示这是原始请求与实际请求桥接的拦截器,前置工作是用于补全各种请求头,其中会默认添加要求服务器对 返回内容进行 gzip 压缩的请求头;后置工作是对响应头做处理,包括解压缩响应内容
-
CacheInterceptor
前置工作:首先查看有没有本地缓存,有则查看是否过期(根据 Expires/max-age) , 如果没有过期并且可用,则直接返回不再执行后续的拦截器,如果没有或者过期就执行下一个拦截器。 后置工作:如果服务端返回的是304(服务器根据 Etag 或者 Last-Modified) 表示之前缓存的本地请求依然有效可以使用,或者判断是否需要缓存新的请求信息
-
ConnectInterceptor
这是实际查找和建立 TCP 连接的拦截器 前置工作就是使用之前创建的ExchangeFinder 来查找连接池中是否有可用连接(没有后置工作):
-
对于全新请求(未重定向或重试):
-
由于每个 Call 都关联了一个连接对象,所以先尝试重用这个连接,由于是全新请求,因此肯定是 null
-
然后以最快的速度在连接池中查找下有没有可用连接(不使用 Route),不使用 Route 表示只需要连接没有超过负载,并且Address 中的所有配置一致(包括Host、port、proxy、protocol、证书等等)就可重用,
requireMultiplexed=false表示http1.1和 Http2都包括 -
如果找不到,那就通过 Route 来深度查找,OkHttp通过解析 DNS 来获取到 IP 地址列表(List),这次查找的目的是找到可以支持coalescing(连接合并) 的连接,这次
requireMultiplexed依然是 false -
如果还是找不到,那就只能新建一个 TCP 连接
-
在一些特殊情况下,可能有多个请求在极短时间内同时发起,并且同时创建了新的连接,这种情况下如果都是 Http2的请求,那这两个请求可以合并放在同一个 TCP 连接中执行,这样就可以省去一个连接,因此,需要再次查找一遍,这次要求
requireMultiplexed=true,也就是只查找Http2 的连接
-
对于重定向或者重试的连接
由于 Call 中可能已经有了一个连接,因此先检查下是否可以重用,后续步骤与👆一致
以下是findConnection 函数的代码(仅展示核心代码):
private fun findConnection(...): RealConnection { // ... 参数和初始检查已省略 ... // 1. 尝试复用当前请求中已有的连接(如果是重试或者重定向的请求) val callConnection = call.connection if (callConnection != null && /* ... 复用条件检查 ... */) { return callConnection // 复用成功 } // 2. 尝试从连接池中获取一个可用连接(Http1.1 或者 Http2的连接但不会拿到**coalescing** 的连接) if (connectionPool.callAcquirePooledConnection(address, call, null, false)) { return call.connection!! } // 3. 进行路由选择 // ... 路由选择逻辑(可能阻塞)... val route = // ... 确定最终要尝试的路线 (Route) ... // 4. 前两种方式都失败,就创建新的 TCP 连接 val newConnection = RealConnection(connectionPool, route) newConnection.connect(...) // 关键的连接建立过程 // 5. 连接建立后,针对特殊竞争状态,再次尝试查询一次连接池(连接合并) if (!connectionPool.callAcquirePooledConnection(address, call, null, false)) { connectionPool.put(newConnection) // 将新连接放入池中 call.acquireConnectionNoEvents(newConnection) } return call.connection ?: newConnection }以上是 OKHttp 的连接池核心机制的介绍,它确保了连接复用效率的最大化!
其中从连接池获取连接的核心方法是
connectionPool.callAcquirePooledConnectionfun callAcquirePooledConnection( address: Address, call: RealCall, routes: List<Route>?, requireMultiplexed: Boolean ): Boolean { for (connection in connections) { synchronized(connection) { //希望查找多路复用连接但这个连接不支持多路复用,就放弃掉 if (requireMultiplexed && !connection.isMultiplexed) return@synchronized //这个连接是可用的 if (!connection.isEligible(address, routes)) return@synchronized //获得这个符合要求的连接 call.acquireConnectionNoEvents(connection) return true } } return false }其中的
isEligible方法是根据传入的 address 以及 route 来判断是否符合要求internal fun isEligible(address: Address, routes: List<Route>?): Boolean { // If this connection is not accepting new exchanges, we're done. if (calls.size >= allocationLimit || noNewExchanges) return false // If the non-host fields of the address don't overlap, we're done. if (!this.route.address.equalsNonHost(address)) return false // If the host exactly matches, we're done: this connection can carry the address. if (address.url.host == this.route().address.url.host) { return true // This connection is a perfect match. } // At this point we don't have a hostname match. But we still be able to carry the request if // our connection coalescing requirements are met. See also: // https://hpbn.co/optimizing-application-delivery/#eliminate-domain-sharding // https://daniel.haxx.se/blog/2016/08/18/http2-connection-coalescing/ // 1. This connection must be HTTP/2. if (http2Connection == null) return false // 2. The routes must share an IP address. if (routes == null || !routeMatchesAny(routes)) return false // 3. This connection's server certificate's must cover the new host. if (address.hostnameVerifier !== OkHostnameVerifier) return false if (!supportsUrl(address.url)) return false // 4. Certificate pinning must match the host. try { address.certificatePinner!!.check(address.url.host, handshake()!!.peerCertificates) } catch (_: SSLPeerUnverifiedException) { return false } return true // The caller's address can be carried by this connection. }我用一张图来解释这个方法的判断逻辑:
PS:关于 coalescing
它表示连接合并,也就是允许不同主机名的请求合并到同一个 TCP 连接中,这是针对「虚拟主机」的一项优化策略,所谓虚拟主机就是在一台物理主机上有多个虚拟的服务,它们 IP 地址相同,但域名不同,因此在 isEligible 方法中会判断当主机名不一致时,仍然尝试去查找合适的连接,这样可以最大限度的复用 TCP 连接。
可能有人会产生疑问:Http1.1也支持虚拟主机,为什么 OkHttp 中要求只有 Http2 的连接才可以尝试 coalescing, 这是因为 Http1.1 中同一个 TCP 连接不支持跨主机名的连接合并,因为这会产生很多问题(具体原因较多),因此虽然 Http1.1的协议中并未明确禁止连接合并,但在实际应用中所有的客户端的实现中都会禁用 Http1.1 的连接合并策略。
-
-
CallServerInterceptor
这是整个拦截器链的最后一环,因此它是实际负责执行网络 I/O 的,写入请求头/体,读取响应头/体,它没有中置和后置工作。