在上一章 用户接口 (ikcp_send / ikcp_recv) 中,我们学习了如何使用 KCP 的“发件箱”和“收件箱”来收发数据。你可能已经注意到了,我们调用的每一个 KCP 函数,无论是 ikcp_send 还是 ikcp_recv,都离不开一个神秘的参数:kcp。
// 回顾一下这两个函数
int ikcp_send(ikcpcb *kcp, const char *buffer, int len);
int ikcp_recv(ikcpcb *kcp, char *buffer, int len);
这个 kcp 到底是什么?为什么它如此重要,以至于每个核心操作都必须持有它?
本章,我们将深入 KCP 的核心控制,揭开这个核心结构——ikcpcb(KCP 控制块)的神秘面纱。
KCP连接的控制中心
KCP 的 ikcpcb 结构体,就扮演着一个“大脑”与“驾驶舱”的角色。每一个 KCP 连接,都有一个与之对应的 ikcpcb 实例。 它记录了该连接从建立到结束的所有关键状态信息。KCP 协议的所有算法和逻辑,本质上都是在读取和修改这个结构体里的数据。
没有 ikcpcb,KCP 就无法工作,就像一辆没有仪表盘和 ECU 的汽车,只是一堆冰冷的钢铁。
ikcpcb 里有什么?—— KCP 的关键状态
ikcpcb 是一个定义在 ikcp.h 中的 C 语言结构体。它包含了非常多的字段,初学者可能会感到不知所措。别担心,我们不需要一次性理解所有内容。我们可以把它拆分成几个逻辑部分来逐一认识。
我们来看看 ikcp.h 中 IKCPCB 结构体的简化版定义,并按功能分组介绍其中最重要的成员:
// ikcp.h: IKCPCB 结构体定义(已简化和注释)
struct IKCPCB
{
// 身份标识
IUINT32 conv; // 会话编号,唯一标识一个连接
// 底层协议信息
IUINT32 mtu; // 最大传输单元 (Maximum Transmission Unit)
IUINT32 mss; // 最大分段大小 (Maximum Segment Size)
int (*output)(const char *buf, int len, struct IKCPCB *kcp, void *user); // 底层数据输出函数
// 发送与接收序列号
IUINT32 snd_una; // 已发送但尚未被确认的最小序号 (Send Unacknowledged)
IUINT32 snd_nxt; // 下一个待发送的序号 (Send Next)
IUINT32 rcv_nxt; // 期望接收的下一个序号 (Receive Next)
// 流量与拥塞控制
IUINT32 snd_wnd; // 发送窗口大小
IUINT32 rcv_wnd; // 接收窗口大小
IUINT32 rmt_wnd; // 远端(对方)的接收窗口大小
IUINT32 cwnd; // 拥塞窗口大小
IUINT32 ssthresh; // 慢启动阈值
// RTT (往返时间) 计算
IINT32 rx_srtt; // 平滑后的往返时间 (Smoothed RTT)
IINT32 rx_rto; // 重传超时时间 (Retransmission Timeout)
// 核心数据队列 (使用双向链表实现)
struct IQUEUEHEAD snd_queue; // 发送队列,存放应用层待发送的数据段
struct IQUEUEHEAD rcv_queue; // 接收队列,存放已排序、可供应用层读取的数据段
struct IQUEUEHEAD snd_buf; // 发送缓冲区,存放送出但未确认的数据段
struct IQUEUEHEAD rcv_buf; // 接收缓冲区,存放收到但可能乱序的数据段
};
让我们像参观汽车驾驶舱一样,来认识一下这些关键的“仪表”和“开关”:
-
身份标识 (
conv):这是连接的“车牌号”。网络上可能同时有成千上万的数据包在飞驰,KCP 通过这个conv(Conversation ID) 来识别哪些数据包是属于当前这个连接的。通信双方的conv必须保持一致。 -
核心序列号 (
snd_nxt,rcv_nxt,snd_una):这就像是物流系统里的包裹追踪号。snd_nxt:记录着下一个要发出的包裹的编号。rcv_nxt:记录着我们期望收到的下一个包裹的编号。snd_una:记录着对方已经签收的最早的那个包裹的编号。通过它,我们知道哪些包裹可以确认送达了。
-
窗口管理 (
snd_wnd,rcv_wnd,rmt_wnd):这定义了通信的“容量”。snd_wnd(发送窗口) 和rcv_wnd(接收窗口):分别代表了本地的发送能力和接收能力。rmt_wnd(远端窗口):代表了对方的接收能力。这是非常重要的信息,KCP 会根据这个值来控制发送速率,防止发得太快导致对方“爆仓”。
-
RTT 计算 (
rx_srtt,rx_rto):这是 KCP 的“秒表”。- RTT (Round-Trip Time) 是指一个数据包从发送到收到对方确认所花费的时间。
rx_srtt(Smoothed RTT) 是一个平滑计算后的 RTT 值,用来代表当前网络的平均延迟。rx_rto(Retransmission Timeout) 是根据 RTT 计算出的重传超时时间。如果一个数据包发出后,超过这个时间还没收到确认,KCP 就会认为它丢失了,并进行重传。
-
核心数据队列:这是 KCP 的“仓库”和“流水线”。我们将在 数据流转与队列管理 章节详细讲解,这里先简单了解:
snd_queue:ikcp_send函数的包裹暂存区。snd_buf:已发货,等待签收的包裹存放区。rcv_buf:收到的、可能乱序的包裹分拣区。rcv_queue:已分拣好、按顺序摆放整齐,等待ikcp_recv来取走的包裹货架。
-
底层输出 (
output):这是 KCP 与外界唯一的“出口”。它是一个函数指针,指向一个由你(用户)提供的函数。当 KCP 决定要发送一个数据包时(例如,一个数据段或者一个 ACK 确认包),它会调用这个output函数,把打包好的二进制数据交给你。你需要在这个函数里,通过真实的物理网络(比如 UDP 的sendto函数)把数据发出去。
创建你的“驾驶舱”:ikcp_create
既然 ikcpcb 如此重要,我们该如何得到它呢?答案是 ikcp_create 函数。
// ikcp.h
ikcpcb* ikcp_create(IUINT32 conv, void *user);
这个函数为你创建一个全新的 KCP 连接实例。
conv: 你为这个连接指定的会话 ID。记住,通信双方必须使用相同的conv。user: 一个用户自定义的指针,KCP 不会动它,只是在调用output函数时原样传回给你。通常用来传递一些与该连接相关的上下文信息,比如一个socket句柄或者一个对象指针。
让我们看一个创建 KCP 实例的简单例子:
// 定义一个底层发送函数
int my_udp_output(const char *buf, int len, ikcpcb *kcp, void *user) {
// 在这里,你可以用 user 指针获取你的 socket 信息
// 然后通过 UDP 发送 buf...
// ... sendto(socket, buf, len, ...);
return 0;
}
// 定义一个会话ID
IUINT32 conversation_id = 12345;
// 1. 创建 KCP 控制块
ikcpcb *kcp_instance = ikcp_create(conversation_id, NULL);
// 2. 设置底层输出函数
kcp_instance->output = my_udp_output;
调用 ikcp_create 后,你会得到一个指向 ikcpcb 结构体的指针。这个结构体内部的各个字段已经被初始化为合理的默认值。例如,MTU、窗口大小、RTO 等。
ikcp_create 幕后探秘
ikcp_create 在 ikcp.c 中的实现非常直观。它主要做了两件事:分配内存和初始化。
// ikcp.c: ikcp_create 核心逻辑简化
ikcpcb* ikcp_create(IUINT32 conv, void *user)
{
// 1. 分配内存
ikcpcb *kcp = (ikcpcb*)ikcp_malloc(sizeof(struct IKCPCB));
if (kcp == NULL) return NULL;
// 2. 初始化核心字段
kcp->conv = conv;
kcp->user = user;
kcp->snd_wnd = IKCP_WND_SND; // 默认发送窗口: 32
kcp->rcv_wnd = IKCP_WND_RCV; // 默认接收窗口: 128
kcp->rmt_wnd = IKCP_WND_RCV; // 默认远端窗口: 128
kcp->mtu = IKCP_MTU_DEF; // 默认 MTU: 1400
kcp->mss = kcp->mtu - IKCP_OVERHEAD;
kcp->rx_rto = IKCP_RTO_DEF; // 默认重传超时: 200ms
// ... 其他字段初始化 ...
// 3. 初始化所有队列
iqueue_init(&kcp->snd_queue);
iqueue_init(&kcp->rcv_queue);
iqueue_init(&kcp->snd_buf);
iqueue_init(&kcp->rcv_buf);
return kcp;
}
可以看到,ikcp_create 为你准备好了一个“万事俱备,只欠东风”的 KCP 实例。这个“东风”,就是你设置的 output 函数以及后续对 ikcp_send, ikcp_input, ikcp_update 的调用。
当一个连接结束时,你需要调用 ikcp_release(kcp) 来释放所有相关的内存资源,避免内存泄漏。
ikcpcb:一切操作的中心
现在我们可以更好地理解 KCP 的工作流了。ikcpcb 是所有操作的中心枢纽。
graph TD
subgraph 应用层
A[ikcp_send]
B[ikcp_recv]
end
subgraph KCP 内部
C(ikcpcb 状态机)
C -- 修改/读取 --> D{conv, snd_nxt, rcv_nxt, ...}
C -- 修改/读取 --> E{snd_wnd, rcv_wnd, cwnd, ...}
C -- 修改/读取 --> F{rx_srtt, rx_rto, ...}
C -- 修改/读取 --> G[snd_queue, rcv_queue, snd_buf, rcv_buf]
end
subgraph 核心驱动
H[ikcp_update]
I[ikcp_input]
end
subgraph 底层网络
J[output 函数]
end
A -- "写入" --> G
B -- "读取" --> G
H -- "驱动" --> C
I -- "输入" --> C
C -- "调用" --> J
style C fill:#f9f,stroke:#333,stroke-width:2px
这张图清晰地展示了:
ikcp_send和ikcp_recv主要与ikcpcb中的数据队列进行交互。- 核心更新与刷新机制 (ikcp_update & ikcp_flush) 和 底层数据输入处理 (ikcp_input) 是驱动
ikcpcb状态变化的两个主要引擎。 ikcpcb内部的所有状态(序列号、窗口、RTT 等)都会被这些引擎读取和修改。- 当需要发包时,
ikcpcb会通过output指针将数据交给底层网络。
总结
在本章中,我们揭开了 KCP “大脑”——ikcpcb 的神秘面纱。
ikcpcb是 KCP 协议的核心,它是一个包含了单个连接所有状态的结构体。- 它是KCP的控制中心,记录了会话ID (
conv)、序列号 (snd_nxt,rcv_nxt)、窗口大小 (snd_wnd,rcv_wnd)、往返时间 (rx_rto) 等关键信息。 - 它还管理着四个核心的数据队列,是 KCP 数据流转的基础。
- 我们通过
ikcp_create来创建一个新的 KCP 实例(即ikcpcb),并通过设置其output函数指针来桥接底层网络,通常我们使用UDP作为传输底层,当然也可以使用ICMP或其他包传输协议。 - 所有 KCP 的接口函数都围绕着操作
ikcpcb中的数据来进行。
现在我们理解了 KCP 的控制中心里都存放了些什么信息。但一个静止的控制中心是无法思考的。KCP 是如何根据这些信息动态地做出决策,比如什么时候发包、什么时候重传、什么时候调整速度的呢?
下一章,我们将探讨 KCP 的“心跳”——核心更新与刷新机制,看看 KCP 是如何运行的。