KCP解析及应用方法

  • Post author:
  • Post category:其他




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课程感兴趣的读者,可以去零声官网查看详细的服务:



版权声明:本文为m0_46833172原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。