1. 连接池核心原理:连接复用机制、HTTP/1.1 Keep-Alive 与 HTTP/2 多路复用

在 OkHttp 中,连接池(ConnectionPool)是性能优化的核心组件。它的根本目标是减少 TCP 三次握手与四次挥手的开销,通过复用已建立的连接来降低请求延迟和服务器负载。理解连接复用的演进过程,是掌握连接池配置的起点。

1.1 连接复用的演进

从无复用到高效复用,HTTP 连接模型经历了三个关键阶段。下表对比了它们的核心差异:

阶段 模型 核心约束 延迟表现
HTTP/1.0 短连接 每次请求独立 TCP 连接,请求完成后立即关闭 高,频繁握手与挥手的延迟叠加
HTTP/1.1 Keep-Alive(持久连接) 同一 TCP 连接可串行处理多个请求,默认开启 中,仅首次请求需要握手,后续复用连接
HTTP/2 多路复用 同一 TCP 连接内并行交错多个请求(流) 低,消除队头阻塞,请求吞吐量大幅提升
关键理解:OkHttp 连接池的核心任务就是对 HTTP/1.1 Keep-Alive 连接和 HTTP/2 多路复用连接进行统一管理、复用与淘汰,避免为每个请求创建新连接。
HTTP 连接模型演进与复用对比 HTTP/1.0 短连接 请求1 → 创建 → 响应 → 关闭 请求2 → 创建 → 响应 → 关闭 延迟高:每次请求都需 TCP 握手 服务端并发压力大 HTTP/1.1 Keep-Alive 同一连接,串行复用 请求1 → 响应1 → 请求2 → 响应2 减少握手次数 存在队头阻塞(HOL) HTTP/2 多路复用 同一连接,并行交错 流1: 请求1 ── 响应1 流2: ── 请求2 ── 响应2 流3: 请求3 ──── 响应3 消除队头阻塞 高吞吐、低延迟 连接池管理对象 HTTP/1.1 连接(RealConnection) + HTTP/2 多路复用连接(RealConnection,可承载多个 Stream) 连接池内部通过双向链表维护,支持按 keep-alive 超时和最大空闲连接数进行清理

1.2 HTTP/1.1 Keep-Alive 机制详解

HTTP/1.1 默认使用持久连接(Connection: keep-alive)。通信流程遵循 请求→响应→请求→响应 的串行模式:

  1. 建立连接:客户端与服务器三次握手建立 TCP 连接。
  2. 发送请求:客户端发送第一个 HTTP 请求。
  3. 接收响应:服务器处理完成后返回响应,并默认保持连接打开。
  4. 复用连接:客户端直接通过同一连接发送第二个请求,无需重新握手。
  5. 连接关闭:闲置超过 keepAliveDuration 或达到最大请求数时,任一方主动关闭。
实践要点:OkHttp 默认的 keep-alive 超时时间为 5 分钟。如果服务端(如 Nginx)的 keep-alive_timeout 设置小于 OkHttp 的超时,连接可能被服务端关闭,导致 OkHttp 在复用失败时重建连接。建议客户端与服务端超时时间保持一致。

OkHttp 对 Keep-Alive 连接的管理:

  • 每个连接在池中以 RealConnection 对象表示。
  • OkHttp 不区分 HTTP/1.1 和 HTTP/2 的连接管理模型,统一放入 ConnectionPool。
  • 当发出请求时,OkHttp 会遍历连接池,寻找路由匹配且连接可用的实线连接。

1.3 HTTP/2 多路复用原理

HTTP/2 通过 流(Stream) 和 帧(Frame) 机制,在单 TCP 连接上实现并行数据传输:

  • 流:一个独立的、双向的请求-响应序列,每个流有唯一的 ID。
  • 帧:HTTP/2 通信的最小单元(如 HEADERS 帧、DATA 帧),多个帧属于不同流可以交错发送。
  • 多路复用:客户端可以在同一连接上同时发起多个流的请求,服务器可以混合返回各流的响应帧。

