KCP和KCP2K协议介绍

2024-11-18 ⏳3.7分钟(1.5千字)

什么是 KCP

KCP 是一个快速可靠协议,能以比 TCP 浪费 10%-20% 的带宽的代价,换取平均延迟降低 30%-40%,且最大延迟降低三倍的传输效果。纯算法实现,并不负责底层协议(如 UDP)的收发,需要使用者自己定义下层数据包的发送方式,以 callback 的方式提供给 KCP。 连时钟都需要外部传递进来,内部不会有任何一次系统调用

特性

TCP 是为流量设计的(每秒内可以传输多少 KB 的数据),讲究的是充分利用带宽。而 KCP 是为流速设计的(单个数据包从一端发送到一端需要多少时间),以 10%-20%带宽浪费的代价换取了比 TCP 快 30%-40%的传输速度。TCP 信道是一条流速很慢,但每秒流量很大的大运河,而 KCP 是水流湍急的小激流。KCP 有正常模式和快速模式两种,通过以下策略达到提高流速的结果:

RTO 翻倍 vs 不翻倍:

TCP 超时计算是 RTOx2,这样连续丢三次包就变成 RTOx8 了,十分恐怖,而 KCP 启动快速模式后不 x2,只是 x1.5(实验证明 1.5 这个值相对比较好),提高了传输速度。

选择性重传 vs 全部重传:

TCP 丢包时会全部重传从丢的那个包开始以后的数据,KCP 是选择性重传,只重传真正丢失的数据包。

快速重传:

发送端发送了 1,2,3,4,5 几个包,然后收到远端的 ACK: 1, 3, 4, 5,当收到 ACK3 时,KCP 知道 2 被跳过 1 次,收到 ACK4 时,知道 2 被跳过了 2 次,此时可以认为 2 号丢失,不用等超时,直接重传 2 号包,大大改善了丢包时的传输速度。

延迟 ACK vs 非延迟 ACK:

TCP 为了充分利用带宽,延迟发送 ACK(NODELAY 都没用),这样超时计算会算出较大 RTT 时间,延长了丢包时的判断过程。KCP 的 ACK 是否延迟发送可以调节。

UNA vs ACK+UNA:

ARQ 模型响应有两种,UNA(此编号前所有包已收到,如 TCP)和 ACK(该编号包已收到),光用 UNA 将导致全部重传,光用 ACK 则丢失成本太高,以往协议都是二选其一,而 KCP 协议中,除去单独的 ACK 包外,所有包都有 UNA 信息。

非退让流控:

KCP 正常模式同 TCP 一样使用公平退让法则,即发送窗口大小由:发送缓存大小、接收端剩余接收缓存大小、丢包退让及慢启动这四要素决定。但传送及时性要求很高的小数据时,可选择通过配置跳过后两步,仅用前两项来控制发送频率。以牺牲部分公平性及带宽利用率之代价,换取了开着 BT 都能流畅传输的效果。

UDP

UDP 没有 TCP 那些用于可靠性保证的一些功能,只是简单的将你要发送的数据到对应的 IP 和端口号上,即使是一个不存在 IP 和端口,调用 Dial 或者 Write 方法都是返回正常。或者对方,或者有数据包丢失,也不会补发,这些都是需要上层应用去校验数据包的合法性和完整性。例如音视频播放、DNS 查询等。

KCP2K

现在基本的 KCP 已经有了 ACK、重传、流控等功能了,还有一点没有,就是检测对端是否在线且消息是否确认送达,所以就有了 KCP2K。其中 KCP2K 分为了可靠消息和不可靠消息,其中可靠消息在 KCP 基础上添加了 OpCode 来告知这是连接包、心跳包还是数据包,不可靠消息只在原来的 KCP 协议的首字节传 0x02 来标记这是一个不可靠消息

KCP2K 的可靠消息协议编码

1 byte 4 bytes 24 bytes 1 byte N bytes
0x01 cookie kcp protocol Kcp2kOpcode data
type Kcp2kOpcode byte

const (
    Hello      Kcp2kOpcode = 1
    Ping       Kcp2kOpcode = 2
    Data       Kcp2kOpcode = 3
    Disconnect Kcp2kOpcode = 4
)

KCP2K 的可靠消息协议编码这类消息即退化为 UDP 消息,只管送出,不管送达

1 byte 4 bytes N bytes
0x02 cookie data

踩坑

1. 服务端传输超过 MTU 阈值的数据包,但是未正确设置 KCP Header 的 Frg 字段,导致客户端无法合并多段数据包

KCP 设计者设计了两种数据传输模式:消息模式和流模式,消息模式会使用 Frg 标记一个过大的数据包被分成了多少段,由第一个 Frg 标记,且每个包递减,例如 MTU=1400,发送 1500 字节,则第一个包的 Frg 应该是 1,第二个是 0(代表已结束)。

因为 kcp-go 作者并不打算支持(服务端库会在 MTU 分割数据包之前,提前在应用层分好了,所以每个包都是相对独立的,没有上下文关系),我猜原因是最大效率利用网络,让每个包装满载荷再发出,同时参考 TCP 的实现,TCP 也是由 TCP 的 MSS 来决定拆分,并不依赖网络 IP 层拆分数据包。

在这一点我是支持 kcp-go 作者的想法的,既然使用了 UDP,那么肯定优先考虑传输效率,Stream 传输模式无疑是比其他更高效的,至于没有 Frg 标记数据包分片,应用层做起来也不难。

好巧的是我们客户端使用的 KCP2K 库作者恰好不支持流模式

            // streaming mode: removed. we never want to send 'hello' and
            // receive 'he' 'll' 'o'. we want to always receive 'hello'.

so, 我 fork 维护了自己的 kcp-go 并修改了源代码支持 frg 分段发送(不知道后期维护这个库是不是个大坑)