强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

07 - HTTP/3 与 QUIC

第 07 章:HTTP/3 与 QUIC

告别 TCP——HTTP 协议的下一次革命


7.1 HTTP/3 概述

HTTP/3 是 HTTP 协议的第三个主要版本,它不再使用 TCP 作为传输层,而是基于 QUIC(Quick UDP Internet Connections)协议运行在 UDP 之上。这是 HTTP 协议历史上最根本的架构变革。

7.1.1 HTTP/2 的遗留问题

HTTP/2 解决了什么:
✓ 应用层队头阻塞(多路复用)
✓ 头部冗余(HPACK)
✓ 无优先级(流优先级)

HTTP/2 没解决什么:
✗ TCP 层队头阻塞
  - 一个 TCP 段丢失 → 整个连接阻塞
  - 所有流被迫等待重传

✗ 连接建立延迟
  - TCP 握手:1 RTT
  - TLS 握手:1-2 RTT
  - 总计:2-3 RTT 才能开始传输数据

✗ 连接迁移困难
  - TCP 连接绑定 (IP, Port) 四元组
  - 网络切换(如 WiFi → 4G)导致连接断开

7.1.2 HTTP/3 协议栈对比

HTTP/2 协议栈:
┌─────────────────────┐
│      HTTP/2         │
├─────────────────────┤
│      TLS 1.2+       │
├─────────────────────┤
│       TCP           │
├─────────────────────┤
│       IP            │
└─────────────────────┘

HTTP/3 协议栈:
┌─────────────────────┐
│      HTTP/3         │
├─────────────────────┤
│   QUIC (内置 TLS)   │
├─────────────────────┤
│       UDP           │
├─────────────────────┤
│       IP            │
└─────────────────────┘

7.2 QUIC 协议

7.2.1 QUIC 是什么

QUIC(Quick UDP Internet Connections)最初由 Google 设计,后由 IETF 标准化。它是一个通用的传输层协议,旨在提供比 TCP 更好的性能,同时保持可靠性。

特性TCPQUIC
传输层TCPUDP
队头阻塞有(连接级)无(流级独立)
握手延迟1-3 RTT0-1 RTT
加密可选 TLS强制内置 TLS 1.3
连接迁移不支持支持(Connection ID)
拥塞控制内核实现用户空间实现(可定制)
头部压缩N/AQPACK(HTTP/3 专用)

7.2.2 QUIC 连接建立

TCP + TLS 1.2(3 RTT):
客户端              服务器
  |--- SYN ----------->|        RTT 1: TCP 握手
  |<-- SYN+ACK --------|
  |--- ACK ----------->|
  |--- ClientHello --->|        RTT 2: TLS 握手
  |<-- ServerHello -----|
  |--- Finished ------->|
  |<-- 数据 ------------|        RTT 3: 开始传输

TCP + TLS 1.3(2 RTT):
客户端              服务器
  |--- SYN ----------->|        RTT 1: TCP 握手
  |<-- SYN+ACK --------|
  |--- ClientHello --->|        RTT 2: TLS 握手 + 数据
  |<-- ServerHello + 数据

QUIC 首次连接(1 RTT):
客户端              服务器
  |--- Initial (ClientHello) -->|  RTT 1: QUIC 握手 + 数据
  |<-- Handshake (ServerHello) -|
  |--- 数据 ------------------->|

QUIC 0-RTT 恢复(0 RTT):
客户端              服务器
  |--- Initial + 0-RTT 数据 -->|  无等待!直接发送
  |<-- 数据 -------------------|

7.3 流级独立性

7.3.1 QUIC 流模型

TCP 的队头阻塞问题:

发送方:[流1-帧1][流2-帧1][流1-帧2][流2-帧2]
传输层:[TCP段1][TCP段2][TCP段3][TCP段4]

如果 TCP 段2 丢失:
  流1-帧2 和 流2-帧1 都被阻塞
  即使流2-帧2 已到达,也无法交付

QUIC 的流级独立:

发送方:[流1-帧1][流2-帧1][流1-帧2][流2-帧2]
传输层:[QUIC包1][QUIC包2][QUIC包3][QUIC包4]

如果 QUIC 包2 丢失(包含流2-帧1):
  流1 的数据继续交付,不受影响
  流2 等待重传

7.3.2 流类型

流类型发起方用途
客户端发起的双向流客户端HTTP 请求/响应
服务器发起的双向流服务器通常不用于 HTTP/3
客户端发起的单向流客户端控制流
服务器发起的单向流服务器推送流、控制流
# QUIC 流 ID 编码规则
def stream_info(stream_id: int) -> dict:
    """解析 QUIC 流 ID"""
    initiator = "客户端" if stream_id & 0x01 == 0 else "服务器"
    stream_type = "双向" if stream_id & 0x02 == 0 else "单向"
    return {
        "id": stream_id,
        "initiator": initiator,
        "type": stream_type,
    }