这种设计从根本上解决了 HTTP/1.1 的队头阻塞(Head-of-Line Blocking,HOL)问题。在 HTTP/1.1 中,前一个请求的响应必须完全返回后,才能发送下一个请求;而在 HTTP/2 中,多个请求可以在同一连接上并行进行,互不等待。

OkHttp 的特殊处理:OkHttp 内部对 HTTP/2 连接做了精细化支持。连接池中的 HTTP/2 连接可以承载 多个活跃 Stream。当池中已有 HTTP/2 连接且未达到最大并发流限制(通常由服务器 SETTINGS_MAX_CONCURRENT_STREAMS 指定)时,OkHttp 会直接复用该连接创建新的 Stream,无需新建 TCP 连接。

1.4 连接池的核心数据结构

OkHttp 的连接池由 ConnectionPool 类实现,核心元素如下:

属性 类型 说明
connections Deque<RealConnection> 存储所有空闲和正在使用的连接的双向队列
maxIdleConnections int 最大空闲连接数,默认 5
keepAliveDuration long 空闲连接保活时间,默认 5 分钟
timeUnit TimeUnit 时间单位,默认 TimeUnit.MINUTES
cleanupRunnable Runnable 后台清道夫线程,定期清理过期连接

连接复用判断逻辑(核心流程图):

  1. 请求经过路由选择后,进入 ConnectionPool.get() 方法。
  2. 遍历 connections 队列,按 路由规则 匹配:相同的 IP、端口、TLS 配置。
  3. 对于 HTTP/1.1 连接:检查连接是否空闲(无正在进行的请求),且未超过 keep-alive 超时。
  4. 对于 HTTP/2 连接:检查连接是否处于可用状态,且当前并发流数未达到上限。
  5. 匹配成功则直接返回该连接(或创建新的流);否则,创建新的 RealConnection。
常见误区:不要认为连接池会复用所有同源站的连接。OkHttp 的连接复用有一个重要前提——路由完全一致,包括代理设置、TLS 参数等。如果路由配置不同,即使目标主机相同,也无法复用连接池中的连接。

1.5 连接生命周期:创建、复用与回收

一个连接在 OkHttp 中的完整生命周期包含以下状态:

  • 创建(Created):通过 TCP 握手或 TLS 握手建立,加入连接池。
  • 活跃(Active):被某个请求占用,正在传输数据。对于 HTTP/2,活跃指至少有一个流处于打开状态。
  • 空闲(Idle):请求结束,连接未被立即关闭,放入池中等待复用。
  • 清理(Evicted):超过最大空闲连接数(maxIdleConnections)或超过保活时间(keepAliveDuration),连接被关闭并从池中移除。

OkHttp 的 清理机制 通过后台线程定期执行,核心算法如下:

// 清理逻辑的伪代码简化描述
while (true) {
    long now = System.nanoTime();
    Long earliestIdleTime = null; // 记录最早空闲连接的到期时间
    int idleCount = 0;

    for (RealConnection connection : connections) {
        if (connection.isIdle()) {
            idleCount++;
            long idleDurationNs = now - connection.idleAtNanos;
            if (idleDurationNs > keepAliveDurationNs) {
                // 标记为待关闭
                connection.closeQuietly();
                connections.remove(connection);
                continue;
            }
            // 记录最早空闲连接的时间,用于计算下次清理的等待时间
            if (earliestIdleTime == null || connection.idleAtNanos < earliestIdleTime) {
                earliestIdleTime = connection.idleAtNanos;
            }
        }
    }

    if (idleCount > maxIdleConnections) {
        // 关闭最长时间空闲的连接,直到空闲数 <= maxIdleConnections
        // 具体实现:按 idleAtNanos 排序,关闭最久未使用的连接
    }

    // 计算下次清理线程的等待时间(最小值)
    long waitTime = computeNextCleanupTime(earliestIdleTime, keepAliveDurationNs);
    if (waitTime > 0) {
        wait(TimeUnit.NANOSECONDS.toMillis(waitTime));
    }
}

这个设计确保连接池既不会无限膨胀(受 maxIdleConnections 限制),也不会长时间占用僵尸连接(受 keepAliveDuration 限制)。