Android 网络全栈攻略系列文章:
Android 网络全栈攻略(一)—— HTTP 协议基础
Android 网络全栈攻略(二)—— 编码、加密、哈希、序列化与字符集
Android 网络全栈攻略(三)—— 登录与授权
Android 网络全栈攻略(四)—— TCPIP 协议族与 HTTPS 协议
Android 网络全栈攻略(五)—— 从 OkHttp 配置来看 HTTP 协议
Android 网络全栈攻略(六)—— 从 OkHttp 拦截器来看 HTTP 协议一
Android 网络全栈攻略(七)—— 从 OkHttp 拦截器来看 HTTP 协议二

上一篇我们介绍了 OkHttp 的责任链以及第一个内置拦截器 —— 重试与重定向拦截器。本篇我们将剩余四个拦截器的解析做完。

1、桥接拦截器

BridgeInterceptor 作为请求准备和实际发送之间的桥梁,自动处理 HTTP 请求头等繁琐工作。比如设置请求内容长度,编码,gzip 压缩,Cookie 等,获取响应后保存 Cookie 等。它的设计目的是为了解决开发者手动处理 HTTP 协议细节的麻烦,特别是那些必须做但很繁琐或难以实现的工作。

它的拦截代码 intercept() 如下:

class BridgeInterceptor(private val cookieJar: CookieJar) : Interceptor {@Throws(IOException::class)override fun intercept(chain: Interceptor.Chain): Response {// 1.前置工作:从责任链上获取请求,添加相关请求头val userRequest = chain.request()val requestBuilder = userRequest.newBuilder()val body = userRequest.bodyif (body != null) {val contentType = body.contentType()if (contentType != null) {requestBuilder.header("Content-Type", contentType.toString())}// 请求体内容长度如果不是 -1 意味着使用 Content-Length 这个请求头展示内容大小,// 否则就是要使用 Transfer-Encoding: chunked 分块传输的方式。这两个头互斥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")}}if (userRequest.header("Host") == null) {requestBuilder.header("Host", userRequest.url.toHostHeader())}// 如果请求头中没有配置 Connection,框架会自动为我们申请一个长连接,如果服务器同意// 长连接,那么会返回一个 Connection:Keep-Alive,否则返回 Connection:closeif (userRequest.header("Connection") == null) {requestBuilder.header("Connection", "Keep-Alive")}// 在没有 Accept-Encoding 与 Range 这两个请求头的情况下,自动添加 gzip 压缩数据var transparentGzip = falseif (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {transparentGzip = truerequestBuilder.header("Accept-Encoding", "gzip")}// 使用构造函数上传入的 cookieJar 补全 Cookie 请求头val cookies = cookieJar.loadForRequest(userRequest.url)if (cookies.isNotEmpty()) {requestBuilder.header("Cookie", cookieHeader(cookies))}// 补全请求头中的 User-Agent 字段,即请求者的用户信息,如操作系统、浏览器等if (userRequest.header("User-Agent") == null) {requestBuilder.header("User-Agent", userAgent)}// 2.中置工作:启动责任链的下一个节点,做接力棒交接val networkResponse = chain.proceed(requestBuilder.build())// 3.后置工作:修改响应cookieJar.receiveHeaders(userRequest.url, networkResponse.headers)val responseBuilder = networkResponse.newBuilder().request(userRequest)// 如果在第 1 步中使用了 gzip 压缩,那么这里在拿到响应 networkResponse 后,需要将响应体// responseBody 解压后放到新的响应体 responseBuilder.body() 中if (transparentGzip &&"gzip".equals(networkResponse.header("Content-Encoding"), ignoreCase = true) &&networkResponse.promisesBody()) {val responseBody = networkResponse.bodyif (responseBody != null) {val gzipSource = GzipSource(responseBody.source())val strippedHeaders = networkResponse.headers.newBuilder().removeAll("Content-Encoding").removeAll("Content-Length").build()responseBuilder.headers(strippedHeaders)val contentType = networkResponse.header("Content-Type")// RealResponseBody 内存放解压后的响应体responseBuilder.body(RealResponseBody(contentType, -1L, gzipSource.buffer()))}}return responseBuilder.build()}
}

桥接拦截器的拦截逻辑还是很清晰的,三步走:

  1. 前置工作为请求添加请求头。当请求体长度 contentLength 不为 -1 时,添加 Content-Length 请求头填入请求体的完整长度;否则意味着要使用分块传输,添加 Transfer-Encoding: chunked 请求头。这两个头互斥,只能存在一个
  2. 中置工作启动下一个责任链节点,进而触发缓存拦截器
  3. 后置工作就一项,如果在前置工作中启动了 gzip 数据压缩,那么在拿到响应后,要把响应体解压放到新的响应中

前置工作中添加的请求头基本上在系列的前几篇文章中已经介绍过了,因此这里就没多啰嗦,就一个框架默认添加的压缩与解压机制值得一谈。

2、缓存拦截器

CacheInterceptor 基于 HTTP 缓存头信息(如 Expires、Last-Modified 等)实现请求缓存机制,减少重复网络请求,这样可以少流量消耗,同时加快响应速度。

CacheInterceptor 通过缓存策略 CacheStrategy 来决定缓存是否可用,它影响了整个 CacheInterceptor 的拦截逻辑,因此我们先了解缓存策略后再看整个拦截逻辑。

2.1 缓存策略

缓存策略 CacheStrategy 有两个成员 networkRequest 和 cacheResponse:

class CacheStrategy internal constructor(/** 需发送的网络请求:若为 null,表示禁止网络请求,直接使用缓存。 */val networkRequest: Request?,/** 可用的缓存响应:若为 null,表示无有效缓存,必须发送网络请求。 */val cacheResponse: Response?
)

这两个成员共同决定了采用哪种缓存策略:

networkRequestcacheResponse说明
NullNot Null直接使用缓存
NullNull请求失败,OkHttp 框架会返回 504
Not NullNull向服务器发起请求
Not NullNot Null发起请求,若得到响应为 304(无修改),则更新缓存响应并返回

可以概括为:若 networkRequest 存在则优先发起网络请求,否则使用 cacheResponse 缓存,若都不存在则请求失败!

CacheStrategy 采用工厂模式,由 CacheStrategy.Factory 负责生产 CacheStrategy 对象,因此需要对这个工厂有所了解。

2.1.1 CacheStrategy.Factory 初始化

工厂初始化主要是在缓存的响应 cacheResponse 不为空时将请求发送时间、响应接收时间以及一些响应头数据保存为成员属性:

  class Factory(private val nowMillis: Long,internal val request: Request,private val cacheResponse: Response?) {init {if (cacheResponse != null) {// 请求发出的本地时间以及接收到这个响应的本地时间this.sentRequestMillis = cacheResponse.sentRequestAtMillisthis.receivedResponseMillis = cacheResponse.receivedResponseAtMillis// 保存 cacheResponse 中的部分响应头val headers = cacheResponse.headersfor (i in 0 until headers.size) {val fieldName = headers.name(i)val value = headers.value(i)when {fieldName.equals("Date", ignoreCase = true) -> {servedDate = value.toHttpDateOrNull()servedDateString = value}fieldName.equals("Expires", ignoreCase = true) -> {expires = value.toHttpDateOrNull()}fieldName.equals("Last-Modified", ignoreCase = true) -> {lastModified = value.toHttpDateOrNull()lastModifiedString = value}fieldName.equals("ETag", ignoreCase = true) -> {etag = value}fieldName.equals("Age", ignoreCase = true) -> {ageSeconds = value.toNonNegativeInt(-1)}}}}}}

对上面涉及到的响应头及其作用稍作解释:

响应头说明示例
Date响应生成的服务器时间(GMT 格式)。用于计算缓存的年龄(Age)。Date: Sat, 18 Nov 2028 06:17:41 GMT
Expires指定响应的绝对过期时间(GMT 格式)。若存在,表示在此时间前缓存有效。Expires: Sat, 18 Nov 2028 06:17:41 GMT
Last-Modified资源最后一次修改的时间(GMT 格式)。用于条件请求(If-Modified-Since)。Last-Modified: Fri, 22 Jul 2016 02:57:17 GMT
ETag资源在服务器的唯一标识符(实体标签)。用于条件请求(If-None-Match)。ETag: “16df0-5383097a03d40”
Age响应在代理缓存中已存储的时间(秒)。用于校正 Date 的实际年龄。Age: 3825683

2.1.2 生产 CacheStrategy

Factory 的 compute() 会根据 RFC 规范计算缓存是否可用,返回的 CacheStrategy 包含 networkRequest(需发送的请求)和 cacheResponse(可用的缓存):

