Skip to main content

RFC 7540 - HTTP/2

文档信息

  • RFC编号: 7540
  • 标题: Hypertext Transfer Protocol Version 2 (HTTP/2)
  • 标题(中文): 超文本传输协议第2版
  • 发布日期: 2015年5月
  • 作者: M. Belshe (BitGo), R. Peon (Google), M. Thomson (Mozilla), ed.
  • 废弃文档: 无(HTTP/1.1并存)
  • 后继版本: RFC 9113 (HTTP/2, 2022更新)
  • 状态: 标准轨道 (Standards Track)

摘要

HTTP/2是HTTP协议的第二个主要版本,解决了HTTP/1.1的性能瓶颈。它通过多路复用、头部压缩、服务器推送等特性,显著提升了Web应用的加载速度和效率。HTTP/2保持了HTTP/1.1的语义,但改变了数据在客户端和服务器之间的传输方式。

核心问题

HTTP/1.1的局限

问题1: 队头阻塞 (Head-of-Line Blocking)
┌─────────────────────────────────┐
│ HTTP/1.1 - 顺序处理 │
├─────────────────────────────────┤
│ Request 1 → Response 1 (慢) │
│ Request 2 等待... │
│ Request 3 等待... │
└─────────────────────────────────┘

解决方案: 打开多个TCP连接
问题: 资源浪费、拥塞窗口小

问题2: 头部冗余
每个请求都携带大量重复的头部
Cookie等可能有几KB大小

问题3: 无服务器推送
服务器只能被动响应请求

HTTP/2的解决方案

解决方案1: 多路复用
┌─────────────────────────────────┐
│ HTTP/2 - 单连接并行处理 │
├─────────────────────────────────┤
│ Request 1 → Response 1 │
│ Request 2 → Response 2 │ 同时进行
│ Request 3 → Response 3 │
└─────────────────────────────────┘

解决方案2: HPACK头部压缩
减少90%以上的头部大小

解决方案3: 服务器推送
服务器主动推送所需资源

核心概念

1. 二进制分帧层 (Binary Framing Layer)

HTTP/2在应用层(HTTP)和传输层(TCP)之间添加了二进制分帧层:

HTTP/1.1文本协议:
GET /index.html HTTP/1.1\r\n
Host: example.com\r\n
\r\n

HTTP/2二进制帧:
┌─────────────────────────────┐
│ Frame Header (9 bytes) │
├─────────────────────────────┤
│ Frame Payload (variable) │
└─────────────────────────────┘

帧结构

 0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Length (24) |
+---------------+---------------+---------------+
| Type (8) | Flags (8) |
+-+-+-----------+---------------+-------------------------------+
|R| Stream Identifier (31) |
+=+=============================================================+
| Frame Payload (0...) ...
+---------------------------------------------------------------+

字段说明

  • Length (24 bits): 帧载荷长度(不包括帧头的9字节)
  • Type (8 bits): 帧类型
  • Flags (8 bits): 帧特定标志
  • R (1 bit): 保留位,必须为0
  • Stream Identifier (31 bits): 流标识符

2. 流 (Stream)

流是HTTP/2连接中的独立、双向的帧序列:

单个TCP连接上的多个流:

TCP连接

├─ Stream 1 (GET /index)
│ ├─ HEADERS帧
│ ├─ DATA帧
│ └─ DATA帧 (END_STREAM)

├─ Stream 3 (GET /style.css)
│ ├─ HEADERS帧
│ └─ DATA帧 (END_STREAM)

└─ Stream 5 (POST /api/data)
├─ HEADERS帧
├─ DATA帧
└─ DATA帧 (END_STREAM)

流的特性

  • 流ID由客户端发起的流为奇数(1, 3, 5...)
  • 流ID由服务器发起的流为偶数(2, 4, 6...)
  • 流ID 0用于连接级控制
  • 流可以并发传输
  • 流可以被赋予优先级

3. 帧类型

HTTP/2定义了10种帧类型:

类型编码用途
DATA0x0传输应用数据
HEADERS0x1传输头部信息
PRIORITY0x2指定流优先级
RST_STREAM0x3终止流
SETTINGS0x4连接配置参数
PUSH_PROMISE0x5服务器推送通知
PING0x6心跳检测
GOAWAY0x7优雅关闭连接
WINDOW_UPDATE0x8流量控制
CONTINUATION0x9继续传输头部

HTTP/2连接建立

方法1: ALPN协商(HTTPS)

1. TLS握手(带ALPN扩展)
Client → Server: ClientHello
Extension: ALPN
Protocol: h2, http/1.1

