在上一章 数据流转与队列管理 中,我们探索了 KCP 内部精密的"仓储物流系统",了解了数据是如何在 snd_queue、snd_buf、rcv_buf 和 rcv_queue 这四个核心队列之间流转的。
这留下了一个关键问题:那些来自网络的、最原始的数据包,是如何进入这个系统的第一站------rcv_buf 的呢?谁是那个负责从"卡车"(UDP)上卸货,并把包裹放到"分拣中心"(rcv_buf)的"码头工人"呢?
本章,我们将聚焦于 KCP 的"收件口",也就是 ikcp_input 函数,看看它是如何处理这关键的第一步的。
问题的提出:处理来自网络的原始数据
想象一下,你是一家大型邮件处理中心的经理。每天,邮政卡车会运来成吨的包裹(UDP 数据包)。这些包裹杂乱无章,里面可能混杂着信件(数据)、回执单(ACK)、各种查询请求(窗口探测)等等。
你需要一个高效的收件流程来处理这一切:
- 拆包:打开每个邮政包裹。
- 识别 :查看里面的单据,确认这是不是发给我们公司的(检查会话 ID conv)。
- 分类 :根据单据类型(cmd),将物品分发到不同的处理流程。信件送到分拣区,回执单送到发货部核销,查询请求转给客服。
ikcp_input 函数就扮演着这样一个角色。它是 KCP 与底层网络之间的桥梁,负责接收和解析所有从网络传来的原始字节流。
核心接口:KCP 的"收件口" ikcp_input
当你通过 UDP 的 recvfrom 之类的函数从网络上收到一个数据包时,你不能直接把它交给 ikcp_recv,因为 ikcp_recv 只处理 KCP 已经整理好的、有序的数据。你需要做的,是立刻把这个原始的、未经处理的 UDP 负载数据"喂"给 ikcp_input。
ikcp_input 的函数原型如下:
            
            
              c
              
              
            
          
          // ikcp.h
// 当你收到一个底层协议的数据包时(例如UDP包),调用此函数
// data: 收到的数据包指针
// size: 数据包大小
int ikcp_input(ikcpcb *kcp, const char *data, long size);它的参数非常简单:
- kcp: KCP 控制块 (ikcpcb)。
- data: 你从网络(如 UDP socket)收到的原始数据包的指针。
- size: 该数据包的长度。
使用方法非常直接:
            
            
              c
              
              
            
          
          // 假设你有一个 UDP socket,并已创建了 kcp 实例
char udp_payload[1500];
int received_bytes;
// 从 socket 接收一个 UDP 数据包
received_bytes = recvfrom(sock, udp_payload, sizeof(udp_payload), 0, ...);
if (received_bytes > 0) {
    // 立即将收到的原始数据喂给 KCP 的"收件口"
    int ret = ikcp_input(kcp, udp_payload, received_bytes);
    if (ret < 0) {
        // 数据包有误,例如 conv 不匹配或数据包损坏
        printf("ikcp_input 处理失败,错误码: %d\n", ret);
    }
}重点 :成功调用 ikcp_input 并不意味着你的应用程序马上就能通过 ikcp_recv 读到数据。它只表示 KCP 已经接收并处理了这个底层数据包 。数据可能被放入了"乱序分拣区" rcv_buf,或者它可能是一个 ACK 包,只是用来更新发送方的状态。
内部探秘:ikcp_input 的拆包流水线
ikcp_input 的内部实现就像一条高效的流水线。一个 UDP 包里可能塞了多个 KCP 段,所以它会循环处理,直到把整个数据包"吃"完。
这条流水线主要包含以下几个关键步骤:
1. 循环解码与校验
ikcp_input 的核心是一个 while 循环,因为它允许一个 UDP 包内包含多个 KCP 数据段。
            
            
              c
              
              
            
          
          // ikcp.c: ikcp_input 核心逻辑简化
int ikcp_input(ikcpcb *kcp, const char *data, long size)
{
    // ...
    while (1) {
        // 如果剩余数据不够一个 KCP 头部,就退出
        if (size < (int)IKCP_OVERHEAD) break;
        IUINT32 conv, cmd, sn, una, len;
        // ... 其他头部字段 ...
        // 1. 解码头部,从字节流中提取信息
        data = ikcp_decode32u(data, &conv);
        
        // 2. 校验会话 ID (conv)
        if (conv != kcp->conv) return -1; // 不是我的包,丢弃
        data = ikcp_decode8u(data, &cmd);
        // ... 继续解码 sn, una, len 等...
        // ... 根据 cmd 进行后续处理 ...
        // 移动指针,处理下一个 KCP 段
        data += len;
        size -= IKCP_OVERHEAD + len;
    }
    return 0;
}- 解码 :ikcp_decode32u等函数负责从字节流中按小端序(LSB)读取出conv,cmd等头部信息。关于这些头部的详细定义,可以参考 KCP 数据段 (IKCPSEG)。
- 校验 conv:这是第一道安检。如果数据包的conv和当前 KCP 连接的conv不匹配,说明这个包不属于这个连接,ikcp_input会立刻返回错误,丢弃整个数据包。
2. 处理 UNA 和远端窗口
在处理具体命令前,ikcp_input 会先处理两项通用信息:
- 更新远端窗口 (rmt_wnd):每个收到的 KCP 段都携带了对方当前的可用接收窗口大小,ikcp_input会用它来更新kcp->rmt_wnd。这让 KCP 可以实时了解对方的接收能力,是流量控制的基础。
- 处理 una:una(Unacknowledged) 字段告诉我们,对方已经收到了所有序号小于una的数据段。ikcp_input会调用ikcp_parse_una,将自己snd_buf中所有序号小于una的数据段全部清除,因为它们已经被确认收到了。
3. 按命令类型分类处理
接下来,ikcp_input 会像一个分拣员,根据 cmd 字段将 KCP 段交给不同的处理逻辑。
情况一:收到数据包 (IKCP_CMD_PUSH)
这是最常见的情况。
            
            
              c
              
              
            
          
          // ikcp_input 中处理数据包 (IKCP_CMD_PUSH) 的部分