# 示例
for sid in [0, 1, 2, 3, 4, 5, 6, 7]:
    info = stream_info(sid)
    print(f"流 {sid}: {info['initiator']}发起, {info['type']}")

7.4 连接迁移

7.4.1 问题场景

传统 TCP 连接的问题:

用户在咖啡店用 WiFi 访问网站:
  源 IP: 192.168.1.100
  源端口: 54321
  目标 IP: 93.184.216.34
  目标端口: 443

用户离开咖啡店,切换到 4G:
  源 IP: 10.0.0.50  (变化了!)
  源端口: 12345     (变化了!)
  
TCP 连接断开,需要重新建立!
所有进行中的请求失败!

7.4.2 QUIC 的连接迁移

QUIC 使用 Connection ID 标识连接:

初始连接:
  Connection ID: 0x1234567890abcdef
  源 IP: 192.168.1.100
  源端口: 54321

网络切换后:
  Connection ID: 0x1234567890abcdef (不变!)
  源 IP: 10.0.0.50
  源端口: 12345

服务器通过 Connection ID 识别连接,无需重建!
// 模拟连接迁移
package main

import (
	"fmt"
	"net"
)

type QUICConnection struct {
	connectionID []byte
	remoteAddr   *net.UDPAddr
	streams      map[uint32]*QUICStream
}

type QUICStream struct {
	id   uint32
	data []byte
}

func NewQUICConnection(connID []byte, addr *net.UDPAddr) *QUICConnection {
	return &QUICConnection{
		connectionID: connID,
		remoteAddr:   addr,
		streams:      make(map[uint32]*QUICStream),
	}
}

func (c *QUICConnection) Migrate(newAddr *net.UDPAddr) {
	fmt.Printf("连接迁移: %s -> %s\n", c.remoteAddr, newAddr)
	fmt.Printf("Connection ID 不变: %x\n", c.connectionID)
	c.remoteAddr = newAddr
}

func main() {
	connID := []byte{0x12, 0x34, 0x56, 0x78}
	
	// 初始连接
	addr1, _ := net.ResolveUDPAddr("udp4", "192.168.1.100:54321")
	conn := NewQUICConnection(connID, addr1)
	fmt.Printf("初始连接: %s\n", conn.remoteAddr)
	
	// 连接迁移
	addr2, _ := net.ResolveUDPAddr("udp4", "10.0.0.50:12345")
	conn.Migrate(addr2)
}

7.5 0-RTT 数据传输

7.5.1 0-RTT 原理

首次连接后,客户端保存服务器的配置:
- 服务器公钥
- 会话票据(Session Ticket)
- 早期数据密钥

后续连接时:
客户端可以在握手的同时发送数据(0-RTT)

安全性注意:
0-RTT 数据没有前向保密性
可能遭受重放攻击

7.5.2 0-RTT 安全限制

# 0-RTT 数据的幂等性检查
class ZeroRTTHandler:
    def __init__(self):
        self.seen_requests = set()  # 防重放
        self.max_0rtt_size = 16384  # 16KB 限制
    
    def handle_0rtt_data(self, request_id: str, data: bytes, method: str) -> bool:
        """处理 0-RTT 数据"""
        # 检查重放
        if request_id in self.seen_requests:
            return False  # 拒绝重放
        
        # 只接受幂等方法
        if method not in ("GET", "HEAD", "OPTIONS"):
            return False  # 非幂等方法拒绝 0-RTT
        
        # 检查大小
        if len(data) > self.max_0rtt_size:
            return False
        
        self.seen_requests.add(request_id)
        return True

# 0-RTT 适用场景
# ✓ GET 请求
# ✓ 静态资源加载
# ✓ 缓存验证(If-None-Match)
# ✗ POST/PUT/DELETE
# ✗ 幂等性不确定的操作

7.6 QPACK:HTTP/3 的头部压缩

7.6.1 QPACK vs HPACK

特性HPACK (HTTP/2)QPACK (HTTP/3)
编码流共享编码上下文独立编码流
队头阻塞有(HEADERS 帧阻塞)无(通过指令流分离)
动态表访问同步异步(通过编码流通知)
复杂度
HPACK 的问题:
- 头部块在 HEADERS 帧中传输
- 如果 HEADERS 帧丢失,后续帧被阻塞

QPACK 的解决方案:
- 使用两个单向流:编码流(Encoder)和解码流(Decoder)
- 编码流传递动态表更新
- HEADERS 帧引用已确认的表条目
- 解码器可延迟处理,不阻塞数据流

7.7 QUIC 拥塞控制

7.7.1 可插拔的拥塞控制

// QUIC 拥塞控制接口(简化)
package main