Server → Client: ServerHello
Extension: ALPN
Selected: h2

2. 连接前言 (Connection Preface)
Client → Server: "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
Client → Server: SETTINGS帧

Server → Client: SETTINGS帧

3. 开始HTTP/2通信

方法2: HTTP Upgrade(HTTP明文)

Client → Server:
GET / HTTP/1.1
Host: example.com
Connection: Upgrade, HTTP2-Settings
Upgrade: h2c
HTTP2-Settings: <base64url编码的SETTINGS>

Server → Client:
HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Upgrade: h2c

[切换到HTTP/2]
Server → Client: SETTINGS帧

连接前言示例

# HTTP/2连接前言(客户端必须发送)
CONNECTION_PREFACE = b'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n'

# 发送
sock.sendall(CONNECTION_PREFACE)

# 发送SETTINGS帧
settings_frame = build_settings_frame({
SETTINGS_HEADER_TABLE_SIZE: 4096,
SETTINGS_ENABLE_PUSH: 1,
SETTINGS_MAX_CONCURRENT_STREAMS: 100,
SETTINGS_INITIAL_WINDOW_SIZE: 65535
})
sock.sendall(settings_frame)

多路复用详解

流的生命周期

                     +--------+
send | | recv
PP | idle | PP
+--------+
| |
send H/ | | recv H
recv H | |
v v
+----------+
send | | recv
ES | open | ES
+----------+
| |
send ES | | recv ES
| |
v v
+--------+ +--------+
send R/ | | | | recv R/
recv R | half | | half | send R
| closed | | closed |
| (local)| |(remote)|
+--------+ +--------+
| |
send R/ | | recv R
recv R | | send R
v v
+--------+
recv | | send
ES | closed | ES
+--------+

图例:
H = HEADERS帧
PP = PUSH_PROMISE帧
ES = END_STREAM标志
R = RST_STREAM帧

并发请求示例

// HTTP/1.1 - 需要6个连接
Promise.all([
fetch('/api/user'), // 连接1
fetch('/api/posts'), // 连接2
fetch('/api/comments'), // 连接3
fetch('/images/logo.png'), // 连接4
fetch('/css/style.css'), // 连接5
fetch('/js/app.js') // 连接6
]);

// HTTP/2 - 1个连接足够
Promise.all([
fetch('/api/user'), // Stream 1
fetch('/api/posts'), // Stream 3
fetch('/api/comments'), // Stream 5
fetch('/images/logo.png'), // Stream 7
fetch('/css/style.css'), // Stream 9
fetch('/js/app.js') // Stream 11
]);
// 全部在同一个TCP连接上并发传输!

HPACK头部压缩

压缩原理

HPACK使用三种压缩技术:

1. 静态表 (Static Table)

预定义的常用头部:

索引 | 头部名称           | 头部值
-----|-------------------|--------
1 | :authority |
2 | :method | GET
3 | :method | POST
4 | :path | /
5 | :path | /index.html
8 | :status | 200
9 | :status | 204
15 | accept-encoding | gzip, deflate
...

2. 动态表 (Dynamic Table)

连接期间构建的头部索引:

首次请求:
:method: GET
:path: /api/users
authorization: Bearer token123...

编码后加入动态表:
索引62 | authorization | Bearer token123...

后续请求:
只需发送: 索引62
节省数百字节!

3. 霍夫曼编码 (Huffman Coding)

原始: "www.example.com"
霍夫曼编码后: 减少30-40%

压缩示例

# 首次请求 - 完整头部
headers = {
':method': 'GET',
':path': '/index.html',
':scheme': 'https',
':authority': 'www.example.com',
'user-agent': 'Mozilla/5.0...',
'accept': 'text/html...'
}
# 编码大小: ~800字节

# 动态表更新后
# 第二次请求 - 引用索引
headers_encoded = [
2, # :method GET (静态表)
4, # :path / (静态表)
7, # :scheme https (静态表)
62, # :authority (动态表)
63, # user-agent (动态表)
64 # accept (动态表)
]
# 编码大小: ~50字节!
# 压缩比: 94%

服务器推送 (Server Push)

工作原理

传统HTTP/1.1流程:
1. Client: GET /index.html
2. Server: 返回HTML
3. Client: 解析HTML,发现需要style.css
4. Client: GET /style.css
5. Server: 返回CSS
总往返: 2次

HTTP/2服务器推送:
1. Client: GET /index.html
2. Server: PUSH_PROMISE (Stream 2: /style.css)
3. Server: 返回HTML (Stream 1)
4. Server: 推送CSS (Stream 2)
总往返: 1次

PUSH_PROMISE帧

