HTTP 协议演进

追溯 HTTP 从 1.0 到 2.0 的演进历程,理解长连接、管线化、多路复用与头部压缩的核心设计

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.0HTTP/1.1HTTP/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.1Keep-Alive、管线化、分块应用层队头阻塞
HTTP/2二进制分帧、多路复用、HPACKTCP 层队头阻塞、连接迁移难
HTTP/3基于 QUIC、0-RTT、连接迁移见下一篇文章

HTTP 的演进是一个不断解决性能瓶颈的过程。HTTP/2 在应用层面消除了队头阻塞,却受制于 TCP 层的可靠性机制,这正是 HTTP/3 选择基于 QUIC(UDP 之上)重写的根本原因。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「network」更多文章

  1. 网络安全:TLS/SSL、证书与加密通信
  2. 负载均衡算法与高可用架构
  3. DNS 系统与智能解析