Zwlin's Blog

kcp 与 kcp-go 的设计与实现

Last updated: 2024/11/22     Published at: 2024/11/22

引言

最近深入研究了 KCP 的实现,通读了 KCP 原版及其 Go 语言实现(kcp-go)的源码。在阅读代码的过程中,还参考了许多优秀的博客文章,十分感谢这些博主的分享。有了这些珠玉在前,我也整理了一些自己对 KCP 的理解,作为学习过程中的记录和总结。

两个核心数据结构

ikcpcb 是 KCP 的控制块(KCP Control Block)数据结构,包含一条 KCP 连接的所有状态信息和参数。作为 KCP 协议的核心数据结构,它负责管理数据传输、重传机制以及流量控制等功能。

segment 是表示 KCP 数据段的结构体,用于描述一个数据包或控制包。每个 segment 都包含数据段的头部信息和数据部分。

这两个数据结构定义了 KCP 使用的所有关键字段。关于它们的具体含义,Luyu Huang 的博文中有详细说明,这里就不再赘述了。

两重队列与窗口控制

在 KCP 中,数据的发送和接收分别依赖于两种队列:

发送队列和发送缓冲区

  1. 发送队列 (snd_queue): 调用者在调用 kcp.send() 时,数据会被加入发送队列,而不会立即被发送。
  2. 发送缓冲区 (snd_buf): 在调用 update(flush) 时,KCP 会尝试将 snd_queue 中的数据移入 snd_buf,然后遍历发送缓冲区,进行数据包的首次发送或重传。通常,发送缓冲区的队首是 snd_una 所代表的数据包。
    • snd_nxt >= snd_una + cwnd(即拥塞窗口已满)时,发送缓冲区中的所有数据包都处于等待 ACK 的状态,此时发送队列中的数据无法移入发送缓冲区。
    • 如果在这种情况下持续调用 kcp.send(),数据会堆积在 snd_queue 中,而不会被真正发送,导致所谓的缓存积累延迟问题

拥塞窗口 (cwnd) 大小受以下因素控制:

如果 KCP 配置禁用了慢启动和拥塞避免,则 cwnd 仅取决于 snd_wndrmt_wnd 的较小值,其中 snd_wnd 是用户配置的固定值。

接收队列和接收缓冲区

  1. 接收缓冲区(rcv_buf: 数据包在接收时会先放入接收缓冲区。由于传输中可能出现丢包或乱序,接收缓冲区确保数据按顺序排列。
  2. 接收队列(rcv_queue: 调用 kcp.recv()kcp.input() 时,从 rcv_buf 中到 rcv_nxt 为止的连续数据包会被移入接收队列,供调用者读取。
    • 如果接收队列的长度 nrcv_que 超过 rcv_wnd(接收窗口大小),数据将不再从 rcv_buf 移入 rcv_queue,表明接收窗口已满。

接收窗口的设计主要是为了处理乱序和丢包问题,只有当 rcv_buf 中的数据按顺序排列时,才会移入 rcv_queue

窗口滑动和速率调整

这种设计平衡了可靠性和效率,确保了数据的有序性和网络传输的流畅性。

RTT/RTO 的计算和设置

KCP 作为一种 ARQ 协议(Automatic Repeat reQuest,自动重传请求协议),发送方需要接收方返回的确认(ACK)来保证数据包成功到达。

发送缓存与超时重传

流水线传输

为了提高传输效率,KCP 支持流水线方式连续发送多个数据包,而不是等待每个数据包被确认后再发送下一个。

RTO 与 RTT 的更新机制

RTO 和 RTT 的计算方法遵循 TCP 标准,RFC6298 中有详细说明。相关文献和博客对其原理有较多解析,这里不再赘述。

重传逻辑

在重传数据包时,KCP 会重新设置超时时间:

KCP 的这些设计既保证了传输的可靠性,又在不同场景下优化了传输效率,尤其是在需要快速响应的情况下。

kcp-go 中的 check/update 机制

KCP 是一个运行在用户态的纯 ARQ 协议实现。为了保证操作的正确性,对同一个 KCP 对象的操作需要在单线程环境中进行,并通过循环调用 kcp.update() 来更新其内部状态,实现协议的运行逻辑。

单线程与多连接场景的优化

kcp-go 的改进

kcp-gokcp.update()kcp.check() 进行了改进,并已经将其标记为 deprecated,引入了更直接的实现方式:

  1. kcp.flush() 的主要功能
    • 发送 ACK: 处理 kp.input() 生成的待发送 ACK。
    • 窗口管理: 检查是否需要发送窗口探测包(探测远端接收窗口)和窗口响应包。
    • 队列迁移: 尽量将数据从 snd_queue 移入 snd_buf
    • 数据发送: 发送未发送的数据包,处理重传(包括快速重传和超时重传)。
    • 拥塞控制(可选): 根据丢包情况调整 cwnd,进行流量控制。
  2. 触发时机
    • kcp-go在调用 kcp.input() 后立即执行 kcp.flush() ,因为此时可能需要:
      • 回复新的 ACK。
      • 收到包后释放 snd_buf 的空间,触发新的数据包发送。
    • kcp.flush() 的返回值等效于原生 KCP 的 kcp.check(),表示下一次需要调用 flush() 的时间点。kcp-go 会根据这个返回值,将任务调度到相应的时间点。

相比原版 KCP,kcp-go 的改动让状态更新和数据处理的逻辑更紧密结合,简化了接口的使用:

这些改动使 kcp-go 在多连接场景下更高效,同时保持了与原版 KCP 思路的一致性。