客户端请求:
Stream 1: GET /index.html

服务器响应:
Stream 1: PUSH_PROMISE帧
:method: GET
:path: /style.css
:scheme: https
:authority: example.com
Promised Stream ID: 2

Stream 2: HEADERS帧 + DATA帧
(包含style.css内容)

Stream 1: HEADERS帧 + DATA帧
(包含index.html内容)

服务器推送示例(Node.js)

const http2 = require('http2');
const fs = require('fs');

const server = http2.createSecureServer({
key: fs.readFileSync('server-key.pem'),
cert: fs.readFileSync('server-cert.pem')
});

server.on('stream', (stream, headers) => {
const path = headers[':path'];

if (path === '/index.html') {
// 推送CSS文件
stream.pushStream(
{
':path': '/style.css',
':method': 'GET'
},
(err, pushStream) => {
if (err) throw err;

pushStream.respond({
':status': 200,
'content-type': 'text/css'
});

const css = fs.readFileSync('./style.css');
pushStream.end(css);
}
);

// 推送JavaScript文件
stream.pushStream(
{
':path': '/app.js',
':method': 'GET'
},
(err, pushStream) => {
if (err) throw err;

pushStream.respond({
':status': 200,
'content-type': 'application/javascript'
});

const js = fs.readFileSync('./app.js');
pushStream.end(js);
}
);

// 响应主请求
stream.respond({
':status': 200,
'content-type': 'text/html'
});

const html = fs.readFileSync('./index.html');
stream.end(html);
}
});

server.listen(8443);

客户端处理推送

// 浏览器自动处理推送的资源
// 资源会被缓存,后续请求直接使用

// 监控服务器推送
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.initiatorType === 'push') {
console.log('服务器推送:', entry.name);
}
}
});
observer.observe({ entryTypes: ['resource'] });

流优先级

优先级模型

HTTP/2使用依赖树模型:

示例优先级树:
Root

┌────┴────┐
│ │
Stream1 Stream3
(HTML) (CSS)
│ weight:16

Stream5
(Image)
weight:8

说明:
- HTML最高优先级(父级)
- CSS高优先级(权重16)
- Image中优先级(权重8,依赖HTML)

设置优先级

// PRIORITY帧
const priorityFrame = {
streamId: 5,
exclusive: false, // 非独占
dependency: 1, // 依赖Stream 1
weight: 8 // 权重 8
};

// 在HEADERS帧中设置
const headersFrame = {
streamId: 5,
flags: PRIORITY_FLAG,
exclusive: false,
dependency: 1,
weight: 16,
headers: {
':method': 'GET',
':path': '/image.png'
}
};

流量控制

HTTP/2实现了两级流量控制:

连接级流量控制

整个连接的流量窗口:

初始窗口: 65535字节
Client发送65KB后必须等待
Server发送WINDOW_UPDATE才能继续

流级流量控制

每个流独立的流量窗口:

Stream 1: 窗口32KB
Stream 3: 窗口64KB
Stream 5: 窗口16KB

各自独立管理,互不影响

WINDOW_UPDATE示例

def send_window_update(stream_id, increment):
"""发送WINDOW_UPDATE帧"""
frame = bytearray(13) # 9字节头 + 4字节载荷

# 载荷长度: 4
frame[0:3] = (4).to_bytes(3, 'big')

# 类型: WINDOW_UPDATE (0x8)
frame[3] = 0x08

# 标志: 无
frame[4] = 0x00

# 流ID
frame[5:9] = stream_id.to_bytes(4, 'big')

# 窗口增量
frame[9:13] = increment.to_bytes(4, 'big')

return bytes(frame)

# 增加Stream 1的窗口32KB
window_update = send_window_update(1, 32768)
sock.sendall(window_update)

完整的HTTP/2请求-响应示例

客户端请求

import socket
import ssl
import h2.connection
import h2.config

# 创建TLS连接
context = ssl.create_default_context()
context.set_alpn_protocols(['h2'])

sock = socket.create_connection(('www.example.com', 443))
sock = context.wrap_socket(sock, server_hostname='www.example.com')

# 验证ALPN
if sock.selected_alpn_protocol() != 'h2':
raise RuntimeError('HTTP/2不支持')

# 初始化HTTP/2连接
config = h2.config.H2Configuration(client_side=True)
conn = h2.connection.H2Connection(config=config)
conn.initiate_connection()
sock.sendall(conn.data_to_send())

# 发送GET请求
stream_id = conn.get_next_available_stream_id()
request_headers = [
(':method', 'GET'),
(':path', '/'),
(':scheme', 'https'),
(':authority', 'www.example.com'),
('user-agent', 'Python HTTP/2 Client'),
]

