在上一章 核心更新与刷新机制 (ikcp_update & ikcp_flush)中,我们揭示了 KCP 定期执行的方法。我们知道,ikcp_send 只是把数据放入一个队列,而 ikcp_flush 负责从队列中取出数据并发送。
这就像我们知道电商仓库的机器人会打包发货,但我们还不知道这个仓库内部是如何划分区域、如何管理成千上万个包裹的。KCP 内部的这个“仓库管理系统”正是本章要探讨的主题——数据流转与队列管理。
KCP 的四大核心队列
这四个核心队列都定义在 KCP 控制块 (ikcpcb) 结构体中,它们是 KCP 实现所有可靠性机制的基础。
// ikcp.h: IKCPCB 结构体中的队列定义
struct IKCPCB
{
// ... 其他字段 ...
struct IQUEUEHEAD snd_queue; // 发送队列(待发货区)
struct IQUEUEHEAD rcv_queue; // 接收队列(最终取货区)
struct IQUEUEHEAD snd_buf; // 发送缓冲区(已发货、待签收区)
struct IQUEUEHEAD rcv_buf; // 接收缓冲区(收货暂存区)
// ... 其他字段 ...
};
接下来,我们以一次完整的“发货”和“收货”过程,来详细了解这四个队列如何协同工作。
发送流程:从 snd_queue 到 snd_buf
当你的应用程序调用 ikcp_send 时,数据包的旅程就开始了。
-
snd_queue(发送队列): 用户的“发件箱”- 角色:待发货区。
- 工作流程:当你调用
ikcp_send,KCP 会将你的数据(如果需要,会先分片)打包成一个个内部的IKCPSEG数据段,然后把这些数据段全部放入snd_queue的队尾。 - 特点:此时,
snd_queue里的数据段还只是“原材料”,它们没有被分配唯一的序列号(sn),也没有发送和重传的时间信息。它们只是静静地排队,等待被处理。
// ikcp.c: ikcp_send 简化逻辑 // ... // 为数据创建一个 IKCPSEG 结构体 IKCPSEG *seg = ikcp_segment_new(kcp, size); // ... // 将数据段添加到发送队列 (snd_queue) 的末尾 iqueue_add_tail(&seg->node, &kcp->snd_queue); kcp->nsnd_que++; // 发送队列中的数据段数量加一 // ... -
snd_buf(发送缓冲区): “已发货,待签收”的追踪区- 角色:已发货但等待对方签收(ACK)的区域。
- 工作流程:当“心跳”函数
ikcp_flush被触发时,它会检查snd_queue。如果发送窗口有余量,ikcp_flush就会从snd_queue队首取出数据段,进行“加工”:- 分配一个唯一的、递增的序列号 (
sn)。 - 记录当前发送时间戳 (
ts) 和计算重传超时时间 (rto)。 - 然后将这个加工好的数据段移入
snd_buf的队尾。 - 最后,
ikcp_flush会遍历snd_buf,将需要发送或重传的数据段通过底层output函数发出去。
- 分配一个唯一的、递增的序列号 (
- 特点:
snd_buf是 KCP 实现可靠性的核心。只要一个数据段还存放在snd_buf中,就意味着 KCP 还没有收到对方对这个数据段的确认。ikcp_flush会周期性地检查snd_buf里的数据段,一旦发现超时,就会自动重传。直到收到对应的 ACK,KCP 才会将该数据段从snd_buf中删除。
// ikcp.c: ikcp_flush 简化逻辑 void ikcp_flush(ikcpcb *kcp) { // ... // 只要发送窗口还有空间 while (/* 窗口有空间 */) { // 从 snd_queue 取出数据段 IKCPSEG *newseg = iqueue_entry(kcp->snd_queue.next, IKCPSEG, node); iqueue_del(&newseg->node); // 放入 snd_buf iqueue_add_tail(&newseg->node, &kcp->snd_buf); // 分配序列号和其他元数据 newseg->sn = kcp->snd_nxt++; newseg->ts = current_time; // ... } // ... }
接收流程:从 rcv_buf 到 rcv_queue
当数据包从网络抵达时,接收方的 KCP 开始了它的处理流程。
-
rcv_buf(接收缓冲区): 乱序包裹的“分拣中心”- 角色:收到的、但顺序可能混乱的包裹的暂存和整理区。
- 工作流程:当底层网络收到一个 UDP 包后,用户调用
ikcp_input将其喂给 KCP。KCP 解析出里面的IKCPSEG数据段后,并不会立即交给应用层。因为网络可能导致数据包乱序(例如,先收到了序列号为 5 的包,再收到序列号为 3 的包)。KCP 会将这些数据段按照序列号sn的大小,插入到rcv_buf这个有序链表中。 - 特点:
rcv_buf就像一个拼图板。收到的每个数据段都是一块拼图,KCP 把它们放到正确的位置上。它起到了缓存和排序的作用。
// ikcp.c: ikcp_parse_data 简化逻辑 (由 ikcp_input 调用) void ikcp_parse_data(ikcpcb *kcp, IKCPSEG *newseg) { // ... // 在 rcv_buf 中找到一个合适的位置,按 sn 排序插入 iqueue_add(&newseg->node, p); // p 是 rcv_buf 中合适的位置 kcp->nrcv_buf++; // ... } -
rcv_queue(接收队列): 准备就绪的“取货架”- 角色:最终的、整理完毕的、等待用户
ikcp_recv来取走的收货队列。 - 工作流程:在每次有新数据段放入
rcv_buf后,KCP 都会立即检查rcv_buf的队首。它会查看队首数据段的序列号sn是否等于它当前正期待的序列号rcv_nxt。- 如果是,太棒了!这正是我们想要的下一个包裹。KCP 会将这个数据段从
rcv_buf中取出,放入rcv_queue的队尾,并更新rcv_nxt(rcv_nxt++)。 - 然后,KCP 会继续检查新的队首,重复这个过程,直到
rcv_buf的队首不再是连续的包。
- 如果是,太棒了!这正是我们想要的下一个包裹。KCP 会将这个数据段从
- 特点:
rcv_queue里的数据永远是完整且有序的。用户的ikcp_recv函数唯一的工作,就是从这个队列的队首简单地取出数据。这极大地简化了应用层的逻辑,开发者无需关心任何乱序和重组问题。
// ikcp.c: ikcp_parse_data 和 ikcp_recv 中都有类似的逻辑 // 将 rcv_buf 中连续的数据段移入 rcv_queue while (!iqueue_is_empty(&kcp->rcv_buf)) { IKCPSEG *seg = iqueue_entry(kcp->rcv_buf.next, IKCPSEG, node); // 如果是期望的下一个包 if (seg->sn == kcp->rcv_nxt && kcp->nrcv_que < kcp->rcv_wnd) { iqueue_del(&seg->node); // 从 rcv_buf 移除 iqueue_add_tail(&seg->node, &kcp->rcv_queue); // 添加到 rcv_queue kcp->nrcv_nxt++; // 更新期望的下一个序号 } else { break; // 不是连续的,停止移动 } } - 角色:最终的、整理完毕的、等待用户
完整数据流转图
现在,让我们用一张图来完整地串联起这四个队列和相关的 KCP 函数。
---
config:
layout: fixed
---
flowchart TD
subgraph s1["应用层"]
A["ikcp_send"]
B["ikcp_recv"]
end
subgraph subGraph1["KCP 内部 (发送方)"]
SQ["snd_queue <br> 待发货区"]
SB["snd_buf <br> 已发货区"]
end
subgraph subGraph2["KCP 内部 (接收方)"]
RB["rcv_buf <br> 乱序分拣区"]
RQ["rcv_queue <br> 最终取货区"]
end
subgraph s2["网络传输"]
O["ikcp_output"]
I["ikcp_input"]
end
A -- 放入数据 --> SQ
SQ -- 数据段 --> SB
SB -- 数据包 --> O
I -- 放入乱序包 --> RB
RB -- 整理排序后<br>连续的包 --> RQ
B -- 取出有序数据 --> RQ
style SQ fill:#cde4ff
style SB fill:#f8d7da
style RB fill:#fff3cd
style RQ fill:#d4edda
这张图清晰地展示了 KCP 可靠有序传输的生命周期:
- 发送:
应用数据->ikcp_send->snd_queue->ikcp_flush->snd_buf->网络。 - 接收:
网络->ikcp_input->rcv_buf-> (内部排序) ->rcv_queue->ikcp_recv->应用数据。
总结
在本章中,我们深入了解了 KCP 内部的“仓储物流系统”——四个核心队列:
snd_queue:用户ikcp_send的第一站,是待发送数据的暂存队列。snd_buf:KCP 可靠性的关键,存放已发送但未被确认的数据,负责超时重传。rcv_buf:处理网络乱序的“分拣中心”,按序列号缓存和排序收到的数据。rcv_queue:交付给用户的“取货架”,存放完全有序且可被读取的数据。
这四个队列如同一套精密协作的齿轮,snd_queue 和 snd_buf 保证了数据的可靠发送,而 rcv_buf 和 rcv_queue 则保证了数据的有序接收。正是这个设计,让 KCP 能够在不可靠的底层协议(如 UDP)之上,构建起一个快速、可靠的传输服务。
我们已经知道了数据是如何进入 KCP 内部,以及如何在队列之间流转。但是,ikcp_input 函数究竟是如何解析从网络传来的原始字节流,并将其变成 KCP 内部可以识别的数据段的呢?下一章,我们将聚焦于 KCP 的“入口”——底层数据输入处理 (ikcp_input)。