Files
16gagent/game-accelerator-tech-plan/04-流量转发系统设计.md
T

642 lines
45 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 第四部分:流量转发系统设计
## 4.1 MVP阶段 — UDP Relay
### 4.1.1 协议设计
```
┌──────────────────────────────────────────────────────────────────┐
│ UDP Relay 协议报文结构 │
├──────────────────────────────────────────────────────────────────┤
│ │
│ 报文头 (16字节): │
│ ┌────────┬────────┬────────┬────────┬────────┬────────┐ │
│ │ Magic │ Version│ Type │ Flags │ SeqNum │ Length │ │
│ │ 2B │ 1B │ 1B │ 1B │ 4B │ 4B │ │
│ │ 0xACE0 │ 0x01 │ │ │ │ │ │
│ └────────┴────────┴────────┴────────┴────────┴────────┘ │
│ │
│ 字段说明: │
│ ┌──────────┬──────────┬────────────────────────────────────┐ │
│ │ 字段 │ 大小 │ 说明 │ │
│ ├──────────┼──────────┼────────────────────────────────────┤ │
│ │ Magic │ 2 bytes │ 魔数 0xACE0,用于识别协议 │ │
│ │ Version │ 1 byte │ 协议版本号(当前0x01) │ │
│ │ Type │ 1 byte │ 报文类型 │ │
│ │ │ │ 0x01=DATA, 0x02=ACK, 0x03=HEARTBEAT │ │
│ │ │ │ 0x04=CONTROL, 0x05=FEC │ │
│ │ Flags │ 1 byte │ 标志位 │ │
│ │ │ │ bit0: 加密 │ │
│ │ │ │ bit1: 压缩 │ │
│ │ │ │ bit2: FEC │ │
│ │ │ │ bit3: 重传 │ │
│ │ │ │ bit4-7: 保留 │ │
│ │ SeqNum │ 4 bytes │ 序列号(用于乱序检测和重传) │ │
│ │ Length │ 4 bytes │ 载荷长度 │ │
│ └──────────┴──────────┴────────────────────────────────────┘ │
│ │
│ 扩展头(可选,当Flags中有加密/压缩时): │
│ ┌────────┬────────┬────────┬────────┬────────┐ │
│ │ ExtLen │ ExtType│ IV │ Padding│ HMAC │ │
│ │ 1B │ 1B │ 12B │ 可变 │ 16B │ │
│ └────────┴────────┴────────┴────────┴────────┘ │
│ │
│ 完整报文: │
│ ┌────────────┬────────────┬────────────┬────────────┐ │
│ │ 报文头 │ 扩展头 │ 加密载荷 │ HMAC校验 │ │
│ │ 16B │ 可变 │ 可变 │ 16B │ │
│ └────────────┴────────────┴────────────┴────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────┘
```
### 4.1.2 数据流转时序图
```
┌──────────────────────────────────────────────────────────────────┐
│ UDP Relay 数据流时序图 │
├──────────────────────────────────────────────────────────────────┤
│ │
│ 玩家客户端 接入节点 中转节点 游戏服务器│
│ │ │ │ │ │
│ │ 1.游戏UDP包 │ │ │ │
│ │─────────────────→│ │ │ │
│ │ │ │ │ │
│ │ │ 2.封装为 │ │ │
│ │ │ Relay包 │ │ │
│ │ │ (加序列号) │ │ │
│ │ │ │ │ │
│ │ │ 3.加密+转发 │ │ │
│ │ │───────────────→│ │ │
│ │ │ │ │ │
│ │ │ │ 4.解密+解封装 │ │
│ │ │ │ │ │
│ │ │ │ 5.转发原始包 │ │
│ │ │ │─────────────────→│ │
│ │ │ │ │ │
│ │ │ │ 6.游戏响应 │ │
│ │ │ │←─────────────────│ │
│ │ │ │ │ │
│ │ │ 7.封装+加密 │ │ │
│ │ │←───────────────│ │ │
│ │ │ │ │ │
│ │ 8.解密+解封装 │ │ │ │
│ │←─────────────────│ │ │ │
│ │ │ │ │ │
│ │ 9.返回游戏 │ │ │ │
│ │ │ │ │ │
│ │
│ 总延迟增加:2-10ms(取决于节点间距离) │
│ │
└──────────────────────────────────────────────────────────────────┘
```
### 4.1.3 伪代码实现
```go
// UDP Relay 核心转发逻辑
package relay
import (
"crypto/aes"
"crypto/cipher"
"encoding/binary"
"net"
"sync"
"time"
)
// 协议常量
const (
Magic = 0xACE0
Version = 0x01
TypeDATA = 0x01
TypeACK = 0x02
TypeHEART = 0x03
TypeCONTROL = 0x04
TypeFEC = 0x05
HeaderSize = 16
)
// RelayHeader 报文头
type RelayHeader struct {
Magic uint16
Version uint8
Type uint8
Flags uint8
SeqNum uint32
Length uint32
}
// Session 会话状态
type Session struct {
SessionID uint64
ClientAddr *net.UDPAddr
ServerAddr *net.UDPAddr
SendSeq uint32
RecvSeq uint32
AESKey []byte
LastActive time.Time
RTT time.Duration
LossRate float64
mu sync.Mutex
}
// RelayServer 转发服务器
type RelayServer struct {
listenAddr *net.UDPAddr
conn *net.UDPConn
sessions sync.Map // map[uint64]*Session
aesGCM cipher.AEAD
}
// Start 启动转发服务
func (s *RelayServer) Start() error {
conn, err := net.ListenUDP("udp4", s.listenAddr)
if err != nil {
return err
}
s.conn = conn
buf := make([]byte, 65535)
for {
n, clientAddr, err := conn.ReadFromUDP(buf)
if err != nil {
continue
}
go s.handlePacket(buf[:n], clientAddr)
}
}
// handlePacket 处理收到的包
func (s *RelayServer) handlePacket(data []byte, clientAddr *net.UDPAddr) {
// 解析报文头
if len(data) < HeaderSize {
return
}
header := parseHeader(data[:HeaderSize])
if header.Magic != Magic {
return
}
switch header.Type {
case TypeDATA:
s.handleData(header, data[HeaderSize:], clientAddr)
case TypeHEART:
s.handleHeartbeat(header, clientAddr)
case TypeACK:
s.handleACK(header, clientAddr)
}
}
// handleData 处理数据包
func (s *RelayServer) handleData(header RelayHeader, payload []byte, clientAddr *net.UDPAddr) {
// 获取或创建会话
session := s.getOrCreateSession(clientAddr)
// 解密载荷
plaintext, err := s.decrypt(payload, session.AESKey)
if err != nil {
return
}
// 更新序列号
session.mu.Lock()
session.RecvSeq = header.SeqNum
session.LastActive = time.Now()
session.mu.Unlock()
// 转发到游戏服务器
_, err = s.conn.WriteToUDP(plaintext, session.ServerAddr)
if err != nil {
// 记录错误,触发重连
s.handleForwardError(session, err)
}
// 发送ACK(如果需要可靠传输)
if header.Flags&0x01 != 0 {
s.sendACK(session, header.SeqNum)
}
}
// handleHeartbeat 处理心跳
func (s *RelayServer) handleHeartbeat(header RelayHeader, clientAddr *net.UDPAddr) {
session := s.getSession(clientAddr)
if session == nil {
return
}
session.mu.Lock()
session.LastActive = time.Now()
// 计算RTT(心跳包中有时间戳)
session.mu.Unlock()
// 回复心跳
s.sendHeartbeatReply(session)
}
// ForwardLoop 转发循环(从游戏服务器到客户端)
func (s *RelayServer) ForwardLoop(session *Session) {
buf := make([]byte, 65535)
for {
n, err := session.ServerConn.Read(buf)
if err != nil {
break
}
// 加密
ciphertext := s.encrypt(buf[:n], session.AESKey)
// 封装
header := RelayHeader{
Magic: Magic,
Version: Version,
Type: TypeDATA,
SeqNum: session.SendSeq,
Length: uint32(len(ciphertext)),
}
session.mu.Lock()
session.SendSeq++
session.mu.Unlock()
// 发送
packet := append(header.Bytes(), ciphertext...)
s.conn.WriteToUDP(packet, session.ClientAddr)
}
}
// encrypt AES-256-GCM加密
func (s *RelayServer) encrypt(plaintext []byte, key []byte) []byte {
block, _ := aes.NewCipher(key)
gcm, _ := cipher.NewGCM(block)
nonce := make([]byte, gcm.NonceSize())
// 生成随机nonce
return gcm.Seal(nil, nonce, plaintext, nil)
}
// CheckSessionTimeout 会话超时检查
func (s *RelayServer) CheckSessionTimeout() {
ticker := time.NewTicker(10 * time.Second)
for range ticker.C {
s.sessions.Range(func(key, value interface{}) bool {
session := value.(*Session)
if time.Since(session.LastActive) > 60*time.Second {
// 会话超时,清理
s.sessions.Delete(key)
session.Close()
}
return true
})
}
}
```
## 4.2 商业版阶段 — QUIC Tunnel
### 4.2.1 为什么选择QUIC
```
┌──────────────────────────────────────────────────────────────────┐
│ QUIC vs UDP Relay vs TCP │
├──────────────────────────────────────────────────────────────────┤
│ │
│ 特性 │ UDP Relay │ QUIC │ TCP │
│ ──────────────┼───────────┼────────────┼──────────── │
│ 连接建立延迟 │ 0-RTT │ 0-RTT │ 3-RTT (TLS) │
│ 多路复用 │ ✗ │ ✓ │ ✗ (HOL阻塞) │
│ 可靠传输 │ 可选 │ 可选 │ 必须 │
│ 流量控制 │ 手动 │ 内置 │ 内置 │
│ 拥塞控制 │ 手动 │ BBR/Cubic │ Cubic │
│ 连接迁移 │ 手动 │ 内置 │ ✗ │
│ 加密 │ 手动 │ 内置(TLS1.3)│ 需要TLS │
│ 前向纠错 │ 手动 │ 可扩展 │ ✗ │
│ 多路径 │ 手动 │ 扩展中 │ MPTCP │
│ ──────────────┼───────────┼────────────┼──────────── │
│ 适合场景 │ 简单转发 │ 游戏加速 │ 文件传输 │
│ │
│ 结论:QUIC是最优选择 │
│ - 0-RTT连接建立(快速切换节点) │
│ - 多路复用无HOL阻塞(多游戏流量并行) │
│ - 内置加密(安全性) │
│ - 连接迁移(IP变化不断线) │
│ - 可扩展(加FEC、多路径) │
│ │
└──────────────────────────────────────────────────────────────────┘
```
### 4.2.2 QUIC Tunnel协议设计
```
┌──────────────────────────────────────────────────────────────────┐
│ QUIC Tunnel 架构 │
├──────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 应用层 │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ Stream 0│ │ Stream 1│ │ Stream 2│ │ Stream N│ │ │
│ │ │ 游戏数据│ │ 控制流 │ │ 心跳流 │ │ 其他 │ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ QUIC层 │ │
│ │ ┌──────────────────────────────────────────────────┐ │ │
│ │ │ QUIC Frames: │ │ │
│ │ │ - STREAM (数据帧) │ │ │
│ │ │ - ACK (确认帧) │ │ │
│ │ │ - CRYPTO (加密握手) │ │ │
│ │ │ - PADDING (填充) │ │ │
│ │ │ - CONNECTION_CLOSE (关闭) │ │ │
│ │ │ - FEC (前向纠错,自定义扩展) │ │ │
│ │ └──────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ TLS 1.3层 │ │
│ │ ┌──────────────────────────────────────────────────┐ │ │
│ │ │ - 握手加密 │ │ │
│ │ │ - 会话密钥轮换 │ │ │
│ │ │ - 证书验证 │ │ │
│ │ └──────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ UDP层 │ │
│ │ ┌──────────────────────────────────────────────────┐ │ │
│ │ │ - UDP Socket │ │ │
│ │ │ - BBR拥塞控制 │ │ │
│ │ │ - 多路径支持 │ │ │
│ │ └──────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────┘
```
### 4.2.3 Multipath多路径传输
```
┌──────────────────────────────────────────────────────────────────┐
│ Multipath 多路径设计 │
├──────────────────────────────────────────────────────────────────┤
│ │
│ ┌────────┐ ┌────────┐ │
│ │ │──── Path 1 (联通) ───→ │ │
│ │ 客户端 │──── Path 2 (电信) ───→ 接入节点│──→ 游戏服务器 │
│ │ │──── Path 3 (移动) ───→ │ │
│ └────────┘ └────────┘ │
│ │
│ 路径管理: │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ PathTable: │ │
│ │ ┌─────────┬────────┬────────┬────────┬────────┐ │ │
│ │ │ PathID │ RTT │ Loss │ Bw │ Weight │ │ │
│ │ ├─────────┼────────┼────────┼────────┼────────┤ │ │
│ │ │ path-1 │ 30ms │ 0.1% │ 50Mbps │ 0.5 │ │ │
│ │ │ path-2 │ 45ms │ 0.5% │ 30Mbps │ 0.3 │ │ │
│ │ │ path-3 │ 60ms │ 1.0% │ 20Mbps │ 0.2 │ │ │
│ │ └─────────┴────────┴────────┴────────┴────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ 数据分配策略: │
│ 1. 关键数据(游戏动作)→ 走最优路径(path-1) │
│ 2. 状态数据(位置同步)→ 分散到多路径 │
│ 3. 冗余数据(FEC)→ 走次优路径 │
│ 4. 实时监控路径质量,动态调整权重 │
│ │
│ 路径切换: │
│ - 主路径故障 → 无缝切换到备用路径 │
│ - 切换延迟 < 100ms │
│ - 连接保持(Session ID不变) │
│ │
└──────────────────────────────────────────────────────────────────┘
```
### 4.2.4 FEC前向纠错
```
┌──────────────────────────────────────────────────────────────────┐
│ FEC前向纠错设计 │
├──────────────────────────────────────────────────────────────────┤
│ │
│ 原理:发送N个数据包 + K个冗余包,丢任意K个包都能恢复 │
│ │
│ 编码方式:Reed-Solomon码 │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 示例:RS(10,4) — 10个数据包 + 4个冗余包 │ │
│ │ │ │
│ │ 发送端: │ │
│ │ D1 D2 D3 D4 D5 D6 D7 D8 D9 D10 ← 数据包 │ │
│ │ P1 P2 P3 P4 ← 冗余包(校验包) │ │
│ │ ───────────────────────────────── │ │
│ │ 共14个包,允许丢4个包 │ │
│ │ │ │
│ │ 接收端(假设D2,D5,D8,P2丢失): │ │
│ │ D1 ✗ D3 D4 ✗ D6 D7 ✗ D9 D10 │ │
│ │ ✗ P3 P4 │ │
│ │ ───────────────────────────────── │ │
│ │ 通过RS解码恢复D2,D5,D8,P2 │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ 动态FEC参数调整: │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 丢包率 │ FEC比例 │ 说明 │ │
│ ├─────────────┼────────────┼──────────────────────────────│ │
│ │ < 1% │ N=10, K=1 │ 低冗余,节省带宽 │ │
│ │ 1% - 5% │ N=10, K=2 │ 中等冗余 │ │
│ │ 5% - 10% │ N=10, K=3 │ 高冗余 │ │
│ │ > 10% │ N=10, K=4 │ 最大冗余,保护关键数据 │ │
│ │ > 20% │ 切换路径 │ 丢包太严重,切换链路 │ │
│ └─────────────┴────────────┴──────────────────────────────┘ │
│ │
│ 实现代码(Go): │
│ │
│ type FECEncoder struct { │
│ dataShards int // 数据分片数 │
│ parityShards int // 冗余分片数 │
│ enc reedsolomon.Encoder │
│ } │
│ │
│ func (f *FECEncoder) Encode(data []byte) [][]byte { │
│ // 将数据分割为dataShards个分片 │
│ shards := f.splitData(data, f.dataShards) │
│ // 生成parityShards个冗余分片 │
│ err := f.enc.Encode(shards) │
│ // 返回所有分片(数据+冗余) │
│ return shards │
│ } │
│ │
│ func (f *FECEncoder) Decode(shards [][]byte) ([]byte, error) { │
│ // 通过RS解码恢复丢失的分片 │
│ err := f.enc.Reconstruct(shards) │
│ // 合并分片为原始数据 │
│ return f.mergeShards(shards[:f.dataShards]), err │
│ } │
│ │
└──────────────────────────────────────────────────────────────────┘
```
### 4.2.5 动态拥塞控制
```
┌──────────────────────────────────────────────────────────────────┐
│ 动态拥塞控制设计 │
├──────────────────────────────────────────────────────────────────┤
│ │
│ 目标:在不丢包的前提下,最大化利用带宽 │
│ │
│ 算法选择:BBR (Bottleneck Bandwidth and Round-trip) │
│ │
│ BBR工作原理: │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 1. 测量瓶颈带宽 (BtlBw) │ │
│ │ - 跟踪最大交付速率 │ │
│ │ - 滑动窗口取最大值 │ │
│ │ │ │
│ │ 2. 测量最小RTT (RTprop) │ │
│ │ - 跟踪最小往返时间 │ │
│ │ - 10秒窗口取最小值 │ │
│ │ │ │
│ │ 3. 计算发送速率 │ │
│ │ cwnd = BtlBw × RTprop × gain │ │
│ │ pacing_rate = BtlBw × gain │ │
│ │ │ │
│ │ 4. 四个阶段循环: │ │
│ │ Startup → Drain → ProbeBW → ProbeRTT │ │
│ │ │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ 游戏场景优化: │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 1. 低延迟优先 │ │
│ │ - 限制最大cwnd,避免缓冲区膨胀 │ │
│ │ - pacing_rate不超过实际带宽的80% │ │
│ │ │ │
│ │ 2. 快速恢复 │ │
│ │ - 丢包时快速降低发送速率 │ │
│ │ - 恢复时快速提升(比标准BBR更快) │ │
│ │ │ │
│ │ 3. 多流公平 │ │
│ │ - 多个游戏流共享带宽时公平分配 │ │
│ │ - 优先级流获得更多带宽 │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────┘
```
### 4.2.6 UDP重传优化
```
┌──────────────────────────────────────────────────────────────────┐
│ UDP重传优化设计 │
├──────────────────────────────────────────────────────────────────┤
│ │
│ 问题:标准UDP不重传,丢包就丢了 │
│ 方案:选择性重传 + 优先级重传 │
│ │
│ 数据分类: │
│ ┌──────────┬────────────────┬────────────┬──────────────┐ │
│ │ 优先级 │ 数据类型 │ 重传策略 │ 超时时间 │ │
│ ├──────────┼────────────────┼────────────┼──────────────┤ │
│ │ P0 │ 玩家操作指令 │ 必须重传 │ 50ms │ │
│ │ P1 │ 游戏状态同步 │ 选择重传 │ 100ms │ │
│ │ P2 │ 位置更新 │ 不重传 │ - │ │
│ │ P3 │ 视觉效果数据 │ 不重传 │ - │ │
│ └──────────┴────────────────┴────────────┴──────────────┘ │
│ │
│ 重传流程: │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 发送端: │ │
│ │ 1. 发送数据包,记录发送时间 │ │
│ │ 2. 等待ACK │ │
│ │ 3. 超时未收到ACK → 检查优先级 │ │
│ │ 4. P0/P1 → 立即重传 │ │
│ │ 5. P2/P3 → 放弃,等新数据 │ │
│ │ │ │
│ │ 接收端: │ │
│ │ 1. 收到数据包 → 发送ACK │ │
│ │ 2. 检测到序列号跳跃 → 发送NACK(选择性否定确认) │ │
│ │ 3. 乱序到达 → 缓存并重排序(10ms窗口) │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ 伪代码: │
│ │
│ type RetransmitQueue struct { │
│ pending map[uint32]*Packet │
│ mu sync.Mutex │
│ } │
│ │
│ func (q *RetransmitQueue) Add(pkt *Packet) { │
│ q.mu.Lock() │
│ defer q.mu.Unlock() │
│ pkt.SendTime = time.Now() │
│ q.pending[pkt.SeqNum] = pkt │
│ } │
│ │
│ func (q *RetransmitQueue) CheckTimeout() []*Packet { │
│ q.mu.Lock() │
│ defer q.mu.Unlock() │
│ var retransmit []*Packet │
│ now := time.Now() │
│ for seq, pkt := range q.pending { │
│ if now.Sub(pkt.SendTime) > pkt.Timeout { │
│ if pkt.Priority <= P1 { │
│ retransmit = append(retransmit, pkt) │
│ pkt.SendTime = now │
│ pkt.RetransmitCount++ │
│ } else { │
│ delete(q.pending, seq) │
│ } │
│ } │
│ } │
│ return retransmit │
│ } │
│ │
└──────────────────────────────────────────────────────────────────┘
```
### 4.2.7 智能链路切换
```
┌──────────────────────────────────────────────────────────────────┐
│ 智能链路切换设计 │
├──────────────────────────────────────────────────────────────────┤
│ │
│ 切换触发条件: │
│ 1. 当前链路延迟 > 最优链路延迟 × 1.5 │
│ 2. 当前链路丢包率 > 5% │
│ 3. 连续3次探测质量下降 │
│ 4. 当前链路断开 │
│ │
│ 切换流程: │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ 探测新 │───→│ 建立新 │───→│ 迁移会话 │ │ │
│ │ │ 链路质量 │ │ 连接 │ │ 状态 │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ │ │
│ │ │ │ │ │
│ │ │ ┌─────────────────────┘ │ │
│ │ │ ↓ │ │
│ │ │ ┌──────────┐ ┌──────────┐ │ │
│ │ │ │ 切换流量 │───→│ 关闭旧 │ │ │
│ │ │ │ 到新链路 │ │ 连接 │ │ │
│ │ │ └──────────┘ └──────────┘ │ │
│ │ │ │ │
│ │ └─── 整个过程 < 200ms,用户无感知 │ │
│ │ │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ 会话保持: │
│ - 使用SessionID标识会话 │
│ - 切换时携带SessionID到新链路 │
│ - 新链路通过SessionID恢复会话状态 │
│ - 序列号连续(不因切换而重置) │
│ │
└──────────────────────────────────────────────────────────────────┘
```