第 12 章 HTTP/2:把文本变二进制,把串行变并发

HTTP/2 的核心,不是”更快”这么简单。它是 HTTP 第一次认真地回答了一个它逃避了 20 年的问题:“如果重新设计传输格式,而不动语义,会怎样?“


12.0 引子:一个页面 100 个请求,HTTP/1.1 优化到极致也没用

你:墨叔,我们把能做的前端优化都做了——域名分片、雪碧图、合并 JS、内联小图片……但页面的加载速度还是上不去。浏览器并发就是卡在 6 个连接。

老张:我看了,你的页面有 120 个请求。HTTP/1.1 下,6 个连接,每个连接串行,加上握手和队头阻塞,天花板就在那。

墨叔:你们做的一切,都是在给 HTTP/1.1 的”连接模型”打补丁。而 HTTP/2 直接换掉了这个模型——把”一个连接跑 6 个串行请求”变成了”一个连接跑无限个并发请求”。


12.1 先把问题问对:HTTP/2 解决了什么,没解决什么

墨叔:先说清楚 HTTP/2 的”边界”,很多人对它有错误的期待。

HTTP/2 解决了:

  1. 应用层的队头阻塞——一个连接上,请求可以并发,响应可以乱序。
  2. 首部冗余——用 HPACK 压缩首部,Cookie 那种重复内容不再反复传。
  3. 连接利用率——一个连接复用所有请求,省掉大量握手。

HTTP/2 没解决:

  1. TCP 层的队头阻塞(第 7 章第 5 题讲过)——丢一个包,整个连接的所有流都卡。
  2. 不改变 HTTP 语义——方法、状态码、首部、URI,全都原样保留。

你:所以 HTTP/2 是”换传输格式,不换语义”?

墨叔:对,这是理解 HTTP/2 的钥匙。它把 HTTP/1.1 的”文本报文”,改成了”二进制帧”,但帧里承载的语义(GET、200、Content-Type)一个都没变。


12.2 为什么是”2.0”而不是”1.2”?

墨叔:先回答一个有趣的问题:为什么 HTTP/2 不是 HTTP/1.2,而是跳到了 2.0?

你:因为变化太大?

墨叔:表面上是。但更深层的原因是**“版本号”的语义**:

  • 1.0 → 1.1 是增量:加了个长连接、chunked,文本格式没变,老实现基本兼容。
  • 1.1 → 2.0 是革命:线上格式从文本变成了二进制。这是一个”老解析器完全不认识”的变化。

所以必须用大版本号来声明:“这不是增量升级,是格式换代,老客户端要么协商到 1.1,要么就别来。”

你:那老客户端怎么知道服务器支不支持 2.0?

墨叔:好问题,这正是 12.6 节要讲的 ALPN 协商。但先记住这个思想:

版本号表达的是”兼容性承诺”。小版本 = 增量兼容,大版本 = 可能不兼容。 这是一个协议对”变化幅度”的诚实声明。


12.3 帧(Frame):HTTP/2 的基本单位

墨叔:HTTP/2 的最小单位是帧。所有东西——请求头、请求体、响应头、响应体——都被拆成帧来传。

每个帧有一个固定的 9 字节帧头:

+-----------------------------------------------+
|  Length (24 bit)  |  Type (8 bit) | Flags (8) |
+-----------------------------------------------+
|  R |  Stream Identifier (31 bit)              |
+-----------------------------------------------+
|  Frame Payload (Length 字节)                   |
+-----------------------------------------------+

你:这个帧头为什么这么设计?每部分干嘛的?

墨叔:逐个拆:

  • Length(24 bit):这个帧的负载多长。24 位 = 最大 16MB,够用。
  • Type(8 bit):帧类型(HEADERS、DATA、SETTINGS、WINDOW_UPDATE、PING、GOAWAY 等)。
  • Flags(8 bit):帧的标志位(比如这个 HEADERS 帧是不是流结束)。
  • R + Stream Identifier(31 bit):这个帧属于哪个”流”。这是最关键的一个字段——它让”一个连接上的无数帧”能够按流 ID 归位。

你:Stream Identifier 就是第 7 章说的”请求 ID”?那个解决乱序的关键?

墨叔:对!第 7 章我们埋过伏笔:队头阻塞的根源是”响应无法标识自己属于哪个请求”。而 HTTP/2 的 Stream ID 就是那个”标识”。 有了它,响应就能乱序返回——因为每个帧都标着”我是属于流 5 的”。


12.4 流(Stream)与多路复用

