KCP源码解析 (2) KCP 控制块 (ikcpcb)

144 阅读8分钟

在上一章 用户接口 (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.hIKCPCB 结构体的简化版定义,并按功能分组介绍其中最重要的成员:

// 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_queueikcp_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_createikcp.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

这张图清晰地展示了:

  1. ikcp_sendikcp_recv 主要与 ikcpcb 中的数据队列进行交互。
  2. 核心更新与刷新机制 (ikcp_update & ikcp_flush)底层数据输入处理 (ikcp_input) 是驱动 ikcpcb 状态变化的两个主要引擎。
  3. ikcpcb 内部的所有状态(序列号、窗口、RTT 等)都会被这些引擎读取和修改。
  4. 当需要发包时,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 是如何运行的。