Android 网络全栈攻略(七)—— 从 OkHttp 拦截器来看 HTTP 协议二

发布于:2025-07-03 ⋅ 阅读:(15) ⋅ 点赞:(0)

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.body
    if (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:close
    if (userRequest.header("Connection") == null) {
      requestBuilder.header("Connection", "Keep-Alive")
    }

    // 在没有 Accept-Encoding 与 Range 这两个请求头的情况下,自动添加 gzip 压缩数据
    var transparentGzip = false
    if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
      transparentGzip = true
      requestBuilder.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.body
      if (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?
)

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

networkRequest cacheResponse 说明
Null Not Null 直接使用缓存
Null Null 请求失败,OkHttp 框架会返回 504
Not Null Null 向服务器发起请求
Not Null Not 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.sentRequestAtMillis
        this.receivedResponseMillis = cacheResponse.receivedResponseAtMillis
        // 保存 cacheResponse 中的部分响应头
        val headers = cacheResponse.headers
        for (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, // 200
        HTTP_NOT_AUTHORITATIVE, // 203
        HTTP_NO_CONTENT, // 204
        HTTP_MULT_CHOICE, // 300
        HTTP_MOVED_PERM, // 301
        HTTP_NOT_FOUND, // 404
        HTTP_BAD_METHOD, // 405
        HTTP_GONE, // 410
        HTTP_REQ_TOO_LONG, // 414
        HTTP_NOT_IMPLEMENTED, // 501
        StatusLine.HTTP_PERM_REDIRECT -> { // 308
          // 以上状态码可以缓存,除非被最后的请求头/响应头中的 cache-control:nostore 禁止了
        }

        HTTP_MOVED_TEMP, // 302
        StatusLine.HTTP_TEMP_REDIRECT -> { // 307
          // 对于 302 和 307,只有响应头正确时才可被缓存。由于 OkHttp 是私有缓存因此没有检查 s-maxage:
          // http://tools.ietf.org/html/rfc7234#section-3
          if (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 = 0
      if (requestCaching.minFreshSeconds != -1) {
        minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds.toLong())
      }

      // 2.3 maxStaleMillis 允许使用已过期的缓存,但不能超过指定的时间
      var maxStaleMillis: Long = 0
      if (!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: String
      val 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.networkRequest
    val cacheResponse = strategy.cacheResponse

    // 2.清理无效缓存
    if (cacheCandidate != null && cacheResponse == null) {
      // 关闭不可用的缓存响应 Body
      cacheCandidate.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? = null
    try {
      // 6.中置工作,交给下一个责任链处理
      networkResponse = chain.proceed(networkRequest)
    } finally { 
      // 网络请求异常时(如 IO 异常或其他异常),清理旧缓存 Body
      if (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 响应的 Body
        networkResponse.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 {
        // 关闭失效的缓存 Body
        cacheResponse.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 RealInterceptorChain
    val 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 = result
    this.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.http2Connection

    return 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() ?: true
      if (routesLeft) continue

      // 存在其他路由选择器(如备用代理组),继续重试
      val routesSelectionLeft = routeSelector?.hasNext() ?: true
      if (routesSelectionLeft) continue

      throw 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 Socket
    val 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.http2Connection
    if (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) {
      // 应该关闭的 Socket
      var toClose: Socket? = null
      synchronized(callConnection) {
        // 1.2.1 若连接被标记为不可用(noNewExchanges)或主机、端口不匹配,则关闭该连接
        if (callConnection.noNewExchanges || !sameHostAndPort(callConnection.route().address.url)) {
          // 关闭连接,会将 call.connection 置为 null
          toClose = call.releaseConnectionNoEvents()
        }
      }

      // 1.2.2 call.connection 还存在的话就复用它,直接返回
      if (call.connection != null) {
        check(toClose == null)
        return callConnection
      }

      // 静默关闭 Socket
      toClose?.closeQuietly()
      eventListener.connectionReleased(call, callConnection)
    }

    // 2.连接池获取阶段
    // 2.1 由于需要一个新的连接,因此重置相关数据
    refusedStreamCount = 0
    connectionShutdownCount = 0
    otherFailureCount = 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 = null
      route = nextRouteToTry!!
      nextRouteToTry = null
    } else if (routeSelection != null && routeSelection!!.hasNext()) { // 3.1.2 现有路由
      // Use a route from an existing route selection.
      routes = null
      route = routeSelection!!.next()
    } else {
      // 3.1.3 新建路由选择器(是一个阻塞操作)
      var localRouteSelector = routeSelector
      if (localRouteSelector == null) {
        localRouteSelector = RouteSelector(address, call.client.routeDatabase, call, eventListener)
        this.routeSelector = localRouteSelector
      }
      val localRouteSelection = localRouteSelector.next()
      routeSelection = localRouteSelection
      routes = localRouteSelection.routes

      if (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.calls
    val index = calls.indexOfFirst { it.get() == this@RealCall }
    check(index != -1)

    // 从请求集合中移除当前 RealCall 并将连接 connection 置为 null
    calls.removeAt(index)
    this.connection = null

    // 如果这个连接没有承载任何请求,那么它就成为了一个闲置连接,如果它的 noNewExchanges
    // 被置位或者连接池允许的最大限制连接数量为 0,就需要关闭这个连接,此时返回该连接的 Socket
    if (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: Boolean
    get() = http2Connection != null

  private 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 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.
  }

在连接可以承载的 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 = connection
    connection.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(
  // 域名和端口号来自于 URL
  uriHost: 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? = null
    val connectionSpecs = route.address.connectionSpecs
    val 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 RealInterceptorChain
    val exchange = realChain.exchange!!
    val request = realChain.request
    val requestBody = request.body
    val sentRequestMillis = System.currentTimeMillis()

    var invokeStartEvent = true
    var responseBuilder: Response.Builder? = null
    var sendRequestException: IOException? = null
    try {
      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.code

      if (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
    }
  }