Android OkHttp HTTP/2 多路复用的具体实现

3 阅读4分钟

简单来说,HTTP/2 的多路复用允许在同一个 TCP 连接上并发处理多个请求/响应,这些请求/响应之间互不干扰。OkHttp 通过一套精心设计的类来完成这个复杂的任务。

下面这张图展示了核心类之间的协作关系,我们先有个整体印象,再逐一拆解:

flowchart TD
    subgraph ConnectionPool[连接池]
        RealConnectionPool
    end

    subgraph RealConnection[物理连接]
        direction TB
        RC[RealConnection] --> H2C[Http2Connection]
        H2C --> Streams[Http2Stream 1\nHttp2Stream 2\n...]
    end

    subgraph StreamAllocation[流分配器]
        SA[StreamAllocation]
    end

    subgraph Exchange[数据交换]
        E1[Exchange] --> Stream1[Http2Stream]
        E2[Exchange] --> Stream2[Http2Stream]
    end

    Request1[请求 A] -->|1. 获取 StreamAllocation| SA
    Request2[请求 B] -->|1. 获取 StreamAllocation| SA
    
    SA -->|2. 从连接池获取/创建| RC
    SA -->|3. 在 RealConnection 上<br>创建新的流| H2C
    H2C -->|4. 返回 Http2Stream| SA
    SA -->|5. 创建 Exchange<br>绑定到 Stream| E1
    SA -->|5. 创建 Exchange<br>绑定到 Stream| E2
    
    style RealConnection fill:#e1f5fe,stroke:#01579b
    style H2C fill:#fff3e0,stroke:#e65100
    style Streams fill:#f3e5f5,stroke:#4a148c

🧱 核心类:各司其职的组件

要实现多路复用,OkHttp 设计了几个关键的类来协同工作:

  • RealConnection:代表一个真实的 Socket 物理连接。对于 HTTP/2,一个 RealConnection 可以对应多个并发的请求/响应流。
  • Http2Connection:这是 HTTP/2 连接的核心管理者。它负责帧的读写、流的创建与管理、以及连接级别的流量控制。每个支持 HTTP/2 的 RealConnection 内部都会持有一个 Http2Connection 实例。
  • Http2Stream:代表一个逻辑上的双向数据流,用于承载单个请求和响应。一个 Http2Connection 可以管理多个 Http2Stream,每个流由一个唯一的整数 ID 标识。
  • StreamAllocation:可以看作是“流分配器”,它负责为一次具体的请求(Call)从连接池中寻找或创建 RealConnection,然后在该连接上分配(或创建)一个 Http2Stream。它本质上是一个引用计数对象,用于跟踪一个 RealConnection 被多少个请求使用着。

🚀 工作流程:多路复用如何发生

当你在同一个客户端连续发起多个到同一主机的请求时,多路复用的魔法就发生了。

  1. 获取连接:第一个请求到来时,StreamAllocation 发现没有可用的 HTTP/2 连接,于是通过 RealConnectionPool 创建一个新的 RealConnection,并完成 TCP 和 TLS 握手(包括 ALPN 协商,确定使用 HTTP/2 协议)。连接建立后,其内部的 Http2Connection 也随之启动。
  2. 创建流:请求需要发送数据。StreamAllocation 会调用 Http2Connection.newStream() 方法,在该连接上创建一个新的 Http2Stream。这个流会获得一个新的、唯一的流 ID(例如,第一个请求的 ID 可能是 1)。
  3. 发送请求Http2Stream 将 HTTP 请求头和方法等数据,打包成一个或多个 HEADERS 帧,通过 Http2Connection 写入 Socket。写入过程由专门的 Writer 线程处理,或者通过锁机制同步。
  4. 第二个请求到达:几乎是同时,第二个请求(例如获取另一张图片)到达。它的 StreamAllocation 在连接池中找到了刚刚建立的那个 HTTP/2 连接。检查发现该连接是健康的,且还有能力处理新的并发流(未达到 maxConcurrentStreams 上限)。
  5. 创建另一个流:在同一个 RealConnection 上,Http2Connection 再次被调用来创建另一个新的 Http2Stream(ID 为 3 或其他奇数,因为客户端发起的流 ID 是奇数且递增的)。
  6. 帧交织与传输:第二个请求的 HEADERS 帧和第一个请求的 DATA 帧(响应体)在 Socket 层面被交织(interleaved)在一起发送。Http2Connection 负责在接收端根据帧头的流 ID,将数据正确地重新组装,分发给对应的 Http2Stream
  7. 并发处理:这两个请求/响应对在同一个 TCP 连接上并发进行,互不阻塞。对于应用层来说,就像有两个独立的通道在同时工作。

🔬 关键技术细节

  • 帧与流:HTTP/2 将请求/响应数据切割成更小的(Frame),并为每个帧打上所属的 ID。这正是多路复用的基础——多个流的帧可以混合发送,接收方再按 ID 分类重组。
  • 并发流限制:并不是可以无限并发。服务器会在 SETTINGS 帧中告知客户端它允许的最大并发流数(maxConcurrentStreams)。Http2Connection 会严格遵守这个限制。
  • 流量控制:HTTP/2 提供了流级别连接级别的流量控制,通过 WINDOW_UPDATE 帧来动态调整发送窗口大小,防止接收方被数据淹没。
  • 线程模型:为了高效处理,每个 Http2Connection 通常会有一个专用的 Reader 线程,负责从 Socket 读取帧并根据流 ID 派发。Writer 端则可能由多个业务线程并发写入,需要通过同步机制来保证帧的完整写入。
  • 连接合并(Coalescing):OkHttp 甚至可以将指向不同域名但解析到同一 IP 且证书合法的 HTTP/2 连接进行合并,进一步复用连接。

✅ 总结

OkHttp 的 HTTP/2 多路复用实现,是通过 RealConnection 代表物理通道,Http2Connection 作为通道的管理者,Http2Stream 代表通道内的逻辑子通道,最后由 StreamAllocation 将这些组件串联起来为每个请求服务。这套机制最大限度地利用了单条 TCP 连接,实现了低延迟、高吞吐的网络通信,也是 OkHttp 性能强悍的关键所在。