土法炼钢兴趣小组的算法知识备份

【网络工程】可靠 UDP 框架:KCP、ENet 与 QUIC 的设计对比

文章导航

分类入口
network
标签入口
#udp#kcp#enet#quic#reliable-udp#arq

目录

上一篇我们分析了 UDP 的工程本质——它是一张白纸,不提供可靠传输、不做流控、不做拥塞控制。这意味着如果你的应用既需要 UDP 的低延迟特性,又需要某种程度的可靠性,就必须在应用层(或传输层框架中)自己实现。

“在 UDP 上实现可靠传输”听起来像是在重新发明 TCP。某种意义上确实是——但关键区别在于:TCP 的可靠性策略是固定的(全量有序交付),而 UDP 上的框架可以提供可定制的可靠性

这三个框架代表了”UDP 上的可靠传输”的三种设计哲学:KCP 追求极致低延迟、ENet 追求灵活性、QUIC 追求全面性。本文逐一剖析它们的内部机制,并在文末做系统对比。

一、ARQ 基础:可靠传输的构建原语

在具体分析框架之前,先回顾 ARQ(Automatic Repeat reQuest,自动重传请求)的基本模式——所有可靠 UDP 框架都是在这些原语上构建的。

1.1 三种 ARQ 模式

1. Stop-and-Wait(停等)
   发送一个包 → 等 ACK → 发下一个
   简单但吞吐量极低(利用率 = 包大小 / (RTT × 带宽))

2. Go-Back-N(回退 N)
   发送窗口内的多个包 → 收到 NAK 或超时 → 从丢失点重发所有后续包
   TCP 的基本模式(累积 ACK 本质上是 Go-Back-N)
   问题:一个包丢失导致大量冗余重传

3. Selective Repeat(选择性重传)
   发送窗口内的多个包 → 只重传丢失的包
   TCP SACK 是选择性重传的实现
   KCP、ENet、QUIC 都使用选择性重传

1.2 ACK 机制的设计选择

ACK 设计空间:

1. 累积 ACK(TCP 默认)
   "我已经收到 序列号 100 之前的所有数据"
   优点:ACK 丢失不影响(下一个 ACK 覆盖)
   缺点:不知道 100 之后收到了什么

2. 选择性 ACK(TCP SACK / QUIC)
   "我收到了 0-99, 150-199"(报告收到的区间)
   优点:精确知道哪些丢了
   缺点:ACK 包更大

3. 逐包 ACK(KCP 默认)
   "我收到了包 #1, #2, #5, #7"(每个包独立确认)
   优点:最精确
   缺点:ACK 流量大

4. UNA + ACK 混合(KCP 实际)
   UNA = "100 之前的都收到了"(累积)
   ACK = "我还收到了 #105, #107"(选择性)
   兼顾效率和精确性

二、KCP:用带宽换延迟

2.1 设计哲学

KCP(由 skywind3000 开发)的核心理念是:在牺牲一定带宽的前提下,将延迟降到最低。它的每个设计决策都在向延迟倾斜:

TCP 的策略(带宽优先):
  重传等待:   RTO 至少 200ms(Linux 最小值),指数退避 (RTO × 2^n)
  延迟 ACK:   等 40ms 再发 ACK(凑 piggyback)
  Nagle 算法: 小包合并,等 ACK 或缓冲区满
  拥塞控制:   丢包后 cwnd 减半

KCP 的策略(延迟优先):
  重传等待:   RTO 最小 30ms,非指数退避 (RTO × 1.5^n)
  即时 ACK:   不延迟,立即发送 ACK
  无 Nagle:   默认关闭 Nagle(可选开启)
  可选拥塞:   拥塞控制可关闭(用带宽换延迟)

2.2 包格式与协议

KCP 包格式 (24 字节头部):
┌──────────────────────────────────────────────────────────┐
│ conv (4B) │ cmd (1B) │ frg (1B) │ wnd (2B) │ ts (4B)   │
│ sn (4B)   │ una (4B) │ len (4B) │ data ... (变长)       │
└──────────────────────────────────────────────────────────┘

conv: 会话编号(用于多路复用)
cmd:  命令类型
      81 = IKCP_CMD_PUSH (数据推送)
      82 = IKCP_CMD_ACK  (确认)
      83 = IKCP_CMD_WASK (询问对端窗口)
      84 = IKCP_CMD_WINS (通告窗口大小)
