Network · #http3#quic#protocol

HTTP/3与QUIC协议深度解析

2025.04.19 5 min 2.2k
// 目录 · contents

前言

HTTP/3是HTTP协议的第三个主要版本,最大的变化是底层传输协议从TCP切换到了QUIC。QUIC(Quick UDP Internet Connections)基于UDP构建,由Google最初设计,后经IETF标准化为RFC 9000。本文深入分析QUIC协议的核心机制以及HTTP/3在其上的运作方式。

协议栈对比

graph TB
    subgraph "HTTP/2 Stack"
        H2[HTTP/2]
        TLS12[TLS 1.2/1.3]
        TCP[TCP]
        IP1[IP]
    end

    subgraph "HTTP/3 Stack"
        H3[HTTP/3]
        QUIC[QUIC]
        UDP[UDP]
        IP2[IP]
    end

    H2 --> TLS12
    TLS12 --> TCP
    TCP --> IP1

    H3 --> QUIC
    QUIC --> UDP
    UDP --> IP2

    style QUIC fill:#f9a825,color:#000
    style H3 fill:#66bb6a,color:#000

QUIC将传输层和加密层合并,自带TLS 1.3加密,避免了TCP+TLS的分层开销。

QUIC vs TCP:核心差异

连接建立延迟

TCP+TLS 1.3需要2-3个RTT来建立连接,而QUIC只需要1个RTT,甚至可以做到0-RTT。

sequenceDiagram
    participant C as Client
    participant S as Server

    Note over C,S: TCP + TLS 1.3 (2 RTT)
    rect rgb(255, 235, 235)
        C->>S: TCP SYN
        S->>C: TCP SYN+ACK
        C->>S: TCP ACK + TLS ClientHello
        S->>C: TLS ServerHello + Finished
        C->>S: TLS Finished + HTTP Request
    end

    Note over C,S: QUIC 首次连接 (1 RTT)
    rect rgb(235, 255, 235)
        C->>S: QUIC Initial (ClientHello)
        S->>C: QUIC Initial (ServerHello) + Handshake
        C->>S: Handshake Complete + HTTP Request
    end

    Note over C,S: QUIC 0-RTT (已知服务器)
    rect rgb(235, 235, 255)
        C->>S: QUIC 0-RTT (ClientHello + Early Data + HTTP Request)
        S->>C: QUIC Response
    end

0-RTT连接恢复

QUIC支持0-RTT连接恢复,客户端可以在首个包中就携带应用数据:

1
2
3
4
5
Client ──> Server:
Initial Packet:
- CRYPTO frame (ClientHello with PSK)
0-RTT Packet:
- STREAM frame (HTTP request data)

但0-RTT有重放攻击风险,因此只适用于幂等请求(如GET)。

队头阻塞(Head-of-Line Blocking)解决

HTTP/2在TCP上实现了多路复用,但TCP层的丢包会阻塞所有流。QUIC在传输层就实现了独立的流控制:

graph TB
    subgraph "HTTP/2 over TCP: 队头阻塞"
        direction TB
        S1A[Stream 1 - Data A] --> TCP_BUF[TCP缓冲区]
        S2A[Stream 2 - Data B] --> TCP_BUF
        S3A[Stream 3 - Data C] --> TCP_BUF
        TCP_BUF --> |"包2丢失<br>所有流阻塞"| BLOCKED[所有Stream等待重传]
    end

    subgraph "HTTP/3 over QUIC: 独立流"
        direction TB
        S1B[Stream 1] --> |正常| OK1[正常接收]
        S2B[Stream 2] --> |"包丢失"| WAIT[仅Stream 2等待]
        S3B[Stream 3] --> |正常| OK3[正常接收]
    end

QUIC核心机制

连接标识(Connection ID)

TCP用四元组(源IP、源端口、目标IP、目标端口)标识连接。QUIC使用Connection ID标识连接,这使得连接迁移成为可能。

