HTTP 协议进化史:从纯文本到二进制帧
本文是《计算机网络学习笔记》系列的第六篇。有了 TCP 保证可靠传输,有了 TLS 保证加密安全,应用层终于可以专注于"发什么"——这就是 HTTP 的职责。从 1991 年诞生至今,HTTP 经历了三次重大进化,每一次都是对上一代痛点的定点突破。
一、三个版本,三种设计哲学
HTTP/1.1 ──────► HTTP/2 ──────► HTTP/3
纯文本 二进制帧 QUIC (UDP)
队头阻塞 多路复用 彻底解决阻塞
Header 冗余 头部压缩 0-RTT 握手目前三个版本同时在用。在 Chrome 的开发者工具(DevTools → Network → Protocol 列)里,可以直接看到每个请求具体走的哪个版本。
二、HTTP/1.1:纯文本时代
HTTP/1.1(1997 年)是使用时间最长的版本,核心特征是纯 ASCII 文本,人类可以直接读懂。
请求报文格式
GET /somedir/page.html HTTP/1.1\r\n ← 请求行:方法 + URL + 版本
Host: www.example.com\r\n ← Header(每行 \r\n 结尾)
Connection: keep-alive\r\n
User-Agent: Mozilla/5.0\r\n
Accept-Language: zh-CN,zh;q=0.9\r\n
\r\n ← 空行,标志 Header 结束
(可选的 Body,GET 请求通常没有)请求行三个字段:
- 方法(Method):
GET(获取资源)、POST(提交数据)、PUT(创建/替换)、DELETE(删除)、HEAD(只要头部)等; - URL:资源路径,可以带查询参数(
?key=value); - 版本:
HTTP/1.1。
响应报文格式
HTTP/1.1 200 OK\r\n ← 状态行:版本 + 状态码 + 原因短语
Date: Tue, 18 Aug 2015 15:44:04 GMT\r\n ← Header
Server: nginx/1.24.0\r\n
Content-Length: 6821\r\n
Content-Type: text/html; charset=utf-8\r\n
\r\n ← 空行,Header 结束
<!DOCTYPE html>... ← Body(实际的 HTML 内容)常见状态码速查
| 范围 | 含义 | 典型例子 |
|---|---|---|
| 1xx | 信息性 | 101 Switching Protocols(WebSocket 升级) |
| 2xx | 成功 | 200 OK、201 Created、204 No Content |
| 3xx | 重定向 | 301 永久重定向、302 临时重定向、304 Not Modified(缓存命中) |
| 4xx | 客户端错误 | 400 Bad Request、401 未认证、403 禁止、404 未找到 |
| 5xx | 服务端错误 | 500 内部错误、502 Bad Gateway、503 服务不可用 |
HTTP/1.1 的改进:Keep-Alive
HTTP/1.0 每次请求都要经历"TCP 三次握手 → 发送请求 → 四次挥手"的完整流程,一个网页包含几十个资源,每个都要单独建连接,极其低效。
HTTP/1.1 引入了持久连接(Keep-Alive):一条 TCP 连接可以复用,连续发多个请求,省去了反复握手的开销。
HTTP/1.0:
TCP 握手 → 请求1 → 响应1 → TCP 挥手
TCP 握手 → 请求2 → 响应2 → TCP 挥手
TCP 握手 → 请求3 → 响应3 → TCP 挥手
HTTP/1.1 Keep-Alive:
TCP 握手
→ 请求1 → 响应1
→ 请求2 → 响应2
→ 请求3 → 响应3
TCP 挥手(复用同一连接)HTTP/1.1 的固有痛点
Keep-Alive 解决了"反复握手"的问题,但没有解决根本矛盾:
① 队头阻塞(Head-of-Line Blocking)
HTTP/1.1 的请求必须串行:必须等上一个响应回来,才能发下一个请求。如果某张图片很大、响应很慢,后面所有请求都得排队等待。(HTTP/1.1 虽然支持"管道化 Pipelining",但实现复杂、各浏览器支持参差不齐,几乎没有被实际采用。)
请求1(大图)————————————————→ 响应1(很慢……)
→ 请求2(等着)
→ 请求3(等着)② Header 冗余
每次请求都要把 User-Agent、Cookie、Accept-Language 等完整地发一遍,哪怕这些内容从来不变。一个 Cookie 动辄几百字节,几十个请求就是几十 KB 的纯冗余数据。
③ 明文传输
HTTP/1.1 是明文的,内容对任何中间节点都是透明的,没有加密(HTTPS = HTTP/1.1 + TLS,是后来叠加上去的)。
三、HTTP/2:二进制帧与多路复用
HTTP/2(2015 年,RFC 7540)是对 HTTP/1.1 痛点的系统性解决,保留了 HTTP 的语义(Method、Header、Body),彻底重构了底层传输格式。
核心变化:一切皆帧
HTTP/2 的最小通信单位是帧(Frame),所有 Header 和 Body 都被拆成帧在网络上传输。帧是二进制格式,不再是人类可读的文本。
HTTP/2 帧的标准格式(共 9 字节固定头部):
+-----------------------------------------------+
| Length (24) | 3 字节:Payload 的长度
+---------------+---------------+---------------+
| Type (8) | Flags (8) | | 各 1 字节
+-+-------------+---------------+---------------+
|R| Stream Identifier (31) | 4 字节(R 为保留位)
+-+---------------------------------------------+
| Frame Payload (...) | 可变长度
+-----------------------------------------------+字段详解:
| 字段 | 大小 | 说明 |
|---|---|---|
| Length | 3 字节 | Payload 的字节长度,不含 9 字节头部本身 |
| Type | 1 字节 | 帧类型,决定 Payload 的含义(见下表) |
| Flags | 1 字节 | 特定于 Type 的标志位 |
| R | 1 位 | 保留位,必须为 0 |
| Stream ID | 31 位 | 流标识符,标识该帧属于哪个"请求/响应对" |
| Payload | 可变 | 实际内容,格式由 Type 决定 |
常见帧类型:
| Type 值 | 名称 | 作用 |
|---|---|---|
0x00 | DATA | 传输 HTTP 包体(Body) |
0x01 | HEADERS | 传输 HTTP 头部(Header),使用 HPACK 压缩 |
0x03 | RST_STREAM | 立即终止某个流(如用户取消了加载) |
0x04 | SETTINGS | 协商配置(如最大并发流数量、初始窗口大小) |
0x08 | WINDOW_UPDATE | 流量控制(类似 TCP 的窗口更新) |
0x09 | CONTINUATION | HEADERS 帧的延续(Header 太大时分多帧) |
常用 Flags:
END_STREAM (0x1):这是当前流的最后一帧,发送方不会再发数据了(相当于"话说完了");END_HEADERS (0x4):这是 Header 块的最后一帧。
多路复用:彻底解决队头阻塞
HTTP/2 引入了流(Stream)的概念:一条 TCP 连接上可以同时存在多个逻辑流,每个流承载一对独立的请求/响应,流之间互不干扰。
Stream ID 是区分流的关键:客户端发起的流使用奇数 ID(1、3、5……),服务端发起的使用偶数 ID。
HTTP/2(同一 TCP 连接上并行多流):
TCP 连接
├── Stream 1:请求首页 HTML → [HEADERS] [DATA] ...
├── Stream 3:请求 main.css → [HEADERS] [DATA] ...
├── Stream 5:请求 logo.png → [HEADERS] [DATA] ...
└── Stream 7:请求 app.js → [HEADERS] [DATA] ...
(四个请求完全并行,谁先响应谁先发回来)以前等一张大图导致所有请求阻塞的问题,在 HTTP/2 里消失了——大图走 Stream 5,其他请求走其他 Stream,互不影响。
头部压缩(HPACK)
HTTP/2 引入了 HPACK 算法来压缩 Header:
- 静态表:预定义了 61 个最常见的 Header 字段(如
:method: GET、:status: 200),用一个 1~2 字节的索引代替整个字符串; - 动态表:在连接期间,双方维护一张共享的"已传输 Header"表,后续请求若有相同的 Header,只发送其在表中的索引,不重复发原始字符串;
- Huffman 编码:对表中无法命中的新 Header 进行 Huffman 压缩。
实际效果:Header 体积通常可以压缩到原来的 85%~95%,对 Cookie 很大的场景(如登录后的请求)效果尤为显著。
如何抓 HTTP/2 的明文包?
HTTP/2 几乎总是跑在 TLS 之上(即 HTTPS),直接抓包只能看到密文。可以结合第五篇《TLS 加密握手》介绍的 SSLKEYLOGFILE + Wireshark 方案,在 Wireshark 里用 http2 过滤器,就能看到解密后的 HTTP/2 帧。
四、HTTP/3:彻底抛弃 TCP
HTTP/2 解决了应用层的队头阻塞,但底层 TCP 还有一个无法消除的阻塞:TCP 层的队头阻塞。
当 HTTP/2 把多个流复用在同一条 TCP 连接上时,如果某个 TCP 报文丢失,TCP 必须等到该报文重传成功,才能继续向上层交付数据——哪怕丢失的数据只属于其中一个流,其余所有流也得跟着等。
根本原因:HTTP/2 解决了"应用层的队头阻塞",但 TCP 是一条有序字节流,一旦中间有洞,后面的所有数据都被堵在 TCP 缓冲区里,无法跨越这个洞送给应用层。
HTTP/3 的解法:彻底换掉 TCP,改用 QUIC。
QUIC:基于 UDP 的可靠传输
QUIC(Quick UDP Internet Connections)是 Google 设计、后来由 IETF 标准化的传输层协议(RFC 9000),运行在 UDP 之上,自己实现了可靠传输、流量控制、拥塞控制等功能:
| 特性 | TCP + TLS | QUIC |
|---|---|---|
| 传输层 | TCP | UDP(可靠性由 QUIC 自己实现) |
| TLS | 独立握手(1~2 RTT) | 内置于 QUIC 握手(0~1 RTT) |
| 队头阻塞 | 有(TCP 层) | 无(流之间完全独立) |
| 连接迁移 | 不支持(IP/端口变了连接断掉) | 支持(基于 Connection ID,换网络不断连) |
| 协议头加密 | 明文 | 大部分头部加密,防中间人干扰 |
0-RTT 建连:首次连接需要 1-RTT,之后重连可以在第一个数据包里就携带应用层数据,实现 0-RTT,极大减少延迟。
连接迁移(Connection Migration):手机从 WiFi 切到 4G,IP 地址变了,TCP 连接必然断开需要重建。QUIC 使用 Connection ID 标识连接,而非 IP+端口,网络切换后连接不中断,对移动场景非常友好。
HTTP/3 在国内的现实
HTTP/3 = HTTP over QUIC。由于 QUIC 运行在 UDP 之上,而国内部分运营商对 UDP 流量限速或封锁(UDP 难以有效识别和计费),所以 HTTP/3 在国内的普及度远低于国际。
Google、YouTube、Meta 等国外大厂已全面支持 HTTP/3,但在国内网络环境下,大多数时候会回退到 HTTP/2。
五、三个版本横向对比
| HTTP/1.1 | HTTP/2 | HTTP/3 | |
|---|---|---|---|
| 数据格式 | ASCII 文本 | 二进制帧 | 二进制帧(QUIC) |
| 连接复用 | Keep-Alive(串行) | 多路复用(并行流) | 多路复用(并行流) |
| 队头阻塞 | 应用层 + TCP 层 | 仅 TCP 层 | 无 |
| 头部压缩 | 无 | HPACK | QPACK |
| 加密 | 可选(+TLS) | 实践上必须(+TLS) | 内置(QUIC 内含 TLS 1.3) |
| 底层传输 | TCP | TCP | UDP(QUIC) |
| 普及程度 | 广泛 | 广泛 | 国际为主 |
六、调试工具
Chrome DevTools
在 Network 面板的列表中右键列头,勾选 Protocol,即可看到每个请求实际走的是 http/1.1、h2(HTTP/2)还是 h3(HTTP/3)。
Fiddler(Windows 首选)
Fiddler 不只是"看",还能:
- 改包重放:截获一个请求,修改参数(如 token、参数值),重新发给服务器,是接口调试的利器;
- 设置断点:在请求发出前或响应返回前暂停,手动修改内容;
- 脚本自动化:编写 FiddlerScript 自动修改匹配规则下的所有请求/响应。
httpbin.org
http://httpbin.org 是一个专门用于测试 HTTP 的回显服务,支持 HTTP/1.1(可以用来练习抓包):
# 用 curl 测试一个 GET 请求
curl -v http://httpbin.org/get
# 测试 POST
curl -X POST http://httpbin.org/post -d '{"key":"value"}' -H 'Content-Type: application/json'
# 测试指定状态码
curl -v http://httpbin.org/status/404响应会把你发的请求内容原样返回,非常适合初学时验证"我发出去的包长什么样"。
总结
HTTP 的三次进化,本质上是一次次对网络延迟的宣战:
- HTTP/1.1:确立了基本的请求-响应语义,Keep-Alive 减少了 TCP 握手次数,但队头阻塞和 Header 冗余是硬伤;
- HTTP/2:用二进制帧和多路复用彻底解决了应用层队头阻塞,HPACK 压缩大幅减少 Header 开销,但 TCP 本身的队头阻塞无法消除;
- HTTP/3:通过 QUIC 把 TCP 的最后一块短板也换掉,连接迁移和 0-RTT 让移动场景体验更佳,是方向正确但仍在普及中的未来。
本系列前五篇:
· 第一篇:《TCP 协议格式详解》
· 第二篇:《TCP 三次握手与四次挥手》
· 第三篇:《TCP 可靠数据传输》
· 第四篇:《TCP 流量控制与拥塞控制》
· 第5篇:《TLS:HTTPS 背后的加密握手》
参考资料:《计算机网络:自顶向下方法》