Skip to content

HTTP/2 二进制分帧与多路复用

基于 HTTP 现代标准 · 核于 2026-06

速查

  • HTTP/2 设计目标:只改"传输方式"不改"语义"——方法、状态码、URI、头部字段全部沿用 HTTP/1.1,应用代码几乎零改动即可享受性能红利。
  • 核心是二进制分帧层:把文本协议改为二进制编码,夹在 socket 与 HTTP API 之间,对上层透明。
  • 三层模型:帧(frame,最小通信单位)→ 消息(message,一个请求/响应对应的帧序列)→ 流(stream,连接内一条双向字节流,可承载多条消息)。
  • 帧头固定 9 字节:Length(24bit) + Type(8bit) + Flags(8bit) + Stream Identifier(31bit);常见类型 HEADERS、DATA、SETTINGS、WINDOW_UPDATE、RST_STREAM、GOAWAY 等。
  • 多路复用:单条 TCP 连接上把多条流的帧交错(interleave)收发再重组,彻底消除 HTTP/1.1 的应用层队头阻塞,不再需要开 6 条连接。
  • 流标识符:客户端发起用奇数、服务端用偶数,0 号保留给连接级控制帧;并发上限由 SETTINGS_MAX_CONCURRENT_STREAMS 约定。
  • 流优先级:原始方案(流依赖 + 权重 1~256)在 RFC 9113 中已废弃,改用 RFC 9218 扩展优先级方案;且优先级始终是"偏好"非"强制",实践中各端支持参差。
  • 流量控制:基于窗口的信用机制,仅 DATA 帧受限,初始窗口 65,535 字节,靠 WINDOW_UPDATE 增窗,分流级与连接级两层、双向、不可关闭。
  • 单连接优势:一个 origin 一条长连接,减少 TLS 握手、提升会话复用与压缩/优先级收益。
  • ⚠️ 仍受 TCP 层队头阻塞:HTTP/2 消除了应用层 HOL,但一个 TCP 包丢失会阻塞该连接上所有流——这是 HTTP/3 改用 QUIC 要解决的问题。

设计目标:换传输,不换语义

HTTP/2 的出发点是纯粹的性能,而非重新设计 HTTP。RFC 与官方文档反复强调:

HTTP/2 不以任何方式修改 HTTP 的应用语义。所有核心概念——HTTP 方法、状态码、URI、头部字段——都原样保留。

也就是说,GET 还是 GET404 还是 404Content-Type 还是 Content-Type。改变的只是这些信息在网络上如何被编码与传输。它瞄准三件事:请求/响应全面多路复用、头部字段高效压缩(HPACK,见下一叶)、以及请求优先级与服务器推送。

对前端意味着什么

你写的 fetch() / XMLHttpRequest / <img src> 代码一行不用改。浏览器与服务器协商出 HTTP/2(通常经 TLS 的 ALPN)后自动启用。许多 HTTP/1.1 时代的"优化"反而变成反模式——这点在第 6 页《版本对比与前端性能实践》详述。

二进制分帧层

HTTP/1.1 是文本协议:请求行、头部、空行、报文体逐行解析。HTTP/2 引入二进制分帧层(binary framing layer),位于 socket 接口与 HTTP API 之间,把所有通信切成二进制帧。

文本好读但难高效解析、易歧义;二进制定长帧头则便于高速、无歧义地切分和重组。这一层对上层应用完全透明——分帧与重组由客户端、服务端协议栈代劳。

三层模型:帧、消息、流

这是理解 HTTP/2 的钥匙。三个概念层层包含:

概念定义类比
帧 frame最小通信单位,含帧头(至少标明所属流的 ID)一节车厢
消息 message映射到一个逻辑请求/响应的完整帧序列一列完整的列车
流 stream连接内一条双向字节流,可承载一条或多条消息一条铁轨

一条 TCP 连接里可以有任意多条流(轨道),每条流上跑着消息(列车),消息又由帧(车厢)组成。关键在于:不同流的帧可以在同一条连接上交错传输,到对端再按 Stream ID 重新归位。

text
单条 TCP 连接
├─ stream 1 ── HEADERS ── DATA ── DATA          (一个请求/响应)
├─ stream 3 ── HEADERS ── DATA                  (另一个,互不阻塞)
└─ stream 5 ── HEADERS ── DATA ── DATA ── DATA

连接上的实际字节流(帧被交错):
… [s1 HEADERS] [s3 HEADERS] [s1 DATA] [s5 HEADERS] [s3 DATA] [s1 DATA] …

帧头与帧类型

每个帧以固定 9 字节帧头开始,后跟变长负载:

text
+-----------------------------------------------+
|                 Length (24)                   |   负载字节数
+---------------+---------------+---------------+
|   Type (8)    |   Flags (8)   |               |   类型 / 标志位
+-+-------------+---------------+---------------+
|R|                Stream Identifier (31)        |   所属流(R 为保留位)
+=+=============================================+
|                Frame Payload (0...)           |   负载
+-----------------------------------------------+

HTTP/2 共定义 10 种帧,前端最常打交道的是前两类:

帧类型作用受流量控制
HEADERS开启流、携带头部字段块(含 :method/:status 等)
DATA携带报文体内容
SETTINGS协商连接参数(如最大并发流)
WINDOW_UPDATE增大流量控制窗口
RST_STREAM立即终止单条流
GOAWAY优雅关闭整条连接
PUSH_PROMISE服务器推送预告(见下一叶)
PRIORITY优先级信令(已废弃,见下文)

伪头部取代请求行

HTTP/2 用以 : 开头的**伪头部(pseudo-headers)**取代文本请求行:请求侧有 :method:scheme:authority:path,响应侧有 :status。它们和普通头部一起被 HPACK 压缩进 HEADERS 帧。