sequenceDiagram
    participant C as Client
    participant S as Server

    Note over C: WiFi (IP: 192.168.1.100)
    C->>S: QUIC Packet (CID: abc123)
    S->>C: QUIC Packet (CID: abc123)

    Note over C: 切换到4G (IP: 10.0.0.50)
    C->>S: QUIC Packet (CID: abc123, new path)
    Note over S: 同一个CID,连接不断开
    S->>C: PATH_CHALLENGE
    C->>S: PATH_RESPONSE
    Note over C,S: 连接在新网络继续使用

当手机从WiFi切换到4G时,TCP连接必然断开,而QUIC连接可以无缝迁移。

包号与ACK机制

QUIC的包号是单调递增的,不同于TCP的序列号。每个包有唯一的包号,这简化了RTT测量和丢包检测:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
QUIC Packet Structure:
┌─────────────────────────────┐
│ Header │
│ - Connection ID │
│ - Packet Number (monotonic) │
│ - Version │
├─────────────────────────────┤
│ Frames │
│ - STREAM frame (data) │
│ - ACK frame │
│ - CRYPTO frame (handshake) │
│ - PADDING frame │
│ ... │
└─────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
# QUIC vs TCP 重传区别
# TCP: 重传包使用相同序列号,导致重传歧义(Retransmission Ambiguity)
# QUIC: 重传包使用新的包号,包含相同的Stream帧

# TCP
# 原始包: seq=100, data="hello"
# 重传包: seq=100, data="hello" # 无法区分是原始包还是重传包的ACK

# QUIC
# 原始包: pkt_num=5, STREAM(offset=0, data="hello")
# 重传包: pkt_num=8, STREAM(offset=0, data="hello") # 包号不同,可以精确测量RTT

流量控制

QUIC提供两级流量控制:

1
2
3
4
5
Connection-level Flow Control:
MAX_DATA frame → 整个连接的最大数据量

Stream-level Flow Control:
MAX_STREAM_DATA frame → 单个流的最大数据量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 概念示意:QUIC流量控制
type FlowControl struct {
// 连接级别
connMaxData uint64 // 连接可接收的最大字节数
connDataSent uint64 // 已发送的字节数

// 流级别
streamMaxData map[uint64]uint64 // 每个流可接收的最大字节数
streamDataSent map[uint64]uint64 // 每个流已发送的字节数
}

func (fc *FlowControl) CanSend(streamID uint64, size uint64) bool {
// 检查连接级别限制
if fc.connDataSent+size > fc.connMaxData {
return false
}
// 检查流级别限制
if fc.streamDataSent[streamID]+size > fc.streamMaxData[streamID] {
return false
}
return true
}

丢包恢复

QUIC使用改进的丢包检测算法,结合了基于时间和基于包号的检测方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# QUIC丢包检测(简化)
class LossDetector:
def __init__(self):
self.kPacketThreshold = 3 # 包序号阈值
self.kTimeThreshold = 9/8 # 时间阈值因子
self.largest_acked = 0
self.smoothed_rtt = 0

def detect_lost_packets(self, sent_packets, ack_packet_number):
lost = []
loss_delay = max(self.smoothed_rtt, self.latest_rtt)
loss_delay = max(loss_delay * self.kTimeThreshold, 1) # ms

for pkt_num, pkt in sent_packets.items():
if pkt_num > ack_packet_number:
continue

# 基于包号的检测
if ack_packet_number - pkt_num >= self.kPacketThreshold:
lost.append(pkt_num)
continue

# 基于时间的检测
if time.now() - pkt.sent_time >= loss_delay:
lost.append(pkt_num)

return lost

HTTP/3帧格式

HTTP/3在QUIC流上传输帧,主要帧类型:

1
2
3
4
5
6
7
8
9
10
11
HTTP/3 Frame Types:
┌──────────────────┬────────────────────────────────┐
│ Frame Type │ Description │
├──────────────────┼────────────────────────────────┤
│ DATA (0x00) │ 传输HTTP消息体 │
│ HEADERS (0x01) │ 传输QPACK压缩的HTTP头 │
│ CANCEL_PUSH(0x03)│ 取消服务端推送 │
│ SETTINGS (0x04) │ 连接设置 │
│ PUSH_PROMISE │ 服务端推送承诺 │
│ GOAWAY (0x07) │ 优雅关闭连接 │
└──────────────────┴────────────────────────────────┘

