本篇文章是接续基于UDP实现可靠传输——KCP协议
基于上一篇内容,了解KCP的工作原理后,让我们深入理解KCP库的核心函数和工作流程。KCP的实现提供了一套函数接口,用于数据的发送、接收以及协议的维护。以下是这些核心函数的详细介绍:
1. 创建和销毁KCP控制块
-
ikcp_create: 此函数用于创建一个KCP控制块。KCP控制块是KCP协议操作的基础,它存储了协议运行时所需的所有状态信息,如发送和接收窗口、超时重传时间等。在使用KCP进行数据传输之前,必须先创建一个控制块。
ikcpcb* ikcp_create(IUINT32 conv, void *user);
conv
是一个会话ID,用于标识KCP会话,确保数据包只在同一会话的两端之间传输。user
是用户自定义的指针,可以在回调中使用,用于传递额外的上下文信息。
-
ikcp_release: 此函数用于销毁一个KCP控制块,释放其占用的资源。在KCP会话结束时,应该调用此函数来清理资源。
void ikcp_release(ikcpcb *kcp);
2. 数据发送和接收
-
ikcp_send: 用于向KCP控制块的发送缓冲区中添加数据。当调用此函数时,数据不会立即发送,而是等到
ikcp_flush
被调用时才进行发送。int ikcp_send(ikcpcb *kcp, const char *buffer, int len);
buffer
指向要发送的数据。len
是数据的长度。
-
ikcp_recv: 用于从KCP控制块的接收缓冲区中提取数据。如果没有可用数据,此函数会根据模式的不同返回错误码或阻塞。
int ikcp_recv(ikcpcb *kcp, char *buffer, int len);
3. 更新和输入
-
ikcp_update: 此函数驱动KCP的内部逻辑,包括数据的发送、接收确认、超时重传等。它需要在应用程序的主循环中定期调用,以确保KCP协议的正常运行。
void ikcp_update(ikcpcb *kcp, IUINT32 current);
current
是当前的系统时间,通常以毫秒为单位。
-
ikcp_input: 当应用程序从网络接收到数据时,应该将这些数据通过
ikcp_input
输入到KCP控制块中,KCP会对这些数据进行处理,比如确认数据包的接收、处理重传等。int ikcp_input(ikcpcb *kcp, const char *data, long size);
4. 刷新
-
ikcp_flush: 该函数负责将发送缓冲区中的数据发送出去。在
ikcp_update
调用过程中,ikcp_flush
会被间接调用,以处理数据的发送。void ikcp_flush(ikcpcb *kcp);
这些函数共同构成了KCP协议的核心,通过它们可以实现基于UDP的可靠数据传输。
补充说明:
在 KCP 协议实现中,ikcp_send
和 ikcp_recv
函数是对外提供的接口,用于发送和接收数据。下面详细介绍这两个函数的作用及其背后的队列和缓冲区(buf)的必要性。
ikcp_send
ikcp_send
函数用于向对端发送数据。由于 KCP 是面向消息的协议,发送的数据会被分割成多个 KCP 分片(segments)。
这个函数的主要步骤包括:
- 分片:如果用户发送的数据大于最大分片大小(MSS),
ikcp_send
会将数据分片。 - 分配序列号(sn):每个分片都会被分配一个唯一的序列号,这用于在接收方重新组装分片和确保数据的顺序性。
- 添加到发送队列(snd_queue):准备好的分片首先被添加到发送队列中,等待发送。
发送队列 (snd_queue
) 是一个等待被发送到网络上的数据分片的集合。当 ikcp_flush
函数被周期性调用时,它会从 snd_queue
中移动数据分片到发送缓冲区 (snd_buf
)。
ikcp_recv
ikcp_recv
函数用于从 KCP 协议接收数据。这个函数从接收队列(rcv_queue
)中提取数据,并将其复制到用户提供的缓冲区中。
主要步骤包括:
- 检查队列:
ikcp_recv
检查接收队列以确定是否有可用的数据。 - 组装消息:如果有多个分片组成一条消息,
ikcp_recv
会将它们组装成一条完整的消息。 - 复制数据:将数据复制到用户的缓冲区中。
接收队列 (rcv_queue
) 是接收到的、已经通过 KCP 协议确认并准备交付给应用层的数据分片集合。而接收缓冲区 (rcv_buf
) 则用于存储接收到但尚未确认可交付的数据分片。
为什么需要队列和缓冲区
队列和缓冲区的使用是 KCP 实现中可靠传输机制的关键部分。它们的作用包括:
- 重传机制:在
snd_buf
中保存已发送的数据分片直到确认接收,允许超时重传。 - 流量控制:
snd_queue
和rcv_queue
用于控制数据的流入和流出,以避免发送方快于接收方处理能力。 - 有序交付:确保即使网络乱序,数据分片也能按正确的顺序交付给应用层。
- 窗口控制:发送和接收窗口控制 KCP 流量的大小,队列的使用可以根据窗口大小调整数据的发送和接收。
这些队列和缓冲区在内部为 KCP 协议处理提供了所需的数据结构,使 KCP 能够实现一个在 UDP 基础上的可靠、高性能的传输协议。
ikcp_update
、ikcp_input
和 ikcp_flush
是 KCP 协议中的关键函数,它们负责维护协议的内部状态,处理数据包的发送和接收。下面详细介绍这些函数:
ikcp_update
ikcp_update
函数是 KCP 协议的主心跳函数,它驱动协议内部状态的更新,包括数据包的发送和重传。这个函数应该被定期调用,通常是在应用程序的主循环中以固定的时间间隔(比如每10ms或20ms)调用。
主要功能:
- 超时重传:检查发送缓冲区中的数据包是否超时,如果超时则进行重传。
- 发送窗口控制:根据对端的接收能力和网络状况调整发送窗口大小。
- 探测远端窗口:如果远端窗口大小为零,发送窗口探测包,等待对端响应可用窗口大小。
ikcp_update
函数会计算下一次应该调用自身的时间,并在内部调用 ikcp_flush
函数来处理发送队列中的数据包。
ikcp_input
ikcp_input
函数用于处理从底层网络(如 UDP)接收到的所有 KCP 数据包。当应用程序从网络上接收到数据时,应该立即将这些数据传递给 ikcp_input
进行处理。
主要功能:
- 解析和确认:解析收到的数据包,并根据数据包类型处理,例如发送 ACK 确认响应。
- 数据重组:将接收到的数据片段组装成完整的消息,并按顺序放入接收队列。
- 窗口和状态更新:更新发送窗口和拥塞控制状态,以响应收到的 ACK 包。
ikcp_flush
ikcp_flush
函数负责将 KCP 协议的数据发送出去。它被 ikcp_update
函数调用,并且在必要时可以直接调用来尝试发送数据包。
主要功能:
- 发送 ACK 包:将 ACK 列表中的 ACK 响应发送给对端。
- 发送数据:处理发送缓冲区,发送待发送队列中的数据包。
- 窗口探测:如果启用了窗口探测,发送探测包以确定对端的窗口大小。
- 快速重传:检查每个数据包的 ACK 跳过次数,决定是否需要快速重传。
这三个函数协同工作,确保 KCP 协议能够提供可靠的数据传输,即使它是建立在不可靠的 UDP 协议之上。ikcp_update
提供了定时的状态更新,ikcp_input
处理进入的数据,ikcp_flush
负责数据的输出。为了最大限度地发挥 KCP 的性能,开发者必须确保这些函数被正确且及时地调用。
代码示例:
这里提供一个简化的示例,展示如何用C语言实现KCP的基本客户端和服务端通信。这个示例将演示最基本的KCP功能:创建KCP实例、配置、发送数据、接收数据。注意,为了保持示例的简洁性,这里不会包含错误处理和网络编程的详细内容(如UDP套接字的创建和绑定)。
先决条件
这个示例假设你已经有了UDP的基础知识,能够处理UDP套接字的基本操作,并且你已经将KCP库集成到你的项目中。
UDP发送和接收函数
首先,我们需要定义一个函数,用于通过UDP发送和接收数据。在实际项目中,你需要根据你的环境(如使用Linux的socket API或Windows的Winsock API)来实现这些基本的网络操作。
这里仅提供伪代码说明:
// 用于发送数据的回调函数,KCP将通过这个函数发送数据
int udp_output(const char *buf, int len, ikcpcb *kcp, void *user)
{
// 将数据发送到网络
// 使用UDP套接字发送数据,具体实现依赖于平台
sendto(udp_socket, buf, len, 0, (struct sockaddr *)&dest_addr, sizeof(dest_addr));
return 0; // 返回0表示成功
}
KCP服务端
以下是KCP服务端的基础框架:
#include "ikcp.h"
#include <stdio.h>
// 这里插入udp_output函数的实现
int main()
{
// 初始化UDP套接字,绑定到特定端口,等待客户端连接
// 这里插入UDP套接字初始化和绑定代码
ikcpcb* kcp_server = ikcp_create(0x11223344, (void*)0);
kcp_server->output = udp_output;
while (1) {
// 模拟从UDP接收数据
char buffer[1024];
int recv_len = recvfrom(udp_socket, buffer, sizeof(buffer), 0, NULL, NULL);
if (recv_len > 0) {
ikcp_input(kcp_server, buffer, recv_len);
}
// 检查是否有数据可以从KCP接收
while (1) {
int hr = ikcp_recv(kcp_server, buffer, sizeof(buffer));
if (hr > 0) {
// 处理接收到的数据
printf("Received: %s\n", buffer);
} else {
break; // 没有更多数据可以接收
}
}
ikcp_update(kcp_server, iclock());
usleep(10000); // 简单的循环延迟
}
ikcp_release(kcp_server);
return 0;
}
KCP客户端
以下是KCP客户端的基础框架:
#include "ikcp.h"
#include <stdio.h>
// 这里插入udp_output函数的实现
int main()
{
// 初始化UDP套接字,配置服务器地址
// 这里插入UDP套接字初始化代码
ikcpcb* kcp_client = ikcp_create(0x11223344, (void*)0);
kcp_client->output = udp_output;
const char *message = "Hello KCP!";
ikcp_send(kcp_client, message, strlen(message));
while (1) {
// 模拟从UDP接收数据
char buffer[1024];
int recv_len = recvfrom(udp_socket, buffer, sizeof(buffer), 0, NULL, NULL);
if (recv_len > 0) {
ikcp_input(kcp_client, buffer, recv_len);
}
ikcp_update(kcp_client, iclock());
usleep(10000); // 简单的循环延迟
}
ikcp_release(kcp_client);
return 0;
}
注意
- 这些示例代码需要你自己实现UDP发送和接收逻辑,这里的
udp_output
函数仅仅是一个占位符。 ikcp_create
函数的第一个参数是会话ID,客户端和服务器必须匹配。- 实际使用中,你需要根据具体的网络状况调整KCP的参数,比如调用
ikcp_nodelay
来设置无延迟模式等。 - 上述代码未包含完整的错误处理和资源清理逻辑,这在实际项目中是必需的。
这个示例仅旨在展示KCP如何在一个简单的客户端-服务器模型中工作。在实际项目中,还需要考虑网络条件、数据包的序列化和反序列化、安全性问题等多个方面。