墨叔:现在讲”流”。流是 HTTP/2 的核心抽象:一个逻辑上的、独立的请求-响应通道。

在一个 TCP 连接上,可以同时有很多个流:

一个 TCP 连接
├── 流 1:GET /a.js
├── 流 3:GET /b.css
├── 流 5:POST /api/login
└── 流 7:GET /c.png

这些流的帧,在连接上交错传输,接收方根据帧头的 Stream ID 把它们重新归位到各自的流。

你:所以”多路复用”就是”多个流共享一个连接,各自独立收发”?

墨叔:对。这就是”多路复用(multiplexing)“的含义——一条物理通道(TCP 连接),承载多个逻辑通道(流)。这跟电话线的”频分复用”是同一个思想。

流 ID 的奇偶规则

墨叔:现在回答引子里问的那个细节——流 ID 的奇偶规则:

客户端发起的流,ID 是奇数(1、3、5…);服务器发起的流,ID 是偶数(2、4、6…)。

你:为什么要有这个规则?

墨叔:为了防止冲突。想象一下:如果客户端和服务器各自随意选 ID,就可能发生”客户端和服务器同时用 ID 7 发起流”,那接收方就分不清了。

奇偶规则让”客户端流”和”服务器流”的 ID 空间天然不重叠——奇数归客户端,偶数归服务器,永无冲突,而且不需要任何协商(双方各自从自己的奇数/偶数序列递增即可)。

更深一层:这个设计体现了一个通用原则——

当多个参与方需要在一个共享的 ID 空间里分配标识时,“按参与方划分 ID 空间”(这里用奇偶)是最简单、最无协调成本的方案。 它避免了”分配 ID 还要先协商谁用哪些 ID”的复杂度。

(对比:第 3 章 Idempotency-Key 用 UUID,是”用随机性避免冲突”;HTTP/2 流 ID 用奇偶,是”用划分避免冲突”。两种思路,各有适用场景。)


12.5 流控(Flow Control):HTTP 层为什么还要流控?

墨叔:HTTP/2 引入了一个 HTTP/1.1 没有的东西——流控。

你:流控?TCP 不是已经有流控了吗?HTTP 层还要再流控,不是重复吗?

墨叔:这个问题问得非常好。答案是:TCP 流控和 HTTP/2 流控,管的是不同的”层级”。

  • TCP 流控:管的是”连接”这一层的字节流——防止发送方把接收方的 TCP 缓冲区打爆。
  • HTTP/2 流控:管的是”流”这一层的字节流——防止某个流把接收方的应用层资源打爆。

关键场景:一个连接上有 100 个流。TCP 流控只能控制”整个连接”的总流量,但控制不了”某个流”的流量。

想象:流 1 是一个大文件下载,流 2 是 99 个 API 请求。如果流 1 疯狂发送,可能把接收方的应用层缓冲(内存)占满,导致流 2 的响应处理不过来——即使 TCP 层的总流量还在可控范围内。

HTTP/2 流控就是来解决这个的:它让你能对每个流、以及整个连接,分别设置”发送窗口”。

# 通过 WINDOW_UPDATE 帧调整窗口
WINDOW_UPDATE 帧:流 5 的窗口 + 65536 字节

你:所以流控是”多路复用”带来的新问题?

墨叔:精确。单路(HTTP/1.1 一个连接一个请求)没有”流之间争抢”的问题,所以不需要流控。多路复用引入了”流之间公平性”的问题,于是需要流控。 这再次印证:新的能力(多路复用)带来新的问题(流之间争抢),于是需要新的机制(流控)。


12.6 HPACK:首部压缩,以及它的隐藏代价

墨叔:讲 HPACK,这是 HTTP/2 的另一个核心,也是它最复杂、最”有故事”的部分。

问题:HTTP/1.1 里,每个请求都重复发送几乎一样的首部——尤其 Cookie,可能几 KB,每个请求都传一遍。

HPACK 的思路:维护一张表(动态表),把常见的首部名值对存进去。以后遇到重复的,只发一个”表索引”(几字节),不发完整的字符串。

你:所以 HPACK 是”首部的字典压缩”?

墨叔:对,三管齐下:

  1. 静态表:内置 61 个最常用的首部(:method: GET、:status: 200、content-type 等),固定索引。
  2. 动态表:连接过程中动态学习”这个连接里反复出现的首部”,存进去。
  3. 霍夫曼编码:对字符串本身做熵编码,进一步压缩。

隐藏代价:动态表是有状态的,它绑定了连接。

你:绑定连接?什么意思?

墨叔:动态表是”发送方和接收方共同维护的一份状态”。发送方往表里加一条,接收方也跟着加,两边必须严格同步。所以:

  1. 动态表不能跨连接共享——一个连接学到的表,另一个连接用不了(因为那边没有这个状态)。
  2. 这就是为什么 HTTP/2 必须”一个连接承载所有请求”——你如果有多个连接,每个连接都要重新”学习”一遍动态表,压缩效果就差了。

你:所以”一个连接多路复用”和”HPACK 动态表”是互相成就的?

墨叔:对。多路复用让所有请求走一个连接,才让动态表有机会”积累”;动态表的积累效果,又反过来要求”尽量一个连接”。 两者是绑定设计。

而且,这个”有状态”还带来了安全问题和第 13 章的重新设计:

  • 安全问题:HPACK 动态表 + 压缩,是 CRIME 攻击的后裔(和第 10 章 BREACH 同类——压缩泄露密文长度)。
  • 顺序依赖:动态表的更新是有顺序的(后一条依赖前一条),这在 HTTP/2 的单流里没问题,但到了 HTTP/3 的多流并发(乱序),就成了问题——于是 HTTP/3 重新设计了 QPACK(第 13 章讲)。

12.7 Server Push 的兴衰:又一个被淘汰的特性

墨叔:HTTP/2 有一个特性,当初被吹上天,现在却死了——Server Push(服务器推送)。

思路:服务器在响应一个请求时,主动把”你接下来肯定会要”的资源也推给你。比如你请求 index.html,服务器主动把 style.css、app.js 也推过来,省掉你”解析 HTML 后发现还要请求这些”的往返。

# 服务器响应 index.html 时,附带 PUSH_PROMISE 帧
PUSH_PROMISE: 我要推给你 /style.css

你:听起来很美好啊,为什么死了?

墨叔:三个致命问题:

  1. 服务器”猜不准”:服务器凭什么知道”你接下来要什么”?猜错了,推了一堆用不上的资源,浪费带宽。
  2. 缓存冲突:浏览器可能已经有 style.css 的缓存了,服务器又推一份,浪费。
  3. 实现复杂、效果有限:要正确实现 Server Push,服务器要精确理解资源依赖,而这个理解通常只有”构建工具”有。

你:所以是”服务器主动推送”这个美好的想法,败给了”服务器根本不知道客户端要什么”这个现实?

墨叔:对。而且还有关键一击——rel=preload(资源预加载提示)取代了它。preload 是”客户端声明式的”——HTML 里写 <link rel="preload" href="style.css">,浏览器自己决定要不要预加载(它知道自己的缓存情况)。

这又是一个”命令式 vs 声明式”的例子(第 8 章第 5 题讲过):

Server Push 是”命令式”的(服务器说”我要推给你”,但服务器不知道你的缓存状态);preload 是”声明式”的(客户端声明”我可能需要这个”,客户端自己判断)。声明式让”知道信息的人”(客户端)来做决策,所以更靠谱。

你:所以 Server Push 又是被”更合适的方案”取代了,和第 11 章 Digest 一样?

墨叔:对。你已经在第 8、11 章见过这个规律了。技术演进里,“美好的主动”往往输给”符合信息分布的设计”。信息在谁手里,决策权就该给谁。


12.8 ALPN:怎么协商出”用 1.1 还是 2.0”

墨叔:讲一个必须知道的机制——ALPN(Application-Layer Protocol Negotiation)。

问题:客户端和服务器怎么约定”这次用 HTTP/1.1 还是 HTTP/2”?

你:客户端说”我支持 2.0”,服务器说”好,用 2.0”?

墨叔:对,但细节在于——这个协商发生在 TLS 握手阶段,而不是 HTTP 层。

具体:TLS 握手的 ClientHello 里,客户端带上一个 ALPN 扩展,列出它支持的应用协议:

ClientHello:
  ALPN: ["h2", "http/1.1"]   <- 客户端:我支持 h2 和 http/1.1

ServerHello:
  ALPN: "h2"                 <- 服务器:那用 h2

你:为什么要在 TLS 握手阶段协商,而不是建立 HTTP 连接后再协商?

墨叔:因为越早协商,越省往返。如果等 HTTP 连接建立后再协商,那又要多一次往返。而 TLS 握手本来就要发生(HTTPS),“顺路”就把应用协议协商了,零额外成本。

这体现了一个设计智慧:

把”协议的协商”和”已有的握手”合并,而不是单独搞一个协商步骤。 能顺路做的,就别单独做。(对比 HTTP/1.1 的 Upgrade 机制,它就是”连接建立后再协商”,多一次往返,所以 WebSocket 用 Upgrade 是”迫不得已”,而 h2 用 ALPN 是”顺路”。)


12.9 Go 落地:net/http 自动升级,和 Server Push 被移除

墨叔:Go 里用 HTTP/2 其实几乎是透明的:

// HTTPS 下,Go 的 net/http 会自动协商 HTTP/2(通过 ALPN)
server := &http.Server{Addr: ":443"}
server.ListenAndServeTLS("cert.pem", "key.pem")
// 就这么简单,HTTPS 下默认启用 HTTP/2
 
// 明文 HTTP/2(h2c)需要手动配置,一般生产不用

你:所以 Go 里我什么都不用做,HTTPS 下自动就是 HTTP/2?

墨叔:对。但有两个Go 特有的点要知道:

  1. Go 移除了 Server Push 的支持。Go 的 http2 包里,Push 相关的方法在标准库里不可用(因为 Server Push 本身已经衰亡)。这是”语言跟着标准走”的体现——特性死了,实现也删了。
  2. HTTP/2 的流控参数,Go 里可以调 http2.Transport 的 MaxReadFrameSize、MaxHeaderListSize 等,但一般默认就够。

一个实际的坑:如果你用 HTTP/2 访问一个不支持 h2 的服务器,Go 会自动回退到 HTTP/1.1(通过 ALPN 协商结果)。这是”协商”在起作用,别以为”我配置了 h2 就一定是 h2”。


12.10 排查清单:HTTP/2 相关问题

第 1 步:确认真的用了 h2 吗?
        → curl -v 看 ALPN 协商结果;浏览器 DevTools 的 Protocol 列

第 2 步:是 TCP 层还是 HTTP 层的队头阻塞?
        → h2 解决了 HTTP 层,TCP 层还在(丢包时卡)

第 3 步:首部压缩生效了吗?
        → HPACK 动态表,看是不是所有请求都走一个连接

第 4 步:流控限制了吗?
        → 大文件下载是否影响其它流?看 WINDOW_UPDATE

第 5 步:中间设备支持 h2 吗?
        → 老代理/网关可能不支持 h2,导致降级或异常

思考题

第 1 题(概念)

HTTP/2 解决了 HTTP/1.1 的什么核心问题?请说清”多路复用”是如何解决”队头阻塞”的,并指出 HTTP/2 没有解决的那部分队头阻塞是什么。

第 2 题(概念)

HTTP/2 的流 ID 为什么”客户端奇数、服务器偶数”?这个设计体现了什么通用原则?请再举一个”用划分代替协商”的例子。

第 3 题(设计)

HTTP/2 用”帧 + 流 ID”实现了多路复用。现在让你设计:在一个连接上,如何让”流控”既控制单个流,又控制整个连接? 请说明你需要哪些机制(提示:窗口、WINDOW_UPDATE),以及为什么”只做连接级流控不够”。

第 4 题(生活类比)

墨叔:一个多车道的收费站,和 HTTP/2 的多路复用,有什么相似之处?

提示:想想——

  1. HTTP/1.1 的”6 个连接”,像什么?(提示:6 条单车道小路)
  2. HTTP/2 的”一个连接多路复用”,像什么?(提示:一条多车道高速)
  3. “队头阻塞”在收费站场景里对应什么?(提示:一辆车抛锚堵住车道)
  4. “流控”对应什么?(提示:车流控制、匝道信号灯)

第 5 题(更深一层的发问)

HPACK 的”动态表”是”有状态的”——它绑定连接,且更新有顺序依赖。

请你深入思考:

  1. 这个”有状态”,给 HTTP/2 带来了什么代价?(提示:连接复用、以及多路复用下的”顺序”问题)
  2. 为什么 HTTP/3 要重新设计首部压缩(QPACK)?HPACK 在”多路复用 + 乱序”下会出什么问题?
  3. 这引出一个深刻的矛盾:“压缩”需要”状态/上下文”,而”并发/乱序”需要”无状态/独立”。 这个矛盾在你设计任何协议时都会遇到,请说说你的理解。

参考答案

第 1 题答案

解决的核心问题:HTTP/1.1 的应用层队头阻塞——一个连接上,请求必须串行,响应必须按序,导致一个慢请求堵死后面所有请求。