QPACK头部压缩

HTTP/3使用QPACK代替HTTP/2的HPACK,解决了HPACK在QUIC上的队头阻塞问题:

graph TB
    subgraph "QPACK Architecture"
        ENC[Encoder Stream<br>单向流] --> DT[Dynamic Table]
        DEC[Decoder Stream<br>单向流] --> DT
        RS1[Request Stream 1] --> |引用| DT
        RS2[Request Stream 2] --> |引用| DT
    end
1
2
3
4
5
6
7
8
// QPACK编码示例
// 静态表引用
:method = GET → 索引 17(静态表)
:path = / → 索引 1(静态表)

// 动态表引用
custom-header: value → 先通过Encoder Stream插入动态表
→ 然后通过索引引用

服务端配置

Nginx配置HTTP/3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
server {
# HTTP/3 (QUIC)
listen 443 quic reuseport;

# HTTP/2 fallback
listen 443 ssl;

http2 on;
http3 on;

ssl_certificate /etc/ssl/certs/server.crt;
ssl_certificate_key /etc/ssl/private/server.key;

# 必须:TLS 1.3
ssl_protocols TLSv1.3;

# QUIC相关配置
quic_retry on;
ssl_early_data on;

# 告知客户端支持HTTP/3
add_header Alt-Svc 'h3=":443"; ma=86400';

location / {
proxy_pass http://backend;
}
}

Caddy配置HTTP/3

1
2
3
4
5
6
7
8
9
example.com {
# Caddy默认启用HTTP/3,无需额外配置
reverse_proxy localhost:8080

# 显式配置
servers {
protocols h1 h2 h3
}
}

客户端使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Go HTTP/3 客户端
import "github.com/quic-go/quic-go/http3"

func main() {
roundTripper := &http3.RoundTripper{
TLSClientConfig: &tls.Config{},
QuicConfig: &quic.Config{
MaxIdleTimeout: 30 * time.Second,
KeepAlivePeriod: 10 * time.Second,
MaxIncomingStreams: 100,
MaxIncomingUniStreams: 100,
Allow0RTT: true,
},
}
defer roundTripper.Close()

client := &http.Client{
Transport: roundTripper,
}

resp, err := client.Get("https://example.com")
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()

fmt.Println("Protocol:", resp.Proto) // HTTP/3.0
}
1
2
3
4
5
# curl 测试HTTP/3
curl --http3 https://example.com -v

# 使用h3spec测试服务端合规性
h3spec -host example.com -port 443

采用现状与建议

pie title HTTP/3 采用率 (2025年估计)
    "HTTP/1.1" : 20
    "HTTP/2" : 50
    "HTTP/3" : 30

适用场景

场景 HTTP/3优势 建议
移动端应用 连接迁移,弱网表现好 强烈推荐
视频流媒体 低延迟,独立流 推荐
API服务 0-RTT减少延迟 推荐
内网服务 优势不明显 可选
UDP被封锁的网络 无法使用 需要回退到HTTP/2

部署注意事项

  1. UDP限速:某些网络环境对UDP有限速或封锁,需要HTTP/2回退方案
  2. 防火墙规则:确保UDP 443端口开放
  3. 负载均衡:需要支持QUIC的LB(如HAProxy 2.6+)
  4. 监控:QUIC连接的监控工具不如TCP成熟

总结

HTTP/3和QUIC代表了传输协议的重大演进:

  1. QUIC基于UDP,将传输控制和加密合二为一,减少握手延迟
  2. 0-RTT连接恢复显著降低了首次请求延迟
  3. 独立的流彻底解决了TCP层面的队头阻塞
  4. 连接迁移让移动端网络切换不再断连
  5. QPACK针对QUIC优化了头部压缩

在2025年,HTTP/3已经被主流浏览器和CDN广泛支持。对于新项目,建议默认启用HTTP/3,同时保留HTTP/2作为回退方案。

作者 · authorzt
发布 · date2025-04-19
篇幅 · length2.2k 字 · 5 min
许可 · licenseCC BY-SA 4.0
$ echo "comments" · 评论