frg:  分片编号(KCP 层面的分片,非 IP 分片)
wnd:  接收窗口大小
ts:   时间戳(用于 RTT 计算)
sn:   序列号(包级别,非字节级别)
una:  未确认的最小序列号(累积确认基准)
len:  数据长度

2.3 KCP 的快速重传

KCP 的重传策略是它区别于 TCP 的核心:

// === RTO 计算 ===
// TCP: RTO = SRTT + 4 × RTTVAR, 最小 200ms (Linux)
// KCP: RTO = SRTT + 4 × RTTVAR, 最小 30ms (可配置)

// === 超时重传的退避 ===
// TCP: RTO, 2*RTO, 4*RTO, 8*RTO ... (指数退避)
// KCP (nodelay=1): RTO, 1.5*RTO, 2.25*RTO ... (1.5 倍退避)
// KCP (nodelay=2): RTO, 2*RTO, 3*RTO ... (线性退避)

// 为什么退避策略重要?
// 假设 RTT=100ms, 首次 RTO=200ms:
//   TCP 连续重传:  200 + 400 + 800 + 1600 = 3000ms (4次重传总等待)
//   KCP 连续重传:  200 + 300 + 450 + 675  = 1625ms (节省 46%)

// === 快速重传(跳过等待 RTO)===
// TCP: 收到 3 个重复 ACK → 快速重传
// KCP: 可配置阈值(默认 fastresend=2)
//      收到 2 个跳过该包的 ACK → 立即重传(不等 RTO)

// 示例:
// 发送: [1] [2] [3] [4] [5]
// 收到 ACK: #1, #3, #4
// 包 #2 被跳过了 2 次(#3 和 #4 的 ACK)
// KCP (fastresend=2): 立即重传 #2,不等 RTO
// TCP: 需要 3 个重复 ACK,且最快也是在 RTO 后重传

2.4 KCP 配置与使用

// === 创建 KCP 对象 ===
ikcpcb *kcp = ikcp_create(conv, user);

// 设置输出回调(KCP 不直接操作网络,需要你提供发送函数)
ikcp_setoutput(kcp, udp_output);

// === 模式配置 ===
// 普通模式(类似 TCP)
ikcp_nodelay(kcp, 0, 40, 0, 0);
// 参数: (kcp, nodelay, interval, resend, nc)
//   nodelay=0: 不启用 nodelay 模式
//   interval=40: 内部更新时钟间隔 40ms
//   resend=0: 不启用快速重传
//   nc=0: 启用拥塞控制

// 极速模式(最低延迟)
ikcp_nodelay(kcp, 1, 10, 2, 1);
//   nodelay=1: 启用 nodelay(RTO 最小 30ms,1.5 倍退避)
//   interval=10: 10ms 更新间隔
//   resend=2: 2 次跳过即快速重传
//   nc=1: 关闭拥塞控制(流量不受限)

// === 窗口配置 ===
ikcp_wndsize(kcp, 128, 128);
// 发送窗口 128 包,接收窗口 128 包
// 每包最大 MSS (默认 1400 字节)
// 带宽 ≈ 128 × 1400 / RTT

// === MTU 配置 ===
ikcp_setmtu(kcp, 1400);
// KCP 会在大于 MTU 的数据上自行分片(frg 字段)

// === 使用循环 ===
// 发送
ikcp_send(kcp, data, len);

// 定时更新(必须按 interval 调用)
ikcp_update(kcp, current_ms);

// 接收
int len = ikcp_recv(kcp, buf, sizeof(buf));

// 输入(将收到的 UDP 数据喂给 KCP)
ikcp_input(kcp, udp_data, udp_len);

2.5 KCP 的带宽代价

KCP 为低延迟付出的带宽代价:

1. 头部开销: 24 字节 (TCP 20 字节 + 选项)
   对小包(如游戏 50 字节载荷): 32% 开销

2. 即时 ACK: 不合并 ACK,每个数据包单独确认
   假设双向通信: ACK 流量 ≈ 数据流量的 30-50%

3. 无 Nagle: 小包不合并,增加包数(增加 IP/UDP 头部开销)

4. 关闭拥塞控制: 不会主动降速,可能在拥塞时加剧丢包
   → 在公网使用 KCP + 关闭拥塞控制是不负责任的行为
   → 仅在受控网络(如 VPN 隧道内)关闭拥塞控制