type CongestionControl interface {
    // 数据包发送确认
    OnPacketSent(sentTime int64, packetNumber uint64, bytes int, isRetransmittable bool)
    
    // 收到 ACK
    OnPacketAcked(packetNumber uint64, ackDelay int64)
    
    // 数据包丢失
    OnPacketLost(packetNumber uint64, lostTime int64)
    
    // 获取拥塞窗口
    GetCongestionWindow() int
    
    // 获取慢启动阈值
    GetSlowStartThreshold() int
    
    // 是否在慢启动
    InSlowStart() bool
    
    // 是否在恢复期
    InRecovery() bool
}

// NewReno 拥塞控制实现(简化)
type NewReno struct {
    congestionWindow    int
    slowStartThreshold  int
    bytesInFlight       int
    recoveryStartTime   int64
    maxCongestionWindow int
    minCongestionWindow int
}

func NewNewReno(initialWindow int) *NewReno {
    return &NewReno{
        congestionWindow:   initialWindow,
        slowStartThreshold: 1 << 30, // 初始为极大值
        maxCongestionWindow: 1 << 24, // 16MB
        minCongestionWindow: 2 * 1460, // 2 * MSS
    }
}

func (nr *NewReno) OnPacketAcked(packetNumber uint64, ackDelay int64) {
    if nr.InSlowStart() {
        // 慢启动:指数增长
        nr.congestionWindow += 1460 // MSS
    } else {
        // 拥塞避免:线性增长
        nr.congestionWindow += 1460 * 1460 / nr.congestionWindow
    }
    
    if nr.congestionWindow > nr.maxCongestionWindow {
        nr.congestionWindow = nr.maxCongestionWindow
    }
}

func (nr *NewReno) OnPacketLost(packetNumber uint64, lostTime int64) {
    if nr.InRecovery() {
        return // 已在恢复期
    }
    
    nr.recoveryStartTime = lostTime
    nr.slowStartThreshold = nr.congestionWindow / 2
    nr.congestionWindow = nr.slowStartThreshold
}

func (nr *NewReno) GetCongestionWindow() int         { return nr.congestionWindow }
func (nr *NewReno) GetSlowStartThreshold() int       { return nr.slowStartThreshold }
func (nr *NewReno) InSlowStart() bool                { return nr.congestionWindow < nr.slowStartThreshold }
func (nr *NewReno) InRecovery() bool                 { return false }

7.8 生态系统支持

7.8.1 浏览器支持

浏览器版本备注
Chrome87+ (2020)默认启用
Firefox88+ (2021)默认启用
Edge87+基于 Chromium
Safari14+ (2021)macOS/iOS
IE不支持已停止更新

7.8.2 服务器支持

服务器版本支持状态
Nginx1.25.0+实验性支持
Caddy2.6+默认启用
LiteSpeed5.4+完整支持
Cloudflare-全面支持

7.8.3 Go 生态

// 使用 quic-go 实现 HTTP/3 服务器
package main

import (
	"fmt"
	"log"
	"net/http"

	"github.com/quic-go/quic-go/http3"
)

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "Hello, HTTP/3!\n")
		fmt.Fprintf(w, "Protocol: %s\n", r.Proto)
	})

	server := &http3.Server{
		Addr:    ":8443",
		Handler: mux,
	}

	log.Println("HTTP/3 服务器启动于 :8443")
	log.Fatal(server.ListenAndServeTLS("cert.pem", "key.pem"))
}
# 使用 curl 测试 HTTP/3(需要支持 quic 的版本)
curl --http3 https://localhost:8443/

# 使用 quiche 工具
docker run --rm ymuski/curl-http3 curl --http3 -k https://localhost:8443/

7.9 HTTP/2 vs HTTP/3 对比

特性HTTP/2HTTP/3
传输层TCPUDP (QUIC)
队头阻塞TCP 层有完全消除
连接建立1-3 RTT0-1 RTT
连接迁移不支持支持
加密TLS 可选强制 TLS 1.3
头部压缩HPACKQPACK
协议僵化严重(中间设备)较轻(UDP 较新)
生态成熟度快速增长中

7.10 注意事项

⚠️ UDP 中间设备问题

  • 部分企业防火墙限制 UDP 流量
  • NAT 设备可能丢弃 QUIC 包
  • 建议同时提供 HTTP/2 降级方案

⚠️ CPU 开销

  • QUIC 在用户空间实现,CPU 开销高于内核 TCP
  • 加密/解密在用户空间完成
  • 高流量场景需评估 CPU 资源

⚠️ 0-RTT 重放攻击

  • 0-RTT 数据可能被重放
  • 仅对幂等请求使用 0-RTT
  • 服务器需实现防重放机制

💡 迁移建议

  • 新项目直接采用 HTTP/3
  • 存量项目先支持 HTTP/2,再逐步升级
  • 使用 Alt-Svc 头部宣告 HTTP/3 支持

7.11 扩展阅读


第 06 章 - 流量控制 | 第 08 章 - gRPC 基础