在上一章 数据流转与队列管理 中,我们探索了 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 的拥塞控制。