实测(100Mbps 链路,RTT=50ms,1% 丢包):
  TCP (CUBIC):  吞吐 ~15 Mbps, P99 延迟 ~800ms
  KCP (极速):   吞吐 ~12 Mbps, P99 延迟 ~120ms
  结论: KCP 吞吐量低 20%,但 P99 延迟低 85%

三、ENet:游戏网络的多频道模型

3.1 设计哲学

ENet 是为多人在线游戏设计的 UDP 网络库。它的核心创新是频道(Channel)模型——一个连接内可以有多个逻辑频道,每个频道独立选择可靠性和有序性。

ENet 的频道模型:
┌──────────────────────────────────────┐
│ ENet Connection (Host ↔ Peer)        │
│                                      │
│  Channel 0: Reliable Ordered         │ → 游戏事件(攻击/道具/聊天)
│  Channel 1: Reliable Unordered       │ → 资源加载/文件传输
│  Channel 2: Unreliable Sequenced     │ → 位置同步
│  Channel 3: Unreliable Unsequenced   │ → 特效/粒子
└──────────────────────────────────────┘

对比 TCP:
  TCP 只有一种模式: Reliable Ordered
  ENet 提供 4 种组合: Reliable/Unreliable × Ordered/Unordered

3.2 四种传输模式

1. Reliable Ordered(可靠有序)
   - 保证到达、保证顺序
   - 类似 TCP,但只在频道内有序(不跨频道阻塞)
   - 用途:关键游戏事件("玩家 A 击杀了玩家 B")

2. Reliable Unordered(可靠无序)
   - 保证到达、不保证顺序
   - 丢了就重传,但不阻塞后续包
   - 用途:文件下载、非顺序资源加载

3. Unreliable Sequenced(不可靠有序)
   - 不保证到达(丢了不重传)
   - 但丢弃乱序/过时的包
   - 用途:位置同步(只关心最新位置,旧的丢了没关系)

4. Unreliable Unsequenced(不可靠无序)
   - 不保证到达、不保证顺序
   - 基本等同于原始 UDP + 包格式
   - 用途:不重要的视觉效果

3.3 ENet 内部机制

ENet 包格式:
┌─────────────────────────────────────┐
│ Peer ID (2B) │ Sent Time (2B)       │ ← 命令头
│ Command (1B) │ Channel ID (1B)      │
│ Sequence # (2B)                     │
│ Data Length (2B) │ Data ...          │
└─────────────────────────────────────┘

一个 UDP 数据报可以包含多个 ENet 命令(打包)

命令类型:
  SEND_RELIABLE:       可靠数据
  SEND_UNRELIABLE:     不可靠数据
  SEND_UNSEQUENCED:    不可靠无序数据
  ACKNOWLEDGE:         确认(ACK)
  CONNECT:             连接请求
  DISCONNECT:          断开连接
  PING:                心跳
  BANDWIDTH_LIMIT:     带宽限制通告
  THROTTLE_CONFIGURE:  限流配置
// === ENet 使用示例 ===
#include <enet/enet.h>

// 初始化
enet_initialize();

// 创建 Host(服务端)
ENetAddress address;
address.host = ENET_HOST_ANY;
address.port = 7777;
ENetHost *server = enet_host_create(
    &address,
    32,     // 最大连接数
    4,      // 频道数
    0,      // 下行带宽限制(0=不限)
    0       // 上行带宽限制
);

// 事件循环
ENetEvent event;
while (enet_host_service(server, &event, 1000) >= 0) {
    switch (event.type) {
        case ENET_EVENT_TYPE_CONNECT:
            printf("Peer connected\n");
            break;

        case ENET_EVENT_TYPE_RECEIVE:
            printf("Channel %u: %s\n",
                   event.channelID,
                   event.packet->data);
            enet_packet_destroy(event.packet);
            break;

        case ENET_EVENT_TYPE_DISCONNECT:
            printf("Peer disconnected\n");
            break;
    }
}

// 发送数据到不同频道
// 频道 0: 可靠有序 — 游戏事件
ENetPacket *packet = enet_packet_create(
    "attack:player2",
    strlen("attack:player2"),
    ENET_PACKET_FLAG_RELIABLE     // 可靠传输
);
enet_peer_send(peer, 0, packet);  // 频道 0

