第 13 章 HTTP/3 与 QUIC:当协议发现瓶颈不在自己身上
HTTP 进化了 30 年,最后发现:瓶颈根本不在 HTTP 自己身上,而在它脚下的 TCP。 于是它做了一件石破天惊的事——把 TCP 换了。
13.0 引子:弱网环境下的性能悬崖
老张:老哥,用户投诉,地铁上、电梯里,App 打开特别慢。同样的代码,WiFi 下好好的,4G 弱信号下就卡死。
你:弱网丢包严重?TCP 重传导致的?
老张:对。而且我们用了 HTTP/2,弱网下反而比 HTTP/1.1 还慢。
你:HTTP/2 还更慢?不是更快吗?
墨叔:这就是 HTTP/2 最反直觉的地方——在丢包严重的弱网里,HTTP/2 可能比 HTTP/1.1 更差。 而这,正是 HTTP/3 要解决的核心问题。
13.1 先把问题问对:HTTP/2 在弱网下为什么反而更差
墨叔:第 12 章我们埋过一个伏笔,现在收网。
HTTP/2 把”6 个连接”变成了”1 个连接多路复用”。这在家用宽带(几乎不丢包)下是巨大的胜利。但弱网(丢包率高)下,它成了灾难:
关键机制:TCP 层的队头阻塞。
- HTTP/1.1:有 6 个连接。丢一个包,只堵住那 1 个连接,另外 5 个连接还能继续跑。
- HTTP/2:只有 1 个连接。丢一个包,这个连接上的所有流全被堵住(因为 TCP 要求有序,丢的包必须重传到位,后面的字节才能交给应用层)。
你:所以”连接变少”是把双刃剑——不丢包时是优化,丢包时是灾难?
墨叔:精确。HTTP/2 把”队头阻塞”从应用层(6 个独立请求)下沉到了传输层(1 个共享连接),丢包时,传输层的队头阻塞范围反而更大了。
数据说话:研究表明,在丢包率 2% 的网络下,HTTP/2 的表现可能比 HTTP/1.1 差——因为一个连接上丢包的概率,比 6 个连接上”全部丢包”的概率高得多。
13.2 为什么不改造 TCP?—— 中间件僵化
墨叔:现在问一个关键问题。既然 TCP 的”有序可靠”是队头阻塞的根源,那为什么不改造 TCP,让它支持”乱序、每流独立”?
你:对啊,改 TCP 不就行了?
墨叔:改不了。 这是全章最深刻的一个概念——中间件僵化(Protocol Ossification)。
你:中间件僵化?
墨叔:TCP 看起来是”端到端”的协议,但它要穿过无数的中间设备:防火墙、NAT、负载均衡器、IDS/IPS 入侵检测系统、运营商的路由器……
这些中间设备,很多是很多年前部署的、基于”当时 TCP 长什么样”来写的。它们会:
- 检查 TCP 头:如果看到”不认识的新字段”,可能直接丢弃(以为是攻击流量)。
- 只认某些行为:比如只认”标准三次握手”,你发一个”改进过的握手”,它可能重置连接。
你:所以是”老中间设备不认识新 TCP,就把新 TCP 当异常流量干掉”?
墨叔:对。而且这些中间设备你根本管不着——它们是运营商、是别人的机房、是十几年前的遗留系统,遍布全球,无法升级。
这就是”僵化”:
一个协议,一旦被海量的、无法升级的中间设备所”依赖”,它就”冻住”了——你无法再改它,因为任何改动都会被沿途的中间设备误判为”异常”而阻断。
你:所以 TCP 已经”改不动”了?
墨叔:对。TCP 的任何一个语义变化,都要面对”全球几十万台老防火墙、老 NAT、老 IDS 会不会误判”的拷问。这个拷问,让 TCP 的任何演进都变得几乎不可能。
13.3 为什么选 UDP?—— 不是因为它快
墨叔:既然 TCP 改不了,那怎么办?答案是——换掉 TCP,用 UDP。
你:UDP?UDP 不是”不可靠、会丢包”吗?用它做 HTTP?
墨叔:这就是全章最反直觉的地方。先纠正一个流传极广的误解:
选 UDP,不是因为 UDP 快。UDP 本身一点也不快,它反而”什么都没有”——没有可靠传输、没有拥塞控制、没有连接。
那为什么选它? 因为 UDP 有一样 TCP 没有的、决定性的东西:
UDP 是”最小”的传输协议——它的头只有 8 个字节,而且中间设备几乎不检查它的内容。
你:不检查?所以中间设备不会”误判”UDP 的新特性?
墨叔:对!这就是关键。回想 13.2 节:TCP 被中间设备”锁死”了,因为中间设备深入检查 TCP 头。而 UDP:
- 中间设备(防火墙、NAT)对 UDP 的态度是”要么放行,要么整个丢掉”,它们不深入检查 UDP 的负载内容。
- 所以,你可以在 UDP 的负载里,自己实现任何东西——包括”改进版的可靠传输”,中间设备不会因为看不懂而阻断你。
你:所以 UDP 是”一块空白的画布”,TCP 是”已经被写满、改不动了的纸”?
墨叔:这个比喻极好。UDP 的价值,不是”快”,而是”自由”——它是唯一一个”中间设备不深查负载”的传输协议,所以它是唯一一个”你还能在上面自由实现新传输语义”的地方。
一句话总结:
HTTP/3 选 UDP,不是因为 UDP 好,而是因为 TCP 已经”僵死”了,而 UDP 是剩下的、唯一还能”自己造可靠传输”的地基。
13.4 QUIC:在 UDP 上自己造一个”更好的 TCP”
墨叔:既然 UDP 本身不可靠,那 QUIC 就得在 UDP 之上、在用户态(应用层)自己实现一套可靠传输。
你:用户态?就是”在自己的程序里实现”,而不是”在内核里实现”?
墨叔:对。这是 QUIC 的一个巨大优势,也是代价。先看它自己实现了什么:
QUIC 在用户态重新实现了 TCP 的核心能力:
| TCP 的能力 | QUIC 怎么重做 |
|---|---|
| 可靠传输(不丢) | 自己的序号 + ACK + 重传 |
| 拥塞控制 | 自己的拥塞控制算法(可更新) |
| 流控 | 自己的流控 |
| 有序性 | 按”流”独立保证有序,而不是按”连接”整体有序 ← 关键 |
你:最后一条是解决队头阻塞的关键?
墨叔:对。这是 QUIC 和 TCP 最本质的区别:
- TCP:一条连接一个”有序字节流”。所有数据(不管属于哪个上层流)都排在一个队里,一个包丢,全队卡。
- QUIC:一个连接里有多个独立的”流”,每个流自己保证自己的有序和可靠。流 A 丢包,只重传流 A,流 B、C 完全不受影响。
这就是”队头阻塞”的真正终结:
QUIC 把”有序性”和”可靠性”下沉到了”每个流”,而不是”整个连接”。所以一个流的丢包,不再拖累其它流。
你:所以 HTTP/3 的队头阻塞,才真正从根上解决了?
墨叔:对。回忆一下这个演进链,你会看到一条清晰的逻辑:
HTTP/1.1:一个连接一个请求,串行 → 队头阻塞在"请求"层
HTTP/2:一个连接多路复用(流)→ 队头阻塞下沉到"TCP 连接"层
HTTP/3:QUIC 每流独立 → 队头阻塞被消灭在"流"这一层
每一代,都把队头阻塞往”更细的粒度”解决。 直到 QUIC 把它拆到”流”这个最小单位,才真正解决干净。
13.5 0-RTT 与连接迁移:QUIC 的两个杀手锏
墨叔:除了队头阻塞,QUIC 还有两个 TCP 做不到的杀手锏。
杀手锏一:0-RTT 握手
你:0-RTT?就是”零往返”?不用握手?
墨叔:严格说是”重连时零往返”。
- TCP + TLS 的经典握手:要 2-3 个 RTT 才能开始发数据。
- QUIC 把传输握手和加密握手合并了,首次连接只需 1 RTT。
- 而再次连接(你之前连过这个服务器)时,客户端可以带着缓存的信息直接发数据,0 RTT——数据跟着第一个包就出去了。
你:所以 QUIC 把”建立连接”这件事也优化了?
墨叔:对。而且 0-RTT 对”移动端、频繁重连”的场景是巨大的利好——用户在网络间切换时,重连几乎无感。
杀手锏二:连接迁移(Connection ID)
墨叔:这是 QUIC 最优雅的设计之一。先理解 TCP 的痛点:
TCP 用四元组(源 IP + 源端口 + 目的 IP + 目的端口)来标识一个连接。问题:你从 WiFi 切换到 4G,你的 IP 变了,于是四元组变了,TCP 连接就断了,必须重新握手。
你:所以手机切换网络,TCP 连接必断?
墨叔:对。而 QUIC 用一个独立的 Connection ID 来标识连接,不依赖 IP/端口。
连接 = Connection ID(一个随机 ID),而不是 四元组
于是:你从 WiFi 切到 4G,IP 变了,但你的 Connection ID 没变,QUIC 连接继续存活,无缝迁移。
你:所以”换 WiFi 不掉线”,就是靠这个 Connection ID?
墨叔:对。它把”连接”从”物理网络位置(IP/端口)“上解耦了——连接成了一个”逻辑实体”,不随物理位置变化而中断。这对移动场景是革命性的。
13.6 QPACK:为什么 HTTP/3 要重做首部压缩
墨叔:第 12 章埋的伏笔,这里收网——为什么 HTTP/3 要重新设计首部压缩,把 HPACK 换成 QPACK?
你:HPACK 不是挺好的吗?
墨叔:因为 HPACK 有个和 QUIC 冲突的根本问题:HPACK 的动态表是”顺序依赖”的。
回忆第 12 章:HPACK 的动态表,后一条条目的索引依赖前一条的插入顺序。在 HTTP/2(TCP 有序)下,这没问题——因为所有字节都是按序到达的。
但 QUIC 是乱序的(每个流独立),于是出问题了:
- 流 A 的首部往动态表加了条目 X。
- 流 B 的首部引用了条目 X。
- 但流 B 的帧先到达,流 A 的帧还没到。
- 接收方解码流 B 时,发现”引用的条目 X 还不存在”——要么报错,要么阻塞等待流 A。
你:所以 HPACK 的”顺序依赖”和 QUIC 的”乱序”是矛盾的?
墨叔:对。QPACK 的解法是把”表”拆成两部分,解耦:
- 编码器侧的动态表(发送方维护)。
- 解码器侧的动态表(接收方维护)。
- 两者之间通过”确认(acknowledgement)“来同步。
关键设计:流 B 引用的条目,必须等”这个条目已经被确认到达解码器”之后,才能被引用。 这样就不会出现”引用不存在的条目”。
你:所以 QPACK 是”用确认机制,换掉 HPACK 的’隐式顺序保证’”?
墨叔:精确。这又是一个深刻的权衡:
HPACK 靠”有序”免费获得一致性(顺序本身就保证了表同步);QPACK 在”乱序”的世界里,必须用”显式确认”来重新获得一致性。 有序时,“顺序”是免费的同步机制;乱序时,你得用”确认”来付费。
13.7 代价:QUIC 不是免费的午餐
墨叔:讲了这么多好处,必须说代价。QUIC 不是银弹。
代价一:CPU 更高。 TCP 在内核里跑(内核态,高度优化,硬件 offload);QUIC 在用户态跑(你的程序里),加密、拥塞控制都靠 CPU 算。CPU 开销明显更高。
代价二:调试困难。 TCP 有成熟的工具链(tcpdump、ss、netstat)。QUIC 是加密的、用户态的,抓包看不懂(全是密文),调试难度大。
代价三:UDP 被”歧视”。 很多企业网络、老旧防火墙直接封锁 UDP(尤其是非 DNS 的 UDP)。这导致 QUIC 在这些网络里直接连不上,只能回退到 TCP。
代价四:回退的复杂性。 因为 UDP 可能被封,HTTP/3 必须支持优雅回退到 HTTP/2/TCP。这个”多路回退”的复杂度,是部署的负担。
你:所以 HTTP/3 的部署现状是”能用就用,不行回退”?
墨叔:对。现在的策略是:通过 Alt-Svc 头(或 DNS 的 HTTPS 记录)告诉客户端”我也支持 h3”,客户端尝试连 h3,连不上就回退 h2。 渐进式部署。
13.8 设计者视角:这一章教会你的”协议演进”智慧
墨叔:这一章表面讲 HTTP/3,实质是全书”协议演进”思想的一次集中演练。给你提炼四条:
智慧一:瓶颈可能在协议之外。 HTTP 优化了 30 年,最后发现瓶颈在 TCP。一个协议的性能,受限于它脚下的每一层。 所以诊断性能,要会”看下面那层”。
智慧二:协议会”僵化”,被无法升级的中间设备锁死。 TCP 的悲剧是:它太成功了,成功到被全世界的中间设备”依赖”,于是改不动了。 这是所有”基础设施协议”的宿命。
智慧三:僵化的解法,不是”硬改”,是”换地基”。 既然 TCP 改不了,就在 UDP 上重新造。QUIC 的本质是”绕开僵化的 TCP,在自由的 UDP 上重建传输层”。
智慧四:自由是有代价的。 UDP 的”自由”(中间设备不深查),换来了”可以自己造传输层”,但也带来了”CPU 高、调试难、可能被封”。每一次”绕开”,都要付出新的代价。
13.9 Go 落地:quic-go 生态
墨叔:Go 里 HTTP/3 的支持,主要靠 quic-go 这个库(github.com/quic-go/quic-go):
// 简化示意:用 quic-go 起一个 HTTP/3 服务器
import "github.com/quic-go/quic-go/http3"
server := &http3.Server{
Addr: ":443",
Handler: myHandler,
}
server.ListenAndServe() // 需要配 TLS 证书你:标准库没有,得用第三方库?
墨叔:对。目前 Go 标准库还没内置 HTTP/3,quic-go 是事实标准。注意:quic-go 在 Windows 上需要一些额外处理(它依赖 UDP 的某些特性),生产环境要谨慎测试。
一个提醒:别为了”追新”强行上 HTTP/3。它的收益在”弱网、移动端、频繁重连”场景最大,普通的内网服务用 HTTP/2 甚至 HTTP/1.1 可能更简单、更好调。选协议,永远先看”我的场景需要什么”,而不是”什么最新”。
13.10 排查清单:HTTP/3 相关问题
第 1 步:确认真的走了 h3 吗?
→ 浏览器 DevTools Protocol 列显示 "h3" / "HTTP/3"
第 2 步:UDP 被封了吗?
→ 很多企业网/防火墙封 UDP → h3 连不上 → 回退 h2
第 3 步:是弱网场景吗?
→ h3 的优势在弱网/移动;强网下 h2/h3 差别不大
第 4 步:CPU 扛得住吗?
→ h3 用户态加密+拥塞控制,CPU 更高
第 5 步:中间设备支持吗?
→ 老 LB/代理可能不支持 QUIC,需要确认链路
思考题
第 1 题(概念)
为什么 HTTP/3 选 UDP 而不是”改进 TCP”?请从”中间件僵化”的角度解释,并说明 UDP 的”自由”具体体现在哪。
第 2 题(概念)
HTTP/2 的队头阻塞和 HTTP/3 的队头阻塞,有什么区别?为什么说”HTTP/3 才真正解决了队头阻塞”?
第 3 题(设计)
QUIC 用 Connection ID 实现了”连接迁移”(换 IP 不掉线)。请你设计:如何让一个”连接”在客户端 IP 变化时仍然有效,同时还要防止攻击者”劫持”别人的连接?(提示:Connection ID 要满足什么安全要求?)
第 4 题(生活类比)
墨叔:TCP 的”僵化”和 HTTP/3 的”换地基”,可以用一个日常场景来理解——一条堵死的、改不动的主干道,和一条新修的、灵活的高速公路。
请回答:
- “堵死的、改不动的主干道”对应什么?为什么它”改不动”?
- “新修的灵活高速”对应什么?
- “修新路”比”改旧路”的代价是什么?(提示:新路要重新建、新路的入口可能被某些地方封锁)
第 5 题(更深一层的发问)
“中间件僵化”是 HTTP/3 诞生的根本原因。请你深入思考:
- 中间件僵化的根源是什么?(提示:想想”中间设备为什么要检查协议内容”,以及”检查”和”透明转发”的矛盾)
- 有没有办法预防一个新协议将来也”僵化”?(提示:Google 发明的 GREASE 机制是干嘛的?)
- 这给”设计长期演进的协议”什么启示?
参考答案
第 1 题答案
为什么不能改进 TCP:因为中间件僵化——TCP 被遍布全球的海量中间设备(防火墙、NAT、IDS)所”依赖”,这些设备基于”当年的 TCP”来检查流量。任何对 TCP 头、握手、语义的改动,都可能被这些老设备误判为”异常流量”而丢弃或重置。而这些设备无法升级、无法协调,所以 TCP 事实上”改不动”了。
UDP 的”自由”体现在哪:中间设备对 UDP 的态度是”要么放行、要么整个丢弃”,它们不深入检查 UDP 的负载内容。所以你在 UDP 负载里实现任何东西(比如新的可靠传输),中间设备不会因为”看不懂”而阻断。UDP 是唯一一个”中间设备不深查内容”的传输协议,因此是唯一一个”还能自由实现新传输语义”的地基。
一句话:TCP 是被”写满、锁死”的纸,UDP 是”空白、自由”的画布。选 UDP 不是因为它快,而是因为它是唯一还”自由”的地方。
第 2 题答案
HTTP/2 的队头阻塞:TCP 层的队头阻塞。HTTP/2 的所有流共享一个 TCP 连接,而 TCP 是”有序可靠”的——一个数据包丢失,TCP 必须重传,且丢失包之后的所有字节(无论属于哪个流)都要排队等待重传完成,才能按序交给应用层。所以”一个包丢”会卡住”所有流”。
HTTP/3 的队头阻塞:在流这一层被消灭了。QUIC 把”有序性”和”可靠性”下沉到了每个独立的流,而不是”整个连接”。流 A 丢包,只重传流 A,流 B、C 的数据完全不受影响,照常乱序、照常交付。
为什么说 HTTP/3 才真正解决:
- HTTP/1.1:队头阻塞在”请求”层(一个连接一个请求,串行)。
- HTTP/2:队头阻塞下沉到”TCP 连接”层(多路复用,但共享 TCP)。
- HTTP/3:队头阻塞被拆到”流”层并独立处理(QUIC 每流独立有序可靠)。
直到把”有序可靠”拆到”流”这个最小单位,一个流的问题才不再拖累其它流——这才是队头阻塞的真正终结。
第 3 题答案
连接迁移的机制:用独立的 Connection ID 标识连接,不依赖四元组(IP/端口)。客户端 IP 变了,但 Connection ID 不变,连接继续有效。
安全要求(防劫持),这是本题的关键:
- Connection ID 必须随机、不可预测:如果 Connection ID 是可预测的(比如自增),攻击者就能猜到别人的 ID,从而”劫持”连接(把数据包伪装成来自那个连接)。
- 必须验证数据包的来源:QUIC 用加密(每个包的包号用连接密钥加密/认证)来保证——只有真正握过手、持有密钥的双方,才能发出合法的包。攻击者即使知道 Connection ID,也无法伪造合法的加密包。
- 连接迁移本身要重新验证:当客户端从一个新 IP 发来数据时,QUIC 会用”路径验证”(Path Validation)机制确认”这个新 IP 真的是原来的客户端”,而不是攻击者伪装成”迁移”。
核心矛盾与解法:
“允许 IP 变化”(连接迁移)和”防止冒充”(安全)是一对矛盾。 解法是:用”随机不可预测的 ID”防”猜”,用”加密认证”防”伪造”,用”路径验证”防”冒充迁移”。 三管齐下,才能”既灵活又安全”。
这和第 9 章 Cookie、第 11 章 token 的”持有即身份”缺陷是同一个主题:标识(Connection ID / Cookie / token)本身只解决”识别”,不解决”验证”。真正的安全,永远要靠”加密认证”来兜底。
第 4 题答案
| 比喻 | 对应 |
|---|---|
| 堵死的、改不动的主干道 | TCP——被中间设备锁死,改不动 |
| 新修的灵活高速 | QUIC(在 UDP 上)——绕开 TCP,重建传输层 |
| 修新路的代价 | QUIC 的代价:CPU 更高(用户态)、调试难、UDP 可能被封 |
1. 为什么主干道”改不动”:主干道(TCP)已经承载了海量的、无法协调升级的”检查站”(中间设备:防火墙、NAT、IDS)。你想拓宽车道、改规则,但这些检查站不认识新规则,会把合法的车当成”违规”拦下来。你又管不着这些检查站(遍布全球、无人维护),所以”改旧路”事实上不可能。
2. 新修高速对应 QUIC:既然旧路改不了,就新修一条路(在 UDP 上),新路可以按最新的设计来,不受旧检查站约束。
3. 修新路的代价:
- 要重新建(CPU 高、要自己实现可靠传输)。
- 入口可能被封(很多网络直接封锁 UDP,等于”新路在有些地方根本不通”)。
- 要和旧路共存(回退机制,h3 连不上就回退 h2)。
升华:这个比喻揭示了”演进 vs 兼容”的永恒困境——当旧基础设施”僵死”时,你只有两个选择:要么在旧约束里打补丁(越来越难),要么另起炉灶(自由但代价大)。 HTTP/3 选择了后者,QUIC 就是那条”另起炉灶的新高速”。
第 5 题答案
1. 中间件僵化的根源:
根源是**“中间设备的功能”和”协议的透明性”之间的矛盾**:
- 中间设备(防火墙、IDS、NAT、负载均衡)之所以要检查协议内容,是因为它们要干这些活:安全检测(识别攻击流量)、NAT(改写地址,必须理解四元组)、负载均衡(按内容路由)、流量整形(识别应用类型)。
- 但”检查”意味着依赖协议的具体格式——中间设备一旦”看懂了 TCP 头”,它就假设 TCP 头永远长这样。
- 于是:协议一变,中间设备的”理解”就失效,它会把”合法的变化”误判为”异常”。
本质:中间设备的”智能”(检查能力),正是协议”僵化”的来源。越聪明的中间设备,越会把协议锁死。
2. 如何预防新协议也僵化——GREASE:
Google 发明了 GREASE(Generate Random Extensions And Sustain Extensibility) 机制,思路非常巧妙:
协议实现者,在握手时故意发送一些随机的、无意义的”未知值”(填充在 TLS 扩展、QUIC 传输参数等扩展点里),用来”测试”中间设备是否”正确忽略未知值”。
目的:
- 如果中间设备正确忽略了这些随机值 → 说明它”容忍未知”,扩展点还活着。
- 如果中间设备因为看不懂而阻断 → 暴露了”这里有个中间设备会锁死这个扩展点”,提醒大家”这个扩展点别用,会被中间设备卡死”。
一句话:GREASE 是”定期往扩展点里塞垃圾,逼中间设备保持’对未知宽容’的习惯”,防止扩展点因为”长期没人用”而慢慢被中间设备”锁死”。
3. 对设计长期演进协议的启示:
- 预留扩展点,并保持它们”活着”:光预留扩展点不够,还要(像 GREASE 那样)定期使用、测试,否则扩展点会”名存实亡”。
- 让中间设备”少检查、多透传”:协议设计上,尽量让中间设备不需要深入理解内容(比如加密让中间设备看不到内容,只能透传)。中间设备越”笨”,协议越不容易僵化。
- “透明性”是协议的寿命保险:一个协议如果要求中间设备”聪明地理解它”,就注定会因中间设备的”聪明”而僵化。让中间设备保持”透明、无脑转发”,协议才有长期演进的自由。
最终结论:
僵化的根源是”依赖”。一个协议越是被中间设备”理解”,越会被中间设备”锁死”。 长期演进的协议,必须刻意保持”中间设备透明”——让中间设备看不懂、只能透传。这正是 QUIC 把传输层内容加密的深层动机之一:不只是为了安全,更是为了”让中间设备闭嘴”,保住自己的演进自由。
一页纸小结
HTTP/2 弱网反而更差:一个连接多路复用,丢包时全连接被 TCP 队头阻塞卡死。
中间件僵化:TCP 被海量无法升级的中间设备”锁死”,改不动了。根源是”中间设备的智能检查 = 协议的僵化”。
选 UDP 不是因为快:是因为 UDP 是唯一”中间设备不深查负载”的传输协议,是还能”自由造传输层”的地基。
QUIC 在用户态重造可靠传输:把”有序可靠”下沉到”每个流”,才真正消灭队头阻塞。
两个杀手锏:0-RTT 握手、Connection ID 连接迁移(换 IP 不掉线)。
QPACK 取代 HPACK:HPACK 顺序依赖,与 QUIC 乱序冲突。QPACK 用”确认”换掉”隐式顺序保证”。
代价:CPU 高、调试难、UDP 可能被封、需回退。
核心智慧:瓶颈可能在协议之外;协议会僵化;僵化的解法是”换地基”;自由有代价。
下一章:HTTP/3 讲完了,我们回到一个更”接地气”但同样深刻的问题——WebSocket 是怎么从 HTTP 里”长”出来的? 它揭示了协议设计里一个极其优雅的模式:协议升级(Upgrade)。我要问你一个细节:WebSocket 握手时那个 Sec-WebSocket-Key,其实不是为了安全——那它是为了什么? 答案会让你重新理解”防御性设计”。