KCP是国人开发的开源项目,作者:林伟 (skywind3000)(这个是真大牛)。
KCP是快速可靠传输协议,纯算法实现,KCP无任何系统调用,不负责底层协议收发,底层可以使用UDP或其他自定义协议进行收发。
开源地址:https://github.com/skywind3000/kcp
KCP关键技术
KCP通常使用UDP做为底层协议,主要对标TCP协议,github README有详细说明。
1、TCP协议是从大局考虑的,均衡速率和整个网络的拥塞,而KCP是自私的,只顾自己的传输效率,不去考虑整个网络的拥堵情况。
2、KCP使用RTO不翻倍、选择性重传、快速重传、非延迟ACK、非退让流控等技术,实现存在网络延时和丢包时传输效率对TCP的超越,以比 TCP 浪费 10%-20% 的带宽的代价,换取平均延迟降低 30%-40%。
3、KCP流控参数可配,以应对不同场景,TCP策略通常是不可配置的。
KCP使用场景
丢包率和延时高的网络环境才能体现出KCP相对TCP的优势,如果网络环境特别好(比如内网),那么TCP和KCP的传输效率相差不大。
KCP源码
纯C语言开发,只有ikcp.h和ikcp.c两个文件,加在一起1700行代码,很容易集成到现有项目。
git clone https://github.com/skywind3000/kcp.git
ikcpcb* ikcp_create(IUINT32 conv, void *user);
创建一个新的kcp对象,“conv”必须在来自同一连接的两个端点中相等。‘user’将被传递到发送数据回调,包括fd和远端地址等信息。
int ikcp_input(ikcpcb *kcp, const char *data, long size);
系统调用接收到数据后,将裸数据交给KCP,这些数据有可能是KCP控制报文。
KCP报文分为ACK报文、数据报文、探测窗口报文、响应窗口报文四种。
int ikcp_recv(ikcpcb *kcp, char *buffer, int len);
ikcp_recv需要通过轮询的方式去调用,如果有数据,将返回完整的消息,如果没有就返回错误。buffer和len由调用者预先分配。
int ikcp_send(ikcpcb *kcp, const char *buffer, int len);
把数据加入发送队列,使用设置的发送回调进行发送处理。
流模式情况下,检测每个发送队列里的分片是否达到最大MSS,如果没有达到就会用新的数据填充分片。接收端会把多片发送的数据重组为一个完整的KCP包。
消息模式下,将用户数据分片,为每个分片设置sn和frag,将分片后的数据一个一个地存入发送队列,接收方通过sn和frag解析原来的包,消息方式一个分片的数据量可能不能达到MSS,也会作为一个包发送出去
void ikcp_update(ikcpcb *kcp, IUINT32 current);
调用ikcp_flush,检测发送队列是否有数据要进行发送,探测远端窗口,检测重传等。
基本思路:
底层以udp为例,绑定本地端口用于接收数据,指定远端IP和端口用于数据发送。
数据接收:
1、使用系统调用比如 recvfrom 进行接收数据,收到数据后使用 ikcp_input 接口把裸数据交给KCP处理;
2、轮询调用 ikcp_recv 接口,返回值大于0则是有我们需要的应用数据。
数据发送:
1、自定义数据发送接口,把接口函数指针赋值给回调函数ikcpcb->output,同时传递udp句柄;
2、应用层调用 ikcp_send 接口进行数据发送,实际上是把数据加入发送队列;
3、轮询调用 ikcp_update 接口,检测发送队列,有数据要发送时调用 ikcpcb->output 回调函数进行发送。
目录结构
├── ikcp.c
├── ikcp.h
├── kcp_client.cpp
├── kcp_client.h
├── kcp_inc.cpp
└── kcp_inc.h
ikcp.h和ikcp.c在github开源地址下载。
kcp_client.h
#ifndef KCP_CLIENT_H
#define KCP_CLIENT_H
#include "ikcp.h"
#include "kcp_inc.h"
class kcp_client
{
public:
kcp_client(char *serIp, uint16_t serPort, uint16_t localPort);
int sendData(const char *buffer, int len);
bool m_isLoop;
//private:
UdpDef *pUdpDef;
ikcpcb *pkcp;
};
#endif // KCP_CLIENT_H
kcp_client.cpp
#include "kcp_client.h"
/**
* @brief run_udp_thread udp线程处理函数
* @param obj UdpHandle
* @return
*/
void* run_udp_thread(void *obj)
{
kcp_client *client = (kcp_client*) obj;
char buffer[2048] = { 0 };
int32_t len = 0;
socklen_t src_len = sizeof(struct sockaddr_in);
while (client->m_isLoop)
{
///5.核心模块,循环调用,处理数据发送/重发等
ikcp_update(client->pkcp,iclock());
struct sockaddr_in src;
memset(&src, 0, src_len);
///6.udp接收到数据
if ((len = recvfrom(client->pUdpDef->fd, buffer, 2048, 0, (struct sockaddr*) &src, &src_len)) > 0)
{
// printf("rcv=%s,len=%d\n\n",buffer,len);//可能时kcp控制报文
///7.预接收数据:调用ikcp_input将裸数据交给KCP,这些数据有可能是KCP控制报文
int ret = ikcp_input(client->pkcp, buffer, len);
if(ret < 0)//检测ikcp_input是否提取到真正的数据
{
continue;
}
///8.kcp将接收到的kcp数据包还原成应用数据
char rcv_buf[2048] = { 0 };
ret = ikcp_recv(client->pkcp, rcv_buf, len);
if(ret >= 0)//检测ikcp_recv提取到的数据
{
printf("ikcp_recv ret = %d,buf=%s\n",ret,rcv_buf);
//9.测试用,自动回复一条消息
if(strcmp(rcv_buf,"hello") == 0)
{
std::string msg = "hello back.";
client->sendData(msg.c_str(),msg.size());
}
}
}
isleep(1);
}
close(client->pUdpDef->fd);
return NULL;
}
kcp_client::kcp_client(char *serIp, uint16_t serPort, uint16_t localPort)
{
pUdpDef = new UdpDef;
///1.创建udp,指定远端IP和PORT,以及本地绑定PORT
uint32_t remoteIp = inet_addr(serIp);
CreateUdp(pUdpDef,remoteIp,serPort,localPort);
///2.创建kcp实例,两端第一个参数conv要相同
pkcp = ikcp_create(0x1, (void *)pUdpDef);
///3.kcp参数设置
pkcp->output = udp_sendData_loop;//设置udp发送接口
// 配置窗口大小:平均延迟200ms,每20ms发送一个包,
// 而考虑到丢包重发,设置最大收发窗口为128
ikcp_wndsize(pkcp, 128, 128);
// 判断测试用例的模式
int mode = 0;
if (mode == 0) {
// 默认模式
ikcp_nodelay(pkcp, 0, 10, 0, 0);
}
else if (mode == 1) {
// 普通模式,关闭流控等
ikcp_nodelay(pkcp, 0, 10, 0, 1);
} else {
// 启动快速模式
// 第二个参数 nodelay-启用以后若干常规加速将启动
// 第三个参数 interval为内部处理时钟,默认设置为 10ms
// 第四个参数 resend为快速重传指标,设置为2
// 第五个参数 为是否禁用常规流控,这里禁止
ikcp_nodelay(pkcp, 2, 10, 2, 1);
pkcp->rx_minrto = 10;
pkcp->fastresend = 1;
}
///4.启动线程,处理udp收发
m_isLoop = true;
pthread_t tid;
pthread_create(&tid,NULL,run_udp_thread,this);
}
int kcp_client::sendData(const char *buffer, int len)
{
//这里只是把数据加入到发送队列
int ret = ikcp_send(pkcp,buffer,len);
return ret;
}
kcp_inc.h
#ifndef KCP_INC_H
#define KCP_INC_H
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include // for open
#include "ikcp.h"
#if defined(WIN32) || defined(_WIN32) || defined(WIN64) || defined(_WIN64)
#include
#elif !defined(__unix)
#define __unix
#endif
#ifdef __unix
#include
#include
#include
#include
#endif
//udp客户端
typedef struct _UdpDef_{
int32_t fd; //fd
struct sockaddr_in local_addr; //本端地址
struct sockaddr_in remote_addr; //对端地址
}UdpDef;
//创建udp套接字
int32_t CreateUdp(UdpDef *udp, uint32_t remoteIp, int32_t remotePort, int32_t plocalPort);
#define UDP_MTU 1460
int udp_sendData_loop(const char *buffer, int len, ikcpcb *kcp, void *user);
/* get system time */
static inline void itimeofday(long *sec, long *usec)
{
#if defined(__unix)
struct timeval time;
gettimeofday(&time, NULL);
if (sec) *sec = time.tv_sec;
if (usec) *usec = time.tv_usec;
#else
static long mode = 0, addsec = 0;
BOOL retval;
static IINT64 freq = 1;
IINT64 qpc;
if (mode == 0) {
retval = QueryPerformanceFrequency((LARGE_INTEGER*)&freq);
freq = (freq == 0)? 1 : freq;
retval = QueryPerformanceCounter((LARGE_INTEGER*)&qpc);
addsec = (long)time(NULL);
addsec = addsec - (long)((qpc / freq) & 0x7fffffff);
mode = 1;
}
retval = QueryPerformanceCounter((LARGE_INTEGER*)&qpc);
retval = retval * 2;
if (sec) *sec = (long)(qpc / freq) + addsec;
if (usec) *usec = (long)((qpc % freq) * 1000000 / freq);
#endif
}
/* get clock in millisecond 64 */
static inline IINT64 iclock64(void)
{
long s, u;
IINT64 value;
itimeofday(&s, &u);
value = ((IINT64)s) * 1000 + (u / 1000);
return value;
}
static inline IUINT32 iclock()
{
return (IUINT32)(iclock64() & 0xfffffffful);
}
/* sleep in millisecond */
static inline void isleep(unsigned long millisecond)
{
#ifdef __unix /* usleep( time * 1000 ); */
struct timespec ts;
ts.tv_sec = (time_t)(millisecond / 1000);
ts.tv_nsec = (long)((millisecond % 1000) * 1000000);
/*nanosleep(&ts, NULL);*/
usleep((millisecond << 10) - (millisecond << 4) - (millisecond << 3));
#elif defined(_WIN32)
Sleep(millisecond);
#endif
}
#include
#include
#endif // KCP_INC_H
kcp_inc.cpp
#include "kcp_inc.h"
#define UDP_MTU 1460
int udp_sendData_loop(const char *buffer, int len, ikcpcb *kcp, void *user)
{
UdpDef *pUdpClientDef = (UdpDef *)user;
int sended = 0;
while(sended < len){
size_t s = (len - sended);
if(s > UDP_MTU) s = UDP_MTU;
ssize_t ret = ::sendto(pUdpClientDef->fd, buffer + sended, s, MSG_DONTWAIT, (struct sockaddr*) &pUdpClientDef->remote_addr,sizeof(struct sockaddr));
if(ret < 0){
return -1;
}
sended += s;
}
return (size_t) sended;
}
int32_t CreateUdp(UdpDef *udp, uint32_t remoteIp, int32_t remotePort, int32_t plocalPort)
{
if (udp == NULL) return -1;
udp->fd = -1;
udp->fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
fcntl(udp->fd, F_SETFL, O_NONBLOCK);//设置非阻塞
if(udp->fd < 0)
{
printf("[CreateUdpClient] create udp socket failed,errno=[%d],remoteIp=[%u],remotePort=[%d]",errno,remoteIp,remotePort);
return -1;
}
udp->remote_addr.sin_family = AF_INET;
udp->remote_addr.sin_port = htons(remotePort);
udp->remote_addr.sin_addr.s_addr = remoteIp;
udp->local_addr.sin_family = AF_INET;
udp->local_addr.sin_port = htons(plocalPort);
udp->local_addr.sin_addr.s_addr = INADDR_ANY;
//2.socket参数设置
int opt = 1;
setsockopt(udp->fd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));//chw
fcntl(udp->fd, F_SETFL, O_NONBLOCK);//设置非阻塞
if (bind(udp->fd, (struct sockaddr*) &udp->local_addr,sizeof(struct sockaddr_in)) < 0)
{
close(udp->fd);
printf("[CreateUdpServer] Udp server bind failed,errno=[%d],plocalPort=[%d]",errno,plocalPort);
return -2;
}
}
测试方法
创建两个客户端相互发消息。
kcp_client *pkcp_client = new kcp_client((char*)"127.0.0.1",9001,9002);
std::string msg = "hello";
pkcp_client->sendData(msg.c_str(),msg.size());