多路复用:消灭应用层队头阻塞

这是 HTTP/2 最重要的能力。多路复用让客户端与服务端能"把一条 HTTP 消息拆成独立的帧、交错发送、再到对端重组"。

回顾上一页(HTTP/1.1 瓶颈与队头阻塞):HTTP/1.1 一条连接同一时刻只能处理一个请求,前一个响应没回来后面就得排队(应用层队头阻塞)。浏览器被迫对每个域名开 6 条连接硬扛并发,代价高昂。

HTTP/2 用一条连接 + 多条流根治了这点:

  • 流 9 不必等流 7 完成,二者的帧交错在连接上同时收发。
  • 一个慢响应不再卡住后面的快响应——应用层队头阻塞被彻底消除
  • 不再需要域名分片(domain sharding)、雪碧图等绕过并发限制的老技巧。
text
HTTP/1.1(6 连接,每条仍串行排队)   HTTP/2(1 连接,帧级并发)
conn1: [req1]→[resp1]                stream1 ┐
conn2: [req2]→[resp2]                stream3 ├─ 帧交错于单条连接
conn3: [req3]→[resp3]                stream5 ┘   同时在途、互不阻塞
… 最多 6 条同时在途                   并发流数可达数百

流的标识、并发与优先级

  • 标识符:客户端发起的流用奇数 ID,服务端发起的用偶数 ID,0 号保留给连接级控制帧。
  • 并发上限:由对端通过 SETTINGS_MAX_CONCURRENT_STREAMS 声明;超限会触发 REFUSED_STREAMPROTOCOL_ERROR
  • 优先级:原始方案让流声明对另一条流的依赖并带 1~256 的权重(如权重 12 与 4 的兄弟流按 3:1 分配资源)。

优先级在实践中支持参差

RFC 9113(HTTP/2 当前标准)已废弃上述基于"流依赖 + 权重"的优先级信令,转而推荐 RFC 9218 的扩展优先级方案urgency + incremental)。即便在旧方案下,规范也明确:流依赖与权重只是"传输偏好,非强制要求",不保证任何特定的处理或传输顺序。现实中各浏览器、各服务器/CDN 的优先级实现差异很大,不能依赖它做正确性保证,只能视作性能提示。

流量控制:基于窗口的信用机制

多路复用让多条流共享一条连接,必须防止某条流(如一个大文件下载)独占带宽、饿死其他流。HTTP/2 用基于窗口的信用机制

  • 接收方"广而告之"自己还能接收多少字节(窗口大小),发送方据此发送 DATA
  • 每发一个 DATA 帧消耗窗口,接收方处理后用 WINDOW_UPDATE 帧补充窗口。
  • 只有 DATA 帧受流量控制,其他帧类型不占窗口。
  • 初始窗口默认 65,535 字节;分流级连接级两层,双向生效,且不可关闭(最小窗口为 0 即暂停该流)。

流量控制 vs TCP 的拥塞控制

HTTP/2 流量控制是应用层、逐跳(hop-by-hop)、非端到端的,解决的是"连接内多条流之间"的带宽分配;它不替代 TCP 自身的拥塞控制,二者分工不同。

单连接 vs HTTP/1.1 多连接

HTTP/2 对每个 origin 只用一条持久连接,相比 HTTP/1.1 的 6 连接有明显收益:

  • 更少握手:TCP 三次握手与 TLS 握手只做一次,省下大量首字节延迟。
  • 更好的会话与压缩复用:HPACK 头部压缩状态、TLS 会话在同一连接内持续受益。
  • 更省资源:服务端维护的连接数、内存占用大幅下降。
  • 更合理的拥塞控制:单连接的拥塞窗口探测优于多连接互相争抢。

官方测试结论:在丢包场景下队头阻塞的负面影响,被压缩和优先级带来的收益所抵消——整体仍划算。

⚠️ 尚未解决:TCP 层队头阻塞

多路复用消灭的是应用层队头阻塞,但 HTTP/2 仍跑在 TCP 之上,于是埋下一个新瓶颈:

我们消除了 HTTP 的队头阻塞,但 TCP 层的队头阻塞依然存在。

TCP 向上层提供的是有序、可靠的字节流:一旦某个 TCP 段丢失,后续即便已到达的段也必须在内核缓冲区里等待重传补齐,才能交付给应用。而 HTTP/2 把所有流复用在这同一条 TCP 连接上——结果就是:

一个丢包,全员卡住

单个 TCP 包丢失会阻塞该连接上所有 HTTP/2 流,哪怕这些流的数据本身已经完整到达。连接数越少,单次丢包的"连坐"范围反而越大。在高丢包/弱网环境下,HTTP/2 的表现甚至可能不如多连接的 HTTP/1.1。

这正是 HTTP/3 放弃 TCP、改用基于 UDP 的 QUIC 的根本动机——把多路复用下沉到传输层,让各条流真正相互独立。详见第 5 页HTTP/3 与 QUIC

小结

HTTP/2 在不动 HTTP 语义的前提下,用二进制分帧层把通信重构为"帧 → 消息 → 流"三层模型,并在单条 TCP 连接上实现多路复用,彻底消除了 HTTP/1.1 的应用层队头阻塞,让域名分片、雪碧图等老优化成为历史。配套的流标识、流量控制(基于窗口、仅约束 DATA 帧)与优先级(原始的流依赖+权重方案已被 RFC 9113 废弃、转向 RFC 9218,且实践支持参差)保障了多流共享连接的秩序。但它仍受TCP 层队头阻塞之困——一个丢包拖累全连接,这是 HTTP/3 用 QUIC 要破解的难题。


上一页:HTTP/1.1 瓶颈与队头阻塞 · 下一页:HPACK 头部压缩与服务器推送