# 自定义 UDP 协议与KCP
TCP 太保守,纯 UDP 不可靠——自己造一个适合游戏的协议
前置知识:第02章 网络同步基础 + 3_2_cs-fundamentals(TCP/UDP原理)
阅读指南(初学者必看)
为什么你需要学习自定义 UDP 协议与KCP?
TCP 的可靠有序特性会导致队头阻塞——一个包丢了,后面所有包都等着。纯 UDP 又不可靠。游戏需要的是"部分可靠、部分有序"的传输——某些消息必须可靠(聊天、交易),某些可以丢(位置更新)。自定义 UDP 协议 + KCP 就是为了满足这个需求。
学完本章,你能回答:
- 为什么游戏不用 TCP 也不用纯 UDP?
- ARQ 三种模式(Stop-and-Wait / Go-Back-N / Selective Repeat)有什么区别?
- FEC 前向纠错怎么做到"丢了包也能恢复"?
- KCP 为什么比 TCP 快40%?怎么在游戏里用?
本文结构
第一部分:为什么需要可靠 UDP?——TCP 的问题和游戏的需求
第二部分:可靠 UDP 实现(ARQ)——三种重传模式
第三部分:FEC(前向纠错)——丢包不用重传也能恢复
第四部分:KCP协议详解——比TCP更快,比UDP更可靠
第五部分:游戏应用实践——多通道协议设计
一、为什么需要可靠 UDP?
TCP 的问题:
1. 队头阻塞:一个包丢了,后面所有包都等着
2. 拥塞控制:丢包时大幅降速(游戏不需要这么保守)
3. 最小延迟:无法低于 TCP 协议本身的开销
游戏的需求:
1. 某些消息必须可靠(聊天、交易)
2. 某些消息可以丢(位置更新、动画同步)
3. 某些消息需要有序(指令序列)
4. 某些消息不需要序(语音、视频)
二、可靠 UDP 实现(ARQ)
ARQ(自动重传请求)三种模式:
1. Stop-and-Wait:
发一个包 → 等ACK → 再发下一个
效率极低
2. Go-Back-N:
连续发 N 个包 → 某个包丢了 → 从丢的包开始全部重传
效率一般
3. Selective Repeat:⭐ 推荐
连续发 N 个包 → 某个包丢了 → 只重传丢的那个
效率最高
三、FEC(前向纠错)
FEC:多发一些冗余数据,丢包时可以用冗余数据恢复
最简单:每发 2 个数据包,加 1 个异或包
data1 ⊕ data2 = fec
如果 data1 丢了,可以用 data2 ⊕ fec = data1 恢复
更高级:Reed-Solomon 编码
N 个数据包 + M 个校验包
任意丢失 M 个包都能恢复
四、KCP协议详解
4.1 KCP vs TCP vs UDP
| 特性 | UDP | TCP | KCP |
|---|---|---|---|
| 可靠性 | 不可靠 | 可靠 | 可配置可靠 |
| 有序性 | 无序 | 有序 | 可配置有序 |
| 拥塞控制 | 无 | 有(保守) | 有(激进) |
| 重传机制 | 无 | 超时重传 | 快速重传 |
| 延迟 | 最低 | 高(多次RTT) | 低(1次RTT) |
| 复杂度 | 简单 | 复杂 | 中等 |
4.2 KCP的核心优势
KCP设计目标:比TCP快40%
- RTO快速重传,不等超时
- 可选有序/无序
- 拥塞控制可配置
- 无队头阻塞
4.3 KCP核心配置
// KCP配置
const ikcp = {
// 发送窗口大小
snd_wnd: 32,
// 接收窗口大小
rcv_wnd: 32,
// 最大传输单元(MTU)
mtu: 1400,
// 最小RTO(重传超时)
// TCP是200ms,KCP是30ms
rx_minrto: 30,
// 是否快速重传
// 0 = 关闭,n = 跳过的包数
fastresend: 2,
// 是否开启拥塞控制
// 0 = 关闭,关闭后相当于快速UDP
nodelay: 1,
// 拥塞窗口
cwnd: 32,
// 最大分片数
mss: 1400,
// 流模式(无序接收)
stream: false
};
4.4 RTO快速重传
TCP重传(等RTO):
发送:1,2,3,4,5
↑ 包2丢了
超时等待:...(等200ms)
重传:2,3,4,5
KCP快速重传(等2次ACK):
发送:1,2,3,4,5
↑ 包2丢了
ACK:收到1,3,4,5
↑ 收到3说明2丢了
立即重传:2(不等超时)
4.5 KCP核心结构
class IKCP {
constructor(conv, user) {
this.conv = conv; // 会话ID
this.user = user; // 底层发送函数
// 发送窗口和接收窗口
this.snd_wnd = 32;
this.rcv_wnd = 32;
// RTT估计
this.srtt = 0; // smoothed RTT
this.rttvar = 0; // RTT variation
this.rto = 100; // Retransmission Timeout
// 发送缓冲区
this.snd_queue = []; // 待发送
this.snd_buf = []; // 已发送未确认
this.snd_nxt = 0; // 下一个发送序号
// 接收缓冲区
this.rcv_queue = []; // 已接收待交付
this.rcv_nxt = 0; // 下一个期待序号
// 往返跟踪
this.delivery = []; // 发送包的发送时间
}
// 发送数据
send(data) {
// 分片
const segs = this.fragment(data);
// 加入发送队列
for (const seg of segs) {
seg.seq = this.snd_nxt++;
seg.len = data.length;
this.snd_queue.push(seg);
}
}
// 输入(接收到的数据)
input(data) {
// 解析KCP包
const kcp = this.parse(data);
// 更新RTT
this.updateRTT(kcp.timestamp);
// 处理ACK
if (kcp.cmd === 1) { // ACK
this.onAck(kcp);
}
// 处理数据
else if (kcp.cmd === 2) { // 数据
this.onData(kcp);
}
}
// 更新RTT
updateRTT(timestamp) {
const rtt = Date.now() - timestamp;
if (this.srtt === 0) {
this.srtt = rtt;
this.rttvar = rtt / 2;
} else {
this.rttvar = (3 * this.rttvar + Math.abs(rtt - this.srtt)) / 4;
this.srtt = (7 * this.srtt + rtt) / 8;
}
// RTO计算
this.rto = this.srtt + Math.max(1, 4 * this.rttvar);
this.rto = Math.max(30, Math.min(this.rto, 1000)); // 限制范围
}
// 发送更新
update() {
const now = Date.now();
// 检查哪些包可以发送
while (this.snd_buf.length < this.snd_wnd) {
if (this.snd_queue.length === 0) break;
const seg = this.snd_queue.shift();
seg.timestamp = now;
seg.resendts = now + this.rto; // 重传时间
this.snd_buf.push(seg);
}
// 检查需要重传的包
for (const seg of this.snd_buf) {
if (now >= seg.resendts) {
// 超时重传
this.transmit(seg);
seg.resendts = now + this.rto;
// 快速重传
if (this.fastresend > 0) {
seg.fastresend++;
}
}
}
// 拥塞控制
this.congestionControl();
}
}
4.6 KCP拥塞控制
// KCP的拥塞控制比TCP激进
congestionControl() {
// cwnd = 发送窗口
if (this.nodelay) {
// 快速模式:没有拥塞控制
// 适合游戏:宁可丢包也不卡
return;
}
// 标准TCP拥塞控制
if (this.inflight > this.cwnd) {
// 拥塞窗口减半
this.cwnd = Math.floor(this.cwnd / 2);
}
}
// 丢包后的处理
onLoss(cancelled) {
if (this.fastresend > 0) {
// 快速重传
for (const seq of cancelled) {
const seg = this.findInBuffer(seq);
if (seg) {
seg.fastresend = 1;
seg.resendts = Date.now(); // 立即重传
}
}
}
if (!this.nodelay) {
// 非快速模式下丢包减窗
this.cwnd = Math.floor(this.cwnd / 2);
}
}
五、游戏应用实践
5.1 KCP前端集成
class KCPConnection {
constructor(ws) {
this.ws = ws;
this.kcp = new IKCP(12345, (data) => {
// 底层发送
this.ws.send(data);
});
// 配置
this.kcp.nodelay = 1; // 快速模式
this.kcp.fastresend = 2; // 2次ACK触发快速重传
this.kcp.snd_wnd = 32;
this.kcp.rcv_wnd = 32;
}
// 接收服务器数据
onMessage(data) {
this.kcp.input(data);
}
// 发送游戏消息
send(message) {
const encoded = this.encode(message);
this.kcp.send(encoded);
}
// 更新(每帧调用)
update() {
this.kcp.update(Date.now());
// 收取消息
while (true) {
const data = this.kcp.recv();
if (!data) break;
this.onReceive(data);
}
}
encode(message) {
// 编码为Uint8Array
return new TextEncoder().encode(JSON.stringify(message));
}
decode(data) {
return JSON.parse(new TextDecoder().decode(data));
}
}
5.2 多通道协议设计
好的游戏协议像一套高效的暗号系统:
- 紧凑:每个字节都要有用
- 分层:底层负责传输,上层负责业务
- 可扩展:未来加新功能不影响旧逻辑
协议分层架构:
应用层(游戏逻辑)
|-- 技能系统协议
|-- 聊天系统协议
|-- 位置同步协议
消息层(可靠性保障)
|-- 序列号管理
|-- ACK机制
|-- 重传与去重
传输层(WebSocket/UDP/KCP)
|-- 连接管理
|-- 心跳检测
|-- 帧封装
消息类型定义:
const MsgType = {
HEARTBEAT: 0x01, // 心跳
HEARTBEAT_ACK: 0x02, // 心跳响应
POSITION: 0x10, // 位置同步(非可靠)
INPUT: 0x11, // 玩家输入(可靠)
SKILL: 0x20, // 技能释放(可靠)
DAMAGE: 0x21, // 伤害计算(可靠)
CHAT: 0x30, // 聊天消息(可靠,有序)
GAME_START: 0x40, // 游戏开始(可靠)
GAME_OVER: 0x41, // 游戏结束(可靠)
SNAPSHOT: 0x50, // 状态快照(状态同步用)
DELTA: 0x51 // 增量更新(状态同步用)
};
自问自答
Q:为什么不用 QUIC 代替自定义 UDP? A:QUIC 确实解决了 TCP 的队头阻塞问题,但它仍然是通用协议,不支持"部分可靠、部分有序"的细粒度控制。而且 QUIC 在某些网络环境下可能被限制。自定义协议可以针对游戏场景做极致优化。
Q:Selective Repeat 比 Go-Back-N 好在哪? A:Go-Back-N 丢一个包要重传后面所有包,Selective Repeat 只重传丢的那个。在网络有 1% 丢包率、窗口大小 30 的情况下,Go-Back-N 可能重传 30 个包,Selective Repeat 只重传 1 个。
Q:FEC 的冗余开销值得吗? A:取决于场景。如果网络丢包率 1%~5%,FEC 用 10%~20% 的带宽换取不重传的延迟节省,非常值得。如果丢包率超过 FEC 的纠错能力,还是得靠 ARQ 重传。最佳方案是 FEC + ARQ 混合。
Q:KCP为什么比TCP快? A:KCP的RTO最小30ms(TCP是200ms),支持快速重传(不等超时),可选择性重传,拥塞控制可配置。这些设计让KCP在实时性要求高的场景下表现远优于TCP。
实践任务
- 任务1:实现 Stop-and-Wait 模式的可靠 UDP,测量吞吐量
- 任务2:实现 Selective Repeat 模式的可靠 UDP,对比 Stop-and-Wait 的吞吐量
- 任务3:实现简单 FEC(异或冗余包),测试丢包恢复能力
- 任务4:在模拟丢包环境下(1%/3%/5%),对比 ARQ-only、FEC-only、ARQ+FEC 的延迟
- 任务5:设计一个多通道协议——可靠通道(交易)+ 不可靠通道(位置)+ 有序通道(指令)
- 任务6:集成KCP到游戏网络层,对比TCP和KCP在100ms延迟+5%丢包下的表现
与其他章节的关联
| 本章内容 | 关联章节 | 关联点 |
|---|---|---|
| 可靠 UDP | 第02章 网络同步基础 | TCP/UDP 的局限性是自定义协议的动机 |
| FEC | 第03章 帧同步Lockstep | 帧同步的输入包丢了可以用 FEC 恢复,避免卡顿 |
| 多通道 | 第04章 状态同步 | 位置同步走不可靠通道,交易走可靠通道 |
| 拥塞控制 | 3_2_cs-fundamentals | TCP 拥塞控制的原理是自定义拥塞控制的基础 |
| KCP | 第13章 弱网优化与实战 | KCP是弱网环境下优化延迟的核心工具 |
| 协议安全 | 第08章 游戏安全与反作弊 | 自定义协议需要自己实现加密和签名 |
上一章:05-预测与回滚 下一章:07-游戏系统设计实战