KCP解析及应用方法
kcp是什么
初识KCP
KCP
(KuaiCong Protocol)是一种快速可靠的协议,它是在用户空间实现的协议。
目前并没有对应的 RFC 文档,因为它不是由标准化组织制定的标准协议,而是由作者极致优化后的 UDP 协议。
其旨在提供一个简单、轻量级、高效的连接建立和数据传输方案。KCP 协议通过在发送和接收端实现自己的
数据包重传
、
拥塞控制
、
流量控制
和
窗口管理算法
等机制,来保证数据的可靠传输,并获取更好的传输性能。
实际上KCP能以比 TCP 浪费 10%-20% 带宽的代价,换取平均延迟降低 30%-40%,最大延迟降低 3 倍的传输速度。
kcp官方: https://github.com/skywind3000/kcp
kcp商业案例
- 原神:米哈游的《原神》使用 KCP 降低游戏消息的传输耗时,提升操作的体验。
- SpatialOS: 大型多人分布式游戏服务端引擎,BigWorld 的后继者,使用 KCP 加速数据传输。
- 西山居:使用 KCP 进行游戏数据加速。
- CC:网易 CC 使用 kcp 加速视频推流,有效提高流畅性
- BOBO:网易 BOBO 使用 kcp 加速主播推流
- UU:网易 UU 加速器使用 KCP/KCPTUN 经行远程传输加速。
- 阿里云:阿里云的视频传输加速服务 GRTN 使用 KCP 进行音视频数据传输优化,动态加速产品也使用 KCP。
- 云帆加速:使用 KCP 加速文件传输和视频推流,优化了台湾主播推流的流畅度。
- 明日帝国:Game K17 的 《明日帝国》 (Google Play),使用 KCP 加速游戏消息,让全球玩家流畅联网仙灵大作战:4399 的 MOBA游戏,使用 KCP 优化游戏同步
kcp格式详解
- [0,3]conv:连接号。UDP是无连接的,conv用于表示来自于哪个客户端。对连接的一种替代
-
[4]cmd:命令字。如,
IKCP CMD ACK确认命令
IKCP_CMD_WASK接收窗口大小询问命令
IKCP CMD WINS接收窗口大小告知命令 - [5]frg:分片,用户数据可能会被分成多个KCP包,发送出去
- [6,7]wnd:接收窗口大小,发送方的发送窗口不能超过接收方给出的数值
- [8,11]ts:时间序列
- [12,15]sn:序列号
- [16,19]una:下一个可接收的序列号。其实就是确认号,收到sn=10的包,una为11
-
[20,23]len: 数据长度
data:用户数据,这一次发送的数据长度
kcp为啥那么快
KCP相较TCP而言有如下优势
TCP | KCP |
---|---|
TCP超时计算是RTOx2,这样连续丢三次包就变成RTOx8了,⼗分恐怖 | KCP启动快速模式后不x2,只是x1.5(实验证明1.5这个值相对⽐较好),提⾼了传输速度。 |
TCP丢包时会全部重传从丢的那个包开始以后的数据 | KCP是选择性重传,只重传真正丢失的数据包。 |
TCP为了充分利⽤带宽,延迟发送ACK(NODELAY都没⽤),这样超时计算会算出较⼤ RTT时间,延⻓了丢包时的判断过程。 | KCP的ACK是否延迟发送可以调节。 |
使⽤公平退让法则,即发送窗⼝⼤⼩由:发送缓存⼤⼩、接收端剩余接收缓存⼤⼩、丢包退让及慢启动这四要素决定。 | KCP正常模式同TCP⼀样,但传送及时性要求很⾼的⼩数据时,可选择通过配置跳过后两步,仅⽤前两项来控制发送频率。以牺牲部分公平性及带宽利⽤率之代价,换取了开着BT都能流畅传输的效果。 |
UNA(此编号前所有包已收到) | UNA + ACK(该编号包已收到),除去单独的 ACK包外,所有包都有UNA信息。 |
注:光⽤UNA将导致
全部重传
,光⽤ACK则
丢失成本太⾼
如何使用kcp
如果仅仅是想利用kcp进行数据传输,那么以上的原理其实没有必要细究,知道技术优势并会选型即可。
kcp源码流程图
使用kcp编程
KCP 整个协议的实现只有 ikcp.h 和 ikcp.c 两个源文件,可以方便的集成到用户自己的协议栈中。
创建 KCP对象:
// 初始化 kcp对象,conv为一个表示会话编号的整数,和tcp的 conv一样,通信双
// 方需保证 conv相同,相互的数据包才能够被认可,user是一个给回调函数的指针
ikcpcb *kcp = ikcp_create(conv, user);
设置回调函数:
// KCP的下层协议输出函数,KCP需要发送数据时会调用它
// buf/len 表示缓存和长度
// user指针为 kcp对象创建时传入的值,用于区别多个 KCP对象
int udp_output(const char *buf, int len, ikcpcb *kcp, void *user)
{
....
}
// 设置回调函数
kcp->output = udp_output;
循环调用 update:
// 以一定频率调用 ikcp_update来更新 kcp状态,并且传入当前时钟(毫秒单位)
// 如 10ms调用一次,或用 ikcp_check确定下次调用 update的时间不必每次调用
ikcp_update(kcp, millisec);
输入一个下层数据包:
// 收到一个下层数据包(比如UDP包)时需要调用:
ikcp_input(kcp, received_udp_packet, received_udp_size);
//处理了下层协议的输出/输入后 KCP协议就可以正常工作了,
//使用 ikcp_send 来向 远端发送数据。而另一端使用 ikcp_recv(kcp, ptr, size)来接收数据。
demo
client
#include "ikcp.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 8888
int make_socket_non_blocking(int sockfd)
{
int flags = fcntl(sockfd, F_GETFL, 0);
if (flags == -1) {
return -1;
}
flags |= O_NONBLOCK;
if (fcntl(sockfd, F_SETFL, flags) == -1) {
return -1;
}
return 0;
}
int main()
{
struct sockaddr_in server_addr;
int sockfd, len, maxfd, nready;
uint32_t conv = 12345; // 这里需要和服务端保持一致
fd_set rset, allset;
ikcpcb *kcp;
// 创建 UDP 套接字
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("socket error");
exit(EXIT_FAILURE);
}
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
server_addr.sin_port = htons(SERVER_PORT);
if (make_socket_non_blocking(sockfd) == -1) {
perror("make_socket_non_blocking error");
exit(EXIT_FAILURE);
}
// 初始化 KCP 协议控制块
kcp = ikcp_create(conv, NULL);
// 将 KCP 协议控制块设为快速模式,以提高传输速度
ikcp_nodelay(kcp, 1, 10, 2, 1);
// 定义用于监听的文件描述符集合
FD_ZERO(&allset);
FD_SET(sockfd, &allset);
maxfd = sockfd;
char buf[512] = {0};
char msg[512] = {0};
int n;
while (1) {
// 从标准输入读取数据并发送给服务端
fgets(msg, sizeof(msg), stdin);
if (strlen(msg) <= 1) {
continue;
}
ikcp_send(kcp, msg, strlen(msg));
// 接收服务端的数据并输出
while (1) {
rset = allset;
nready = select(maxfd + 1, &rset, NULL, NULL, NULL);
if (nready < 0) {
if (errno == EINTR) {
continue;
} else {
perror("select error");
exit(EXIT_FAILURE);
}
}
if (FD_ISSET(sockfd, &rset)) {
len = recvfrom(sockfd, buf, sizeof(buf), 0, NULL, NULL);
if (len < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
break;
} else {
perror("recvfrom error");
exit(EXIT_FAILURE);
}
} else if (len == 0) {
continue;
} else {
printf("received data from server: %s\n", buf);
}
}
}
}
ikcp_release(kcp);
close(sockfd);
return 0;
}
server
#include "ikcp.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 8888
int make_socket_non_blocking(int sockfd)
{
int flags = fcntl(sockfd, F_GETFL, 0);
if (flags == -1) {
return -1;
}
flags |= O_NONBLOCK;
if (fcntl(sockfd, F_SETFL, flags) == -1) {
return -1;
}
return 0;
}
int main()
{
struct sockaddr_in server_addr, client_addr;
int sockfd, len, client_len, maxfd, nready;
uint32_t conv = 12345; // 这里可以设置协议的会话编号
fd_set rset, allset;
ikcpcb *kcp;
// 创建 UDP 套接字并绑定到指定端口
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("socket error");
exit(EXIT_FAILURE);
}
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(SERVER_PORT);
if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("bind error");
exit(EXIT_FAILURE);
}
if (make_socket_non_blocking(sockfd) == -1) {
perror("make_socket_non_blocking error");
exit(EXIT_FAILURE);
}
// 初始化 KCP 协议控制块
kcp = ikcp_create(conv, NULL);
// 将 KCP 协议控制块设为快速模式,以提高传输速度
ikcp_nodelay(kcp, 1, 10, 2, 1);
// 定义用于监听的文件描述符集合
FD_ZERO(&allset);
FD_SET(sockfd, &allset);
maxfd = sockfd;
char buf[512] = {0};
int n;
// 接收客户端的数据包并发送 ACK 确认包
while (1) {
rset = allset;
nready = select(maxfd + 1, &rset, NULL, NULL, NULL);
if (nready < 0) {
if (errno == EINTR) {
continue;
} else {
perror("select error");
exit(EXIT_FAILURE);
}
}
if (FD_ISSET(sockfd, &rset)) {
while (1) {
client_len = sizeof(client_addr);
len = recvfrom(sockfd, buf, sizeof(buf), 0, (struct sockaddr*)&client_addr, &client_len);
if (len < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
break;
} else {
perror("recvfrom error");
exit(EXIT_FAILURE);
}
} else if (len == 0) {
continue;
} else {
ikcp_input(kcp, buf, len);
// 发送 ACK 确认包
while (1) {
n = ikcp_recv(kcp, buf, sizeof(buf));
if (n < 0) {
break;
} else {
printf("received data from client: %s\n", buf);
sendto(sockfd, buf, n, 0, (struct sockaddr*)&client_addr, client_len);
}
}
}
}
}
}
ikcp_release(kcp);
close(sockfd);
return 0;
}
实际操作中,可能存在由于网络问题发送回调函数不会被正常触发,需要进一步定位问题。
本专栏知识点是通过<零声教育>的系统学习,进行梳理总结写下文章,对c/c++linux课程感兴趣的读者,可以去零声官网查看详细的服务: