bigywater.tech
网络协议 · 85

HTTP/2 多路复用、流量控制与 HPACK 头部压缩

HTTP/2 的三大招:一条连接上并发多个流(多路复用),每个流有自己的流量控制窗口,头部用 HPACK 压缩。这篇在本机起一个 HTTP/2 服务器,用 netlab 造 100 ms 的往返,逐个量:并发到底省多少、窗口小了会怎样、动态表压缩了多少。

4143 → 814 ms
10 个 300 ms 的请求:HTTP/1.1 一条连接顺序发 与 HTTP/2 一条连接并发
65535 字节
客户端不发 WINDOW_UPDATE 时,服务器发了这么多就被窗口卡住
210 → 8 字节
同样的请求头部块,第 1 个与第 2 个起的大小(HPACK 动态表)

多路复用

实测 · 本机执行输出
  HTTP/1.1 一条连接顺序发:4143 ms(≈ 握手 1 RTT + 10 × (1 RTT + 300 ms) = 100 + 10 × 400 = 4100 ms)
  HTTP/1.1 6 条并行连接:913 ms(10 个请求分两批:6 + 4,每批 1 RTT + 300 ms,加握手 100 ms ≈ 900 ms)
  HTTP/2 一条连接:814 ms(含 TCP 握手 1 RTT + TLS 握手 1 RTT + 10 个流同时 1 RTT + 300 ms ≈ 600 ms);10 个流完成的时刻(ms,从发出请求起):[503, 503, 503, 503, 503, 503, 503, 603, 603, 604]

10 个请求一条连接并发,总耗时 814 ms,接近 6 条 HTTP/1.1 连接(913 ms);顺序发需要 4143 ms。

流量控制

实测 · 本机执行输出
  不确认时:服务器发出 65535 字节(4 个 DATA 帧)就停了(初始流窗口 65535、连接窗口 65535;停了 1.5 秒也不动)
  补发 WINDOW_UPDATE(流 + 连接各 +1 MiB)后:又过 13 ms 传完,总共 1048576 字节
  窗口大小决定吞吐:往返 100 ms 时,下载 4 MiB,客户端宣告不同的初始流窗口(连接窗口放到 16 MiB)的耗时
    初始窗口    65535 字节:13.09 秒,  0.32 MB/s(窗口 ÷ RTT =   0.66 MB/s)
    初始窗口   262144 字节: 3.37 秒,  1.24 MB/s(窗口 ÷ RTT =   2.62 MB/s)
    初始窗口  1048576 字节: 1.11 秒,  3.79 MB/s(窗口 ÷ RTT =  10.49 MB/s)
    初始窗口  4194304 字节: 0.71 秒,  5.93 MB/s(窗口 ÷ RTT =  41.94 MB/s)

窗口用完发送方就停;WINDOW_UPDATE 才能让它继续。初始窗口越小下载越慢,但实测吞吐低于窗口 ÷ RTT。

HPACK

实测 · 本机执行输出
  10 个相同请求的头部块大小(字节):[210, 8, 8, 8, 8, 8, 8, 8, 8, 8];同样内容用 HTTP/1.1 文本写要 398 字节左右(每个头部名字 + ": " + 值 + CRLF)
  第 1 个 210 字节(新头部要写出来,同时放进动态表),第 2 个起 8 字节(只是引用动态表的索引),压缩到 2.0%
  把动态表关掉(HEADER_TABLE_SIZE = 0):[211, 210, 210]——每次都得重新写出来(只剩霍夫曼编码和静态表的压缩)
  只改一个头部的值(换一个 200 字节的 Cookie):下一个请求的头部块 160 字节(新的 Cookie 要整个重发,其余仍是引用)
  对比真实服务器:对 example.com 连续在同一个连接上发 3 个请求,看响应 HEADERS 帧的头部块大小
  example.com 三次响应的头部(解码后名字+值总字节):[243, 243, 243](线上 HPACK 编码的字节数在上一篇里看到过是 160 字节左右;解码后的原始大小在这里)

动态表让重复的头部只需要 8 字节的引用;关掉动态表则每次 210 字节。

分步演示

HTTP/2 多路复用与 HPACK
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64(Apple 芯片)/ Python 3.14.8 / tcpdump(libpcap)/ scapy 2.8 / dpkt / dnspython / h2 / aioquic / OpenSSL / curl 8.7.1;抓包在 lo0(环回)上做,少数实验用真实网卡 en0(这台机器上 en0 是 Wi-Fi,tcpdump 看到的是系统转成以太网格式的帧,所以文中的“以太网头”都指这个 14 字节的头)或访问公网(会在文章里注明);需要丢包 / 延迟 / 限速时,用一个只作用于 lo0 端口 9400-9499 的 dummynet 辅助脚本(netlab);所有服务端都是本机自己起的进程;时间戳取自 tcpdump 的 -tt,数字每次运行会有小幅抖动。全部在本机:HTTP/2 服务器是我用 h2 库写的(自签名证书只用于本机,客户端不校验),netlab 每方向 50 ms;第一节的 HTTP/2 与 HTTP/1.1 对比,HTTP/2 含 TLS 握手而 HTTP/1.1 是明文,不完全公平;每个数字只测 2 次;第二节吞吐低于 窗口 ÷ RTT 的原因没有查明
    • 没有验证:HTTP/2 优先级;同一个连接上大响应的真实交错;MAX_CONCURRENT_STREAMS 被用满时的排队行为;HPACK 的敏感头部(never-indexed)与 CRIME 类攻击;真实浏览器的并发策略;真实服务器上的 HPACK 线上字节(只量了解码后大小)
    京ICP备2026067502号-1京公网安备11010502063487号