HTTP 发展简史:从 HTTP/1.0 到 HTTP/3
前面三篇我们把 TCP 怎么把字节可靠送达讲透了。现在往上走一层:这些字节表示什么?这就是应用层协议 HTTP 的职责。很多人天天写接口、调接口,但说不清 HTTP 这些年到底变了什么。
讲 HTTP 演进,最忌讳变成背年表。真正有价值的视角是:每一代协议都在修上一代留下的具体性能漏洞,而且这些漏洞和它脚下的传输层特性死死绑在一起。 你会看到一条清晰的因果链——HTTP/1.1 的队头阻塞逼出了 HTTP/2 的多路复用,而 HTTP/2 没解决的 TCP 层队头阻塞,又逼出了 HTTP/3 干脆换掉 TCP。
所以这一篇仍然先机制后设计:先看清每一代具体怎么收发请求、卡在哪个机制上,再看下一代用什么机制补这个洞。 抓住"队头阻塞"这条主线,整段历史就立起来了。
一、HTTP/0.9 与 1.0:请求-响应模型的雏形
最早的 HTTP/0.9 简单到只有一行 GET /index.html,连头部和状态码都没有,服务器直接吐回 HTML 就关连接。
HTTP/1.0 把这个模型补完整了:引入了请求行 + 头部 + 空行 + 实体的报文格式,有了方法、状态码、Content-Type 等。它确立了沿用至今的请求-响应骨架。
但它有个要命的机制缺陷:默认短连接。每发一个请求,就要新建一条 TCP 连接,响应完就关闭。
回想前面学的:建一条 TCP 连接要三次握手,花一个 RTT;如果是 HTTPS 还要叠加 TLS 握手。一个稍微复杂的网页有几十个资源(HTML、CSS、JS、图片),每个资源都要重新握手、重新慢开始爬 cwnd——连接建立的开销,远远超过了传一个小资源本身的开销。
HTTP/1.0 最大的代价不在于传数据,而在于反复建连。每个资源一次握手、一次慢开始,RTT 和
cwnd爬升被白白浪费。这正是后续所有优化的起点。
二、HTTP/1.1:用持久连接和管线化续命二十年
HTTP/1.1 是历史上生命力最强的版本,至今仍大量在用。它针对 1.0 的痛点做了几个关键机制改进:
- 默认持久连接(Keep-Alive):一条 TCP 连接默认不关,可以连续承载多个请求-响应。这一下省掉了反复握手和反复慢开始的巨大开销,是最重要的改进。
Host头:让一台服务器(一个 IP)能托管多个域名(虚拟主机)。没有它,就没有后来的虚拟主机和共享 IP 的云托管。- 分块传输编码(chunked):响应可以不预先知道总长度,一块一块地发,适合动态生成的内容和流式响应。
- 更完善的缓存控制(
Cache-Control、ETag)和范围请求(Range,支持断点续传)。
但持久连接带来一个新问题:一条连接上能不能同时跑多个请求?HTTP/1.1 尝试用管线化(pipelining)——允许不等前一个响应回来就发下一个请求。可它栽在一条铁律上:
HTTP/1.1 要求响应必须按请求的顺序返回。 即使第 2 个请求的响应早就准备好了,只要第 1 个响应还没生成完,它就得排队干等。这就是应用层队头阻塞(Head-of-Line Blocking)。
管线化因为这个缺陷几乎没被真正启用。浏览器只能用一个笨办法绕过去:对同一个域名同时开 6 条 TCP 连接,靠并行连接提升并发。但这又带来新开销——6 条连接就是 6 次握手、6 份 cwnd 慢开始、6 份服务器内存。治标不治本。
三、HTTP/2:用多路复用干掉应用层队头阻塞
HTTP/2 的设计目标非常明确:在不改变 HTTP 语义(方法、状态码、头部含义全不变)的前提下,彻底改造传输方式,解决 1.1 的队头阻塞和连接低效。
它的核心机制是二进制分帧 + 多路复用:
- 二进制分帧:HTTP/2 不再用文本,而是把消息拆成一个个二进制帧(frame)。每个帧头里带一个流 ID(stream ID),标明它属于哪个请求。
- 多路复用(multiplexing):多个请求和响应被拆成帧后,可以在同一条 TCP 连接上交错传输。属于不同流的帧混在一起发,接收端按流 ID 重新组装。
这一下解决了应用层队头阻塞:第 1 个请求的响应还没好不要紧,第 2、3 个请求的帧可以照常传、照常先返回——它们靠流 ID 区分,互不阻塞。一条连接就能承载成百上千个并发请求,浏览器不必再开 6 条连接。
此外 HTTP/2 还引入:
- 头部压缩(HPACK):HTTP 头部有大量重复(每个请求都带几乎一样的
User-Agent、Cookie)。HPACK 用静态表 + 动态表 + 哈夫曼编码,把冗余头部压到很小。 - 服务器推送(Server Push):服务器可以在客户端请求 HTML 时,主动把它即将需要的 CSS、JS 一起推过去。(实践中收益有限、复杂度高,后来逐渐被弃用。)
四、HTTP/2 没能解决的:TCP 层队头阻塞
HTTP/2 看起来很完美,但它栽在一个它自己控制不了的地方——它脚下还是 TCP。
回忆 19 篇讲的:TCP 保证字节流严格有序交付。接收端的某个段丢了,后面的段即便到了也只能在接收缓冲区里干等,不交付给上层(累计确认下窗口卡住)。
把这个特性和 HTTP/2 的多路复用放一起,问题就来了:HTTP/2 的多个流共享同一条 TCP 连接。如果 TCP 层丢了一个段,而这个段恰好属于流 A——TCP 不知道也不在乎流的概念,它只会把后续所有字节(哪怕属于完全无关的流 B、流 C)全部卡住,直到流 A 那个段被重传补齐。
HTTP/2 在应用层消灭了队头阻塞,却被 TCP 在传输层重新引入了队头阻塞。一个包丢失,连接上所有流一起遭殃。这不是 HTTP/2 的设计失误,而是它建立在 TCP 之上的宿命——只要还用 TCP,这个洞就堵不上。
弱网、高丢包环境下,这个 TCP 层队头阻塞会让 HTTP/2 的表现甚至不如开多条连接的 HTTP/1.1(因为多条连接里,一条丢包不影响其他连接)。
五、HTTP/3:干脆把 TCP 换成 QUIC
既然问题的根在 TCP,HTTP/3 的选择简单粗暴:不用 TCP 了,改用 QUIC。 而 QUIC 跑在 UDP 之上。
为什么是 UDP?因为 TCP 的有序交付逻辑固化在操作系统内核里,改不动;而 UDP 只是个最薄的数据报通道,QUIC 可以在它之上的用户态自由实现一套全新的、理解"流"概念的可靠传输。QUIC 带来几个关键机制改进:
- 真正的流级多路复用:QUIC 原生支持多个独立的流,每个流有自己的有序保证。流 A 丢包只阻塞流 A,流 B、流 C 照常交付——彻底根除了 TCP 层队头阻塞。这是 HTTP/3 最核心的收益。
- 更快的建连:QUIC 把传输握手和 TLS 1.3 加密握手合并,首次连接通常 1 个 RTT 就能开始传数据;对访问过的服务器还支持 0-RTT,第一个请求就能带数据,几乎零等待。(对比:TCP + TLS 要好几个 RTT。)
- 连接迁移:QUIC 用一个连接 ID 标识连接,而非 TCP 的四元组(源/目的 IP + 端口)。所以手机从 Wi-Fi 切到蜂窝、IP 变了,连接也不断——这对移动场景极其友好。
这张图把演进的因果链串成一条:每个箭头上的标注,就是"上一代的痛点 + 下一代的解法"。从短连接到持久连接、到应用层多路复用、再到传输层换底,每一步都在填上一步暴露出来的、且越来越深的队头阻塞洞。
六、看懂机制后,再问:为什么是这样演进
为什么不一步到位,非要分这么多代? 因为每一代的瓶颈,只有在上一代普及、网页规模涨上来之后才会暴露。HTTP/1.1 时代网页还没那么重,队头阻塞不致命;等到单页应用、几百个资源成为常态,它才成了非解决不可的问题。协议演进是被真实负载逼出来的,不是设计者预先想全的。
为什么始终保持 HTTP 语义不变? 方法、状态码、头部这套语义是 Web 生态的契约,无数应用和中间件依赖它。HTTP/2、HTTP/3 只动"怎么传"(传输与编码),不动"传什么"(语义)。这让升级可以平滑——你的业务代码几乎不用改,换个底层实现就能享受性能红利。
为什么 HTTP/3 宁可牺牲 TCP 的成熟生态也要换 UDP? 因为 TCP 的队头阻塞是内核级的、改不动的硬约束。在用户态基于 UDP 重写,虽然要自己实现可靠性、拥塞控制(等于把 TCP 那套重做一遍),但换来了流级独立和快速迭代的自由——协议逻辑在应用里,更新一个客户端就能升级,不必等操作系统。
七、和工程实践 / 后端开发的联系
- 理解性能优化的时代背景:HTTP/1.1 时代流行的"雪碧图、域名分片、合并 JS"等优化,本质都是在绕开队头阻塞和多连接开销。到了 HTTP/2,这些优化反而可能有害(多路复用下不需要分片,雪碧图破坏了细粒度缓存)。优化手段要跟着协议版本走。
- HTTP/2 的连接复用:一个域名一条连接承载所有请求,意味着后端要重新考虑连接级的限流、超时、负载均衡粒度。
- HTTP/3 的部署位置:通常在 CDN 边缘和负载均衡器终止 HTTP/3,回源到后端可能仍是 HTTP/1.1 或 HTTP/2。所以 HTTP/3 的收益主要在"用户到边缘"这一段弱网链路上。
- 降级与兼容:HTTP/3 靠
Alt-Svc头通告"我支持 h3,下次用 UDP 来"。客户端会优雅降级——UDP 被防火墙挡了就退回 TCP。理解这套协商,才能排查"为什么有的用户没走上 HTTP/3"。
学习这一部分最容易踩的坑
1. 把"队头阻塞"当成一个概念
它其实有两层:HTTP/1.1 的应用层队头阻塞(响应必须按序返回),和 HTTP/2 仍存在的 TCP 传输层队头阻塞(一个段丢了卡住所有流)。HTTP/2 只解决了前者,HTTP/3 才解决了后者。混在一起就理解不了 HTTP/3 存在的意义。
2. 以为 HTTP/2 多路复用就万事大吉
多路复用在应用层并发,但所有流仍挤在一条 TCP 连接上,丢包时一损俱损。弱网下 HTTP/2 可能不如多连接的 HTTP/1.1。
3. 以为换版本要改业务代码
HTTP 语义(方法、状态码、头部)跨版本不变。升级 HTTP/2、HTTP/3 改的是传输层实现,业务代码基本无感。
4. 把 HTTP/3 等同于"更快"
HTTP/3 的主要收益在弱网、高丢包、移动切换场景。在稳定的低延迟内网,它相比 HTTP/2 优势不明显,甚至因 UDP 处理开销略逊。要看场景,不要无脑上。
总结
这一篇我们用"队头阻塞"这条主线串起了 HTTP 的演进,坚持看清每一代的机制瓶颈:
- HTTP/1.0 默认短连接,反复握手和慢开始的开销是一切优化的起点;
- HTTP/1.1 用持久连接续命,但响应必须按序返回,造成应用层队头阻塞,只能靠多开连接绕过;
- HTTP/2 用二进制分帧 + 流 ID 实现多路复用,消灭应用层队头阻塞,并用 HPACK 压缩头部;
- HTTP/2 受制于 TCP 的有序交付,一个段丢失会阻塞连接上所有流——传输层队头阻塞堵不上;
- HTTP/3 改用 QUIC(跑在 UDP 上)实现流级独立、合并握手(1-RTT/0-RTT)、连接迁移,根除传输层队头阻塞;
- 每一代都保持 HTTP 语义不变、只改传输方式,所以升级对业务代码基本无感。
理解了这条因果链,你再看具体的报文结构和 HTTPS,就有了清晰的上下文——下一篇我们就深入 HTTP 报文,看方法、状态码、头部、实体每一部分到底在表达什么。
参考资源:
- 《Web 性能权威指南》(High Performance Browser Networking)
- 《图解 HTTP》
- RFC 9114 - HTTP/3
- RFC 9000 - QUIC: A UDP-Based Multiplexed and Secure Transport