HTTP(HyperText Transfer Protocol)是 Web 的基石。从 HTTP/1.0 到 HTTP/2,每一次演进都解决了前一代的核心痛点。
一、HTTP/1.0 的问题
1.1 短连接之痛
HTTP/1.0 默认短连接:
GET /a.css ─────→
←───── 200 OK + 数据
(TCP 关闭)
GET /b.js ──────→
←───── 200 OK + 数据
(TCP 关闭)
GET /c.png ─────→
←───── 200 OK + 数据
(TCP 关闭)
问题:每个请求需要独立的 TCP 三次握手,延迟高
1.2 队头阻塞
浏览器同时只能发 6 个并行请求(同域名):
请求 1 ──────────────→
请求 2 ──────→
请求 3 ────────→
请求 4 ──→
请求 5 ───────────→
请求 6 ─→
第 7 个请求必须等待前 6 个之一完成
→ 即使是小资源,也可能被大资源阻塞
二、HTTP/1.1 改进
2.1 持久连接(Keep-Alive)
Connection: keep-alive
GET /a.css ─────→
←───── 200 OK + 数据
GET /b.js ──────→ (复用同一 TCP 连接)
←───── 200 OK + 数据
GET /c.png ─────→ (复用同一 TCP 连接)
←───── 200 OK + 数据
(TCP 可选复用或关闭)
// Java HttpURLConnection 启用 Keep-Alive
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestProperty("Connection", "keep-alive");
// 或通过系统属性全局配置
System.setProperty("http.keepAlive", "true");
System.setProperty("http.maxConnections", "10");
2.2 管线化(Pipelining)
HTTP/1.1 Pipelining(理想情况):
请求 1 ──→
请求 2 ───────→
请求 3 ─────────────→
响应 1 ←── 200 OK
响应 2 ←────── 200 OK
响应 3 ←──────────── 200 OK
问题:响应必须按请求顺序返回(队头阻塞仍存在)
请求 2 的响应很小,但必须等待请求 1 的大响应先返回
2.3 分块传输(Chunked Transfer)
HTTP/1.1 200 OK
Content-Type: text/html
Transfer-Encoding: chunked
Connection: keep-alive
1a
← 十六进制:26 bytes
<div>First chunk</div>
← 结束标记
8
← 8 bytes
<div>End</div>
← 结束标记
0
← 最后一个 chunk
2.4 域名分片
<!-- HTTP/1.1 的 workaround:将资源分散到多个域名 -->
<link rel="stylesheet" href="https://static1.example.com/a.css">
<script src="https://static2.example.com/b.js"></script>
<img src="https://static3.example.com/c.png">
<!-- 浏览器对每个域名可并行 6 个连接,3 个域名 = 18 个并行 -->
三、HTTP/2 革命
3.1 核心特性
| 特性 | 说明 | 解决的问题 |
|---|---|---|
| 二进制分帧 | 将数据拆分为二进制帧 | 解析效率更高 |
| 多路复用 | 单个 TCP 连接并行传输多个流 | 队头阻塞(应用层) |
| 头部压缩 | HPACK 算法压缩头部 | 冗余头部开销 |
| Server Push | 服务端主动推送资源 | 减少往返次数 |
| 优先级 | 流优先级与依赖关系 | 关键资源优先加载 |
3.2 二进制分帧层
HTTP/2 帧格式(9 bytes 头部 + 可变负载):
+-----------------------------------------------+
| Length (24 bits) |
+---------------+---------------+---------------+
| Type (8) | Flags (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31 bits) |
+=+=============================================================+
| Payload (Length bytes) |
+---------------------------------------------------------------+
帧类型:
- HEADERS (0x1): HTTP 头部
- DATA (0x0): 应用数据
- SETTINGS (0x4): 连接配置
- PRIORITY (0x2): 流优先级
- RST_STREAM (0x3): 流终止
- PING (0x6): 保活检测
- GOAWAY (0x7): 连接关闭通知
- WINDOW_UPDATE (0x8): 流量控制
3.3 多路复用
HTTP/2 单个 TCP 连接上的多路复用:
TCP 连接
│
├── Stream 1 (请求首页 HTML) ────────→
│
├── Stream 3 (请求 CSS) ──────→
│
├── Stream 5 (请求 JS) ─────────→
│
├── Stream 7 (请求图片) ───────────────────→
│
←── Stream 3 响应 (完成) ───────
←── Stream 1 响应 (完成) ─────────────
←── Stream 5 响应 (完成) ───────────
←── Stream 7 响应 (完成) ─────────────────────
所有流交错传输,互不影响
3.4 HPACK 头部压缩
静态表 + 动态表 + Huffman 编码
请求 1:
:method: GET → 静态表索引 2
:path: /index.html → 字面量(加入动态表)
:authority: www.example.com → 字面量(加入动态表)
accept: text/html → 字面量(加入动态表)
请求 2(复用动态表):
:method: GET → 静态表索引 2
:path: /style.css → 字面量(加入动态表)
:authority: www.example.com → 动态表索引 62
accept: text/css → 字面量
请求 3(高度复用):
:method: GET → 静态表索引 2
:path: /script.js → 字面量
:authority: www.example.com → 动态表索引 62
accept: application/javascript → 字面量
3.5 Server Push
客户端 服务端
│ │
│ ── GET /index.html ─→│
│ │
│ ←─ /index.html ──────│
│ ←─ PUSH /style.css ──│ (服务端主动推送)
│ ←─ PUSH /script.js ──│ (服务端主动推送)
│ │
│ 使用推送的资源 │
注意:Push 的资源需客户端同意(SETTINGS_ENABLE_PUSH)
且可能被缓存或拒绝
四、HTTP 版本对比
| 特性 | HTTP/1.0 | HTTP/1.1 | HTTP/2 |
|---|---|---|---|
| 连接 | 短连接 | 持久连接 | 单一连接多路复用 |
| 并行 | 无 | 6 连接/域 | 无限流 |
| 队头阻塞 | 严重 | 存在 | 应用层消除 |
| 头部压缩 | 无 | 无 | HPACK |
| 服务端推送 | 无 | 无 | 支持 |
| 管道化 | 无 | 支持(缺陷多) | 原生多路复用 |
| 二进制 | 否 | 否 | 是 |
| TLS 要求 | 无 | 无 | 事实上必需 |
五、HTTP/2 的局限
5.1 TCP 层队头阻塞
HTTP/2 多路复用仍受 TCP 制约:
Stream 1 数据包 1 ─────→ (丢失!)
Stream 3 数据包 2 ─────→
Stream 5 数据包 3 ─────→
Stream 7 数据包 4 ─────→
TCP 必须按序确认,丢包后所有 Stream 都需等待重传
→ 应用层多路复用,传输层仍串行
5.2 连接迁移困难
TCP 连接由四元组标识(源IP:源Port, 目IP:目Port)
WiFi → 4G 切换时:
源 IP 改变 → TCP 连接中断 → 需重新建立 HTTP/2 连接
这催生了 HTTP/3 的设计动机
六、协议协商
6.1 ALPN(Application-Layer Protocol Negotiation)
TLS 握手时协商应用层协议:
ClientHello
+ ALPN Extension: ["h2", "http/1.1"]
ServerHello
+ ALPN Extension: "h2"
后续通信使用 HTTP/2
// Java 11+ HTTP Client 自动协商
HttpClient client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2) // 优先 HTTP/2
.build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://example.com"))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.version()); // HTTP_2
七、总结
| 协议 | 核心改进 | 主要局限 |
|---|---|---|
| HTTP/1.0 | 基础请求-响应 | 短连接,高延迟 |
| HTTP/1.1 | Keep-Alive、管线化、分块 | 应用层队头阻塞 |
| HTTP/2 | 二进制分帧、多路复用、HPACK | TCP 层队头阻塞、连接迁移难 |
| HTTP/3 | 基于 QUIC、0-RTT、连接迁移 | 见下一篇文章 |
HTTP 的演进是一个不断解决性能瓶颈的过程。HTTP/2 在应用层面消除了队头阻塞,却受制于 TCP 层的可靠性机制,这正是 HTTP/3 选择基于 QUIC(UDP 之上)重写的根本原因。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。