KCP源码解析 (5) 底层数据输入处理 (ikcp_input)

在上一章 数据流转与队列管理 中,我们探索了 KCP 内部精密的"仓储物流系统",了解了数据是如何在 snd_queuesnd_bufrcv_bufrcv_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 段,所以它会循环处理,直到把整个数据包"吃"完。

sequenceDiagram participant Net as 底层网络 (UDP) participant App as 你的应用 participant Input as ikcp_input participant KCP_Internal as KCP 内部处理 participant rcv_buf as 接收缓冲区 participant snd_buf as 发送缓冲区 Net->>App: 收到一个 UDP 数据包 App->>Input: 调用 ikcp_input(kcp, data, size) Input->>Input: 循环解析数据包 Input->>KCP_Internal: 解码 KCP 头部 (conv, cmd, sn...) KCP_Internal-->>Input: 返回解码后的信息 alt 包是数据 (CMD_PUSH) Input->>KCP_Internal: 创建 IKCPSEG 数据段 KCP_Internal->>rcv_buf: 将数据段按 sn 排序插入 Input->>KCP_Internal: 记录 ACK (sn) 到待发送列表 else 包是确认 (CMD_ACK) Input->>KCP_Internal: 更新 RTT 估算 KCP_Internal->>snd_buf: 移除已被确认的数据段 (sn) else 包是其他命令 Input->>KCP_Internal: 执行相应操作 (如更新窗口) end Input-->>App: 返回处理结果 (0)

这条流水线主要包含以下几个关键步骤:

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 可以实时了解对方的接收能力,是流量控制的基础。
  • 处理 unauna (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); 
        }
    }
}
  1. 登记待办 ACK :调用 ikcp_ack_push 将这个数据段的 snts 记录到一个 acklist 数组里。ikcp_flush 在下次运行时会检查这个列表,并生成 ACK 包发回给对方。
  2. 创建数据段 :如果这个数据包不是一个重复的、过时的数据包(sn >= rcv_nxt),KCP 会为它创建一个 IKCPSEG 结构。
  3. 放入分拣区 :最后,调用 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
}
  1. 更新 RTTikcp_update_ack 函数会利用收到的 ACK 中的时间戳 ts 和当前时间 kcp->current 计算出这次通信的往返时间(RTT),并更新平滑 RTT (rx_srtt) 和重传超时时间 (rx_rto)。这是 KCP 自适应网络变化的核心。
  2. 清理已确认包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 的拥塞控制。

相关推荐
OKkankan1 小时前
string类的模拟实现
开发语言·数据结构·c++·算法
蝸牛ちゃん1 小时前
万字深度详解DHCP服务:动态IP地址分配的自动化引擎
网络·网络协议·tcp/ip·系统架构·自动化·软考高级·dhcp
帽儿山的枪手8 小时前
HVV期间,如何使用SSH隧道绕过内外网隔离限制?
linux·网络协议·安全
charlie1145141919 小时前
设计自己的小传输协议 导论与概念
c++·笔记·qt·网络协议·设计·通信协议
(Charon)10 小时前
【C语言网络编程】HTTP 客户端请求(基于 Socket 的完整实现)
网络·网络协议·http
程序员编程指南11 小时前
Qt 并行计算框架与应用
c语言·数据库·c++·qt·系统架构
努力的小帅12 小时前
C++_红黑树树
开发语言·数据结构·c++·学习·算法·红黑树
CN-Dust12 小时前
【C++】指针
开发语言·c++
逐花归海.13 小时前
『 C++ 入门到放弃 』- 哈希表
数据结构·c++·程序人生·哈希算法·散列表
筏.k13 小时前
C++现代Redis客户端库redis-plus-plus详解
c++·redis