// 频道 2: 不可靠有序 — 位置更新
ENetPacket *pos = enet_packet_create(
    pos_data, pos_len,
    ENET_PACKET_FLAG_UNSEQUENCED  // 不可靠无序
);
enet_peer_send(peer, 2, pos);     // 频道 2

3.4 ENet 的拥塞控制

ENet 实现了一个简单的带宽限流机制(Throttle),而非 TCP 式的拥塞窗口:

ENet Throttle 机制:
  1. 测量 RTT 和 RTT 变化
  2. 根据 RTT 变化调整 throttle 值(0-32)
  3. throttle 决定发送速率的比例

  throttle = 32: 全速发送
  throttle = 16: 50% 速率
  throttle = 0:  停止发送

  调节逻辑:
  if (RTT < RTT_avg - RTT_variance) {
      throttle += acceleration;     // 网络变好,加速
  } else if (RTT > RTT_avg + RTT_variance) {
      throttle -= deceleration;     // 网络变差,减速
  }

  可配置参数:
  enet_peer_throttle_configure(peer,
      interval,      // 测量间隔(ms)
      acceleration,  // 加速步长
      deceleration   // 减速步长
  );

这个机制比 TCP 的拥塞控制简单得多,适合局域网或延迟敏感的游戏场景,但在公网高丢包环境下表现不如 CUBIC/BBR。

3.5 ENet 的连接管理

ENet 实现了完整的连接生命周期,这是它与 KCP 的重要区别:

连接建立(类似 TCP 三次握手):
  Client                      Server
    |--- CONNECT ----------->|    # 包含 MTU、窗口等协商参数
    |<-- VERIFY_CONNECT -----|    # 确认连接参数
    |--- ACKNOWLEDGE ------->|    # 连接建立完成

断开连接(优雅关闭):
  Peer A                     Peer B
    |--- DISCONNECT -------->|    # 请求断开
    |<-- ACKNOWLEDGE --------|    # 确认
    # Peer A 清理资源             # Peer B 触发 DISCONNECT 事件

心跳机制:
  - 默认每 500ms 发送 PING
  - 超过 timeout(默认 32 秒)无响应 → 判定断连
  - 可配置:
    peer->timeoutMinimum = 5000;   // 最小超时 5 秒
    peer->timeoutMaximum = 30000;  // 最大超时 30 秒
    peer->timeoutLimit = 32;       // 超时重试次数

MTU 协商:
  - 连接时探测 MTU(从 1400 递减到 576)
  - 选择双方和路径都支持的最大 MTU
  - 避免 IP 分片
// ENet 连接参数配置
// 设置连接超时
enet_peer_timeout(peer,
    32,     // timeoutLimit: 超时次数阈值
    5000,   // timeoutMinimum: 最小超时 (ms)
    30000   // timeoutMaximum: 最大超时 (ms)
);

// 设置 Ping 间隔
enet_peer_ping_interval(peer, 500);  // 每 500ms 发一次 ping

// 获取连接统计信息
printf("RTT: %u ms\n", peer->roundTripTime);
printf("RTT Variance: %u ms\n", peer->roundTripTimeVariance);
printf("Packet Loss: %u%%\n", peer->packetLoss / 65536 * 100);
printf("Throttle: %u/32\n", peer->packetThrottle);

四、QUIC 的可靠传输设计

4.1 QUIC vs KCP/ENet 的定位差异

KCP:  轻量级 ARQ 库(~3000 行 C),嵌入你的应用
ENet: 游戏网络库(~10000 行 C),提供完整的连接管理
QUIC: 完整的传输协议(RFC 9000),替代 TCP+TLS

功能层次:
┌─────────────────────────────────────────┐
│ KCP:    ARQ + 流控                      │ ← 最薄
├─────────────────────────────────────────┤
│ ENet:   连接管理 + 多频道 + ARQ + 限流  │ ← 中等
├─────────────────────────────────────────┤
│ QUIC:   连接管理 + 多路复用 + ARQ       │
│         + 流控 + 拥塞控制 + 加密        │
│         + 连接迁移 + 0-RTT              │ ← 最完整
└─────────────────────────────────────────┘

4.2 QUIC 的 Stream 多路复用

QUIC 解决了 TCP 最根本的结构性问题——字节流模型导致的队头阻塞:

TCP + HTTP/2 的队头阻塞:
  Stream A: [A1] [A2] [__] [A4]
  Stream B: [B1] [B2] [B3]
                   ↑
                   A3 丢了
  TCP 字节流: 全部阻塞等待 A3 重传
  HTTP/2:    Stream A 和 B 都被阻塞

QUIC 的 Stream 独立:
  Stream A: [A1] [A2] [__] [A4]
  Stream B: [B1] [B2] [B3]
                   ↑
                   A3 丢了
  QUIC: Stream A 等待 A3 重传
        Stream B 正常交付 B1, B2, B3(不受影响)

原因: QUIC 在传输层就是多 Stream 模型
      每个 Stream 有独立的序列号空间和流控
      丢包只影响对应 Stream

4.3 QUIC 的 ACK 机制

QUIC 的 ACK 设计比 TCP 更精确:

TCP ACK:
  ACK Number: 100 (累积确认)
  SACK: 150-200, 250-300 (选择性确认)
  问题: 分不清"原始包的 ACK"和"重传包的 ACK"
        → RTO 计算不准确(重传歧义,Karn 算法的限制)

QUIC ACK:
  Largest Acknowledged: 100
  ACK Ranges: 0-100, 150-200, 250-300
  ACK Delay: 25ms (接收端的处理延迟)

  关键改进:
  1. 包编号(Packet Number)单调递增,重传用新编号
     → 永远不会有重传歧义
     → RTT 测量更准确

  2. ACK 延迟显式报告
     → 发送端可以更准确地计算 RTT

  3. ACK 频率可协商 (ACK_FREQUENCY frame)
     → 高带宽场景减少 ACK 频率
     → 低延迟场景增加 ACK 频率

4.4 QUIC 的丢包检测(RFC 9002)

QUIC 的丢包检测比 TCP 更精确,这直接影响重传延迟:

QUIC 丢包判定(两个条件,满足任一即认为丢包):

1. 包序号间隔(Packet Threshold)
   如果一个包之后有 3 个更大序号的包被确认 → 该包被认为丢失
   类似 TCP 的 3 个重复 ACK,但更精确(因为包编号不重用)

2. 时间阈值(Time Threshold)
   如果一个包发送后超过 max(SRTT, latest_RTT) × 9/8 仍未被确认 → 丢失
   比 TCP 的 RTO 更激进(TCP RTO 通常 = SRTT + 4×RTTVAR)

QUIC vs TCP 丢包检测对比:
  TCP:  等 RTO(至少 200ms)或 3 个重复 ACK(需要后续数据触发)
  QUIC: 等 ~1.125 × SRTT 或 3 个后续包确认(更快触发)

  RTT=50ms 场景:
    TCP RTO:  max(200ms, 50+4×12.5) = 200ms
    QUIC:     50 × 1.125 = 56.25ms

  → QUIC 的丢包检测比 TCP 快 3-4 倍

4.5 QUIC 的流控

QUIC 有两层流控——Stream 级别和连接级别:

Stream 级别流控:
  每个 Stream 有独立的接收窗口
  MAX_STREAM_DATA frame 通告窗口更新
  → 慢 Stream 不会阻塞快 Stream

连接级别流控:
  所有 Stream 共享一个连接级接收窗口
  MAX_DATA frame 通告窗口更新
  → 防止总内存使用超限

示例:
  连接窗口: 1 MB
  Stream A 窗口: 256 KB
  Stream B 窗口: 256 KB
  Stream C 窗口: 512 KB

  Stream A 满了 → 只阻塞 Stream A
  连接窗口满了 → 所有 Stream 暂停(但实际中连接窗口会动态调整)

4.6 QUIC 的拥塞控制

QUIC 的拥塞控制是可插拔的——RFC 9002 推荐了一个类似 Reno 的默认算法,但实现可以自由替换:

# 主流 QUIC 实现的拥塞控制:

# Google (Chromium/gQUIC):  BBR v2
# Cloudflare (quiche):      CUBIC (默认) / BBR v2
# Facebook (mvfst):         CUBIC / Copa / BBR
# Microsoft (msquic):       CUBIC (默认)
# Rust (quinn):             CUBIC (默认) / BBR
# Go (quic-go):             Reno (默认) / CUBIC

# 可插拔拥塞控制的优势:
# 1. 用户态实现 → 升级不需要更新内核
# 2. 可以针对不同连接使用不同算法
#    → CDN: BBR(吞吐优先)
#    → 游戏: Copa(延迟优先)
#    → 数据中心: DCTCP 风格