    fun compute(): CacheStrategy {// 1.生成初步候选策略(基于缓存有效性、过期时间、验证头等)val candidate = computeCandidate()// 2.处理 only-if-cached 约束if (candidate.networkRequest != null && request.cacheControl.onlyIfCached) {// 如果强制禁用网络且无可用缓存 → 返回双 null 策略(触发 504 错误)return CacheStrategy(null, null)}return candidate}

computeCandidate() 会综合各种情况生成初步的候选策略。由于该方法内代码细节还是比较多的,一次贴出所有代码阅读体验不佳,因此分段讲解该方法内容。

检查缓存响应

当被缓存的响应由于某些情况无效时,需要发起网络请求,此时只返回 CacheStrategy(request, null)

    private fun computeCandidate(): CacheStrategy {// 1.缓存的响应为空:必须发起网络请求,因此返回只有网络请求的策略if (cacheResponse == null) {return CacheStrategy(request, null)}// 2.是 HTTPS 请求但缓存缺少握手信息:意味着缓存不安全或不完整,因此忽略缓存,直接发起网络请求if (request.isHttps && cacheResponse.handshake == null) {return CacheStrategy(request, null)}// 3.缓存不可用:调用 isCacheable() 检查缓存是否有效。如果无效,同样返回需要网络请求的策略。// 如果这个响应不应该被存储,则永远不应作为响应源使用。只要持久化存储表现良好且规则保持不变,// 这个检查应该是多余的。if (!isCacheable(cacheResponse, request)) {return CacheStrategy(request, null)}...}

这一段主要看 isCacheable() 是如何检查 cacheResponse 是否可以被缓存的:

	fun isCacheable(response: Response, request: Request): Boolean {// 对于不可缓存的响应代码(RFC 7231 第 6.1 节),始终访问网络。此实现不支持缓存部分内容。when (response.code) {HTTP_OK, // 200HTTP_NOT_AUTHORITATIVE, // 203HTTP_NO_CONTENT, // 204HTTP_MULT_CHOICE, // 300HTTP_MOVED_PERM, // 301HTTP_NOT_FOUND, // 404HTTP_BAD_METHOD, // 405HTTP_GONE, // 410HTTP_REQ_TOO_LONG, // 414HTTP_NOT_IMPLEMENTED, // 501StatusLine.HTTP_PERM_REDIRECT -> { // 308// 以上状态码可以缓存,除非被最后的请求头/响应头中的 cache-control:nostore 禁止了}HTTP_MOVED_TEMP, // 302StatusLine.HTTP_TEMP_REDIRECT -> { // 307// 对于 302 和 307,只有响应头正确时才可被缓存。由于 OkHttp 是私有缓存因此没有检查 s-maxage:// http://tools.ietf.org/html/rfc7234#section-3if (response.header("Expires") == null &&response.cacheControl.maxAgeSeconds == -1 &&!response.cacheControl.isPublic &&!response.cacheControl.isPrivate) {return false}}else -> {// All other codes cannot be cached.return false}}// 请求或响应上的 'no-store' 指令会阻止响应被缓存return !response.cacheControl.noStore && !request.cacheControl.noStore}

检查思路可以分为两级:

  1. 先检查状态码:
    • 可以缓存的状态码:200、203、204、300、301、404、405、410、414、501、308
    • 只有响应头正确才可缓存的状态码:302、307,当响应中没有 Expires 响应头、响应的 Cache-Control 响应头没有设置资源有效期 max-age、且既不是 public 也不是 private 时,不能缓存
    • 其余状态码不能缓存
  2. 再检查请求或响应上是否有 Cache-Control: no-store 明确禁止使用缓存,这个判断级别要高于状态码判断结果

需要说一下 response.cacheControl,它表示 Cache-Control 头,是控制缓存机制的核心工具,允许客户端和服务器定义缓存策略,优化性能并确保数据的新鲜度。它可同时用于请求头(客户端指令)和响应头(服务器指令),多个指令以逗号分隔,如Cache-Control: max-age=3600, public

响应头指令包括:

指令作用
public允许任何缓存(共享或私有)存储响应,即使默认不可缓存(如带Authorization的响应)。
private仅允许用户私有缓存(如浏览器)存储,禁止 CDN 等共享缓存存储。
no-store禁止缓存存储响应内容,每次请求必须从服务器获取。
no-cache缓存必须向服务器验证有效性后,才能使用缓存副本(即使未过期)。
max-age=<秒>资源有效期(相对时间),如max-age=3600表示 1 小时内有效。
s-maxage=<秒>覆盖max-age,但仅作用于共享缓存(如 CDN),优先级高于max-age
must-revalidate缓存过期后,必须向服务器验证有效性,不得直接使用过期资源。
immutable资源永不变更(如带哈希的静态文件),客户端可无限期使用缓存。
proxy-revalidate类似must-revalidate,但仅针对共享缓存。
no-transform禁止代理修改资源(如压缩图片或转换格式)。

请求头指令包括:

指令作用
no-cache强制服务器返回最新内容,缓存需验证(发送If-Modified-Since等头)。
no-store要求中间缓存不存储任何响应,用于敏感数据请求。
max-age=<秒>只接受缓存时间不超过指定秒数的资源(如max-age=0需最新内容)。
max-stale=<秒>允许接受过期但不超过指定秒数的缓存(如max-stale=300接受过期5分钟内)。
min-fresh=<秒>要求资源至少保持指定秒数的新鲜度(如min-fresh=60需至少1分钟有效)。
only-if-cached仅返回缓存内容,若缓存无效则返回504(不发起网络请求)。

可以在不进行验证的情况下提供的响应服务日期后的持续时间。

检查请求头

接下来检查请求头:

    private fun computeCandidate(): CacheStrategy {// 请求的缓存控制val requestCaching = request.cacheControl// 如果请求头包含 noCache 或者请求有条件头 If-Modified-Since、// If-None-Match 二者之一,则忽略缓存直接发起网络请求if (requestCaching.noCache || hasConditions(request)) {return CacheStrategy(request, null)}}

如果请求头的 Cache-Control 设置了 no-cache,或者包含 If-Modified-SinceIf-None-Match 请求头则不可缓存,需发起网络请求:

	private fun hasConditions(request: Request): Boolean =request.header("If-Modified-Since") != null || request.header("If-None-Match") != null

这两个请求头的含义:

请求头说明
If-Modified-Since:[Time]值一般为 Date 或 lastModified,如果服务器没有在指定的时间后修改请求对应资源,会返回 304(无修改)
If-None-Match:[Tag]值一般为 Etag,将其与请求对应资源的 Etag 值进行比较;如果匹配,则返回 304
检查响应的缓存有效期

响应缓存只是在一定时间内有效,并不是永久有效,判定缓存是否在有效期的公式:

缓存存活时间 < 缓存新鲜度 - 缓存最小新鲜度 + 过期后继续使用时长

在缓存有效期判断上,需要先计算缓存的新鲜度,再调整缓存新鲜生存期,判断缓存是否新鲜可用。如果可用则可以返回缓存而无需发起网络请求,否则需要构造条件请求头,发起条件请求。具体代码如下:

    private fun computeCandidate(): CacheStrategy {// 响应的缓存控制指令val responseCaching = cacheResponse.cacheControl// 1.计算缓存新鲜度// 1.1 缓存年龄。cacheResponseAge() 计算缓存已经存在了多久val ageMillis = cacheResponseAge()// 1.2 新鲜生存期。computeFreshnessLifetime() 根据 Cache-Control 计算缓存应该保持新鲜的时间var freshMillis = computeFreshnessLifetime()// 2.调整缓存新鲜生存期// 2.1 如果请求设置了 max-age 头,则取 maxAge 和原新鲜生存期的较小值if (requestCaching.maxAgeSeconds != -1) {freshMillis = minOf(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds.toLong()))}// 2.2 minFreshMillis 表示客户端希望缓存至少在接下来的多少秒内保持新鲜var minFreshMillis: Long = 0if (requestCaching.minFreshSeconds != -1) {minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds.toLong())}// 2.3 maxStaleMillis 允许使用已过期的缓存,但不能超过指定的时间var maxStaleMillis: Long = 0if (!responseCaching.mustRevalidate && requestCaching.maxStaleSeconds != -1) {maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds.toLong())}// 3.判断缓存是否新鲜可用:如果缓存年龄加上 minFresh 小于新鲜生存期加上 maxStale,// 说明缓存仍然有效,可以返回缓存而不发起请求if (!responseCaching.noCache && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {val builder = cacheResponse.newBuilder()// 添加警告头:缓存已过期但还在允许的 maxStale 时间内if (ageMillis + minFreshMillis >= freshMillis) {builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"")}val oneDayMillis = 24 * 60 * 60 * 1000L// 添加警告头:启发式过期if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"")}// 使用缓存return CacheStrategy(null, builder.build())}// 4.构造条件请求头:根据缓存中的 ETag、Last-Modified 或 Date 头,添加对应的条件头 If-None-Match// 或 If-Modified-Since,发起条件请求。如果服务器返回 304,则使用缓存,否则下载新内容val conditionName: Stringval conditionValue: String?when {etag != null -> {conditionName = "If-None-Match"conditionValue = etag}lastModified != null -> {conditionName = "If-Modified-Since"conditionValue = lastModifiedString}servedDate != null -> {conditionName = "If-Modified-Since"conditionValue = servedDateString}else -> return CacheStrategy(request, null) // No condition! Make a regular request.}val conditionalRequestHeaders = request.headers.newBuilder()conditionalRequestHeaders.addLenient(conditionName, conditionValue!!)val conditionalRequest = request.newBuilder().headers(conditionalRequestHeaders.build()).build()return CacheStrategy(conditionalRequest, cacheResponse)}

2.2 拦截逻辑

弄清了缓存策略后,来看 CacheInterceptor 完整的拦截逻辑:

// cache 成员实际传入的是 OkHttpClient 的 cache 属性
class CacheInterceptor(internal val cache: Cache?) : Interceptor {@Throws(IOException::class)override fun intercept(chain: Interceptor.Chain): Response {// 1. 初始化及缓存策略计算val call = chain.call()// 1.1 根据当前请求的 Key(这里是 Request 对象)查找缓存响应val cacheCandidate = cache?.get(chain.request())val now = System.currentTimeMillis()// 1.2 计算缓存策略,决定是发送网络请求还是使用缓存val strategy = CacheStrategy.Factory(now, chain.request(), cacheCandidate).compute()val networkRequest = strategy.networkRequestval cacheResponse = strategy.cacheResponse// 2.清理无效缓存if (cacheCandidate != null && cacheResponse == null) {// 关闭不可用的缓存响应 BodycacheCandidate.body?.closeQuietly()}// 3. 处理强制仅缓存(only-if-cached)且无可用缓存// 请求头包含 Cache-Control: only-if-cached 但无有效缓存,返回 504 错误,表示无法满足请求if (networkRequest == null && cacheResponse == null) {return Response.Builder().request(chain.request()).protocol(Protocol.HTTP_1_1).code(HTTP_GATEWAY_TIMEOUT) // 504.message("Unsatisfiable Request (only-if-cached)").body(EMPTY_RESPONSE).sentRequestAtMillis(-1L).receivedResponseAtMillis(System.currentTimeMillis()).build().also {listener.satisfactionFailure(call, it)}}// 4. 直接使用缓存。策略判定无需网络请求,缓存有效,因此直接使用缓存if (networkRequest == null) {return cacheResponse!!.newBuilder()// stripBody() 避免后续操作修改原始缓存的 Body.cacheResponse(stripBody(cacheResponse)).build().also {// 触发 cacheHit 事件,通知监听器listener.cacheHit(call, it)}}// 5.处理条件请求或缓存未命中if (cacheResponse != null) {// 条件请求命中:存在缓存但需验证(如发送 If-None-Match)listener.cacheConditionalHit(call, cacheResponse)} else if (cache != null) {// 完全未命中:无任何可用缓存,完全依赖网络listener.cacheMiss(call)}var networkResponse: Response? = nulltry {// 6.中置工作,交给下一个责任链处理networkResponse = chain.proceed(networkRequest)} finally { // 网络请求异常时(如 IO 异常或其他异常),清理旧缓存 Bodyif (networkResponse == null && cacheCandidate != null) {cacheCandidate.body?.closeQuietly()}}// 7. 处理 304 响应(缓存仍有效)if (cacheResponse != null) {// 服务器返回 304,那就使用缓存作为本次请求的响应,但是需要更新时间等数据if (networkResponse?.code == HTTP_NOT_MODIFIED) {// 更新 cacheResponse 的发送、接收时间等数据,但是响应体并没有动,还用原来的val response = cacheResponse.newBuilder()// 合并缓存与 304 响应的头信息(304 通常只包含更新的头,如 Date,需合并到原缓存响应中).headers(combine(cacheResponse.headers, networkResponse.headers)).sentRequestAtMillis(networkResponse.sentRequestAtMillis).receivedResponseAtMillis(networkResponse.receivedResponseAtMillis).cacheResponse(stripBody(cacheResponse)).networkResponse(stripBody(networkResponse)).build() // 关闭 304 响应的 BodynetworkResponse.body!!.close()// Update the cache after combining headers but before stripping the// Content-Encoding header (as performed by initContentStream()).cache!!.trackConditionalCacheHit()// 更新缓存头cache.update(cacheResponse, response)// 更新缓存后返回新响应,避免重复验证return response.also {// 触发缓存命中(更新后)listener.cacheHit(call, it)}} else {// 关闭失效的缓存 BodycacheResponse.body?.closeQuietly()}}// 8.处理非 304 网络响应:代码走到这里说明缓存不可用,缓存已过期或服务器返回新内容,构建最终响应val response = networkResponse!!.newBuilder().cacheResponse(stripBody(cacheResponse)) // 关联原始缓存(用于日志).networkResponse(stripBody(networkResponse)) // 关联网络响应.build()// 9.缓存新响应(如可缓存)if (cache != null) {if (response.promisesBody() && CacheStrategy.isCacheable(response, networkRequest)) {// 写入新缓存val cacheRequest = cache.put(response)// 写入并返回响应return cacheWritingResponse(cacheRequest, response).also {if (cacheResponse != null) {// This will log a conditional cache miss only.listener.cacheMiss(call)}}}// 10. 处理破坏性请求(如 POST),非幂等方法(如 POST、PUT)可能修改资源,需清除旧缓存。// 因此需要通过 invalidatesCache() 检查方法是否需要清除缓存if (HttpMethod.invalidatesCache(networkRequest.method)) {try {// 移除相关缓存cache.remove(networkRequest)} catch (_: IOException) {// The cache cannot be written.}}}return response}
}

总结:

  • 缓存策略优先级:遵循 HTTP RFC 规范,优先使用 Cache-Control 指令。
  • 资源管理:确保所有 Response Body 正确关闭,防止内存泄漏。
  • 事件通知:通过 EventListener 提供详细的缓存命中/未命中跟踪。
  • 条件请求优化:通过 304 响应减少数据传输,提升性能。

该拦截器通过精细的条件分支和资源管理,实现了高效且符合规范的 HTTP 缓存机制。

3、连接拦截器

连接拦截器的作用是建立与目标服务器的连接,为后续请求提供网络通道。它看似简单,整个类只有 9 行代码,只有前置与中置工作,但内部实现复杂:

object ConnectInterceptor : Interceptor {@Throws(IOException::class)override fun intercept(chain: Interceptor.Chain): Response {// 1.前置工作:创建连接val realChain = chain as RealInterceptorChainval exchange = realChain.call.initExchange(chain)val connectedChain = realChain.copy(exchange = exchange)// 2.中置工作:执行下一个责任链return connectedChain.proceed(realChain.request)}
}

ConnectInterceptor 的前置工作就是通过 initExchange() 找到一个 Exchange 对象并更新到责任链对象中;中置工作仍然是启动下一个责任链;理论上还应该有一个后置工作 —— 断开连接,这项工作由连接池自动处理了,因此 ConnectInterceptor 没有后置工作,所以主要就是看 initExchange() 都干了啥。

3.1 初始化 Exchange

Exchange 可以理解为“请求交换”,指代请求发送和响应接收的完整交互过程。它的作用是传输单个 HTTP 请求与响应对,比如写请求头、请求体,读取响应头、响应体的工作是由 Exchange 主导的:

class Exchange(internal val call: RealCall,internal val eventListener: EventListener,internal val finder: ExchangeFinder,private val codec: ExchangeCodec // Exchange 编解码器 
) {@Throws(IOException::class)fun writeRequestHeaders(request: Request) {...codec.writeRequestHeaders(request)...}@Throws(IOException::class)fun createRequestBody(request: Request, duplex: Boolean): Sink {...val rawRequestBody = codec.createRequestBody(request, contentLength)...}@Throws(IOException::class)fun readResponseHeaders(expectContinue: Boolean): Response.Builder? {...codec.readResponseHeaders(expectContinue)...}@Throws(IOException::class)fun openResponseBody(response: Response): ResponseBody {...codec.openResponseBodySource(response)...}
}

通过精简的代码能看出,Exchange 在生成请求与读取响应这方面是对 ExchangeCodec 这个编解码器做了一个封装,ExchangeCodec 才是真正执行请求的生成与响应读取的类。举个例子,Exchange.writeRequestHeaders() 是要写请求头,交给 ExchangeCodec.writeRequestHeaders():

  // ExchangeCodec 是接口,这里举得是 Http1ExchangeCodec 的实现override fun writeRequestHeaders(request: Request) {val requestLine = RequestLine.get(request, connection.route().proxy.type())writeRequest(request.headers, requestLine)}fun writeRequest(headers: Headers, requestLine: String) {check(state == STATE_IDLE) { "state: $state" }sink.writeUtf8(requestLine).writeUtf8("\r\n")for (i in 0 until headers.size) {sink.writeUtf8(headers.name(i)).writeUtf8(": ").writeUtf8(headers.value(i)).writeUtf8("\r\n")}sink.writeUtf8("\r\n")state = STATE_OPEN_REQUEST_BODY}

最终请求头的字符串都是由编解码器写的,所以在初始化 Exchange 之前,必须先找到合适的 ExchangeCodec 才行。因此就有了 initExchange() 创建 Exchange 的逻辑:

  // 获取新连接或复用连接池中的连接,以承载后续的请求和响应internal fun initExchange(chain: RealInterceptorChain): Exchange {...val exchangeFinder = this.exchangeFinder!!// 1.找到发送请求与处理响应的编解码器val codec = exchangeFinder.find(client, chain)// 2.用编解码器等参数创建 Exchange 对象val result = Exchange(this, eventListener, exchangeFinder, codec)this.interceptorScopedExchange = resultthis.exchange = result...if (canceled) throw IOException("Canceled")return result}

接下来要关注如何获取 Exchange 编解码器对象。

3.2 获取 ExchangeCodec

ExchangeFinder 的 find() 会根据传入的 OkHttpClient 以及责任链 RealInterceptorChain 查找到一个健康连接,并返回该连接的编解码器:

  fun find(client: OkHttpClient,chain: RealInterceptorChain): ExchangeCodec {try {// 1.查找健康连接val resultConnection = findHealthyConnection(connectTimeout = chain.connectTimeoutMillis,readTimeout = chain.readTimeoutMillis,writeTimeout = chain.writeTimeoutMillis,pingIntervalMillis = client.pingIntervalMillis,connectionRetryEnabled = client.retryOnConnectionFailure,doExtensiveHealthChecks = chain.request.method != "GET")// 2.生成健康连接的编解码器并返回return resultConnection.newCodec(client, chain)} catch (e: RouteException) {trackFailure(e.lastConnectException)throw e} catch (e: IOException) {trackFailure(e)throw RouteException(e)}}

主要工作是第 1 步如何查找到一个健康连接,代码层次很深,后面主要就是介绍它。所以我们先看第 2 步,拿到一个健康连接后,如何生成它的编解码器:

  // RealConnection 根据 HTTP 连接类型生成对应的编解码器@Throws(SocketException::class)internal fun newCodec(client: OkHttpClient, chain: RealInterceptorChain): ExchangeCodec {val socket = this.socket!!val source = this.source!!val sink = this.sink!!val http2Connection = this.http2Connectionreturn if (http2Connection != null) {Http2ExchangeCodec(client, this, chain, http2Connection)} else {socket.soTimeout = chain.readTimeoutMillis()source.timeout().timeout(chain.readTimeoutMillis.toLong(), MILLISECONDS)sink.timeout().timeout(chain.writeTimeoutMillis.toLong(), MILLISECONDS)Http1ExchangeCodec(client, this, source, sink)}}

由于 HTTP1 与 HTTP2 的编解码方式是不同的,因此 ExchangeCodec 被抽象成一个接口,当 RealConnection 的 http2Connection 不为空时,说明它是一个 HTTP2 连接,所以此时会返回 HTTP2 的编解码器 Http2ExchangeCodec,否则视为 HTTP1 连接返回 Http1ExchangeCodec。

3.3 查找健康连接

3.2 中的第 1 步通过 findHealthyConnection() 返回一个健康连接:

  @Throws(IOException::class)private fun findHealthyConnection(connectTimeout: Int,readTimeout: Int,writeTimeout: Int,pingIntervalMillis: Int,connectionRetryEnabled: Boolean,doExtensiveHealthChecks: Boolean): RealConnection {while (true) {// 1.查找候选连接val candidate = findConnection(connectTimeout = connectTimeout,readTimeout = readTimeout,writeTimeout = writeTimeout,pingIntervalMillis = pingIntervalMillis,connectionRetryEnabled = connectionRetryEnabled)// 2.检查候选连接是否健康if (candidate.isHealthy(doExtensiveHealthChecks)) {return candidate}// 如果连接不健康,则将 noNewExchanges 标记置位,连接池会移除该连接candidate.noNewExchanges()// 3.确保我们还有可以尝试的路由。一种可能耗尽所有路由的情况是:// 当新建连接后立即被检测为不健康时,需要检查是否还有其他可用路由if (nextRouteToTry != null) continue// 当前路由选择器中还有未尝试的路由,继续重试val routesLeft = routeSelection?.hasNext() ?: trueif (routesLeft) continue// 存在其他路由选择器(如备用代理组),继续重试val routesSelectionLeft = routeSelector?.hasNext() ?: trueif (routesSelectionLeft) continuethrow IOException("exhausted all routes")}}

在一个死循环内不断做三件事:

  • 查找候选连接
  • 检查连接是否健康,如健康则作为结果返回,否则要将该连接的 noNewExchanges 置位
  • 检查是否还有可用路由(线路),如有则继续循环,否则意味着所有路由都被尝试完也未找到健康连接,抛出 IO 异常

由于第 1 步通过 findConnection() 查找候选连接的内容非常多,还是放到下一节介绍,这里先看找到连接的后续工作。

检查连接是否健康

  /** Returns true if this connection is ready to host new streams. */fun isHealthy(doExtensiveChecks: Boolean): Boolean {assertThreadDoesntHoldLock()val nowNs = System.nanoTime()// 底层 TCP Socketval rawSocket = this.rawSocket!!// 应用层 Socket,如果是 HTTPS 协议通信的话,就是在 rawSocket 之上的// SSLSocket,否则就是 rawSocket 本身val socket = this.socket!!val source = this.source!!// 1.底层与应用层的 Socket 均为关闭或停止if (rawSocket.isClosed || socket.isClosed || socket.isInputShutdown ||socket.isOutputShutdown) {return false}// 2.如果是 HTTP2 连接的话,做保活/心跳相关检查:如果当前时间超过 pong 响应截止时间,// 且如果已发送的降级 ping 数 > 已接收的降级 pong 数,判定为不健康val http2Connection = this.http2Connectionif (http2Connection != null) {return http2Connection.isHealthy(nowNs)}// 3.扩展检查:当连接空闲时间超过健康阈值且需要深度检查时,检查应用层 socket 是否健康val idleDurationNs = synchronized(this) { nowNs - idleAtNs }if (idleDurationNs >= IDLE_CONNECTION_HEALTHY_NS && doExtensiveChecks) {return socket.isHealthy(source)}return true}

这一步分多个层级检查连接是否健康,如果不健康,需要通过 noNewExchanges() 将 noNewExchanges 标记置位:

  /*** 如果为 true,则不能在此连接上创建新的数据交换(exchange)。当从连接池中移除连接时* 必须设为 true,否则在竞争条件下,调用方可能本不应该获取到此连接却从连接池中获取到了。* 对称地,在从连接池返回连接前必须始终检查此标志。* 一旦为 true 将始终保持为 true。由 this 对象(当前连接实例)的同步锁进行保护。*/var noNewExchanges = false@Synchronized internal fun noNewExchanges() {noNewExchanges = true}

检查是否还有可用连接

3.4 查找候选连接

这节我们来看 3.3 中的第 1 步,findConnection() 是如何查找到一个连接的:

  /*** 获取承载新数据流的连接,优先级顺序:复用现有连接、连接池中的连接、建立全新连接。* 每个阻塞操作前都会检查是否已取消请求。*/@Throws(IOException::class)private fun findConnection(connectTimeout: Int,readTimeout: Int,writeTimeout: Int,pingIntervalMillis: Int,connectionRetryEnabled: Boolean): RealConnection {// 1.连接复用检查阶段// 1.1 检查请求是否已取消if (call.isCanceled()) throw IOException("Canceled")// 1.2 验证现有连接是否可用val callConnection = call.connection // This may be mutated by releaseConnectionNoEvents()!if (callConnection != null) {// 应该关闭的 Socketvar toClose: Socket? = nullsynchronized(callConnection) {// 1.2.1 若连接被标记为不可用(noNewExchanges)或主机、端口不匹配,则关闭该连接if (callConnection.noNewExchanges || !sameHostAndPort(callConnection.route().address.url)) {// 关闭连接,会将 call.connection 置为 nulltoClose = call.releaseConnectionNoEvents()}}// 1.2.2 call.connection 还存在的话就复用它,直接返回if (call.connection != null) {check(toClose == null)return callConnection}// 静默关闭 SockettoClose?.closeQuietly()eventListener.connectionReleased(call, callConnection)}// 2.连接池获取阶段// 2.1 由于需要一个新的连接,因此重置相关数据refusedStreamCount = 0connectionShutdownCount = 0otherFailureCount = 0// 2.2 尝试从连接池获取一个连接if (connectionPool.callAcquirePooledConnection(address, call, null, false)) {val result = call.connection!!eventListener.connectionAcquired(call, result)return result}// 3.路由选择阶段val routes: List<Route>? // 可能的路由列表val route: Route // 最终选择的路由// 3.1 三级路由获取策略if (nextRouteToTry != null) { // 3.1.1 预置路由// Use a route from a preceding coalesced connection.routes = nullroute = nextRouteToTry!!nextRouteToTry = null} else if (routeSelection != null && routeSelection!!.hasNext()) { // 3.1.2 现有路由// Use a route from an existing route selection.routes = nullroute = routeSelection!!.next()} else {// 3.1.3 新建路由选择器(是一个阻塞操作)var localRouteSelector = routeSelectorif (localRouteSelector == null) {localRouteSelector = RouteSelector(address, call.client.routeDatabase, call, eventListener)this.routeSelector = localRouteSelector}val localRouteSelection = localRouteSelector.next()routeSelection = localRouteSelectionroutes = localRouteSelection.routesif (call.isCanceled()) throw IOException("Canceled")// 3.2 获取一组 IP 地址后,再次尝试从连接池获取连接(连接合并提高了匹配的可能性)if (connectionPool.callAcquirePooledConnection(address, call, routes, false)) {val result = call.connection!!eventListener.connectionAcquired(call, result)return result}route = localRouteSelection.next()}// 4.新建连接阶段// Connect. Tell the call about the connecting call so async cancels work.// 4.1 创建 RealConnection 实例val newConnection = RealConnection(connectionPool, route)// 4.2 设置可取消标记call.connectionToCancel = newConnection// 4.3 执行 TCP/TLS 握手try {newConnection.connect(connectTimeout,readTimeout,writeTimeout,pingIntervalMillis,connectionRetryEnabled,call,eventListener)} finally {call.connectionToCancel = null}// 4.4 更新路由数据库call.client.routeDatabase.connected(newConnection.route())// 5.连接合并优化// If we raced another call connecting to this host, coalesce the connections. This makes for 3// different lookups in the connection pool!// 5.1 最终检查连接池是否有合并机会if (connectionPool.callAcquirePooledConnection(address, call, routes, true)) {val result = call.connection!!nextRouteToTry = route// 5.2 若合并成功则关闭新建连接newConnection.socket().closeQuietly()eventListener.connectionAcquired(call, result)return result}// 5.3 合并失败,将新连接加入连接池synchronized(newConnection) {connectionPool.put(newConnection)call.acquireConnectionNoEvents(newConnection)}eventListener.connectionAcquired(call, newConnection)return newConnection}

按照注释标注的 5 步序号逐一来看。

连接复用检查

首先看 RealCall 自身保存的 connection 是否可以复用,主要判断条件:

  • noNewExchanges 若为 true 表示该连接不能再接收更多任务了,此时不可复用
  • sameHostAndPort() 的判断不成立,即主机域名与端口号不同时不可复用

如果不可复用,需要以无事件方式关闭连接:

  /*** 连接的资源分配列表(calls)中移除此任务(RealCall)。返回调用方应当关闭的 socket。*/internal fun releaseConnectionNoEvents(): Socket? {val connection = this.connection!!connection.assertThreadHoldsLock()// 这个连接承载的请求,是一个 MutableList<Reference<RealCall>>val calls = connection.callsval index = calls.indexOfFirst { it.get() == this@RealCall }check(index != -1)// 从请求集合中移除当前 RealCall 并将连接 connection 置为 nullcalls.removeAt(index)this.connection = null// 如果这个连接没有承载任何请求,那么它就成为了一个闲置连接,如果它的 noNewExchanges// 被置位或者连接池允许的最大限制连接数量为 0,就需要关闭这个连接,此时返回该连接的 Socketif (calls.isEmpty()) {connection.idleAtNs = System.nanoTime()if (connectionPool.connectionBecameIdle(connection)) {return connection.socket()}}return null}

关闭连接需要将当前 RealCall 任务从 connection 承载的任务列表中移除,并将 connection 置为 null。如果当前连接没有承载任何任务便成为空闲连接,如果它自身的 noNewExchanges 被置为 true 或者连接池不允许有空闲连接,需要关闭该连接,此时要返回 connection 的 Socket。

如果在 1.2.2 中检查连接不为 null 说明满足复用 1.2.1 的复用条件,直接返回,否则就要关闭 releaseConnectionNoEvents() 返回的 Socket 对象。

检查连接池

如果 RealCall 自身的连接不可复用,尝试从连接池中找一个可复用的连接。主要是通过 2.2 的 callAcquirePooledConnection() 查找满足复用条件的连接:

  /*** 尝试从连接池获取可复用的连接,用于服务指定[address]的[call]请求。当成功获取连接时返回 true*/fun callAcquirePooledConnection(address: Address,call: RealCall,routes: List<Route>?,requireMultiplexed: Boolean): Boolean {// 遍历连接池中的所有连接for (connection in connections) {synchronized(connection) {// 1.多路复用要求检查。由于只有 HTTP2 支持多路复用,因此这是一项针对 HTTP2 的检查if (requireMultiplexed && !connection.isMultiplexed) return@synchronized// 2.检查是否有复用资格,主要是对地址路由的匹配检查if (!connection.isEligible(address, routes)) return@synchronized// 3.检查通过以无事件方式获取连接call.acquireConnectionNoEvents(connection)return true}}return false}

首先是多路复用检查,如果参数 requireMultiplexed 要求强制使用多路复用,但 connection 不是 HTTP2 连接不支持多路复用时,该 connection 不能复用:

  /*** RealConnection 的 isMultiplexed 属性会在该 RealConnection 是 HTTP2 连接时* 返回 true,这些连接可同时用于多个 HTTP 请求*/internal val isMultiplexed: Booleanget() = http2Connection != nullprivate var http2Connection: Http2Connection? = null

然后是复用资格检查,主要是地址路由相关检查:

  /*** 判断当前连接是否可用于承载目标地址的流分配。若 routes 参数非空,则表示该连接已解析的具体路由信息*/internal fun isEligible(address: Address, routes: List<Route>?): Boolean {assertThreadHoldsLock()// 如果这个连接所承载的请求(RealCall)已经到达上限,或者该连接不能创建新交换,视为不可用// HTTP1 只允许 1 个请求,而 HTTP2 最大允许 4 个if (calls.size >= allocationLimit || noNewExchanges) return false// 地址的非主机字段(DNS、代理、端口等等)对比if (!this.route.address.equalsNonHost(address)) return false// 如果主机匹配则连接可以承载地址请求,直接返回if (address.url.host == this.route().address.url.host) {return true // This connection is a perfect match.}// 到这里主机没匹配,但如果满足我们的连接合并(connection coalescing)要求,// 仍然可以继续处理请求,实际上就是判断 HTTP2 连接。更多信息查看:// 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 falseif (!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.}

在连接可以承载的 RealCall 未到上限且可以接收更多 Exchanges 的情况下:

  • 如果非主机字段与主机都相同,可以直接复用该连接
  • 如果非主机字段相同但主机不同,可以进一步检查是否满足 HTTP2 多路复用条件:
    • 连接必须是 HTTP2 的连接
    • 有可用路由且满足匹配条件:直连且 IP 地址相同
    • 连接的服务器证书必须能覆盖新的主机
    • 证书锁定(Certificate Pinning)必须与主机匹配

先看 equalsNonHost() 都检查了哪些非主机配置:

  internal fun equalsNonHost(that: Address): Boolean {return this.dns == that.dns &&this.proxyAuthenticator == that.proxyAuthenticator &&this.protocols == that.protocols &&this.connectionSpecs == that.connectionSpecs &&this.proxySelector == that.proxySelector &&this.proxy == that.proxy &&this.sslSocketFactory == that.sslSocketFactory &&this.hostnameVerifier == that.hostnameVerifier &&this.certificatePinner == that.certificatePinner &&this.url.port == that.url.port}

这些配置我们在前面讲 OkHttp 配置时基本都已经解释过,所以这里就不多说了。

举个例子,原本有一个请求的 URL 是 http://test.com/1,那么后续的新请求:

  • http://test.com/2 可以有复用 http://test.com/1 的连接的资格,因为域名一样,http 协议端口又都是 80(说可以有资格是因为除了域名和端口,还有其他条件)
  • https://test.com/1 就一定不满足复用条件,因为 https 协议的端口号是 443

然后再看 HTTP2 多路复用的条件,它适用于不同域名的主机配置了相同 IP 的情况。比如两个网站,主机名分别为 https://dabendan.com 与 https://xiaobendan.com,它们在同一个虚拟主机上,被配置到同一个 IP 地址 123.123.123.123。这种情况下,只是域名不同,但 IP、端口以及其他配置(equalsNonHost() 中检查的)都一样,也是可以使用同一个 HTTP2 连接的。只不过,为了验证 https://dabendan.com 与 https://xiaobendan.com 确实是同一个网站,需要额外再进行证书验证。

看具体代码。routes 是一个路由列表,表名可用的路由,routeMatchesAny() 会从 routes 中检查是否有任意一个 IP 地址匹配的路由:

  /*** 检查当前连接的路由地址是否与候选列表中的任意路由匹配。注意:* 1.要求双方主机都已完成 DNS 解析(需在路由规划之后)* 2.代理连接不可合并,因为代理会隐藏原始服务器 IP 地址* 当存在完全匹配的路由时返回 true*/private fun routeMatchesAny(candidates: List<Route>): Boolean {return candidates.any {// 必须都要是直连(不是代理)且地址相同it.proxy.type() == Proxy.Type.DIRECT &&route.proxy.type() == Proxy.Type.DIRECT &&route.socketAddress == it.socketAddress}}

需要注意的是,代理类型需要为直连 DIRECT(实际上就是没有使用代理),否则原始服务器 IP 会被隐藏,不知道原始服务器 IP 就不能合并连接。

接下来需要验证连接的证书是否能覆盖到新的主机,首先需要地址的主机验证器是否为 OkHostnameVerifier。因为 HostnameVerifier 是 Java 原生 javax 包下的一个接口,有多种实现,在需要验证地址时,需要符合 OkHttp 的规范。

然后检查 URL 是否满足复用条件:

  private fun supportsUrl(url: HttpUrl): Boolean {...val routeUrl = route.address.url// 1.端口不同不可复用if (url.port != routeUrl.port) {return false // Port mismatch.}// 2.如果主机名和端口都相同,则可直接复用if (url.host == routeUrl.host) {return true}// 3.如果主机名不同,但是连接允许合并且存在 TLS 握手信息,证书也能验证通过的话,也可复用// We have a host mismatch. But if the certificate matches, we're still good.return !noCoalescedConnections && handshake != null && certificateSupportHost(url, handshake!!)}private fun certificateSupportHost(url: HttpUrl, handshake: Handshake): Boolean {val peerCertificates = handshake.peerCertificates// 使用 OkHostnameVerifier 验证服务器证书(前面说过,证书链的第一个证书是服务器证书)return peerCertificates.isNotEmpty() && OkHostnameVerifier.verify(url.host,peerCertificates[0] as X509Certificate)}

如果主机名与端口都相同,那就符合复用条件。如果端口相同,但主机不同,可以进一步看连接是否支持合并,如果支持的话,验证这个签名是否支持主机。

最后是证书固定:

  /*** 确认至少有一个为`hostname`预置的证书指纹存在于`peerCertificates`证书链中。* 若未设置该主机名的证书锁定规则,则不执行任何操作。OkHttp 在成功完成 TLS 握手后、* 使用连接前调用此方法。*/@Throws(SSLPeerUnverifiedException::class)fun check(hostname: String, peerCertificates: List<Certificate>) {return check(hostname) {(certificateChainCleaner?.clean(peerCertificates, hostname) ?: peerCertificates).map { it as X509Certificate }}}

经过 isEligible() 的检查,如果连接符合复用条件,则通过 acquireConnectionNoEvents() 复用连接:

  fun acquireConnectionNoEvents(connection: RealConnection) {...check(this.connection == null)this.connection = connectionconnection.calls.add(CallReference(this, callStackTrace))}

路由选择

在看代码之前我们先举一个实例。比如要连接的服务器地址为 https://test.com,该域名可以有多个 IP 地址:

  • 1.2.3.4:443
  • 5.6.7.8:443

服务器可以有代理服务器 https://testproxy.com,代理服务器也可能有多个 IP 地址:

  • 9.10.11.12:443
  • 13.14.15.16:443

在解析的时候,URL 可以提供域名和端口号信息组成 Address:

class Address(// 域名和端口号来自于 URLuriHost: String,uriPort: Int,// 其余信息通过 OkHttpClient 获取@get:JvmName("dns") val dns: Dns,@get:JvmName("socketFactory") val socketFactory: SocketFactory,@get:JvmName("sslSocketFactory") val sslSocketFactory: SSLSocketFactory?,@get:JvmName("hostnameVerifier") val hostnameVerifier: HostnameVerifier?,@get:JvmName("certificatePinner") val certificatePinner: CertificatePinner?,@get:JvmName("proxyAuthenticator") val proxyAuthenticator: Authenticator,@get:JvmName("proxy") val proxy: Proxy?,protocols: List<Protocol>,connectionSpecs: List<ConnectionSpec>,@get:JvmName("proxySelector") val proxySelector: ProxySelector
)

Address 又作为路由 Route 的成员:

class Route(@get:JvmName("address") val address: Address,@get:JvmName("proxy") val proxy: Proxy,@get:JvmName("socketAddress") val socketAddress: InetSocketAddress
)

路由选择器 RouteSelector 在进行路由选择时,会按组遍历。比如直连的 IP 下的两个 IP 地址 List<Route> 分到一个组中,这个组就是 RouteSelector.Selection,然后代理的 IP 又是另一个 Selection,每个 Selection 下面都有一个 List<Route>

现在再看路由选择,它的最终目的是通过遍历这些路由,再去连接池获取一次连接。因为在上一步检查连接池去获取连接时,没有传 List<Route>,也就是在没有指定 IP 地址的情况下去尝试获取可以复用的连接。那么在主机名不同的情况下,去检查 HTTP2 多路复用的条件时,就会因为没有具体的 IP 地址而无法复用已经存在的 HTTP2 连接。所以这一次传入 List<Route> 是拓宽了连接池中连接的可选择性,可能匹配到之前不能匹配的连接(得益于连接合并)。

具体到路由选择的三级策略上:

  • 首先检查前置连接 nextRouteToTry,它是之前合并连接时(下一小节要讲合并连接)保存的路由,相同 IP、端口的 HTTPS 连接可以复用
  • 使用现有路由选择器的路由 Selection,就是我们举例的分组。比如直连的 Selection 中有 1.2.3.4 和 5.6.7.8 两个 IP,先看这一组中的路由是否有可以复用的
  • 需要计算新的路由选择。假如直连的两个路由不能复用,那么就检查下一个 Selection,也就是代理这一组内的路由是否有满足

新建连接

  fun connect(connectTimeout: Int,readTimeout: Int,writeTimeout: Int,pingIntervalMillis: Int,connectionRetryEnabled: Boolean,call: Call,eventListener: EventListener) {check(protocol == null) { "already connected" }var routeException: RouteException? = nullval connectionSpecs = route.address.connectionSpecsval connectionSpecSelector = ConnectionSpecSelector(connectionSpecs)// 路由中的地址 Address 的 sslSocketFactory 为空,说明该地址不是 HTTPS 协议if (route.address.sslSocketFactory == null) {// 没有开启明文传输(不是 HTTPS 那就是 HTTP 了,HTTP 需要明文传输你又没配置,抛异常)if (ConnectionSpec.CLEARTEXT !in connectionSpecs) {throw RouteException(UnknownServiceException("CLEARTEXT communication not enabled for client"))}val host = route.address.url.host// 网络安全政策不允许明文传输,那 HTTP 没法工作,也得抛异常if (!Platform.get().isCleartextTrafficPermitted(host)) {throw RouteException(UnknownServiceException("CLEARTEXT communication to $host not permitted by network security policy"))}} else {// H2_PRIOR_KNOWLEDGE 指客户端在建立连接时就已经知道服务器支持 HTTP/2 协议,故而不先进行// 协商而直接发送 HTTP2 帧。从安全性考虑,OkHttp 不允许它与 HTTPS 一起使用if (Protocol.H2_PRIOR_KNOWLEDGE in route.address.protocols) {throw RouteException(UnknownServiceException("H2_PRIOR_KNOWLEDGE cannot be used with HTTPS"))}}while (true) {try {// 使用 HTTP 隧道连接if (route.requiresTunnel()) {connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener)if (rawSocket == null) {// We were unable to connect the tunnel but properly closed down our resources.break}} else {// 正常情况下无需使用 Tunnel,就正常建立一个 TCP 连接connectSocket(connectTimeout, readTimeout, call, eventListener)}// 建立 HTTP 与 HTTP2 连接establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener)eventListener.connectEnd(call, route.socketAddress, route.proxy, protocol)break} catch (e: IOException) {...}}if (route.requiresTunnel() && rawSocket == null) {throw RouteException(ProtocolException("Too many tunnel connections attempted: $MAX_TUNNEL_ATTEMPTS"))}idleAtNs = System.nanoTime()}

HTTP 隧道是标准的使用 HTTP 代理 HTTPS 的方式。

第五步

同时做请求,同时都创建一个新连接,先创建好的把连接放连接池里,后创建好的把连接扔掉用刚刚创建好的。

复用连接,非多路复用,可以多路复用(非多路复用 + 多路复用),自己创建,只拿多路复用连接

第一次是上面的流程,但如果是第二次,比如发生了重试或者重定向,那么第一部分判断 supportUrl() 检测就有可能不行了。比如重定向,原本是访问 http://test.com,重定向的 URL 是 https://test.com,由于协议发生了切换,端口不一样了,这样不满足 supportUrl() 的条件,所以需要把连接释放掉。

4、请求服务拦截器

  @Throws(IOException::class)override fun intercept(chain: Interceptor.Chain): Response {val realChain = chain as RealInterceptorChainval exchange = realChain.exchange!!val request = realChain.requestval requestBody = request.bodyval sentRequestMillis = System.currentTimeMillis()var invokeStartEvent = truevar responseBuilder: Response.Builder? = nullvar sendRequestException: IOException? = nulltry {exchange.writeRequestHeaders(request)if (HttpMethod.permitsRequestBody(request.method) && requestBody != null) {// If there's a "Expect: 100-continue" header on the request, wait for a "HTTP/1.1 100// Continue" response before transmitting the request body. If we don't get that, return// what we did get (such as a 4xx response) without ever transmitting the request body.if ("100-continue".equals(request.header("Expect"), ignoreCase = true)) {exchange.flushRequest()responseBuilder = exchange.readResponseHeaders(expectContinue = true)exchange.responseHeadersStart()invokeStartEvent = false}if (responseBuilder == null) {if (requestBody.isDuplex()) {// Prepare a duplex body so that the application can send a request body later.exchange.flushRequest()val bufferedRequestBody = exchange.createRequestBody(request, true).buffer()requestBody.writeTo(bufferedRequestBody)} else {// Write the request body if the "Expect: 100-continue" expectation was met.val bufferedRequestBody = exchange.createRequestBody(request, false).buffer()requestBody.writeTo(bufferedRequestBody)bufferedRequestBody.close()}} else {exchange.noRequestBody()if (!exchange.connection.isMultiplexed) {// If the "Expect: 100-continue" expectation wasn't met, prevent the HTTP/1 connection// from being reused. Otherwise we're still obligated to transmit the request body to// leave the connection in a consistent state.exchange.noNewExchangesOnConnection()}}} else {exchange.noRequestBody()}if (requestBody == null || !requestBody.isDuplex()) {exchange.finishRequest()}} catch (e: IOException) {if (e is ConnectionShutdownException) {throw e // No request was sent so there's no response to read.}if (!exchange.hasFailure) {throw e // Don't attempt to read the response; we failed to send the request.}sendRequestException = e}try {if (responseBuilder == null) {responseBuilder = exchange.readResponseHeaders(expectContinue = false)!!if (invokeStartEvent) {exchange.responseHeadersStart()invokeStartEvent = false}}var response = responseBuilder.request(request).handshake(exchange.connection.handshake()).sentRequestAtMillis(sentRequestMillis).receivedResponseAtMillis(System.currentTimeMillis()).build()var code = response.codeif (shouldIgnoreAndWaitForRealResponse(code)) {responseBuilder = exchange.readResponseHeaders(expectContinue = false)!!if (invokeStartEvent) {exchange.responseHeadersStart()}response = responseBuilder.request(request).handshake(exchange.connection.handshake()).sentRequestAtMillis(sentRequestMillis).receivedResponseAtMillis(System.currentTimeMillis()).build()code = response.code}exchange.responseHeadersEnd(response)response = if (forWebSocket && code == 101) {// Connection is upgrading, but we need to ensure interceptors see a non-null response body.response.newBuilder().body(EMPTY_RESPONSE).build()} else {response.newBuilder().body(exchange.openResponseBody(response)).build()}if ("close".equals(response.request.header("Connection"), ignoreCase = true) ||"close".equals(response.header("Connection"), ignoreCase = true)) {exchange.noNewExchangesOnConnection()}if ((code == 204 || code == 205) && response.body?.contentLength() ?: -1L > 0L) {throw ProtocolException("HTTP $code had non-zero Content-Length: ${response.body?.contentLength()}")}return response} catch (e: IOException) {if (sendRequestException != null) {sendRequestException.addSuppressed(e)throw sendRequestException}throw e}}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/web/87681.shtml
繁体地址,请注明出处:http://hk.pswp.cn/web/87681.shtml
英文地址,请注明出处:http://en.pswp.cn/web/87681.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

45-使用scale实现图形缩放

45-使用scale实现图形缩放_哔哩哔哩_bilibili45-使用scale实现图形缩放是一次性学会 Canvas 动画绘图&#xff08;核心精讲50个案例&#xff09;2023最新教程的第46集视频&#xff0c;该合集共计53集&#xff0c;视频收藏或关注UP主&#xff0c;及时了解更多相关视频内容。http…

软件开发早期阶段,使用存储过程的优势探讨:敏捷开发下的利器

在现代软件开发中&#xff0c;随着持续集成与敏捷开发的深入推进&#xff0c;开发团队越来越重视快速响应需求变更、快速上线迭代。在这种背景下&#xff0c;传统将业务逻辑全部放在应用层的方式在某些阶段显得笨重。本文将探讨在软件开发初期&#xff0c;特别是在需求尚不稳定…

『 C++入門到放棄 』- string

C 學習筆記 - string 一、什麼是string ? string 是 C 中標準函數庫中的一個類&#xff0c;其包含在 中 該類封裝了C語言中字符串操作&#xff0c;提供內存管理自動化與更多的操作 支持複製、比較、插入、刪除、查找等功能 二、常用接口整理 類別常用方法 / 說明建立與指…

ARM架构下C++程序堆溢出与栈堆碰撞问题深度解析

ARM架构下C程序堆溢出与栈堆碰撞问题深度解析 一、问题背景&#xff1a;从崩溃现象到内存异常 在嵌入式系统开发中&#xff0c;程序崩溃是常见但棘手的问题。特别是在ARM架构设备上&#xff0c;一种典型的崩溃场景如下&#xff1a;程序在执行聚类算法或大规模数据处理时突然终…

.NET9 实现排序算法(MergeSortTest 和 QuickSortTest)性能测试

在 .NET 9 平台下&#xff0c;我们对两种经典的排序算法 MergeSortTest&#xff08;归并排序&#xff09;和 QuickSortTest&#xff08;快速排序&#xff09;进行了性能基准测试&#xff08;Benchmark&#xff09;&#xff0c;以评估它们在不同数据规模下的执行效率、内存分配及…

RabbitMQ - SpringAMQP及Work模型

一、概述RabbitMQ是一个流行的开源消息代理&#xff0c;支持多种消息传递协议。它通常用于实现异步通信、解耦系统组件和分布式任务处理。Spring AMQP是Spring框架下的一个子项目&#xff0c;提供了对RabbitMQ的便捷访问和操作。本文将详细介绍RabbitMQ的工作模型&#xff08;W…

微信小程序51~60

1.界面交互-loading提示框 loading提示框用于增加用户体验&#xff0c; 对应的API有两个&#xff1a; wx.showLoading()显示loading提示框wx.hideLoading()关闭loading提示框 Page({getData () {//显示loading提示框wx.showLoading({//提示内容不会自动换行&#xff0c;多出来的…

SqueezeBERT:计算机视觉能为自然语言处理在高效神经网络方面带来哪些启示?

摘要 人类每天阅读和撰写数千亿条消息。得益于大规模数据集、高性能计算系统和更优的神经网络模型&#xff0c;自然语言处理&#xff08;NLP&#xff09;技术在理解、校对和组织这些消息方面取得了显著进展。因此&#xff0c;将 NLP 部署于各类应用中&#xff0c;以帮助网页用…

Springboot开发常见注解一览

注解用法常用参数Configuration用于标记类为配置类&#xff0c;其中通过Bean方法定义Spring管理的组件。它替代XML配置&#xff0c;用Java代码声明对象创建逻辑&#xff0c;并确保单例等容器特性生效。相当于给Spring提供一个“制造说明书”来组装应用部件RestControllerRestCo…

Maven高级——分模块设计与开发

目录 ​编辑 分模块设计与开发 拆分策略 继承与聚合 版本锁定 聚合 作用 实现 Maven中继承与聚合的联系与区别&#xff1f; 联系 区别 私服 分模块设计与开发 将一个大项目拆分成若干个子模块&#xff0c;方便项目的管理维护&#xff0c;扩展&#xff0c;也方便模…

线程池的七个参数设计源于对高并发场景下资源管理、系统稳定性与性能平衡的深刻洞察

⚙️ 一、核心参数设计目标与解决的问题 参数设计目标解决的核心问题典型取值策略corePoolSize&#xff08;核心线程数&#xff09;维持常备线程资源避免频繁创建/销毁线程的开销&#xff0c;提高响应速度CPU密集型&#xff1a;N_cpu 1 IO密集型&#xff1a;2 N_cpu maximum…

少样本学习在计算机视觉中的应用:原理、挑战与最新突破

在深度学习的黄金时代&#xff0c;大量标注数据似乎成了算法性能的前提。然而在许多现实场景中&#xff0c;如医疗图像分析、工业缺陷检测、遥感识别、甚至个性化视觉服务中&#xff0c;高质量、成规模的标注数据往往昂贵、稀缺&#xff0c;甚至难以获得。这种场景正是**少样本…

github在线图床

github做的图床&#xff0c;原理是利用github API实现的在线上传&#xff0c;就一个页面&#xff0c;css和js都是集成在页面&#xff0c;相关信息保存在浏览器缓存中&#xff0c;配置一下即可使用 效果演示&#xff1a; github在线图床 打开网站填写下列信息 github用户名&a…

css-多条记录,自动换行与自动并行布局及gap兼容

实现这样的内容布局&#xff0c;当一段文案长度超过当前行的时候自动占据一行&#xff0c;其他相近的不超过一行自动放在一行间隔隔开 关键实现原理&#xff1a; 弹性布局容器&#xff1a; .history-container {display: flex;flex-wrap: wrap;gap: 12px; }使用flex-wrap: wr…

Redis 哨兵模式部署--docker版本

redis sentinel 简介 Redis Sentinel 是 Redis 官方提供的高可用&#xff08;HA&#xff09;解决方案&#xff0c;用于监控主从架构中的故障并自动完成故障转移。当主节点&#xff08;Master&#xff09;宕机时&#xff0c;Sentinel 能自动选举新的主节点&#xff0c;通知从节…

Java线程中的守护线程

Java线程中的守护线程在Java中&#xff0c;守护线程&#xff08;Daemon Thread&#xff09;是一种特殊类型的线程&#xff0c;它在后台运行&#xff0c;主要用于支持其他线程&#xff08;如用户线程&#xff09;的工作。守护线程不会阻止JVM&#xff08;Java虚拟机&#xff09;…

Flink-状态恢复-isRestore分析

isRestored 方法返回值依赖 restoredCheckpointId 是否为空&#xff1a;restoredCheckpointId 在算子状态句柄&#xff08;StreamOperatorStateHandler&#xff09;中从 StreamOperatorStateContext 获取并赋值给 StateInitializationContext&#xff08;该 context 就是 initi…

rk3128 emmc显示剩余容量为0

机器emmc 容量显示异常&#xff0c;显示剩余容量为0&#xff0c;这时候做了一个让 系统不检测GPP分区部分的操作&#xff0c;此问题才得以解决&#xff0c;如下&#xff1a; system/vold/DirectVolume.cpp -33,6 33,8 #include "VolumeManager.h"#include "Re…

WebAssembly国际化多语种支持

icu linux数据裁剪 先linux编译出所有的工具 mkdir build && cd build ../configure --prefix=$(pwd)/build_wasm/install --enable-static --disable-shared --with-data-packaging=static --enable-tools=yes --enable-extras=yes --e…

Ubuntu 安装 etcd 与 etcd-cpp-apiv3

目录 安装 etcd 安装 etcd-cpp-apiv3 安装 etcd sudo apt update sudo apt install etcd-server sudo apt install -y etcd-client 在 /etc/default/etcd 配置文件中配置&#xff0c;下面示例是单个服务器内进程之间交换信息且只有一个etcd节点。 #节点名称&#xff0c;默认为…