大图片丢了一段,小 JSON 为什么也在等?
设想浏览器向 shop.example.com 同时请求大图片 A 和小 JSON B。服务器已经准备好两个响应,业务计算没有先后依赖,客户端希望 B 尽快完成。
如果使用同一条 HTTP/1.1 连接,B 可能排在 A 后面。换成 HTTP/2,两个响应可以交错发送;可是一旦底层 TCP 丢失一段较早的字节,已经抵达的 B 仍可能无法交给 HTTP 层。换成 HTTP/3,B 可以摆脱另一个流的字节缺口,却仍要面对带宽、压缩字典和业务依赖。
讨论队头阻塞(Head-of-Line Blocking,HOL),要先说清谁排队、前面缺了什么、后面的谁因此不能前进。 本章轨迹是协议规则的教学推导,数字不代表真实抓包或性能测量。
HTTP/1.1:复用连接不等于交错响应
持久连接让 A、B 使用同一条 TCP 通道,省下重复建连。普通串行使用时,客户端收到 A 的响应后再发 B;流水线(pipelining)允许不等前一个响应就继续发送请求,但服务端必须按收到请求的顺序发送对应响应。RFC 9112 §9.3.2:流水线
假如 A 先请求,即使 B 已准备好,也不能把 B 的正文插进 A 的正文。客户端靠消息边界识别当前响应,没有 HTTP/2 那样的流标识来区分交错片段。分块传输只把 A 的正文分成块,不允许偷偷插入 B 的完整响应。
客户端可以用多条连接让 A、B 分开走,代价是更多连接状态和握手、各自的拥塞控制。连接数量受客户端、代理和服务器策略影响,“每个域名永远只能六条连接”不是 HTTP 规范的通用结论。
A 占着响应顺序的问题,才引出“能否在一条连接里给不同请求分配身份”。
HTTP/2:把响应拆成带身份的帧
HTTP/2 用二进制帧(frame)承载消息。每个普通请求/响应交换有自己的流(stream),帧头带流标识;HEADERS 承载字段区,DATA 承载正文。服务器可以发送 A 的 DATA、B 的 HEADERS 和 DATA,再继续 A,因此 B 可以先完成。交错也有帧级规则:未结束的字段块所需 CONTINUATION 帧必须连续发送,不能在中间任意插入其他流的帧。
这里的“二进制”指协议编码。JSON 正文仍可为 UTF-8 文本,并不会自动变成压缩或加密的数据;GET、状态码、缓存和认证的 HTTP 语义也仍然存在。请求方法、目标等控制信息通过 :method、:scheme、:authority、:path 等伪首部表达,响应使用 :status。RFC 9113 §4/§5/§8.3
不过,帧最后仍进入同一条 TCP 字节流。帧可以跨 TCP 段,一个段也可以包含多个帧的字节。TCP 不认识“这段属于 B、比较急”,只按连续字节交付。
假设接收端期待序号 1001:携带 A 相关字节的 [1001, 2001) 丢失,后到的 B 相关字节 [2001, 2501) 已收到。区间左闭右开,分别为 1000 和 500 字节;这是简化的 TCP 载荷编号,包含协议编码开销,不代表正文恰好这么大。即使接收端缓存了后一段,也要等前面的缺口补齐,才能按序向上交付。
HTTP/2 消除了流水线要求响应按请求顺序发送的限制,没有消除 TCP 交付顺序造成的跨流等待。HTTPS 场景下还有 TLS 记录处理,但它不会让底层 TCP 跳过这个缺口。
HTTP/3:把有序范围缩到每条 QUIC 流
HTTP/3 将 HTTP 消息映射到 QUIC 流。QUIC 在 UDP 之上实现可靠传输,每条流通过流标识和字节偏移维护自己的顺序;不同流之间没有一条必须连续交付的统一 TCP 字节序列。RFC 9000 §2.2:流数据
设握手已完成,B 的首部解码依赖已满足,额度允许;三个包都处于同一应用数据包号空间。包 10 携带 A 流偏移 [0, 1000),丢失;包 11 携带 B 流 [0, 500),抵达。这些范围是流字节,包含 HTTP/3 编码,不是 UDP 包的总长度。B 可以按自己的完整字节范围继续处理,A 则等待自己的缺口。示意图只追踪载荷范围,省略 ACK 和检测丢包所需时间。
查看 Mermaid 源码
sequenceDiagram
participant S as 服务器
participant C as 客户端
alt HTTP/2:同一条 TCP 字节流
S--xC: A 相关字节 [1001, 2001) 丢失
S->>C: B 相关字节 [2001, 2501) 抵达
Note right of C: 缓存 B,等待 TCP 缺口
S->>C: 补齐 [1001, 2001)
Note right of C: 连续字节可交付,B 才能继续
else HTTP/3:不同 QUIC 流分别有序
S--xC: 包 10:A 流 [0, 1000) 丢失
S->>C: 包 11:B 流 [0, 500) 抵达
Note right of C: B 依赖已满足,可以继续
S->>C: 包 12:补发 A 流 [0, 1000)
Note right of C: A 的缺口补齐
endQUIC 恢复的是丢失的数据和必要信息,重发时使用新包号,不能把包 10 原样再当包 10 发出去。包号标记一次发送,流偏移标记数据位置,两者不能混为 TCP 序号。RFC 9000 §12.3/§13.3
如果同一个包携带 A、B 两个流的数据,包丢失就可能让两个流都缺字节;如果 B 自己的前半段丢失,B 的后半段也仍需等待。独立交付减少的是跨流的顺序牵连,不让丢包免费消失。
用一张表固定三代协议的差异
| 比较项 | HTTP/1.1 | HTTP/2 | HTTP/3 |
|---|---|---|---|
| 常见传输 | TCP;HTTPS 再使用 TLS | TCP;HTTPS 场景使用 TLS | QUIC over UDP,集成 TLS 1.3 |
| 一条连接上的并发 | 流水线可连续发请求,响应按请求顺序 | 不同流的帧可交错 | 不同 QUIC 请求流并发 |
| 消息表达 | 文本起始行和字段,正文另行编码 | 二进制帧、伪首部 | HTTP/3 帧、伪首部,映射到 QUIC 流 |
| 字段压缩 | 没有 HPACK/QPACK 机制 | HPACK | QPACK |
| 丢包后的顺序等待 | 同一 TCP 字节流受影响 | 同一 TCP 字节流上的多个流可能受影响 | 通常局限于缺数据的流;其他依赖仍可等待 |
| 方法与缓存语义 | HTTP 语义 | HTTP 语义 | HTTP 语义 |
QUIC 不是“UDP 天生可靠”。它自行管理确认、丢失恢复、流量控制与拥塞控制,并使用 TLS 1.3 建立密钥和认证对端,保护相应的数据包;QUIC 不套用 TCP 上的 TLS 记录层。RFC 9001 §4:TLS 与 QUIC
握手、安全和传输整合能减少某些建连等待,但第 19 章的 0-RTT 接受条件、重放风险仍然重要。不能把“HTTP/3”当作每次请求都零握手等待的承诺。
HPACK 压缩什么,QPACK 为什么还能卡住?
A、B 的首部可能重复出现目标主机、内容类型等信息。HPACK 用预定义的静态表、随连接更新的动态表及可选 Huffman 字符串编码,减少重复字段的表示成本。动态表像两端共同维护的字典:发送端引用条目,接收端按同一状态还原。RFC 7541 §2.3/§4/§5.2
它压缩字段区,不是用 gzip 压缩全部正文。正文采用何种表示仍由 Content-Encoding 等机制决定。HTTP/2 在每个方向维护共享压缩状态,字段块必须按规则处理,不能把任意片段乱序解码。RFC 9113 §4.3
QUIC 允许不同流独立抵达,照搬共享字典的顺序假设就会出问题:B 的首部先到,引用的动态条目却还在路上。QPACK 将动态表更新与反馈放在专门的编码器、解码器流上;如果字段区要求的插入数量尚未满足,解码仍会阻塞。
接收端用 SETTINGS_QPACK_BLOCKED_STREAMS 限制可能阻塞的流数量。编码器也可以减少对尚未确认条目的引用,在压缩率和等待风险之间取舍。QPACK 控制压缩依赖造成的阻塞,不保证所有字段区永远立即解码。RFC 9204 §2.1.2/§2.2/§4.5.1.1
所以前面的 B 能独立前进,必须带上“首部解码依赖已满足”这个条件。
动手找出B正在等待的那一层
分别用HTTP/2与HTTP/3走到B抵达。再让B的QPACK依赖尚未满足:补齐A的缺口,是否就足以让B继续?
A与B没有业务依赖,额度允许。逐步重放一份丢失和后续抵达,比较不同协议的交付范围。
TCP:[1001,2001)
尚未发送
TCP:[2001,2501)
尚未收到
下一步观察后续B抵达。丢包不会因为协议升级而消失。
固定教学轨迹,无抓包或真实QUIC;省略ACK、丢失检测与计时。A/B分属不同发送包,B自身无缺口;同包丢失、B流内缺口、业务依赖和共享拥塞仍可能影响B。QPACK分支假设许可阻塞且依赖仅有该条目。HTTP/3减少跨流传输顺序牵连,不保证所有等待消失。
升级后仍然慢,先找哪一层的等待?
| 等待位置 | A、B 场景中的具体原因 | 判断方向 |
|---|---|---|
| 应用处理 | B 查询数据库必须等 A 创建记录 | 协议升级不能删除业务依赖 |
| HTTP/1.1 响应顺序 | A 的响应先占据同一连接 | 检查实际协议与连接复用方式 |
| TCP 交付 | A 相关的较早字节丢失,挡住 B | 看丢包/恢复轨迹,区别服务端计算慢 |
| QPACK 解码 | B 引用了尚未抵达的动态条目 | 看字段区与编码器流的依赖 |
| 额度与网络资源 | 流/连接额度不足,或共享路径拥塞 | 看接收消费速度、流量额度、拥塞与带宽 |
HTTP/2 的 DATA 同时受流级、连接级流量控制,底层 TCP 还有自己的接收窗口。QUIC 也有流级与连接级额度,防止接收端被数据淹没;拥塞控制则限制网络中的发送负担,两类限制不要混为一个“窗口”。RFC 9113 §5.2、RFC 9000 §4
QUIC 的流并发使用同一路径的发送资源,丢包可能触发拥塞控制,使其他流后续发送也减慢。因此,“B 不必等 A 的字节补齐”不等于“B 的速度完全不受 A 或丢包影响”。优先级和调度可以帮助安排发送,却不保证小响应一定先完成。RFC 9002:丢失检测与拥塞控制
实际排查时,建议先用开发者工具确认本次请求协商到的协议,再区分等待首字节、正文下载和浏览器渲染。若已经是 h3,却主要慢在数据库,继续改 QUIC 参数就偏离了当前证据。
开启 TLS 1.3 就自动变成 HTTP/3 吗?
不会。HTTP/3 需要客户端和服务端建立 QUIC 连接,并在 QUIC 使用的 TLS 握手中协商相应的 ALPN(例如 h3)。在 TCP 上协商 TLS 1.3、选择 h2,仍然是 HTTP/2。
客户端可以通过 Alt-Svc 等途径发现 HTTP/3 服务。若 UDP 被阻断或 QUIC 建连失败,客户端可以尝试基于 TCP 的 HTTP 版本;实际是否回退、何时回退由客户端和部署策略决定,不能只看到配置写着“开启 h3”就认为每次请求都走 h3。RFC 9114 §3.1/§3.2
QUIC 的连接 ID 还支持在一定条件下跨地址变化维持连接,但迁移受端点策略与路径验证约束。Wi-Fi 切到移动网络可以成为它的使用场景,不代表任意地址变化都无需验证,也不代表业务会话永不掉线。RFC 9000 §9:连接迁移
30—60 秒参考回答
HTTP/1.1 默认持久连接,流水线能提前发多个请求,但同一连接的响应仍按请求顺序发送。HTTP/2 引入带流标识的二进制帧和 HPACK,让多个响应交错传输,不过它们共享 TCP 字节流,较早字节丢失仍会挡住其他流。HTTP/3 把 HTTP 映射到 QUIC,基于 UDP 实现可靠传输和 TLS 1.3 安全,每个流分别有序,减少丢包造成的跨流交付阻塞。它仍有流内缺口、QPACK 字典依赖、共享拥塞和业务等待,所以是否更快要结合丢包、连接复用和实际瓶颈判断。
四个追问及解析
1. 已经有 Keep-Alive,为什么还需要多路复用?
Keep-Alive 解决多个请求能否继续使用已有连接;多路复用解决多个交换能否同时在连接上推进、用标识分清交错片段。保留一条通道并不自动具备第二种能力。
2. HTTP/2 的 B 比 A 先结束,违反 TCP 有序吗?
不违反。服务器可以先把 B 的完整响应帧写入 TCP,再继续 A 的剩余帧;TCP 按这个实际写入顺序交付字节。HTTP 层的响应完成顺序与请求发出顺序不同,不等于 TCP 乱序交付。
3. QUIC 丢包为什么不阻塞所有请求?
各流用自己的偏移维护连续范围,B 不需要等待 A 的缺口。但缺失包含 B 数据的包、B 自己的流内缺口或首部依赖仍会影响 B;共享拥塞也会降低发送速度。回答时要限定具体阻塞来源。
4. 头部压缩、正文压缩和加密是一回事吗?
不是。HPACK/QPACK 减少字段的表示成本,Content-Encoding 可描述正文压缩,TLS/QUIC 的密码机制提供相应的安全保护。压缩不能替代认证或加密,加密也不意味着消除重复字段的传输成本。
两个容易说错的结论
“HTTP/1.1 一条连接只能发一个请求。” 可以依次复用,也有流水线;真正受限的是同一连接上的响应发送顺序与不能按流交错正文。讨论浏览器使用方式时,还要区分规范能力和具体实现选择。
“HTTP/3 没有队头阻塞,所以一定比 HTTP/2 快。” 它改善跨流的传输交付等待。若本来不丢包、连接已复用,或者主要时间花在计算与正文量上,收益未必突出;QPACK、流内顺序与共享资源仍然存在。
A、B 如今可以并发完成,但它们仍是请求和响应。订单状态稍后发生变化时,服务器怎样及时通知浏览器,断线后又怎样补回错过的消息?那需要另外选择应用层的消息通道与恢复机制。
资料核验日期:2026-10-09。本文依据 IETF 的 HTTP/1.1、HTTP/2、HTTP/3、HPACK、QPACK 与 QUIC 规范;丢包区间为教学设定。