五、三框架对比

5.1 功能对比

┌──────────────────┬─────────────────┬─────────────────┬─────────────────┐
│                  │ KCP             │ ENet            │ QUIC            │
├──────────────────┼─────────────────┼─────────────────┼─────────────────┤
│ 代码规模         │ ~3000 行 C      │ ~10000 行 C     │ 50000+ 行       │
│ 协议标准         │ 无(开源实现)  │ 无(开源实现)  │ RFC 9000-9002   │
│ 连接管理         │ 无              │ 有              │ 有              │
│ 多路复用         │ 无(单流)      │ 多频道          │ 多 Stream       │
│ 可靠性           │ 可靠有序        │ 4 种模式可选    │ Stream 内可靠   │
│ 流量控制         │ 简单窗口        │ Throttle        │ 双层流控        │
│ 拥塞控制         │ 可选(可关闭)  │ 简单限流        │ 可插拔算法      │
│ 加密             │ 无              │ 无              │ 内置 TLS 1.3   │
│ NAT 穿越         │ 无(需自行)    │ 无(需自行)    │ Connection ID   │
│ 连接迁移         │ 无              │ 无              │ 有              │
│ 0-RTT            │ 无              │ 无              │ 有              │
│ 生态             │ 中(游戏/VPN)  │ 中(游戏)      │ 大(Web/CDN)   │
│ 集成复杂度       │ 低(嵌入式)    │ 中              │ 高              │
└──────────────────┴─────────────────┴─────────────────┴─────────────────┘

5.2 延迟对比

测试环境: 模拟 100ms RTT, 2% 丢包率, 64 字节小包

                TCP    KCP(极速)  ENet(Reliable)  QUIC(CUBIC)
P50 延迟:       52ms    51ms       53ms            52ms
P90 延迟:       55ms    54ms       56ms            55ms
P99 延迟:       310ms   82ms       180ms           220ms
P999 延迟:      1200ms  130ms      350ms           650ms

分析:
- P50: 差距不大(主要是 RTT/2,与重传无关)
- P99: KCP 大幅领先(快速重传 + 低 RTO 最小值 + 非指数退避)
- P999: KCP 领先 10 倍(TCP 的指数退避在连续丢包时代价极高)

注意: 这个对比中 KCP 关闭了拥塞控制
      如果开启拥塞控制,KCP 的 P99 会接近 TCP
      "用带宽换延迟"的前提是你有足够的带宽

5.3 吞吐量对比

测试环境: 模拟 50ms RTT, 0.1% 丢包率, 批量传输

                TCP(CUBIC)  KCP(极速)  ENet(Reliable)  QUIC(CUBIC)
吞吐量:          92 Mbps    68 Mbps     45 Mbps         88 Mbps
CPU 使用率:      2%         8%          12%             15%

分析:
- TCP 吞吐最高: 内核态实现 + TSO/GRO 硬件卸载
- QUIC 接近 TCP: 用户态实现有开销,但算法等价
- KCP 中等: 头部开销 + 即时 ACK + 无 Nagle
- ENet 最低: 频道管理开销 + 简单限流算法

结论: 需要高吞吐 → TCP 或 QUIC
      需要低延迟 → KCP(接受吞吐损失)
      需要灵活性 → ENet(接受两方面都不极致)

六、选型决策

你的场景是...
│
├── 实时竞技游戏(FPS/MOBA/格斗)
│   ├── 自研网络层: KCP (极速模式)
│   │   └── 位置同步用不可靠通道 (不走 KCP)
│   │       关键事件走 KCP
│   └── 快速原型: ENet (多频道天然契合游戏需求)
│
├── MMO / 休闲游戏
│   └── ENet (频道模型灵活)
│       或 TCP (延迟要求不极端)
│
├── Web 应用(HTTP API / 实时推送)
│   └── QUIC (HTTP/3 生态)
│       或 TCP (如果不能用 QUIC)
│
├── VPN / 代理
│   ├── 高丢包环境(跨境): KCP (如 kcptun)
│   └── 正常环境: WireGuard (内核态 UDP)
│
├── 音视频通信
│   ├── WebRTC: 自带 RTP/RTCP 和 SCTP
│   └── 自研: RTP over UDP (不需要 KCP/ENet)
│
├── IoT / 传感器数据
│   ├── 需要可靠传输: MQTT (over TCP)
│   └── 需要低延迟 + 容忍丢包: CoAP (over UDP)
│
└── 数据中心内部 RPC
    ├── 常规: TCP (最优化的内核实现)
    ├── 低延迟: QUIC + Copa CC
    └── 超低延迟: DPDK + 自定义协议