else if (cmd == IKCP_CMD_PUSH) {
    // 检查 sn 是否在接收窗口内
    if (_itimediff(sn, kcp->rcv_nxt + kcp->rcv_wnd) < 0) {
        // 1. 记录下来,稍后需要回复 ACK
        ikcp_ack_push(kcp, sn, ts);
        
        // 2. 如果是当前需要或未来的包
        if (_itimediff(sn, kcp->rcv_nxt) >= 0) {
            IKCPSEG *seg = ikcp_segment_new(kcp, len); // 创建数据段
            // ... 填充 seg 结构体 ...
            memcpy(seg->data, data, len); // 拷贝数据
            
            // 3. 将数据段放入 rcv_buf,由它负责排序
            ikcp_parse_data(kcp, seg); 
        }
    }
}- 登记待办 ACK :调用 ikcp_ack_push将这个数据段的sn和ts记录到一个acklist数组里。ikcp_flush在下次运行时会检查这个列表,并生成 ACK 包发回给对方。
- 创建数据段 :如果这个数据包不是一个重复的、过时的数据包(sn >= rcv_nxt),KCP 会为它创建一个IKCPSEG结构。
- 放入分拣区 :最后,调用 ikcp_parse_data,将这个新的数据段插入到rcv_buf(接收缓冲区)的正确位置(按sn排序)。我们在 数据流转与队列管理 中已经知道,rcv_buf正是处理乱序和等待数据连续的地方。
情况二:收到确认包 (IKCP_CMD_ACK)
当收到对方的确认时,KCP 会更新自己的发送状态。
            
            
              c
              
              
            
          
          // ikcp_input 中处理 ACK (IKCP_CMD_ACK) 的部分
if (cmd == IKCP_CMD_ACK) {
    // 1. 根据 ACK 包中的时间戳更新 RTT
    if (_itimediff(kcp->current, ts) >= 0) {
        ikcp_update_ack(kcp, _itimediff(kcp->current, ts));
    }
    // 2. 从 snd_buf 中移除已被确认的数据段
    ikcp_parse_ack(kcp, sn);
    ikcp_shrink_buf(kcp); // 整理 snd_buf,更新 snd_una
}- 更新 RTT :ikcp_update_ack函数会利用收到的 ACK 中的时间戳ts和当前时间kcp->current计算出这次通信的往返时间(RTT),并更新平滑 RTT (rx_srtt) 和重传超时时间 (rx_rto)。这是 KCP 自适应网络变化的核心。
- 清理已确认包 :ikcp_parse_ack函数会根据 ACK 包中的sn,在snd_buf中找到对应的那个数据段并将其删除。这意味着我们成功地发送了一个包,并且得到了确认,可以把它从"待签收"列表里划掉了。
情况三:其他控制命令
ikcp_input 还会处理 IKCP_CMD_WASK (窗口探测请求) 和 IKCP_CMD_WINS (窗口大小通知) 等控制命令,分别用于探测对方窗口和被动更新对方窗口信息。这些都是 KCP 流量控制和拥塞控制的一部分。
总结
在本章中,我们揭开了 KCP "收件口"------ikcp_input 函数的神秘面纱。它是连接底层网络和 KCP 内部世界的关键枢纽。
- 职责 :ikcp_input负责接收和解析从网络传来的原始字节流。你必须在收到 UDP 包后立即调用它。
- 拆包与校验 :它会循环解码数据包,校验会话 ID (conv),确保处理的是正确的连接。
- 分类处理 :它像一个智能分拣员,根据命令 cmd将 KCP 段分发给不同的处理逻辑:- 数据包 (PUSH) :放入rcv_buf进行排序,并准备发送 ACK。
- 确认包 (ACK) :更新 RTT,并从snd_buf中移除已确认的包。
- 控制包 (WASK/WINS):参与流量控制和窗口管理。
 
- 数据包 (
我们已经看到 ikcp_input 是如何处理 ACK 并更新 RTT 的,这些是 KCP 智能调节发送速度的基础。那么 KCP 究竟是如何利用这些信息来避免网络拥堵,实现既快又稳的传输呢?下一章,我们将深入探讨 KCP 的拥塞控制。