在上一章 KCP 控制块 (ikcpcb) 中,我们了解了 KCP 的ikcpcb 结构体,它静态地存储了连接的所有状态信息。但是,一个静止的控制数据结构是无法工作的。这就引出了一个核心问题:
我们调用 ikcp_send 后,数据只是被安安静静地放进了发送队列。那到底是谁、在什么时候,把这些数据真正打包发出去?如果数据包丢了,又是谁负责发现并重传它呢?
答案就是本章的主角:ikcp_update 和 ikcp_flush。它们是 KCP 协议的“心跳”和“执行引擎”,负责驱动整个 KCP 状态机向前运转。
KCP 的“心跳”:ikcp_update
想象一下,你雇佣了一位非常勤奋的管家。但你必须告诉他每隔多久需要检查一次待办事项清单。你不能指望他自己“凭感觉”工作。你可能会设置一个闹钟,每 10 毫秒响一次,闹钟一响,管家就开始工作。
ikcp_update 就是这个“闹钟”触发器。它本身并不处理复杂的逻辑,它最核心的任务就是:根据用户传入的当前时间,判断是否到了该处理 KCP 待办事项的时刻。
因此,作为 KCP 的使用者,你必须在一个循环中,以固定的频率(通常是 10ms 到 100ms)调用 ikcp_update。
如何使用 ikcp_update
ikcp_update 的函数原型非常简单:
// ikcp.h
void ikcp_update(ikcpcb *kcp, IUINT32 current);
kcp: 我们熟悉的 KCP 控制块。current: 当前的时间戳,以毫秒为单位。这是最重要的参数,KCP 内部的所有时间相关的判断都依赖于此。
一个典型的使用场景如下:
// 假设 kcp 实例已经创建
ikcpcb *kcp = ikcp_create(0x11223344, NULL);
// ... 设置 output 回调等 ...
// 获取当前时间的函数(需要用户自己实现)
IUINT32 get_current_ms();
// 主循环
while (1) {
// 获取当前毫秒级时间戳
IUINT32 current_time = get_current_ms();
// 驱动 KCP 状态机
ikcp_update(kcp, current_time);
// ... 处理你的其他业务逻辑,比如调用 ikcp_recv ...
// 休眠一小段时间,例如 10 毫秒
sleep_ms(10);
}
重点:你必须持续、周期性地调用 ikcp_update。如果你停止调用它,KCP 就会“心跳停止”,所有待发送的数据将滞留在队列中,所有超时重传逻辑也将失效。
ikcp_update 幕后探秘
ikcp_update 的内部逻辑非常简单,我们可以把它看作一个时间检查器。
sequenceDiagram
participant App as 应用主循环
participant Update as ikcp_update
participant Flush as ikcp_flush
App->>Update: 调用 ikcp_update(kcp, 当前时间)
Update->>Update: 检查当前时间是否超过了下次刷新时间(ts_flush)?
alt 是,时间到了
Update->>Flush: 调用 ikcp_flush(kcp)
Flush-->>Update: 完成所有待办事项
Update->>Update: 更新下一次刷新时间
else 否,时间未到
Update-->>App: 直接返回
end
让我们看看 ikcp.c 中对应的简化代码:
// ikcp.c: ikcp_update 核心逻辑简化
void ikcp_update(ikcpcb *kcp, IUINT32 current)
{
kcp->current = current; // 更新 kcp 控制块中的当前时间
// 计算从上次刷新到现在的逝去时间
IINT32 slap = _itimediff(kcp->current, kcp->ts_flush);
// 如果时间到了 (slap >= 0),或者时间严重错乱
if (slap >= 10000 || slap < -10000) {
kcp->ts_flush = kcp->current;
slap = 0;
}
if (slap >= 0) {
// 计算下一次需要刷新的时间点
kcp->ts_flush += kcp->interval;
// 如果当前时间已经超过了计算出的下一次时间,
// 就把下一次刷新时间设置为 "当前时间 + interval"
if (_itimediff(kcp->current, kcp->ts_flush) >= 0) {
kcp->ts_flush = kcp->current + kcp->interval;
}
// 时间到了,调用真正的“执行引擎”
ikcp_flush(kcp);
}
}
这段代码的核心思想是:ikcp_update 维护了一个 ts_flush 字段(下次刷新时间)。每次调用时,它都会用当前时间 current 和 ts_flush 比较。一旦 current 越过了 ts_flush,就意味着“闹钟响了”,是时候调用 ikcp_flush 来干活了。
KCP 的“执行引擎”:ikcp_flush
如果说 ikcp_update 是“闹钟”,那么 ikcp_flush 就是那个被闹钟唤醒后,拿起待办事项清单开始逐一处理的“管家”。它是 KCP 所有主动操作的执行中心,负责将 KCP 的内部状态(队列、窗口等)变化,物化为实际的网络发包行为。
ikcp_flush 的工作可以概括为一个长长的清单:
- 发送 ACK:检查是否有已收到的数据包需要确认(ACK)?把它们打包发出去。
- 发送窗口探测:对方的接收窗口是不是为 0 了?如果是,需要发送探测包(Probe)询问对方窗口是否已更新。
- 更新本地窗口:是否收到了对方的窗口探测请求?如果是,需要告知对方自己当前的接收窗口大小。
- 数据转移:把应用层通过 ikcp_send 放入
snd_queue的数据,转移到“待确认缓冲区”snd_buf,并分配序列号sn。 - 发送与重传:遍历
snd_buf,检查是否有:- 从未发送过的新数据包?—— 发送它!
- 已发送但超时未收到 ACK 的数据包?—— 重传它!
- 被判定需要快速重传的数据包?—— 重传它!
- 更新拥塞窗口:根据丢包情况,调整拥塞控制相关的参数(如
ssthresh,cwnd)。
所有这些操作最终都会把需要发送的数据(无论是 ACK、探测包还是业务数据)编码成二进制流,并通过我们在 KCP 控制块 (ikcpcb) 中设置的 output 函数发送出去。
ikcp_flush 幕后探秘
ikcp_flush 函数是 KCP 中最复杂的函数之一,但我们可以分步来理解它的核心逻辑。
第一步:数据从 snd_queue 转移到 snd_buf
ikcp_flush 首先会检查发送窗口是否还有空间。如果有,它就会把 snd_queue(ikcp_send 放数据的地方)里的数据段一个个取出来,赋予它们序列号(sn)、时间戳(ts)等关键信息,然后放入 snd_buf(已发送但待确认的缓冲区)。
// ikcp.c: ikcp_flush 中数据转移的简化逻辑
void ikcp_flush(ikcpcb *kcp) {
// ... 省略其他逻辑 ...
// 计算有效发送窗口大小
IUINT32 cwnd = _imin_(kcp->snd_wnd, kcp->rmt_wnd);
if (kcp->nocwnd == 0) cwnd = _imin_(kcp->cwnd, cwnd);
// 只要发送窗口还有空间 (snd_nxt < snd_una + cwnd)
while (_itimediff(kcp->snd_nxt, kcp->snd_una + cwnd) < 0) {
if (iqueue_is_empty(&kcp->snd_queue)) break;
// 从 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);
kcp->nsnd_que--;
kcp->nsnd_buf++;
// 为这个数据段分配序列号和其他元数据
newseg->sn = kcp->snd_nxt++;
newseg->ts = current;
newseg->rto = kcp->rx_rto;
// ...
}
// ... 后续逻辑 ...
}
第二步:遍历 snd_buf,决定发送或重传
完成数据转移后,ikcp_flush 会遍历 snd_buf 里的每一个数据段,根据多种条件判断它是否需要被发送。
// ikcp.c: ikcp_flush 中发送与重传的简化逻辑
void ikcp_flush(ikcpcb *kcp) {
// ... 省略 ACK 和数据转移逻辑 ...
// 遍历发送缓冲区
for (p = kcp->snd_buf.next; p != &kcp->snd_buf; p = p->next) {
IKCPSEG *segment = iqueue_entry(p, IKCPSEG, node);
int needsend = 0;
// 条件1:这是第一次发送
if (segment->xmit == 0) {
needsend = 1;
}
// 条件2:超时重传
else if (_itimediff(current, segment->resendts) >= 0) {
needsend = 1;
}
// 条件3:快速重传
else if (segment->fastack >= kcp->fastresend) {
needsend = 1;
}
if (needsend) {
// 如果需要发送,更新它的发送次数和时间戳
segment->xmit++;
segment->ts = current;
segment->resendts = current + segment->rto;
// 将这个 segment 的内容编码到发送缓冲区 buffer 中
// ...
}
}
// 将最终打包好的 buffer 通过 output 函数发送出去
// ... ikcp_output(kcp, buffer, size); ...
}
这个过程完美地诠释了 KCP 的可靠性:snd_buf 中的数据段只有在被确认为止,才会从这个缓冲区中移除。在此之前,ikcp_flush 会在每次执行时都检查它们的状态,确保它们在丢失或延迟的情况下能被及时重传。
一个重要的优化:ikcp_check
你可能会想,如果网络空闲,没有任何数据收发,那么每 10ms 调用一次 ikcp_update(进而可能调用 ikcp_flush)是不是有点浪费 CPU?
KCP 的作者也考虑到了这一点,因此提供了一个辅助函数 ikcp_check。
// ikcp.h
IUINT32 ikcp_check(const ikcpcb *kcp, IUINT32 current);
这个函数可以告诉你:下一次需要调用 ikcp_update 的精确时间点。
它的返回值是下次需要执行 ikcp_flush 的时间戳。你可以用这个返回值来设置一个定时器,而不是使用固定的 sleep。当定时器触发时,再调用 ikcp_update。这种方式在需要管理大量 KCP 连接(成千上万个)的服务器上尤其有用,可以显著降低 CPU 的空转消耗。
对于大多数应用场景,特别是客户端,使用固定的 sleep 循环调用 ikcp_update 已经足够简单高效。
总结
本章我们学习了驱动 KCP 运转的核心机制:
ikcp_update是 KCP 的“心跳”或“闹钟”。你必须在应用主循环中,以固定的时间间隔(如 10ms)调用它,并传入当前时间。ikcp_update的主要工作是检查时间,判断是否到了执行待办事项的时刻。ikcp_flush是 KCP 的“执行引擎”或“管家”。它被ikcp_update在特定时间点调用,负责执行所有主动的网络操作,包括:- 发送 ACK 确认。
- 将
snd_queue的数据移入snd_buf。 - 发送新数据、重传超时数据、进行快速重传。
- 处理窗口探测等。
ikcp_send只是把数据放入队列,真正将数据发出去的工作是由ikcp_flush完成的。这个机制保证了 KCP 的可靠性和实时性。
我们现在知道了数据是如何被主动发送出去的。但是,ikcp_send、ikcp_flush 这些操作背后,数据到底是在哪些队列之间流转的呢?下一章,我们将深入 KCP 的“队列系统”——数据流转与队列管理,详细看看 snd_queue, rcv_queue, snd_buf, rcv_buf 这四个核心队列是如何协同工作的。