上一篇我们分析了 UDP 的工程本质——它是一张白纸,不提供可靠传输、不做流控、不做拥塞控制。这意味着如果你的应用既需要 UDP 的低延迟特性,又需要某种程度的可靠性,就必须在应用层(或传输层框架中)自己实现。
“在 UDP 上实现可靠传输”听起来像是在重新发明 TCP。某种意义上确实是——但关键区别在于:TCP 的可靠性策略是固定的(全量有序交付),而 UDP 上的框架可以提供可定制的可靠性:
- KCP:激进的 ARQ 策略,用带宽换延迟——适合实时交互场景
- ENet:多频道模型,每个频道独立选择可靠性级别——适合游戏网络
- QUIC:完整的现代传输协议,多路复用 + 集成加密——适合替代 TCP+TLS
这三个框架代表了”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); // 频道 23.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 的策略是”一个策略适用所有场景”,这在大多数情况下足够好,但在极端场景下(高丢包、实时交互、多路复用)会成为瓶颈。
几个核心判断:
KCP 是延迟优化器,不是 TCP 替代品。 它的价值在于将尾延迟(P99/P999)从秒级降到毫秒级。代价是 20-50% 的额外带宽消耗。在带宽充足、延迟敏感的场景(游戏、VPN 加速)中,这个交换是值得的。
ENet 是游戏网络的最佳起步选择。 它的多频道模型天然匹配游戏的通信需求——关键事件可靠传输、位置更新尽力而为。API 设计成熟,学习成本低。但它的拥塞控制太简单,不适合公网大带宽传输。
QUIC 是长期方向。 如果你在做新项目且需要”比 TCP 更好的传输”,优先考虑 QUIC 而非 KCP/ENet。QUIC 有标准化协议、成熟的实现、内置加密、浏览器支持。唯一的例外是:你需要比 QUIC 更低的延迟(关闭拥塞控制)——此时用 KCP。
FEC + ARQ 的组合在高丢包环境中远优于纯 ARQ。 kcptun 的实践证明:在 10% 丢包的链路上,30% 带宽换近零重传延迟是非常划算的交易。
不要在公网上关闭拥塞控制。 KCP 和 ENet 都允许你关闭拥塞控制。在受控环境(数据中心、VPN 隧道内)可以这么做,但在公网上这是不负责任的——你的流量会挤占其他用户的带宽。
性能对比数据要在你自己的场景下验证。 本文中的延迟和吞吐量数据来自特定测试环境。不同的包大小、丢包模式(随机 vs 突发)、RTT 分布会显著影响各框架的表现。选型前务必用你自己的流量模式做基准测试。
最后一个工程建议:如果你的可靠 UDP 需求仅仅是”TCP 太慢了”,先确认是不是 TCP 的配置问题(缓冲区太小、拥塞算法不合适、Nagle/Delayed ACK 交互)。TCP 调优往往比切换到 UDP 框架更简单、更可靠。只有在 TCP 的结构性限制(有序交付导致的 HoL、固定的重传策略、无法关闭拥塞控制)真正阻碍了你的场景时,才考虑 UDP 框架。
下一篇我们来看另一个传输层替代——SCTP。它在协议层面解决了 TCP 的多流和多宿主问题,但在部署路径上遇到了比 QUIC 更大的障碍。
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【网络工程】传输层选型决策:TCP vs UDP vs QUIC vs SCTP
传输层协议的选择决定了应用的延迟、吞吐量、可靠性和部署可行性。本文从延迟、吞吐、可靠性、NAT 穿越四个维度系统对比 TCP、UDP、QUIC、SCTP 四种传输协议,给出 Web 服务、游戏、IoT、实时音视频、RPC 等典型场景的选型决策框架和迁移路径。
【网络工程】QUIC 生态与工程部署:从实验到生产
QUIC 已经不是实验性协议——HTTP/3 标准化后,CDN、浏览器和主流服务端框架都在推进 QUIC 支持。本文从工程视角对比主流 QUIC 库的成熟度和性能特征,讲解 CDN/负载均衡器的 QUIC 适配方案、从 TCP 迁移到 QUIC 的渐进路径、QUIC 调试工具链,以及生产环境的部署陷阱和性能调优实践。
【网络工程】UDP 工程:何时用 UDP、怎么用好 UDP
UDP 不是'不可靠的 TCP',它是一张白纸——不做连接管理、不做流控、不做重传,把所有决策权交给应用层。本文从 UDP 的协议本质出发,剖析它在游戏、视频、DNS、QUIC 等场景中的工程用法,深入分析 MTU/分片/NAT 穿越等工程陷阱,并给出高性能 UDP 编程的内核调优方法。
【网络工程】WebTransport 与 WebCodecs:下一代浏览器传输
深入分析 WebTransport 基于 QUIC 的浏览器传输能力:可靠流与不可靠数据报的工程场景、WebCodecs 的媒体处理管线、与 WebSocket/WebRTC 的对比、当前浏览器支持现状与迁移路径。