HTTP/2 与 HTTP/3
用生活化的比喻,让你理解 HTTP 协议的演进——多路复用、头部压缩、服务器推送,以及 HTTP/3 为什么基于 QUIC
前置知识:第06章 TCP/IP + 第07章 UDP与可靠传输(理解 TCP 队头阻塞和 QUIC)
阅读指南(初学者必看)
为什么你需要学习 HTTP/2 与 HTTP/3?
你每天都在用 HTTP——网页加载、游戏资源下载、API 请求都是 HTTP。但 HTTP 从 1.0 到 3.0 经历了巨大变革:
- HTTP/1.1 的管道化为什么没有解决问题?
- HTTP/2 的多路复用解决了什么问题?还有什么没解决?
- HTTP/3 为什么不再基于 TCP?
学完本章,你能回答:
- HTTP/1.0 → 1.1 → 2 → 3 每一代解决了什么问题?
- HTTP/2 的多路复用和帧机制是怎么工作的?
- HPACK 头部压缩的原理是什么?
- HTTP/3 基于 QUIC 解决了什么问题?
本文结构
第一部分:HTTP 协议演进(理解每一代的改进)
第二部分:HTTP/2 多路复用(理解帧和流)
第三部分:头部压缩 HPACK(理解压缩原理)
第四部分:HTTP/3 与 QUIC
第五部分:WebSocket
8.1 HTTP 协议演进
生活比喻:HTTP 就像餐厅点餐。
HTTP/1.0:
每次点菜都要重新排队、找座位
HTTP/1.1:
一次排队,可以点多个菜(持久连接)
HTTP/2:
一次排队,多个服务员同时上菜(多路复用)
HTTP/3:
不用排队,直接扫码点餐(QUIC)
HTTP/1.0 → HTTP/1.1 → HTTP/2 → HTTP/3
HTTP/1.0 问题:
- 每个请求需要单独的 TCP 连接
- 10 个资源 = 10 次 TCP 握手
HTTP/1.1 改进:
- 持久连接(Keep-Alive)
- 管道化(Pipelining)── 但有队头阻塞问题
- 仍然:6~8 个并发连接限制
HTTP/2 核心改进:
- 多路复用:一个 TCP 连接上并行多个请求/响应
- 头部压缩:HPACK 算法压缩请求头
- 服务器推送:主动推送资源
- 二进制帧:不再是文本协议
HTTP/3 核心改进:
- 基于 QUIC(UDP)而非 TCP
- 解决了 TCP 层的队头阻塞
- 0-RTT 连接建立
- 连接迁移
8.2 HTTP/2 多路复用
HTTP/1.1:
连接1: 请求A ─────── 响应A ──────
连接2: 请求B ─────── 响应B ──────
连接3: 请求C ─────── 响应C ──────
→ 需要多个 TCP 连接,每个连接成本高
HTTP/2:
连接1: ── 请求A ── 请求B ── 请求C ──
── 响应A ── 响应B ── 响应C ──
→ 一个 TCP 连接,多个流并行
帧(Frame)结构
帧(Frame)结构:
┌──────────┬──────────┬──────────┬───────────┐
│ Length │ Type │ Flags │ Stream ID │
│ (3bytes) │ (1byte) │ (1byte) │ (4bytes) │
└──────────┴──────────┴──────────┴───────────┘
流(Stream)= 双向的字节流
消息(Message)= 一个完整的请求或响应
帧(Frame)= 最小通信单位
每个帧带 Stream ID,接收方按 Stream ID 重组
→ 不同流的帧可以交错发送
class HTTP2Frame {
constructor(type, flags, streamId, payload) {
this.length = payload ? payload.length : 0;
this.type = type;
this.flags = flags;
this.streamId = streamId;
this.payload = payload || Buffer.alloc(0);
}
static TYPES = {
DATA: 0x00, HEADERS: 0x01, PRIORITY: 0x02,
RST_STREAM: 0x03, SETTINGS: 0x04,
PUSH_PROMISE: 0x05, PING: 0x06,
GOAWAY: 0x07, WINDOW_UPDATE: 0x08,
CONTINUATION: 0x09
};
static FLAGS = {
END_STREAM: 0x01, END_HEADERS: 0x04,
PADDED: 0x08, ACK: 0x01
};
}
class HTTP2Connection {
constructor() {
this.streams = new Map();
this.nextStreamId = 1;
this.settings = {
HEADER_TABLE_SIZE: 4096,
ENABLE_PUSH: 1,
MAX_CONCURRENT_STREAMS: 100,
INITIAL_WINDOW_SIZE: 65535,
MAX_FRAME_SIZE: 16384
};
}
createStream() {
let id = this.nextStreamId;
this.nextStreamId += 2;
let stream = { id, state: 'idle', data: [] };
this.streams.set(id, stream);
return stream;
}
}
和游戏开发的关系:
- 游戏资源加载用 HTTP/2 可以大幅减少连接数
- CDN 普遍支持 HTTP/2
- HTTP/3 基于 QUIC,进一步减少延迟
8.3 头部压缩(HPACK)
HTTP/1.1 问题:
每次请求都带完整的头部
Cookie 可能达到数 KB
100 个请求 × 1KB 头部 = 100KB 纯头部开销
HPACK 算法:
1. 静态表:61 个常见头部字段预定义
:method GET → 索引 2
:path / → 索引 4
2. 动态表:双方维护,记录之前发过的头部
第一次:cookie: abcdefg → 加入动态表(索引 62)
第二次:只需发索引 62 → 1 字节!
3. 哈夫曼编码:字符串本身也压缩
压缩率:通常 80%~90%
class HPACKEncoder {
constructor() {
this.staticTable = [
[':authority', ''],
[':method', 'GET'],
[':method', 'POST'],
[':path', '/'],
[':path', '/index.html'],
[':scheme', 'http'],
[':scheme', 'https'],
[':status', '200'],
['accept', '*/*'],
['accept-encoding', 'gzip, deflate']
];
this.dynamicTable = [];
this.maxTableSize = 4096;
}
encode(headers) {
let encoded = [];
for (let [name, value] of Object.entries(headers)) {
let index = this.findIndex(name, value);
if (index !== -1) {
encoded.push(this.encodeIndexed(index));
} else {
let nameIndex = this.findNameIndex(name);
if (nameIndex !== -1) {
encoded.push(this.encodeLiteralWithIndexing(nameIndex, value));
} else {
encoded.push(this.encodeLiteralWithNewName(name, value));
}
}
}
return Buffer.concat(encoded);
}
findIndex(name, value) {
for (let i = 0; i < this.staticTable.length; i++) {
if (this.staticTable[i][0] === name && this.staticTable[i][1] === value) {
return i + 1;
}
}
for (let i = 0; i < this.dynamicTable.length; i++) {
if (this.dynamicTable[i][0] === name && this.dynamicTable[i][1] === value) {
return this.staticTable.length + i + 1;
}
}
return -1;
}
}
8.4 HTTP/3 与 QUIC
HTTP/3 核心改进:
- 基于 QUIC(UDP)而非 TCP
- 解决了 TCP 层的队头阻塞
- 0-RTT 连接建立
- 连接迁移
为什么 HTTP/3 不用 TCP?
1. TCP 层的队头阻塞——多路复用解决了 HTTP 层的问题,但 TCP 丢包仍然阻塞所有流
2. TCP 的握手延迟——TCP 三次握手 + TLS 握手需要 2-3 个 RTT,QUIC 只需 1 个 RTT(重连 0-RTT)
3. TCP 不支持连接迁移——IP 变化导致连接断开,QUIC 用 Connection ID 不受影响
class QUICConnection {
constructor() {
this.connectionId = this.generateConnectionId();
this.state = 'idle';
this.streams = new Map();
this.nextStreamId = 0;
this.rtt = 0;
this.congestionWindow = 10;
}
generateConnectionId() {
// 生成随机 Connection ID
return crypto.randomBytes(8);
}
createStream(bidirectional = true) {
let id = this.nextStreamId;
this.nextStreamId += bidirectional ? 4 : 2;
let stream = { id, state: 'idle', data: [], sendOffset: 0, recvOffset: 0 };
this.streams.set(id, stream);
return stream;
}
}
游戏开发应该用 HTTP/2 还是 HTTP/3? A:对于游戏资源加载,HTTP/3 更好:1)更快的连接建立(0-RTT 重连);2)丢包不影响其他流(WiFi/4G 环境尤其重要);3)连接迁移(WiFi→4G 不断连)。但 HTTP/3 的浏览器支持还在完善中。对于游戏实时通信,还是应该用 WebSocket 或自定义 UDP 协议。
8.5 WebSocket
WebSocket = 在 TCP 上的全双工通信协议
- 基于 HTTP 握手升级
- 连接建立后,双方可以随时发送消息
- 消息帧格式轻量
游戏场景:
- 实时聊天
- 游戏状态同步
- 服务器推送
class GameWebSocket {
constructor(url) {
this.url = url;
this.ws = null;
this.handlers = new Map();
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
}
connect() {
return new Promise((resolve, reject) => {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
this.reconnectAttempts = 0;
this.emit('connected');
resolve();
};
this.ws.onmessage = (event) => {
let message = JSON.parse(event.data);
this.emit(message.type, message.data);
};
this.ws.onclose = () => {
this.emit('disconnected');
this.tryReconnect();
};
this.ws.onerror = (error) => {
this.emit('error', error);
reject(error);
};
});
}
tryReconnect() {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
setTimeout(() => this.connect(), 1000 * this.reconnectAttempts);
}
}
emit(event, data) {
let handler = this.handlers.get(event);
if (handler) handler(data);
}
send(data) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(data));
}
}
}
自问自答
Q:HTTP/2 的多路复用和 HTTP/1.1 的管道化有什么区别? A:HTTP/1.1 的管道化允许在同一个连接上连续发送多个请求,但响应必须按顺序返回(FIFO),如果第一个请求的响应慢,后面的响应都要等——这就是"HTTP 层队头阻塞"。HTTP/2 的多路复用通过 Stream ID 和帧机制,允许请求和响应交错传输,不同流的响应可以乱序返回,没有 HTTP 层的队头阻塞。
Q:HTTP/2 解决了队头阻塞吗? A:解决了 HTTP 层的队头阻塞,但没有解决 TCP 层的队头阻塞!HTTP/2 的多个流复用同一个 TCP 连接,如果 TCP 丢了一个包,所有流都要等重传——这就是 TCP 层的队头阻塞。HTTP/3 基于 QUIC(UDP),每个流独立,彻底解决了这个问题。
Q:服务器推送有什么用? A:服务器推送允许服务器在客户端请求之前主动发送资源。比如客户端请求 HTML,服务器知道还需要 CSS 和 JS,就主动推送。减少了一个 RTT 的等待时间。但对游戏来说用处不大——游戏资源通常预加载,不需要服务器推送。
Q:为什么 HTTP/3 不用 TCP? A:三个原因:1)TCP 层的队头阻塞——多路复用解决了 HTTP 层的问题,但 TCP 丢包仍然阻塞所有流;2)TCP 的握手延迟——TCP 三次握手 + TLS 握手需要 2-3 个 RTT,QUIC 只需 1 个 RTT(重连 0-RTT);3)TCP 不支持连接迁移——IP 变化导致连接断开,QUIC 用 Connection ID 不受影响。
实践任务
- 任务1:在浏览器 DevTools Network 面板查看 HTTP/2 的流(Stream)信息,理解多路复用
- 任务2:用
nghttp工具查看 HTTP/2 帧详情,观察帧类型和 Stream ID - 任务3:配置 Nginx 开启 HTTP/2,用 Chrome DevTools 对比 HTTP/1.1 和 HTTP/2 的加载性能
- 任务4:用
curl --http3测试 HTTP/3,对比 HTTP/2 的连接建立时间 - 任务5:在丢包环境下对比 HTTP/2 和 HTTP/3 的性能差异(用
tc netem模拟)
与其他章节的关联
| 本章内容 | 关联章节 | 关联点 |
|---|---|---|
| HTTP/2 多路复用 | 第06章 TCP/IP | HTTP/2 的多路复用仍受 TCP 队头阻塞影响 |
| HTTP/3 + QUIC | 第07章 UDP与可靠传输 | HTTP/3 基于 QUIC,解决了 TCP 的队头阻塞 |
| TLS | 第09章 网络安全 | HTTP/2 和 HTTP/3 都强制 HTTPS |
| 资源加载 | 第10章 实战篇 | HTTP/2/3 优化游戏资源加载性能 |
上一章:07-UDP与可靠传输 | 下一章:09-网络安全基础