KCP 协议如何在 UDP 之上实现低延迟可靠传输
声明: KCP 是一个开源项目,作者为 林伟 (skywind3000)。本文是阅读该项目源码和文档后整理的学习笔记,用于理解可靠传输协议的工程实现方式。本文作者不是该项目的开发者,未参与该项目的任何代码贡献。 文中所有工程细节均来自对开源代码的分析,不代表本文作者的设计决策。
TCP 在公网丢包场景下的延迟表现并不理想——超时重传时 RTO 翻倍、丢包后触发全部重传、延迟 ACK 抬高 RTT 估算。这些机制为最大化吞吐量设计,代价是丢包时延迟显著上升。
游戏、实时视频、VPN 隧道等场景对延迟敏感,能容忍少量丢包和带宽冗余,但不能接受几百毫秒的卡顿。KCP 正是为这类场景设计的一套快速可靠 ARQ 协议:以比 TCP 多消耗 10%-20% 带宽的代价,换取平均延迟降低 30%-40%、最大延迟降低约三倍的效果。
整个协议只有 ikcp.h 和 ikcp.c 两个文件,不包含任何系统调用。底层数据包的收发、时钟源、定时调度全部由使用者通过回调提供。
一、协议定位:纯算法层
KCP 不是一套完整的网络库——它只负责 ARQ 协议的算法部分,连时钟都需要外部传入。
创建一个 KCP 对象后,使用者需要提供三个东西:
输出回调:KCP 组装好协议包后,通过 output 回调交给使用者发送。底层是 UDP、串口还是其他通道,KCP 不关心。
时钟:ikcp_update(kcp, current) 需要外部传入当前毫秒时间戳。内部所有超时判断都基于这个时间。
定时调度:使用者需要周期调用 ikcp_update,或通过 ikcp_check 获取下次调度的精确时间点。KCP 内部不启动任何定时器。
完整帧输入:ikcp_input 每次调用必须传入一个完整的 KCP 段——24 字节头部加上 len 字段指定的数据体,缺一不可。源码中 size < len 直接返回 -2,内部没有对不完整段的缓冲。这是因为 KCP 设计时假设底层是 UDP 这类数据报传输,一个 send 对应一个完整段。如果底层是串口、TCP 隧道等流式通道,调用方必须自己做帧定界,收齐一个完整段再交给 ikcp_input,否则会被当作协议错误。
收到数据时调用 ikcp_input 喂入完整段,发送数据时调用 ikcp_send,接收时调用 ikcp_recv。五个 API 构成完整的交互面。
这种"拆开"的设计让 KCP 可以嵌入任何协议栈:下面加 FEC 纠删码,上面加流加密,握手层做非对称密钥交换。每个协议单元独立可替换。
二、数据包结构与队列流转
KCP 只有一种包格式,数据和控制消息共用同一个 24 字节头部:
0 4 5 6 8 (BYTE)
+---------------+---+---+-------+
| conv |cmd|frg| wnd |
+---------------+---+---+-------+ 8
| ts | sn |
+---------------+---------------+ 16
| una | len |
+---------------+---------------+ 24
| |
| DATA (optional) |
+-------------------------------+- conv:会话编号,两端必须一致,用于区分不同连接
- cmd:命令类型,PUSH(81) 数据推送、ACK(82) 确认、WASK(83) 窗口探测、WINS(84) 窗口通知
- frg:分片编号,从
count-1递减到 0,接收端据此合并分片 - wnd:接收窗口剩余大小,用于流量控制
- ts:时间戳,用于 RTT 计算
- sn:包序号
- una:未确认序号,此编号之前的包全部已收到
注意 una 字段:KCP 的每个包(包括数据包)都携带 UNA 信息。TCP 只在 ACK 包中确认,KCP 让每个发出的包同时承担 ACK 职责。这意味着即使反向没有独立 ACK 包,确认信息也能顺带到达。
KCP 内部维护四条队列:
ikcp_send → snd_queue → snd_buf → [网络] → rcv_buf → rcv_queue → ikcp_recvsnd_queue 存放应用层写入但尚未分配序号的数据。ikcp_flush 将数据从 snd_queue 搬到 snd_buf,分配序号和时间戳,交给 output 发送。snd_buf 中已发送的包等待对方确认,确认后才移除。
接收端,ikcp_input 将收到的包插入 rcv_buf(按序号排序的接收缓冲),然后按序搬到 rcv_queue。rcv_queue 中的数据是连续可读的,ikcp_recv 从这里取走。
三、RTO 不翻倍与选择性重传
TCP 在超时重传时将 RTO 翻倍(RTO × 2),连续丢三次包就变成 RTO × 8。丢包越多,等待时间指数增长。
KCP 在快速模式下,RTO 每次只乘 1.5。RTO 增长曲线被压低,丢包后恢复更快。配合 rx_minrto 可设到 30ms(TCP 默认最小 RTO 为 200ms),进一步压低等待下限。
重传策略上,TCP 采用累积确认——收到 ACK=N 只表示 N 之前的包全收到。如果包 2 丢失、包 3/4/5 已到达,TCP 仍只确认到包 1,发送方必须从包 2 开始全部重传(Go-Back-N)。
KCP 同时使用 UNA 和独立 ACK。收到包 3 时,除了 UNA 告诉"包 1 之前全收到",还会为包 3 单独生成 ACK。发送方收到 ACK 1、3、4、5 后,知道包 2 被跳过,只重传包 2。
两种确认的组合让 KCP 不必在"全部重传"和"每条都独立 ACK 开销过大"之间二选一。
四、快速重传:不等超时
选择性重传虽然避免了全部重传,但仍要等到超时。KCP 的快速重传机制在超时前就可以触发重传。
每个已发送的段(IKCPSEG)内部维护一个 fastack 计数器。当收到的 ACK 跳过了某个包——收到 ACK 3 时,包 2 的 fastack 加 1;收到 ACK 4 时再加 1。fastack 达到阈值(fastresend 参数,通常设为 2)时,不等超时,立即重传。
源码中 ikcp_parse_fastack 负责更新计数器,ikcp_flush 在遍历 snd_buf 时检查 segment->fastack >= resent,满足条件就标记 needsend = 1。
快速重传配合 RTO × 1.5 和低 rx_minrto,形成了 KCP 降低延迟的三层机制:丢包时先靠快速重传抢时间,抢不到再靠压低后的超时兜底。
五、流量控制:可关闭的拥塞控制
KCP 内置的流控算法和 TCP 类似:慢启动阶段 cwnd 指数增长,拥塞避免阶段线性增长,丢包时 ssthresh 减半、cwnd 回退。
但 KCP 允许跳过拥塞控制。调用 ikcp_nodelay(kcp, 1, 10, 2, 1) 时,最后一个参数 nc=1 关闭流控。此时的发送窗口仅由 snd_wnd 和远端 rmt_wnd 决定,不再受丢包退让和慢启动约束。代价是牺牲公平性——网络上其他 TCP 流可能在竞争中吃亏。
KCP V2 引入了可插拔的拥塞控制接口 IKCPOPS:
struct IKCPOPS {
const char *name;
int (*init)(ikcpcb *kcp);
void (*release)(ikcpcb *kcp);
void (*on_ack)(ikcpcb *kcp, IUINT32 acked_segs, IUINT32 acked_bytes, IUINT32 prior_in_flight);
void (*on_fast_retransmit)(ikcpcb *kcp, IUINT32 fast_retrans, IUINT32 inflight, IUINT32 prior_cwnd);
void (*on_timeout)(ikcpcb *kcp, IUINT32 prior_cwnd);
// ...
};通过 ikcp_setcc 可以替换整套流控算法,不需要修改协议本身。内置算法和自定义算法可在同一份 KCP 代码上切换。
六、适用场景
KCP 已被多个用户规模上亿的项目采用:米哈游《原神》用 KCP 降低游戏消息传输耗时,SpatialOS 用它加速大型多人游戏服务端,网易 CC 和 BOBO 用它加速视频推流,kcptun 用它做 TCP over UDP 隧道加速。
KCP 适合的场景特征:延迟敏感、能容忍适度带宽增加、数据包较小且频繁发送。不适合的场景:需要严格公平共享带宽、传输大文件追求最高吞吐量。
工程上要注意两点:KCP 默认不加密,上层需要自行处理加密;KCP 不负责握手和连接管理,会话建立和 conv 分配由上层协议完成。
源码只有两个文件,阅读入口清晰:
ikcp.h:数据结构定义(IKCPSEG、IKCPCB、IKCPOPS)和全部 API 声明ikcp.c:ikcp_input解析输入包并驱动 ACK/重传逻辑,ikcp_flush组装输出包并执行发送,ikcp_update按间隔触发 flush
本文核对的源码基线:KCP commit b1a7a21