实际工程中的混合策略

很多生产系统不是单选——而是混合使用:

游戏《原神》的网络架构(推测):
  登录/支付:       TCP (可靠,延迟不敏感)
  世界同步:        KCP (低延迟,容忍少量丢包)
  语音:            RTP/UDP (实时)
  资源下载:        HTTP/TCP (吞吐优先)

视频会议系统:
  信令:            WebSocket/TCP (可靠)
  媒体:            SRTP/UDP (实时)
  屏幕共享:        QUIC Stream (需要可靠但延迟敏感)
  文件传输:        QUIC Stream (需要可靠)

CDN:
  静态资源:        HTTP/3 (QUIC)
  动态 API:        HTTP/2 (TCP) 或 HTTP/3 (QUIC)
  实时日志:        UDP (不需要可靠,吞吐优先)

七、工程实践:KCP 在 VPN 场景的部署

# === 背景 ===
# 跨境 VPN 隧道,底层链路丢包率 5-15%
# 使用 TCP 隧道时:TCP over TCP 问题(重传叠加),延迟 500ms+
# 使用原始 UDP 隧道时:5-15% 的数据包直接丢失
# 需要:可靠传输 + 低延迟重传

# === 方案:kcptun(KCP 隧道)===
# kcptun 在 UDP 上建立 KCP 隧道,加速 TCP 流量

# 服务端
kcptun-server -l :4000 -t 127.0.0.1:22 \
    --key "secret" \
    --crypt aes-128 \
    --mode fast3 \
    --datashard 10 \
    --parityshard 3 \
    --rcvwnd 2048 \
    --sndwnd 2048

# 参数说明:
# --mode fast3:     最激进的快速重传 (nodelay=1, resend=2, nc=1)
# --datashard 10:   FEC: 每 10 个数据包
# --parityshard 3:  FEC: 生成 3 个冗余包
#                   → 13 个包中任意丢 3 个都能恢复
#                   → 带宽开销 30%,但在 10% 丢包时几乎零延迟损失
# --rcvwnd/sndwnd:  KCP 窗口大小

# 客户端
kcptun-client -l :1080 -r SERVER:4000 \
    --key "secret" \
    --crypt aes-128 \
    --mode fast3 \
    --datashard 10 \
    --parityshard 3 \
    --rcvwnd 2048 \
    --sndwnd 2048

# === 效果对比 ===
# 底层链路: 200ms RTT, 10% 丢包

# TCP 直连:
#   SSH 命令响应: 500ms-3s(重传导致的抖动)
#   SCP 下载速度: ~200 KB/s(拥塞窗口频繁减半)

# kcptun + KCP:
#   SSH 命令响应: 220ms(稳定,P99 ~300ms)
#   SCP 下载速度: ~800 KB/s(FEC 补偿了丢包)
#   带宽消耗: 实际数据的 ~1.5 倍(KCP 头部 + FEC + 即时 ACK)

# === FEC 的工程价值 ===
# Forward Error Correction(前向纠错)
# 不是等丢包再重传,而是提前发冗余数据
# 在已知高丢包环境下,FEC 比 ARQ 延迟更低:

# ARQ (纯重传):
#   丢包 → 等 ACK 超时 → 重传 → 等 ACK → 成功
#   延迟 = 至少 1 RTT(快速重传)到 RTO(超时重传)

# FEC (前向纠错):
#   10+3 编码 → 丢 ≤3 包 → 接收端直接恢复
#   延迟 = 0 额外延迟(在 FEC 容量内)
#   代价 = 30% 额外带宽(永久性开销,不管有没有丢包)

# FEC 参数选择指南:
#   丢包率 1-3%:   datashard=20, parityshard=1 (5% 冗余)
#   丢包率 3-5%:   datashard=10, parityshard=1 (10% 冗余)
#   丢包率 5-10%:  datashard=10, parityshard=2 (20% 冗余)
#   丢包率 10-15%: datashard=10, parityshard=3 (30% 冗余)
#   丢包率 >15%:   链路质量太差,FEC 也无法有效补偿

