KCP源码解析 (4) 数据流转与队列管理

105 阅读6分钟

在上一章 核心更新与刷新机制 (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_queuesnd_buf

当你的应用程序调用 ikcp_send 时,数据包的旅程就开始了。

  1. 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++; // 发送队列中的数据段数量加一
    // ...
    
  2. 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_bufrcv_queue

当数据包从网络抵达时,接收方的 KCP 开始了它的处理流程。

  1. 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++;
        // ...
    }
    
  2. rcv_queue (接收队列): 准备就绪的“取货架”

    • 角色:最终的、整理完毕的、等待用户 ikcp_recv 来取走的收货队列。
    • 工作流程:在每次有新数据段放入 rcv_buf 后,KCP 都会立即检查 rcv_buf 的队首。它会查看队首数据段的序列号 sn 是否等于它当前正期待的序列号 rcv_nxt
      • 如果是,太棒了!这正是我们想要的下一个包裹。KCP 会将这个数据段从 rcv_buf 中取出,放入 rcv_queue 的队尾,并更新 rcv_nxtrcv_nxt++)。
      • 然后,KCP 会继续检查新的队首,重复这个过程,直到 rcv_buf 的队首不再是连续的包。
    • 特点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 可靠有序传输的生命周期:

  1. 发送应用数据 -> ikcp_send -> snd_queue -> ikcp_flush -> snd_buf -> 网络
  2. 接收网络 -> ikcp_input -> rcv_buf -> (内部排序) -> rcv_queue -> ikcp_recv -> 应用数据

总结

在本章中,我们深入了解了 KCP 内部的“仓储物流系统”——四个核心队列:

  • snd_queue:用户 ikcp_send 的第一站,是待发送数据的暂存队列。
  • snd_buf:KCP 可靠性的关键,存放已发送但未被确认的数据,负责超时重传。
  • rcv_buf:处理网络乱序的“分拣中心”,按序列号缓存和排序收到的数据。
  • rcv_queue:交付给用户的“取货架”,存放完全有序且可被读取的数据。

这四个队列如同一套精密协作的齿轮,snd_queuesnd_buf 保证了数据的可靠发送,而 rcv_bufrcv_queue 则保证了数据的有序接收。正是这个设计,让 KCP 能够在不可靠的底层协议(如 UDP)之上,构建起一个快速、可靠的传输服务。

我们已经知道了数据是如何进入 KCP 内部,以及如何在队列之间流转。但是,ikcp_input 函数究竟是如何解析从网络传来的原始字节流,并将其变成 KCP 内部可以识别的数据段的呢?下一章,我们将聚焦于 KCP 的“入口”——底层数据输入处理 (ikcp_input)。