计算机网络 26:HTTP/2 的多路复用解决了哪一层阻塞,帧、流、HPACK 与窗口
一个慢响应还没结束,同一连接上的小响应能不能先完成?如果能,为什么丢失一个 TCP 段仍可能让多个响应一起等待?这两个问题分别涉及 HTTP 消息的组织方式和底层字节流的交付条件。
前置是第 18 篇接收窗口、第 22 篇消息边界与第 24 篇 TLS。本篇把流标识、字段压缩、接收窗口和 TCP 顺序交付分开,避免把“多路复用”解释成每个请求拥有独立网络通道。
消息拆成帧后,响应可以交错
HTTP/1.1 流水线允许前一个请求尚未收到响应时继续发送请求,但响应仍按请求顺序返回。一个响应占用字节流时,后一个响应不能随意把正文插进去,否则接收者无法按原有消息规则区分归属。
HTTP/2 在帧头中携带流标识。接收端先识别帧,再把内容交给相应的流。一个连接可以同时存在多个未关闭的流,因此服务器能够在一个大响应的两个 DATA 帧之间发送另一个流的响应。请求和响应仍保留 HTTP 语义,改变的是表达方式。
RFC 9113 §4.1规定固定 9 字节帧头,包括长度、类型、标志和流标识。长度描述帧载荷,不包含这 9 字节。流标识 0 用于连接范围的控制,不能当成普通请求编号;客户端发起的流使用递增奇数标识。
HEADERS 承载字段块,DATA 承载正文。END_HEADERS 表示字段块结束,END_STREAM 表示该发送方向的流结束,两者不是同一个完成条件。TCP 的一次读取也仍然不等于一个 HTTP/2 帧,解析器必须处理分次读取和连续多帧。
流控决定是否有额度,调度决定先发送谁
HTTP/2 同时维护连接级与流级的发送额度。一个流还有额度,不代表连接还有额度;连接有额度,也不能替耗尽窗口的流授权。RFC 9113 §5.2中的窗口限制作用于 DATA 帧载荷(存在填充时也计入填充),不能把 HEADERS 等控制帧一并按正文额度阻断。
一个手算例子可以分开这两种约束。假设连接剩余额度 12 字节,流 1 剩余 8 字节,流 3 剩余 10 字节。流 1 先发送 8 字节 DATA 后,流 1 额度为 0,连接额度为 4;此时流 3 最多发送 4 字节。只给流 3 增加额度不能解除连接额度耗尽。
这个算式仅讨论流控许可。真实发送还受帧大小、应用是否提供数据、实现调度、TCP 接收窗口与拥塞窗口等条件约束。HTTP/2 的 WINDOW_UPDATE 不等于 TCP ACK,也不直接增加 TCP 的 cwnd。
调度器在符合发送条件的流之间分配机会。协议支持并发,不承诺每条流平均分配带宽,更不承诺小流总先完成。RFC 9113 §5.3已废弃 RFC 7540 的旧优先级信号机制;不能把旧依赖树和权重描述为所有当前实现一致执行的规则。
HPACK 的状态属于连接的方向
重复字段可以用表项索引表达,字段值也可以采用 Huffman 编码。RFC 7541 §2.3区分固定的静态表与会随处理更新的动态表。动态表起初为空,更新和淘汰受到容量限制;压缩不是加密。
每个端点维护编码上下文与解码上下文,用于连接上的字段块。因此不能把某个流中间的一段压缩字节单独取出,就假定无需先前上下文也能解码。请求方向与响应方向也不能混用同一张动态表。
RFC 9113 §4.3还要求一个字段块对应的 HEADERS 与后续 CONTINUATION 连续发送,期间不能插入其他流或其他类型的帧。这是“可以交错”必须带上的条件。字段压缩、流控与 TCP 顺序交付各有约束,不能统称为一种队头阻塞。
取附录 C.3 的一个字段作为手算::authority: www.example.com 用未启用 Huffman 的增量索引字面量表达,字节为 41 0f 加 15 字节域名,共 17 字节。静态表有 61 项,新插入的动态项使用索引 62,随后引用该项可写成 be。它成立的前提是同方向先前已插入且尚未淘汰该项。
RFC 7541 §4.1定义的表项容量计算在这里为名称 10 字节、值 15 字节、固定开销 32 字节,共 57 字节。这个数是规范的容量记账,不是线上字节数或进程真实堆内存。附件只对这个有限例子做断言,没有实现通用 HPACK 解码器。
TCP 缺口仍在帧解析之前
假设接收端期望下一个字节序号为 1000,序号 1000–1099 的数据丢失,而 1100–1199 已到达。即使后面的字节属于另一个 HTTP/2 流,TCP 也不会为 HTTP/2 跳过缺口交付它。HTTP/2 解析器拿不到后续连续字节,就无法凭流标识绕过等待。
这不意味着丢失发生后“所有流的所有工作都停止”。已经交付给应用的数据可以继续处理,另一个传输方向也不必具有同一个缺口。受影响的是依赖该方向缺口后字节的交付;共享拥塞控制还可能改变后续发送速度,应与顺序交付等待分开判断。
1 | |
上图是机制示意。实验若只看到两个请求都变慢,尚不足以证明存在该缺口:服务器计算、流控或应用串行也可能制造等待。需要同时记录请求并发、实际丢弃、TCP 序号与应用交付时间。
实验与证据
本机实验采用已安装的 Node v22.22.2 与其 nghttp2 1.64.0,实现来自成熟 HTTP/2 库。2026-09-22 运行记录的系统为 Darwin 27.0.0 arm64。服务仅监听 127.0.0.1 随机端口,使用明确配置的明文 prior knowledge,不执行 HTTP/1.1 Upgrade,也不验证 TLS 或 ALPN。
客户端只创建一个 session,再同时发起 /slow 与 /fast。服务端直到两个请求流都到达才开始响应:先给慢流发送 16 字节但不结束,再发送快流的完整正文。只有客户端收到完整快响应后,实验控制逻辑才释放慢流的剩余正文。这是受控的应用调度,用来证明后发请求不必等待先发响应结束,不能据此声称 Node 总会优先调度小响应。
| 实际观察 | 本轮结果 | 可以支持的判断 |
|---|---|---|
| 服务端 session 数 | 1 | 两个请求共用本轮连接 |
| 两个请求的流 ID | 1、3 | 存在两个不同请求流 |
| 服务端看到的客户端端口 | 两者均为 51049 | 与同 session 记录相符,复跑端口可变 |
| 快流完整接收 | 7 字节,内容 FAST-26 | 快流在慢流结束前完成 |
| 慢流完整接收 | 65536 字节,全部为字节 83 | 释放后正文完整到达 |
日志中的 both_requests_open 先于响应入队,快流 client_complete 又先于 release_slow_after_fast_received,最终慢流才完成。两个正文均逐字节断言,另保存 SHA-256。验收依赖这些事件与内容,不用“快了若干毫秒”作为并发证据。
客户端配置初始流窗口 1024,并等到 localSettings 事件记录确认;服务端快照中的连接发送窗口仍为 65535。两者针对不同层次,不能互相替代。实验最终传完大于初始流窗口的正文,但没有逐帧保存 WINDOW_UPDATE 或连续窗口轨迹,不能声称已独立观察窗口耗尽时长或具体调度策略。流控约束由前面的手算单独验证。
服务端还记录了动态表大小快照,但没有保存并解码线上字段块,不能拿这一快照替代完整 HPACK 抓包验证。手算里的 57 字节来自固定示例,与本轮实现选择的全部编码细节不能混同。
复现入口与未执行部分
附件包含Node 回环实验、本轮原始事件、窗口与 HPACK 手算断言、手算输出及环境探测和证据边界。下载到同一目录后执行:
1 | |
Node 程序有 8 秒总期限,结束时清理自建服务和 session。Python 脚本只依赖标准库,不联网,也不产生真实协议流量。两种输出分别标为本机实测与离线计算,不拼成同一种证据。
计划要求的隔离 TCP 丢包实验本轮未执行。已有 Podman 专用 VM 的 PATH 没有 Node、nghttp/nghttpd,Python 也没有 h2;已有四个镜像在禁联网、禁拉取、只读、替换默认入口的探测中,也未在 PATH 找到这些实现。没有因此安装依赖或修改宿主路由,VM 在探测后停止。此结果只说明已检查的入口不可用,不断言所有镜像文件中绝不存在相关二进制。
补证需要在具备成熟 HTTP/2 实现的专用 Linux 环境中,用同一 TCP 连接建立两条流,在双端捕获下仅丢弃指定方向的数据段,并对齐缺口、重传、帧与应用交付。当前已验证的并发行为不能代替这项验收,TCP 丢包部分仅有规范依据与机制推导。
练习
在前面的窗口例子中,连接额度和流 1 额度都耗尽后,接收端只给流 1 发送增量 8 的 WINDOW_UPDATE,流 1 能否立刻发送 8 字节?不能,连接额度仍为 0。若再给连接增加 5,忽略其他限制时最多发送 5 字节。
构造一个反例说明“发生丢包后所有流都停止”过强:流 1 的完整响应已在缺口出现前交付,应用仍能解析和保存它;流 3 的后续响应字节可能等待缺口补齐。这个反例不证明流 3 的等待时长,也不排除拥塞反应。
一个 HEADERS 没有 END_HEADERS,随后立刻出现另一个流的 DATA,这是否是合法多路复用?不是。当前字段块的 CONTINUATION 序列尚未结束,不能插入该帧。流间交错不能破坏字段块连续性。
一手资料与验证边界
本篇当次核对 RFC 9113 的帧格式、流并发、字段块连续性与流控章节;RFC 7541的动态表、容量记账和附录 C.3;实现接口参照Node v22.22.2 官方 HTTP/2 文档源文件。旧依赖树优先级不作为本实验的调度契约。
证据覆盖本机明文 HTTP/2 单连接双流交付、受控完成顺序和有限手算。尚未覆盖真实 TCP 丢包、TLS/ALPN、浏览器互通、字段块抓包解码、持续流控阻塞或公网性能;程序正常退出与页面构建成功均不能补足这些缺口。






