计算机网络 28:HTTP/3 怎样运行在 QUIC 上,映射、QPACK、协商与回退
第 26 篇已经说明,HTTP/2 把请求拆成流,却仍受同一条 TCP 字节流的有序交付约束。第 27 篇进一步拆开了 QUIC 的包号、流偏移、流量控制和共享拥塞控制。HTTP/3 站在这套传输能力之上,但它并不等于“把 HTTP/2 帧原样塞进 QUIC”。请求怎样占用流、连接级状态放在哪里、字段压缩如何同步、客户端何时回退,都需要新的映射规则。
本篇回答一个有限问题:HTTP 语义怎样落到 QUIC 流上,以及一次等待究竟来自请求流缺口、QPACK 依赖、流量控制还是共享拥塞窗口。验证采用 RFC 核查和标准库手算。当前环境的 curl 8.5.0 只列出 HTTP/2,没有 HTTP/3 功能;没有运行成熟 HTTP/3 客户端与服务端,也不提交协议性能排名。
HTTP 语义没有因传输层改变
HTTP/3 仍使用 HTTP 的方法、状态码和字段语义。变化发生在消息的分帧与承载层:HTTP/2 在一条 TCP 字节流内维护自己的 stream id;HTTP/3 直接使用 QUIC stream 作为请求并发的基本单位。
客户端发起的每条双向 QUIC 流都可以承载一组请求和响应。请求一方发送 HEADERS、可选 DATA 和可选 trailers,响应在同一条双向流的反方向返回。流的结束由 QUIC stream 的结束状态表达,不再靠 HTTP/1.1 的连接关闭划分消息。RFC 9114 还禁止把任意帧放到任意流:DATA 只能出现在请求或推送流,SETTINGS 必须是控制流上的第一个帧。
连接级状态不能散落到请求流。每个端点都要建立一条单向 HTTP control stream,并首先发送 SETTINGS;关闭控制流是连接错误。QPACK 另外占用 encoder stream 和 decoder stream。因而,一个最小 HTTP/3 端点至少要给对端留下三条单向流的额度:HTTP 控制、QPACK encoder、QPACK decoder。请求流负责消息,控制流负责整个连接的规则,QPACK 流负责压缩状态同步。
这种映射带来一个可迁移的判断:先确定状态的作用域,再确定它应放在哪条流。请求或响应正文属于单个请求;SETTINGS、GOAWAY 影响连接;动态表更新跨多个字段块复用。把连接级状态复制到每条请求流会产生冲突,把请求正文放到控制流则会破坏帧的合法位置。
发现 HTTP/3 不等于已经用上 HTTP/3
对于 https URI,客户端可以直接尝试目标地址上的 QUIC,也可以先从已有 HTTP 连接收到 Alt-Svc,获知同一源站提供的 HTTP/3 端点。Alt-Svc 记录显式给出 UDP 端口,例如 Alt-Svc: h3=":443"。无论如何发现端点,TLS 握手里选中的 ALPN token 必须是 h3,才能把这条 QUIC 连接解释为 HTTP/3。
这条链上至少有四个不同判断:
| 阶段 | 成功条件 | 失败说明 |
|---|---|---|
| 发现 | 获得可尝试的 HTTP/3 地址与 UDP 端口 | 没有发现信息不等于服务器永远不支持 |
| QUIC 建连 | UDP 可达、版本与传输参数可接受 | UDP 被阻断、超时或版本不兼容都可能失败 |
| 身份验证 | 证书对 URI 的源站有效 | 换端口不改变主机名验证要求 |
| 应用协议选择 | TLS ALPN 选中 h3 |
建成某种 QUIC 连接不自动等于 HTTP/3 |
RFC 9114 在 QUIC 建连因 UDP 阻断等连接问题失败时,建议客户端尝试基于 TCP 的 HTTP 版本。这是客户端恢复可用性的行为,不是服务端把同一条连接“降级”为 TCP:QUIC 与 TCP 是不同连接,握手、拥塞状态和请求重试判断都要重新开始。
命令行工具还会把策略暴露给调用者。curl 官方文档区分允许 HTTP/3 尝试并保留其他版本路径的 --http3,以及只接受 HTTP/3 的 --http3-only。具体二进制是否能使用这些选项取决于构建时链接的 QUIC 与 HTTP/3 库,不能只看 curl 版本号。本机 curl --version 没有列出 HTTP3 feature,因此没有执行 HTTP/3 网络实验。
QPACK 避开全连接顺序,却没有消灭依赖
HTTP/2 的 HPACK 动态表更新与字段块沿同一条有序 TCP 字节流到达。HTTP/3 不能依赖不同 QUIC 流之间的到达顺序,于是 QPACK 把动态表更新放在专用 encoder stream,HEADERS 中的字段块只引用表状态。这样,一个请求流的传输丢失通常不会把其他不相关请求流的字段块一并卡在 TCP 缺口之后。
代价是显式依赖。每个编码字段块都携带 Required Insert Count。解码端已经处理的动态表插入数记作 Insert Count:
1 | |
假设请求流 0 的字段块要求插入计数 3,请求流 4 使用静态表,不依赖动态插入。解码端目前只处理到 2。流 0 即使所有 HEADERS 字节都已经到达,也必须等 encoder stream 上的第 3 次插入;流 4 可以立即解码。随后 Insert Count 变为 3,流 0 才解除阻塞。
QPACK 允许接收端用 SETTINGS_QPACK_BLOCKED_STREAMS 限制可能被阻塞的流数,默认值为 0。编码端可以少引用尚未确认的动态表项,以较差压缩率换取较低阻塞风险。它不能无视接收端声明的上限。动态表容量为 0 时仍可使用静态表和字面量,这也是不依赖动态表即可工作的反例。
QPACK 等待不能被归入 QUIC 的流内有序交付。前者是应用层字段压缩依赖尚未满足;后者是同一 stream 的字节 offset 有缺口。两者可能同时发生,也可能只发生一个。
四类“卡住”需要分别取证
同一个现象“请求还没有交给应用”至少有四类原因。
请求流缺口发生在 QUIC stream 内。例如 offset [0, 100) 和 [120, 160) 已到达,中间 20 字节缺失;该流只能按序交付到 100。另一条没有缺口的请求流不因此停止交付。
QPACK 阻塞发生在字段块已经到达、但 Required Insert Count 大于当前 Insert Count 时。补齐请求流字节不能消除这项依赖;需要对应的 encoder stream 指令到达并被处理。
流量控制由接收端缓冲能力决定。某条流达到 MAX_STREAM_DATA 是流级额度不足;所有流合计达到 MAX_DATA 是连接级额度不足。这两者都不是网络拥塞判断。
拥塞控制限制路径上的在途发送量。一个连接的多个流共享路径容量和拥塞控制状态,HTTP/3 没有为每条请求流建立独立拥塞控制。大响应占满拥塞窗口时,小请求即使没有流内缺口或 QPACK 依赖,也可能等待发送机会。
| 观察 | 首先检查 | 不能直接推出 |
|---|---|---|
| 单条流停在某个 offset | 该流缺口、RESET_STREAM、最终大小 | 其他流也被传输层阻塞 |
| HEADERS 字节齐全但不能解码 | Required Insert Count 与 encoder stream | QUIC 丢失恢复失败 |
| 发送端报告 DATA_BLOCKED | 连接级流控额度 | 网络已经拥塞 |
| 多条流同时缺少发送机会 | 拥塞窗口、在途字节、调度 | 每条流拥有独立 cwnd |
“QUIC 消除了队头阻塞”只能缩写为“QUIC 避免 TCP 字节流造成的跨流传输层队头阻塞”。流内有序交付仍会等待;QPACK 可以引入跨流压缩状态依赖;共享拥塞窗口与实现调度也会影响多条流何时获得发送机会。
同条件比较才有意义
计划要求固定内容和链路条件比较 HTTP/2 与 HTTP/3。有效比较至少要固定对象大小、并发数、连接是否复用、RTT、带宽、MTU、丢包注入方式、预热、重复次数和证书验证。还要保存实际协商协议:HTTP/2 的 ALPN 应为 h2,HTTP/3 应为 h3。只看到 HTTP 200,无法证明走的是哪种传输。
若在同一位置丢失一个承载流 A 数据的传输单元,HTTP/2 的 TCP 接收端要等缺口恢复,才能继续向 HTTP/2 层交付字节;暂存字节中可能已经包含流 B 的帧。HTTP/3 中,流 B 的连续 QUIC stream 数据原则上可以继续交付;但它仍可能等待共享拥塞预算或 QPACK 表项。这个对照是协议机制推导,不是本机测得的延迟差。
比较结果也不能写成无条件排名。QUIC 握手状态、UDP 路径质量、实现成熟度、CPU 加密成本、对象组合和丢包位置都会改变结果。回退成功只证明客户端找到了另一条可用协议路径,不证明 HTTP/3 更快或更慢。
可复现手算
同名素材目录中的 handcalc.py 使用 Python 标准库检查请求流编号、QPACK 阻塞和协议选择:
1 | |
脚本断言客户端发起的双向流编号为 0、4、8;Required Insert Count 为 3 而 Insert Count 为 2 时只有依赖动态表的流被阻塞;QUIC 失败且允许回退时新建 TCP 路径,http3-only 策略则返回失败。它验证的是有限状态推导,不产生 QUIC 报文,也不测量 HTTP/2 或 HTTP/3 性能。
练习
练习一:三个请求流的 Required Insert Count 分别为 0、5、3,解码端 Insert Count 为 3。列出可立即解码和被阻塞的流。若接收端把允许阻塞流数设为 1,编码端还能否同时发送两个引用未确认表项的字段块?说明该限制约束的是 QPACK 风险还是 QUIC 拥塞窗口。
练习二:一次并发请求中,流 0 缺少 offset [100, 120),流 4 字节连续但 Required Insert Count 为 8,当前 Insert Count 为 7,连接又耗尽 MAX_DATA。分别写出流 0、流 4 和发送端新增数据的等待原因,并说明补到第 8 个动态表项后哪些等待仍不会消失。
模式速查表
| 现象关键词 | 定位模式 | 首要证据 |
|---|---|---|
| 协商后不是 HTTP/3 | 逐阶段确认协议选择 | 地址、UDP 可达性、证书、ALPN |
| 一条请求流字节不连续 | 流内缺口 | stream id、offset 范围 |
| HEADERS 完整却不能解码 | 显式依赖计数 | Required Insert Count、Insert Count |
| 多流一起停发 | 共享预算 | MAX_DATA、cwnd、bytes in flight |
| HTTP/3 失败后请求成功 | 新连接回退 | 两次连接与最终 ALPN,不能只看状态码 |
官方参考资料
- RFC 9114:HTTP/3,IETF,2022-06;连接发现、ALPN、请求流、控制流、帧映射与失败回退。
- RFC 9204:QPACK,IETF,2022-06;动态表同步、Required Insert Count、blocked streams 与反馈流。
- RFC 9000:QUIC Transport,IETF,2021-05;流、流量控制和跨流传输等待边界。
- RFC 9002:QUIC Loss Detection and Congestion Control,IETF,2021-05;连接路径上的恢复与拥塞控制。
- curl 官方 HTTP/3 构建文档,curl project,访问于 2026-09-23;HTTP/3 后端与构建条件。
验证边界
已验证:2026-09-23 重新核查 RFC 9114、RFC 9204、RFC 9000、RFC 9002 与 curl 官方文档;标准库脚本复算流编号、QPACK 依赖和回退决策;Hexo 构建与页面资源检查只用于验证发布形态。
NOT_RUN:当前 curl 8.5.0 的 feature 列表没有 HTTP3,也没有可用的 ngtcp2、quiche 或其他成熟 HTTP/3 客户端/服务端。因此没有执行 HTTP/2 与 HTTP/3 的同内容、同链路丢包比较,没有抓取 QUIC 包或 qlog,也没有验证 Alt-Svc 缓存、UDP 阻断后的真实回退时序。手算、静态检查、HTTP 200 和 Hexo 构建均不替代真实端到端结果。