conn.send_headers(stream_id, request_headers, end_stream=True)
sock.sendall(conn.data_to_send())

# 接收响应
while True:
data = sock.recv(65536)
if not data:
break

events = conn.receive_data(data)
for event in events:
if isinstance(event, h2.events.ResponseReceived):
print('响应头部:')
for name, value in event.headers:
print(f' {name}: {value}')

elif isinstance(event, h2.events.DataReceived):
print('响应数据:', event.data.decode('utf-8'))
conn.acknowledge_received_data(event.flow_controlled_length, stream_id)

elif isinstance(event, h2.events.StreamEnded):
print('流结束')
sock.sendall(conn.data_to_send())
break

sock.sendall(conn.data_to_send())

服务器响应(Nginx配置)

server {
listen 443 ssl http2;
server_name example.com;

ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;

# HTTP/2配置
http2_max_concurrent_streams 128;
http2_max_field_size 4k;
http2_max_header_size 16k;

# 服务器推送
location = /index.html {
http2_push /style.css;
http2_push /script.js;
http2_push /logo.png;
}

# 优先级提示
location ~* \.(css|js)$ {
add_header Link "</style.css>; rel=preload; as=style" always;
}
}

性能优化最佳实践

1. 域名分片不再必要

HTTP/1.1: 需要域名分片
- static1.example.com
- static2.example.com
- static3.example.com
目的: 突破浏览器6连接限制

HTTP/2: 单域名即可
- example.com
多路复用已解决并发问题

2. 资源合并策略调整

HTTP/1.1: 合并所有CSS/JS
bundle.css (500KB)

HTTP/2: 按页面分割
home.css (50KB)
about.css (30KB)
contact.css (20KB)

优势:
- 更好的缓存利用率
- 按需加载
- 更快的首屏渲染

3. 内联资源重新考虑

HTTP/1.1: 内联小图片
<img src="data:image/png;base64,iVBOR...">

HTTP/2: 使用独立文件
<img src="/icons/logo.png">

原因:
- 可以被缓存
- 并行加载不阻塞
- 服务器推送优化

4. 合理使用服务器推送

推荐推送:
✅ 关键CSS
✅ 关键JavaScript
✅ 网页字体

不推荐推送:
❌ 大文件
❌ 可能已缓存的资源
❌ 不是所有页面都需要的资源

检查缓存:
使用Cookie或LocalStorage标记
避免推送已缓存资源

调试和监控

Chrome DevTools

1. 打开DevTools
2. Network标签
3. 查看Protocol列
4. 筛选"h2"查看HTTP/2请求

查看推送资源:
Initiator列显示"Push / Other"

Wireshark抓包

# 过滤HTTP/2流量
tls.handshake.extensions_alpn_str == "h2"

# 查看帧
http2.frame

# 查看SETTINGS帧
http2.frame.type == 4

curl测试

# HTTP/2请求
curl --http2 -I https://www.example.com

# 查看详细信息
curl --http2 -v https://www.example.com

# 强制HTTP/2先验知识(明文)
curl --http2-prior-knowledge http://localhost:8080

常见问题

1. HTTP/2是否总是更快?

不一定!

更快的场景:
✅ 多个小资源
✅ 高延迟网络
✅ 复杂页面

可能更慢的场景:
❌ 单个大文件下载
❌ 服务器配置不当
❌ 过度使用服务器推送

2. 是否需要改代码?

前端代码: 无需修改
API调用: 无需修改
语义保持: 完全兼容HTTP/1.1

需要调整:
- 资源打包策略
- 域名分片移除
- CDN配置

3. 如何回退到HTTP/1.1?

浏览器自动处理:
1. 尝试ALPN协商HTTP/2
2. 如果失败,使用HTTP/1.1
3. 对用户透明

强制HTTP/1.1:
curl --http1.1 https://example.com

总结

HTTP/2是Web性能的重大飞跃:

  • : 准确实现多路复用
  • : 清晰的二进制协议
  • : 优雅的向后兼容

核心优势

  1. 🚀 多路复用 - 单连接并发
  2. 📦 头部压缩 - 减少开销
  3. 🔄 服务器推送 - 主动优化
  4. ⚖️ 流优先级 - 智能调度

适用场景

  • 现代Web应用
  • API服务
  • 高延迟网络
  • 移动应用

相关RFC

  • RFC 7540: HTTP/2(本文档)
  • RFC 7541: HPACK头部压缩
  • RFC 9113: HTTP/2(2022更新)
  • RFC 9114: HTTP/3

返回RFC列表