二进制分帧层

9 字节帧头的每个比特、十种帧类型与标志位的精确语义。

作者 David更新于 第 2 篇(共 16 篇)

帧头格式

所有 h2 帧共用 9 字节定长头:

+-----------------------------------------------+
|                 Length (24)                   |
+---------------+---------------+---------------+
|   Type (8)    |   Flags (8)   |
+-+-------------+---------------+-------------------------------+
|R|                 Stream Identifier (31)                      |
+=+=============================================================+
|                   Frame Payload (0...)                      ...
+---------------------------------------------------------------+

逐字段:

字段位宽说明
Length24载荷长度(不含这 9 字节头)。默认上限 16384(2^14),由 SETTINGS_MAX_FRAME_SIZE 调整,最大 16777215
Type8帧类型。未知类型 MUST 被忽略而非报错——这是协议的扩展点
Flags8类型专属标志位,未定义位 MUST 置 0 且被忽略
R1保留位,MUST 置 0,接收时忽略
Stream Identifier31流 ID。0 表示连接级,非零表示某个流

关键改进:长度前置且定长。1.1 必须扫描字节找 CRLF、再按 Content-Length 或分块解析——正是这种「靠扫描定界」造成了请求走私。h2 直接读 24 位长度,边界无歧义,走私类攻击在协议层被消除。

十种帧类型

Type名称流 ID作用
0x00DATA非零消息体
0x01HEADERS非零字段块,开启流
0x02PRIORITY非零优先级(已废弃)
0x03RST_STREAM非零立即终止单个流
0x04SETTINGS0连接参数协商
0x05PUSH_PROMISE非零服务端推送预告(已废弃)
0x06PING0保活与 RTT 测量
0x07GOAWAY0优雅关闭连接
0x08WINDOW_UPDATE0 或非零流量控制窗口增量
0x09CONTINUATION非零字段块续帧

流 ID 用错(如 SETTINGS 带非零流 ID)是 PROTOCOL_ERROR,必须关闭整个连接。

DATA

承载消息体,等价于 1.1 的 message-body。

标志位:

标志值语义
END_STREAM0x01本帧是该流方向上的最后一帧
PADDED0x08载荷带填充
  • 受流量控制约束——是唯一受控的帧类型。
  • 填充用于混淆真实长度以抵御流量分析(BREACH 类);Pad Length 字段占 1 字节,填充内容 MUST 全零且接收方 MUST NOT 解释。
  • 填充长度大于剩余载荷 → PROTOCOL_ERROR。

HEADERS

承载 HPACK 编码后的字段块,同时开启一个流。

标志位:

标志值语义
END_STREAM0x01该流方向到此结束(如无体的 GET)
END_HEADERS0x04字段块到此完整,无后续 CONTINUATION
PADDED0x08带填充
PRIORITY0x20载荷含优先级信息(已废弃)

注意 END_STREAM 与 END_HEADERS 是两个独立标志:一个无请求体的 GET 会同时置位;一个带体的 POST 只置 END_HEADERS,体由后续 DATA 帧发送并在最后一帧置 END_STREAM。

CONTINUATION

字段块超过 MAX_FRAME_SIZE 时的续帧。

最严格的排序约束:HEADERS/PUSH_PROMISE 与其后续 CONTINUATION 之间 MUST NOT 插入任何其他帧(哪怕是其他流的帧)。违反即 PROTOCOL_ERROR。

原因:HPACK 动态表是有状态且顺序相关的,字段块必须作为不可分割的整体解码。这也意味着巨大的头部会短暂阻塞整条连接——即所谓 CONTINUATION Flood 攻击面。

RST_STREAM

载荷是 4 字节错误码,立即终止单个流,不影响连接。

这是 1.1 完全没有的能力:1.1 想取消一个进行中的请求,只能关掉整条 TCP 连接。

代价:RST_STREAM 极廉价(客户端发一帧即可让服务端丢弃工作),2023 年的 HTTP/2 Rapid Reset(CVE-2023-44487)正是利用「快速开流再立即重置」绕过 MAX_CONCURRENT_STREAMS 限制发起 DDoS。缓解手段是限制单位时间内的重置速率。

PING

8 字节不透明载荷,流 ID 必须为 0。

  • 收到未置 ACK 的 PING → MUST 原样回送并置 ACK(0x01)。
  • 载荷长度不等于 8 → FRAME_SIZE_ERROR。
  • 用于测量 RTT 与探测连接存活,优先级应高于其他帧。

帧与 1.1 的映射

一次完整的 h2 请求/响应:

客户端 → HEADERS[END_HEADERS]              请求头
客户端 → DATA                              请求体
客户端 → DATA[END_STREAM]                  请求体末尾
服务端 → HEADERS[END_HEADERS]              响应头(含 :status)
服务端 → DATA                              响应体
服务端 → DATA[END_STREAM]                  响应体末尾

对照 1.1:HEADERS ≈ 起始行 + 字段行,DATA ≈ message-body,END_STREAM ≈ Content-Length 耗尽或 last-chunk。

trailer 字段在 h2 中表示为:END_STREAM 之前再发一个 HEADERS 帧(且该帧置 END_STREAM)——比 1.1 的分块 trailer 区干净得多。

下一篇:流与多路复用。

本页目录