KCP 在 UDP 上做可靠传输:RTO 不翻倍、选择性重传与快速重传

声明: KCP 是一个开源项目,作者为 林伟 (skywind3000)。本文是阅读该项目源码和文档后整理的学习笔记,用于理解可靠传输协议的工程实现方式。本文作者不是该项目的开发者,未参与该项目的任何代码贡献。 文中所有工程细节均来自对开源代码的分析,不代表本文作者的设计决策。

项目仓库:github.com/skywind3000/kcp(UDP 之上的快速可靠 ARQ 协议)

TCP 在公网丢包时延迟表现不理想。超时重传让 RTO 翻倍,丢包后触发全部重传,延迟 ACK 又抬高 RTT 估算。这些机制为最大化吞吐量设计,代价是丢包时延迟显著上升。

游戏、实时视频、VPN 隧道对延迟敏感,能容忍少量丢包和带宽冗余,但不能接受几百毫秒的卡顿。KCP 为这类场景设计,是一套快速可靠 ARQ 协议:比 TCP 多消耗 10% 至 20% 带宽,换取平均延迟下降 30% 至 40%、最大延迟下降约三倍。

协议只有 ikcp.hikcp.c 两个文件,不含任何系统调用。底层收发、时钟源、定时调度全部由使用者通过回调提供。

协议分层:KCP 只做 ARQ 算法层

KCP 是一套纯 ARQ 算法层,不含完整网络库的收发与调度职责,连时钟也由外部传入。

创建 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 字节头部:

conv 4 B cmd 1 B frg 1 B wnd 2 B ts 4 B sn 4 B una 4 B len 4 B

← 24 字节固定头部 → DATA(可选)

注意 una 字段:KCP 的每个包(包括数据包)都携带 UNA 信息。TCP 只在 ACK 包中确认,KCP 让每个发出的包同时承担 ACK 职责。即使反向没有独立 ACK 包,确认信息也能顺带到达。

KCP 内部维护四条队列,收发两条路径各自独立:

发送路径 ikcp_send snd_queue ikcp_flush 分配序号/时间戳 snd_buf 网络/UDP

接收路径 ikcp_input rcv_buf rcv_queue ikcp_recv

ikcp_send 把数据写入 snd_queueikcp_flush 将其搬到 snd_buf,分配序号与时间戳,交给 output 发送。snd_buf 中已发送的包等待对方确认,确认后才移除。

接收端 ikcp_input 把收到的包插入 rcv_buf(按序号排序的接收缓冲),再按序搬到 rcv_queuercv_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。

TCP 累积确认(Go-Back-N) 1 2✕ 3 4 5 ACK = 1(只确认到包 1) 重传 2、3、4、5(全部) KCP UNA + 独立 ACK 1 2✕ 3 4 5 ACK 1、3、4、5(跳过 2) 只重传 2

两种确认的组合让 KCP 不必在"全部重传"与"每条都独立 ACK 开销过大"之间二选一。

快速重传:不等超时的 fastack 计数

选择性重传避免了全部重传,但仍要等到超时。KCP 的快速重传机制在超时前即可触发重传。

每个已发送的段(IKCPSEG)内部维护一个 fastack 计数器。当收到的 ACK 跳过了某个包,包 2 的 fastack 随之累加:收到 ACK 3 时加 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 降低延迟的三层机制:丢包时先靠快速重传抢时间,抢不到再靠压低后的超时兜底。

可关闭的拥塞控制与可插拔 CC 接口

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 分配由上层协议完成。


源码只有两个文件,阅读入口清晰:

本文核对的源码基线:KCP commit b1a7a21