多路复用如何解决:HTTP/2 把请求拆成”流”,每个流有独立的 ID。一个连接上可以同时有多个流,它们的帧交错传输,接收方根据帧头的 Stream ID 重新归位。这样:

  • 请求可以并发发送(不用等前一个响应)。
  • 响应可以乱序返回(因为每个帧都标着”属于流几”)。

所以”请求 1 慢”不再堵死”请求 2”——请求 2 的帧可以插在请求 1 的帧之间先行到达。

没解决的那部分:TCP 层的队头阻塞。HTTP/2 的所有流共享同一个 TCP 连接,而 TCP 是”可靠有序字节流”。一旦这个 TCP 连接上有一个数据包丢失,TCP 必须重传,且丢失包后面的所有字节(无论属于哪个流)都必须排队等重传完成,才能按序交给应用层。所以”一个包丢失”会卡住”所有流”——这就是 TCP 层队头阻塞,HTTP/2 无能为力(是 HTTP/3 要解决的问题)。

第 2 题答案

奇偶规则的原因:防止”客户端流”和”服务器流”的 ID 冲突。客户端和服务器各自从奇数/偶数序列分配流 ID,ID 空间天然不重叠,无需任何协商。

通用原则:

当多个参与方需要在共享的 ID 空间里分配标识时,“按参与方预先划分 ID 空间”是最简单、零协调成本的方案。

其它”用划分代替协商”的例子:

  • MAC 地址的 OUI:前 24 位是”厂商号”(由 IEEE 分配给各厂商),后 24 位由厂商自行分配。这样不同厂商的设备永无 MAC 冲突,且不需要互相协商。
  • UUID 的版本/变体位:用固定位标识”这是哪种 UUID 生成算法”,避免不同算法产生的 UUID 冲突。
  • 数据库主键的”雪花算法(Snowflake)“:把 64 位 ID 划分为”时间戳段 + 机器号段 + 序号段”,每台机器用自己的机器号段,无需中心协调就能全局唯一。

核心:“划分”用”预先分配空间”换来”零协调”,“协商”用”运行时沟通”换来”空间利用率”。 当参与方固定、且 ID 空间充裕时,“划分”更优。

第 3 题答案

需要的机制:

  1. 流级窗口:每个流有自己的”接收窗口”(还能接收多少字节)。发送方不能超过接收方该流的窗口。
  2. 连接级窗口:整个连接有一个总窗口,所有流共享。
  3. WINDOW_UPDATE 帧:接收方处理完数据后,发送 WINDOW_UPDATE 帧,增加某个流(或整个连接)的窗口。

为什么”只做连接级流控不够”:

因为连接级流控无法防止”某个流独占资源,饿死其它流”。

具体:一个连接上有流 1(大文件下载)和流 2~100(API 请求)。如果只做连接级流控,那么:

  • 连接级窗口很大(比如 10MB),流 1 可以疯狂发送 10MB 的数据,把接收方的应用层缓冲(内存)占满。
  • 此时流 2100 的数据,虽然也在连接窗口内,但接收方已经没内存去处理它们了——流 2100 被”饿死”。

所以需要流级流控:给每个流单独设窗口,让流 1 不能无限霸占,流 2~100 也能公平地分到带宽。

总结:

连接级流控解决”总流量不爆”,流级流控解决”流之间公平”。多路复用让”流之间争抢”成为新问题,于是需要”流级流控”这个新机制。 两者叠加,才是完整的 HTTP/2 流控。

第 4 题答案

收费站HTTP/2
HTTP/1.1 的”6 个连接”6 条单车道小路——每条路只能走一辆车(串行),一辆抛锚整条路堵死,且每条路都要单独维护
HTTP/2 的”一个连接多路复用”一条多车道高速——很多车(流)同时跑,互不干扰
”队头阻塞”一条车道上,一辆车抛锚,堵住了它后面的所有车(即使别的车本来能走)
“流控”匝道信号灯——控制进入高速的车流,防止太多车同时涌入导致全线瘫痪

关键洞察:

  • 6 条单车道 vs 1 条多车道:单车道”看起来有 6 条路”,但每条都窄、都串行、都要单独管理(6 次握手);多车道”一条路”,但车道多、并发高、管理集中(1 次握手)。
  • 队头阻塞的本质:在收费站场景里,“抛锚车堵住后面”对应”一个慢请求/丢包堵住整个流”——这是”串行”结构固有的问题,只有”并行结构”(多车道/多路复用)才能缓解。
  • 流控的必要性:多车道高速如果不控制入口流量,会全线瘫痪(所有车都堵在匝道上)——这对应”多路复用下,如果不做流控,某个流会独占资源饿死其它流”。