八、结论

在 UDP 上构建可靠传输不是”重新发明 TCP”——而是为特定场景定制传输策略。TCP 的策略是”一个策略适用所有场景”,这在大多数情况下足够好,但在极端场景下(高丢包、实时交互、多路复用)会成为瓶颈。

几个核心判断:

  1. KCP 是延迟优化器,不是 TCP 替代品。 它的价值在于将尾延迟(P99/P999)从秒级降到毫秒级。代价是 20-50% 的额外带宽消耗。在带宽充足、延迟敏感的场景(游戏、VPN 加速)中,这个交换是值得的。

  2. ENet 是游戏网络的最佳起步选择。 它的多频道模型天然匹配游戏的通信需求——关键事件可靠传输、位置更新尽力而为。API 设计成熟,学习成本低。但它的拥塞控制太简单,不适合公网大带宽传输。

  3. QUIC 是长期方向。 如果你在做新项目且需要”比 TCP 更好的传输”,优先考虑 QUIC 而非 KCP/ENet。QUIC 有标准化协议、成熟的实现、内置加密、浏览器支持。唯一的例外是:你需要比 QUIC 更低的延迟(关闭拥塞控制)——此时用 KCP。

  4. FEC + ARQ 的组合在高丢包环境中远优于纯 ARQ。 kcptun 的实践证明:在 10% 丢包的链路上,30% 带宽换近零重传延迟是非常划算的交易。

  5. 不要在公网上关闭拥塞控制。 KCP 和 ENet 都允许你关闭拥塞控制。在受控环境(数据中心、VPN 隧道内)可以这么做,但在公网上这是不负责任的——你的流量会挤占其他用户的带宽。

  6. 性能对比数据要在你自己的场景下验证。 本文中的延迟和吞吐量数据来自特定测试环境。不同的包大小、丢包模式(随机 vs 突发)、RTT 分布会显著影响各框架的表现。选型前务必用你自己的流量模式做基准测试。

最后一个工程建议:如果你的可靠 UDP 需求仅仅是”TCP 太慢了”,先确认是不是 TCP 的配置问题(缓冲区太小、拥塞算法不合适、Nagle/Delayed ACK 交互)。TCP 调优往往比切换到 UDP 框架更简单、更可靠。只有在 TCP 的结构性限制(有序交付导致的 HoL、固定的重传策略、无法关闭拥塞控制)真正阻碍了你的场景时,才考虑 UDP 框架。

下一篇我们来看另一个传输层替代——SCTP。它在协议层面解决了 TCP 的多流和多宿主问题,但在部署路径上遇到了比 QUIC 更大的障碍。


上一篇:UDP 工程:何时用 UDP、怎么用好 UDP

下一篇:SCTP 协议工程:多宿主与多流的传输层替代

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2025-07-27 · network

【网络工程】传输层选型决策:TCP vs UDP vs QUIC vs SCTP

传输层协议的选择决定了应用的延迟、吞吐量、可靠性和部署可行性。本文从延迟、吞吐、可靠性、NAT 穿越四个维度系统对比 TCP、UDP、QUIC、SCTP 四种传输协议,给出 Web 服务、游戏、IoT、实时音视频、RPC 等典型场景的选型决策框架和迁移路径。

2025-08-04 · network

【网络工程】QUIC 生态与工程部署:从实验到生产

QUIC 已经不是实验性协议——HTTP/3 标准化后,CDN、浏览器和主流服务端框架都在推进 QUIC 支持。本文从工程视角对比主流 QUIC 库的成熟度和性能特征,讲解 CDN/负载均衡器的 QUIC 适配方案、从 TCP 迁移到 QUIC 的渐进路径、QUIC 调试工具链,以及生产环境的部署陷阱和性能调优实践。

2025-07-25 · network

【网络工程】UDP 工程:何时用 UDP、怎么用好 UDP

UDP 不是'不可靠的 TCP',它是一张白纸——不做连接管理、不做流控、不做重传,把所有决策权交给应用层。本文从 UDP 的协议本质出发,剖析它在游戏、视频、DNS、QUIC 等场景中的工程用法,深入分析 MTU/分片/NAT 穿越等工程陷阱,并给出高性能 UDP 编程的内核调优方法。


By .