【从0开始学计算机网络】| HTTP 协议的进化史
前言
调 Nginx 时见过 keep-alive,看浏览器 DevTools 时见过 h2,面试里被问过"HTTP/2 为什么快"------这些零散的片段其实都指向同一条主线:HTTP 这三十多年是怎么一步步进化过来的。这篇文章把 1.0 到 3.0 的进化逻辑串起来,重点讲清楚每个版本当时卡在哪儿、怎么解决、又留下了什么新问题。
一、使用场景
这个知识点会在三类场景里反复出现。
排查页面加载慢。一个页面几十个请求,到底慢在建连接、慢在排队、还是慢在传输,得先搞清楚当前走的是哪个协议版本、队头阻塞发生在哪一层。
配置服务器和网关。Nginx 的 keepalive_timeout、upstream 连接池、Spring Boot 的 HTTP/2 开关,这些配置的含义全部挂在版本演进上,不了解演进史就只能照抄配置。
准备面试。HTTP 版本区别是计算机网络的高频考题,只背特性表很难答出层次,理解进化逻辑才能把"为什么"讲清楚。
二、核心概念
HTTP 本质上是一份通信约定:浏览器把"我要这个资源"翻译成一段固定格式的报文发出去,服务器按同样的格式把内容和状态传回来。
GET /index.html HTTP/1.1
Host: www.example.com
这就是一次最小的 HTTP 请求。四个版本各自解决了一个瓶颈,先看总览:
| 版本 | 核心变化 | 解决了什么 |
|---|---|---|
| HTTP/0.9 | 只有 GET 方法 | 只能传 HTML 文档 |
| HTTP/1.0 | 引入状态码和头字段,支持多类型资源 | 内容从文字扩展到图片、音视频 |
| HTTP/1.1 | 长连接、Host 头、分块传输、缓存控制 |
反复重建连接的开销 |
| HTTP/2.0 | 二进制分帧、多路复用、HPACK 压缩 | 同一连接内串行排队 |
| HTTP/3.0 | 底层换成 QUIC,跑在 UDP 上 | TCP 队头阻塞、握手慢、连接不能迁移 |
看这张表的方式不是背,而是顺着"瓶颈→解法"往下读:1.0 卡在连接,1.1 解决了连接又带来排队,2.0 解决了排队又撞上 TCP 本身,3.0 干脆把传输层一起换掉。
三、最小代码
不用写任何后端代码,几条命令就能把版本差异摸一遍。
用 nc 手写一个 HTTP/1.0 请求:
bash
printf 'GET / HTTP/1.0\r\nHost: example.com\r\n\r\n' | nc example.com 80
响应读完连接立刻关闭。同样的请求换成 1.1:
bash
printf 'GET / HTTP/1.1\r\nHost: example.com\r\n\r\n' | nc example.com 80
响应结束后连接会保持一段时间,还能在这条连接上继续发第二个请求------这就是长连接最直观的形态。
再用 curl 看协议协商结果:
bash
curl -sv --http1.1 -o /dev/null https://example.org 2>&1 | grep -i http
curl -sv --http2 -o /dev/null https://example.org 2>&1 | grep -i http
后一条的输出里会出现 Using HTTP2 的字样,说明服务端支持且协商成功。想在自己项目里打开 HTTP/2,Spring Boot 加一个配置即可(配置键随版本略有差异,以所用版本的官方文档为准):
yaml
server:
http2:
enabled: true
四、执行流程
同一次页面加载,在四个版本下走的是完全不同的流程。
**HTTP/1.0:一个请求一条连接。**页面里要用到 index.html、main.js、style.css 和 logo.png 四个资源,浏览器就得做四轮"三次握手、发请求、收响应、四次挥手",时间大头花在建连接而不是传数据上。1.0 真正的历史意义,是把可传输的内容从纯文字扩展到了图片、音视频、CSS 和 JS。
**HTTP/1.1:连接复用,但请求串行。**一条 TCP 连接建立后反复用于多轮请求和响应,建连接的开销被摊薄,握手次数大幅下降;Host 头让一台服务器能靠域名区分多个站点,虚拟主机因此跑了起来。但同一条连接里,响应必须按请求顺序返回:第一个请求是一张几 MB 的大图,后面的小请求就算服务器早就处理完,也得排队等------这就是队头阻塞。
**HTTP/2.0:二进制分帧加多路复用。**报文拆成二进制帧,同一连接上的多路数据交错传输,不存在排队。两个配套优化:HPACK 让 Cookie 和 User-Agent 这类每次重复的字段只传增量;Server Push 让服务器在浏览器请求 HTML 时顺手把关联资源推过去。要注意,HTTP/2 消灭的只是 HTTP 层的排队,传输层的排队原封不动------TCP 坚持按序交付,中间缺了一个包,后面已到达的数据全部积压,整条连接上的流都得陪着等。
**HTTP/3.0:把传输层从 TCP 换成 QUIC。**协议栈从 HTTP → TCP → IP 变成 HTTP → QUIC → UDP → IP。QUIC 在用户态实现可靠传输,每条流独立做丢包恢复,一条流丢包不再拖累别的流;握手与 TLS 合并,首次连接 1-RTT,重连可 0-RTT;连接靠连接 ID 标识而不是 IP 加端口,手机从 WiFi 切到移动网络连接不断,这对移动端体验是质的变化。
五、常见坑
**把 HTTP/2 当万能提速。**高丢包的弱网环境下它可能反而更慢:1.1 时代浏览器对同一域名开 6 条连接,丢包只卡住一条;HTTP/2 把所有请求挤进一条 TCP 连接,丢一次包全部卡住。
**协议版本没生效却不自知。**Nginx 监听参数里少了 http2,或 TLS 握手没协商出 ALPN,浏览器会静默降级回 1.1,页面照样能打开,只是快不起来。这种问题只能靠 DevTools 的 Protocol 列或 curl 抓出来。
**keep-alive 两端超时不一致。**Nginx 到后端开着 keepalive 连接池,Tomcat 的 keep-alive 超时却比 Nginx 短,Nginx 拿一条已被后端关掉的连接发请求,就会出现偶发 502 或 connection reset。两头超时要配成上游略长于下游。
**把 Server Push 当长期依赖。**它的实际收益长期低于预期,Chrome 从 106 版本起已移除支持,新设计用 preload 或 103 Early Hints 代替。
**默认信任 0-RTT 的重放安全性。**QUIC 重连时的 0-RTT 数据会在服务器确认身份之前就被处理,攻击者可以原样重放这段数据来重复触发操作。下单、支付这类非幂等请求不能放进 0-RTT,需要在应用层单独拦截。
六、验证方式
每条演进结论都能在本机验证。
验证长连接:curl -v 连续请求同一站点两次,输出里第二次会出现 Re-using existing connection;加 --http1.0 再试,两次都会新建连接。
验证当前页面的协议版本:DevTools 的 Network 面板,表头右键勾选 Protocol 列,能看到每个请求走的是 h2 还是 http/1.1。
验证队头阻塞:1.1 下对同一域名并发请求一个大文件和几个小文件,观察小文件的 Queueing 和 Stalled 时间;同样的请求切到 HTTP/2,小文件不再排在大文件后面。
连接迁移的完整验证需要 QUIC 环境,成本较高,可以退而求其次:读 RFC 9000 里连接 ID 的章节,或用 wireshark 抓 QUIC 包看 Initial 包里的连接 ID 字段。
七、总结
一个可迁移的方法:看协议演进不要背特性表,先问当时卡在哪个瓶颈。1.0 卡在连接,1.1 卡在排队,2.0 卡在 TCP,3.0 把传输层一起换掉。带着这个框架去看 Nginx、Tomcat、Netty 的连接管理配置,或者去答面试题,都会顺很多。