升华:HTTP/2 的整个设计,就像”把 6 条混乱的单车道小路,改造成 1 条井然有序的多车道高速”——代价是你要引入一整套”交通管理”(流 ID 分流、流控、优先级),而这就是”多路复用”这个能力带来的新复杂度。

第 5 题答案

1. HPACK”有状态”的代价:

  • 绑定连接,不能跨连接复用:动态表是收发双方共同维护的状态,一个连接学到的表,换一个连接就没了。所以 HTTP/2 的压缩效果强依赖”所有请求走同一个连接”——连接一多,压缩效果就变差。
  • 顺序依赖:动态表的更新是有严格顺序的(后一条条目的索引,依赖前一条的插入顺序)。这就要求”收发双方看到的表,必须严格一致、严格有序”。

2. 为什么 HTTP/3 要重做(QPACK):

因为 HTTP/3 跑在 QUIC 上,而 QUIC 是真正的多路复用 + 乱序——不同的流可以独立、乱序地到达。这直接和 HPACK 的”顺序依赖”冲突:

  • 想象流 A 和流 B 并发,流 A 的首部里”往动态表加了一条新条目”,流 B 的首部里”引用了这条新条目”。
  • 但流 B 的帧先到了(乱序),流 A 的帧还没到。
  • 接收方看到流 B 引用了”还不存在的表条目”——解码失败,或者必须阻塞等待流 A 到达。

这就是 HPACK 在”乱序多路复用”下的死穴:它的”顺序依赖”和”乱序”是根本矛盾的。于是 HTTP/3 设计了 QPACK,把动态表拆成”编码侧的表”和”解码侧的表”,中间用”确认”来同步,从而允许乱序。

3. “压缩需要状态/上下文” vs “并发乱序需要无状态/独立” 的矛盾:

这是一个非常深刻的矛盾,本质是:

压缩的本质,是”利用重复”(用上下文/历史来减少冗余)。而”重复”意味着”相关性”——后一个数据依赖前一个数据。但”并发乱序”要求”每个数据独立、可任意重排”。 一个要”相关”,一个要”独立”,天然冲突。

这在你设计任何协议时都会遇到,处理方式大致有三条路:

  1. 牺牲压缩:为了并发,放弃”跨数据的有状态压缩”(比如每个数据独立压缩,像 gzip 每个流独立压)。
  2. 牺牲并发:为了压缩,接受”有顺序”(像 HPACK 那样,但要接受”顺序阻塞”)。
  3. 重新设计:想办法两者兼得——像 QPACK 那样,用”分状态 + 确认机制”,让”表”和”数据”解耦,允许乱序但压缩还在。

最终启示:

协议的每一个”优化”(压缩、缓存、复用)都有代价,通常是”引入状态/耦合”。而协议的每一个”灵活性”(并发、乱序、无状态)都会削弱这些优化。 设计协议的过程,本质就是在”利用重复的优化”和”支持并发的灵活性”之间,反复权衡、重新切分状态边界。

这和第 1 章”约束之间互相打架”、第 9 章”复杂度放哪层”,是同一个方法论的延续:没有免费的午餐,只有明智的权衡。


一页纸小结

HTTP/2 = 换传输格式(二进制帧),不换语义(方法/状态码/首部全保留)。

帧:9 字节帧头,关键是 Stream ID——它是”乱序响应”的基石(呼应第 7 章)。

多路复用:一个 TCP 连接承载多个流。流 ID 奇偶划分,客户端奇数、服务器偶数——用”划分”代替”协商”。

流控:多路复用带来”流间争抢”,于是需要流级流控(不只是 TCP 的连接级流控)。

HPACK:首部压缩(静态表+动态表+霍夫曼)。动态表”有状态、绑定连接、顺序依赖”——为 HTTP/3 的 QPACK 埋下伏笔。

Server Push 之死:服务器”猜不准”,被声明式的 rel=preload 取代。命令式输给符合信息分布的设计。

ALPN:在 TLS 握手里”顺路”协商 h2/1.1,零额外往返。


下一章:HTTP/2 解决了应用层队头阻塞,但 TCP 层的队头阻塞还在——丢一个包,全连接卡死。怎么彻底解决?HTTP/3 的答案石破天惊:换掉 TCP,用 UDP 自己造一个可靠传输。 但我要先问你一个反直觉的问题:为什么选 UDP?难道 UDP 更快吗? 答案不是你想的那样。