基于UDP的可靠性传输协议-KCP简介
RTO翻倍vs不翻倍:
TCP超时计算是RTOx2,这样连续丢三次包就变成RTOx8了,十分恐怖,而KCP启动快速模式后不x2,只是x1 .5(实验证明1 .5这个值相对⽐较好),提高了传输速度。
if (kcp->nodelay == 0) {segment->rto += _imax_(segment->rto, (IUINT32)kcp->rx_rto);
} else {IINT32 step = (kcp->nodelay < 2)? ((IINT32)(segment->rto)) : kcp->rx_rto;segment->rto += step / 2;
}
选择性重传 vs 全部重传:
TCP丢包时会全部重传从丢的那个包开始以后的数据, KCP是选择性重传,只重传真正丢失的数据包。
快速重传:
设置快速重传次数 fastresend 为2。发送端发送了 1 ,2,3,4,5几个包,然后收到远端的ACK: 1 , 3, 4, 5,当收到ACK3时, KCP知道2被跳过1次,收到ACK4时,知道数据包2被跳过了 2次,此时可以认为 2号丢失,不用等超时,直接重传2号包,大大改善了丢包时的传输速度。
else if (segment->fastack >= resent) { //3 segment的累计被跳过次数大于快速重传设定,需要重传 if ((int)segment->xmit <= kcp->fastlimit || kcp->fastlimit <= 0) {needsend = 1;segment->xmit++;segment->fastack = 0; // 收到ack时计算该分片被跳过的累计次数,此字段用于快速重传,自定义需要几次确认开始快速重传segment->resendts = current + segment->rto; // 充值重传时间change++;}
}if (needsend) {int need;segment->ts = current;segment->wnd = seg.wnd; // 剩余接收窗口大小(接收窗口大小-接收队列大小), 告诉对方目前自己的接收能力segment->una = kcp->rcv_nxt; // 待接收的下一个包序号, 即是告诉对方una之前的包都收到了, 你不用再发送发送缓存了size = (int)(ptr - buffer);need = IKCP_OVERHEAD + segment->len;if (size + need > (int)kcp->mtu) { // 小包封装成大包取发送 500 500 , 按1000发ikcp_output(kcp, buffer, size);ptr = buffer;}......
}
延迟ACK vs 非延迟ACK:
TCP为了充分利用带宽,延迟发送ACK(NODELAY都没用),这样超时计算会算出较大RTT时间,延长了丢包时的判断过程。 KCP的ACK是否延迟发送可以调节。
UNA vs ACK+UNA:
ARQ模型响应有两种, UNA(此编号前所有包已收到,如TCP)和ACK(该编号包已收到),光用 UNA将导致全部重传,光用ACK则丢失成本太高,以往协议都是二选其一,而KCP协议中,除去单独的 ACK包外,所有包都有UNA信息。
struct IKCPSEG
{struct IQUEUEHEAD node;IUINT32 conv; // 会话编号,和TCP的con一样,确保双方需保证conv相同,相互的数据包才能被接收.conv唯一标识一个会话IUINT32 cmd; // 区分不同的分片.IKCP_CMD_PUSH数据分片;IKCP_CMD_ACK:ack分片;IKCP_CMD_WASK:请求告知窗口大小;IKCP_CMD_WINS:告知窗口大小IUINT32 frg; // 标识segment分片ID,用户数据可能被分成多个kcp包发送 IUINT32 wnd; // 剩余接收窗口大小(接收窗口大小-接收队列大小),发送方的发送窗口不能超过接收方给出的数值IUINT32 ts; // 发送时刻的时间戳IUINT32 sn; // 分片segment的序号,按1累加递增IUINT32 una; // 待接收消息序号(接收滑动窗口左侧).对于未丢包的网络来说,una是下一个可接收的序号,如收到sn=10的包,una为11IUINT32 len; // 数据长度IUINT32 resendts; // 下次超时重传时间戳IUINT32 rto; //该分片的超时等待时间,其计算方法同TCPIUINT32 fastack; // 收到ack时计算该分片被跳过的累计次数,此字段用于快速重传,自定义需要几次确认开始快速重传IUINT32 xmit; // 发送分片的次数,每发一次加1.发送的次数对RTO的计算有影响,但是比TCP来说,影响会小一些.char data[1];
};
非退让流控:
KCP正常模式同TCP一样使用公平退让法则,即发送窗口大小由:发送缓存大小、接收端剩余接收缓存大小、丢包退让及慢启动这四要素决定。但传送及时性要求很高的小数据时,可选择通过配置跳过后两步,仅用前两项来控制发送频率。以牺牲部分公平性及带宽利⽤率之代价,换取了开着BT都能流畅传输的效
果。
KCP协议在网络分层模型的位置
KCP的设计者有意识的把KCP依赖的网络通讯给解耦了。KCP是纯算法实现,并不负责底层协议(如 UDP)的收发,需要使用者自己定义下层数据包的发送方式,以 callback的形式提供给 KCP。
KCP特征总结:
1、非延迟ACK
2、快速重传(TCP协议也有)
3、非退让流控(拥塞控制,和TCP实现类同 )
4、FEC(Forward Error Correction)前向纠错
KCP数据包如下:
conv :连接号。 UDP是非连接的, conv用于表示来自于哪个客户端。对连接的一种替代, 因为有 conv , 所
以KCP也是支持多路复用的。
cmd :命令类型,只有四种
frg :分片,用户数据可能会被分成多个KCP包,发送出去
在 xtac i / kcp- go 的实现中,这个字段始终为 0,以及没有意义了 , 详情issues/1 21
wnd :接收窗口大小,发送方的发送窗⼝不能超过接收⽅给出的数值, (其实是接收窗⼝的剩余大小,这个
大小是动态变化的)
ts : 时间序列
sn : 序列号
una :下一个可接收的序列号。其实就是确认号,收到sn=1 0的包, una为 11
len :数据长度(DATA的⻓度)
data :用户数据
CMD的四种类型
其中,IKCP_CMD_PUSH 和 IKCP_CMD_ACK 关联,IKCP_CMD_WASK 和 IKCP_CMD_WINS 关联
kCP协议提供了⼀种能⼒把不同的 消息 (应用程序)划分在不同的KCP包中。KCP定义 MSS 的默认大小为1400 bytes, MSS (maximum segment size)表示最大段大小,它本身是
TCP中的概念,表示包含TCP header,整个数据包的最大大小。在KCP协议中,概念类似,表示包含
KCP header在内,整个KCP包的最大大小。
超过 MSS 的数据将会被拆分到成多个KCP包。根据是否拆包将会分成2种情况。
1)不拆包
3条消息 Msg1 , Msg2 , Msg3 分别包含在 sn 为 90、91、92的KCP包中
- 拆包
假定这个消息是个图片消息,比较大,大小为3252 bytes
Msg被拆成了3部分,包含在3个KCP包中。注意, frg 的序号是从大到小的,一直到0为止。这样接收端
收到KCP包时,只有拿到 frg 为0的包,才会进行组装并交付给上层应用程序。由于 frg 在header中占1
个字节,也就是最大能支持(1400 – 24) * 256 / 1024 = 344kB的消息
总结:在消息模式下,每个KCP包最多包含一个上层应用的消息。
- 流模式
消息模式减少了上层应⽤从流中拆解出消息的麻烦,但是它对⽹络的利⽤率较低。Payload(有效载荷)少,
KCP头占用过大。
在流模式下,KCP试图让每个KCP包尽可能装满。一个KCP包中可能包含多个 消息 。在上图中,Msg1 、 Msg2 、 Msg3 的一部分被包含在 sn 为234的KCP包中。 上层应用需要自己来判断每个消息的边界。
在实际实现中每⼀个需要保护的点,都有与之对应的参数,先上结论
(1)使用发送端的发送窗⼝( snd_wnd )保护本机的发送缓冲区
(2)使用拥塞窗⼝( cwnd )来保护发送端与接收端之间的链路
(3)使用接收端的接收窗⼝( rmt_wnd , 表示接收窗⼝的空闲大小)保护接收端的接收缓冲区。rmt_wnd 对应KCP协议的 wnd , 由接收端汇报
wireshark抓包KCP的插件:
https://download.csdn.net/download/qq_23350817/86506443
打开wireshark安装目录,将kcp_dissector.lua文件放到wireshark安装目录下
修改init.lua文件,末尾增加dofile(DATA_DIR.."kcp_dissector.lua")--add this line
然后将KCP的客户端和服务器的端口配置成8081。
慢启动、拥塞避免、拥塞发生、快速重传的相关代码!!!!!!!
Client代码
#include "ikcp.h"
#include <iostream>
#include <chrono>
#include <thread>
#include <atomic>
#include <WinSock2.h>#pragma comment(lib,"ws2_32.lib")#define RECV_BUF 1500
#define DELAY_TEST2_N 100
#define DELAY_BODY_SIZE 1300
#define UDP_RECV_BUF_SIZE 1500std::atomic_char32_t number;int recv_objs = 0;SOCKET socketfd;struct sockaddr_in clientAddr; //存放客户机信息的结构体int udp_output(const char *buf, int len, ikcpcb *kcp, void *user)
{int n = sendto(socketfd, buf, len, 0, (struct sockaddr *)&clientAddr, sizeof(struct sockaddr_in));if (n >= 0){//会重复发送,因此牺牲带宽printf("send: %d bytes\n", n); //24字节的KCP头部return n;}else{printf("error: %d bytes send, error\n", n);return -1;}return 0;
}int main()
{int port = 8081;WSADATA wsaData;if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0){printf("Failed to load Winsock.\n"); //Winsock 初始化错误return -1;}socketfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); //创建UDP套接字if (socketfd == INVALID_SOCKET){printf("socket() Failed: %d\n", WSAGetLastError());return -1;}clientAddr.sin_family = AF_INET; //初始化服务器地址信息clientAddr.sin_port = htons(port); //端口转换为网络字节序clientAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //IP 地址转换为网络字节序printf("udp init ok\n");ikcpcb *pkcp = ikcp_create(0x1, NULL); //创建kcp对象把send传给kcp的user变量ikcp_setmtu(pkcp, 1400);pkcp->output = udp_output; //设置kcp对象的回调函数ikcp_nodelay(pkcp, 0, 10, 0, 0);//(kcp1, 0, 10, 0, 0); 1, 10, 2, 1ikcp_wndsize(pkcp, 128, 128);ikcp_setmtu(pkcp, 1400);/int len = sizeof(struct sockaddr_in);int n, ret;//接收到第一个包就开始循环处理int recv_count = 0;std::this_thread::sleep_for(std::chrono::milliseconds(1));ikcp_update(pkcp, GetTickCount64());int i = 0;while (1){Sleep(1000);uint16_t seqno = i++;int64_t send_time = GetTickCount64();uint8_t body[DELAY_BODY_SIZE] = "Hello World!";ret = ikcp_send(pkcp, (char *)&body, sizeof(body));if (ret < 0){printf("send %d seqno:%u failed, ret:%d\n", i, seqno, ret);return -1;}ikcp_update(pkcp, GetTickCount64());//不是调用一次两次就起作用,要loop调用char recvBuf[DELAY_BODY_SIZE] = { 0 };int n = recvfrom(socketfd, recvBuf, UDP_RECV_BUF_SIZE, 0, (struct sockaddr *) &clientAddr, &len);if (n < 0) {//检测是否有UDP数据包 // isleep(1);continue;}ret = ikcp_input(pkcp, recvBuf, n); // 从 linux api recvfrom先扔到kcp引擎if (ret < 0)//检测ikcp_input是否提取到真正的数据{//printf("ikcp_input ret = %d\n",ret);continue; // 没有读取到数据}ret = ikcp_recv(pkcp, (char *)&recvBuf, sizeof(recvBuf)); //从 buf中 提取真正数据,返回提取到的数据大小if (ret < 0){ // 没有检测ikcp_recv提取到的数据//isleep(1);printf("ikcp_recv1 ret = %d\n", ret);continue;}std::cout << "recv buf:" << recvBuf << std::endl;}closesocket(socketfd);ikcp_release(pkcp);getchar();return 0;
}
server代码:
#include "ikcp.h"
#include <iostream>
#include <chrono>
#include <thread>
#include <atomic>
#include <WinSock2.h>#pragma comment(lib,"ws2_32.lib")#define RECV_BUF 1500std::atomic_char32_t number;UINT64 first_recv_time = 0;int clientfd;struct sockaddr_in CientAddr; //存放客户机信息的结构体int udp_output(const char *buf, int len, ikcpcb *kcp, void *user)
{int n = sendto(clientfd, buf, len, 0, (struct sockaddr *)&CientAddr, sizeof(struct sockaddr_in));if (n >= 0){//会重复发送,因此牺牲带宽printf("send: %d bytes, t:%lld\n", n, GetTickCount64() - first_recv_time); //24字节的KCP头部return n;}else{printf("error: %d bytes send, error\n", n);return -1;}return 0;
}int main()
{/char buff[RECV_BUF] = { 0 };char Msg[] = "Server:Hello!"; //与客户机后续交互memcpy(buff, Msg, sizeof(Msg));ikcpcb *pkcp = ikcp_create(0x1, (void *)&send); //创建kcp对象把send传给kcp的user变量ikcp_setmtu(pkcp, 1400);pkcp->output = udp_output; //设置kcp对象的回调函数ikcp_nodelay(pkcp, 1, 10, 0, 0); //1, 10, 2, 1ikcp_wndsize(pkcp, 128, 128);///int port = 8081;struct sockaddr_in server;WSADATA wsaData;if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0){printf("Failed to load Winsock.\n"); //Winsock 初始化错误return -1;}server.sin_family = AF_INET; //初始化服务器地址信息server.sin_port = htons(port); //端口转换为网络字节序server.sin_addr.s_addr = inet_addr("127.0.0.1"); //IP 地址转换为网络字节序clientfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); //创建UDP套接字if (clientfd == INVALID_SOCKET){printf("socket() Failed: %d\n", WSAGetLastError());return -1;}if (!bind(clientfd, (LPSOCKADDR)&server, sizeof(server)) == SOCKET_ERROR){printf("绑定IP和端口\n");return 0;}printf("udp init ok\n");/int len = sizeof(struct sockaddr_in);int n, ret;//接收到第一个包就开始循环处理int recv_count = 0;//isleep(1);std::this_thread::sleep_for(std::chrono::milliseconds(1));ikcp_update(pkcp, GetTickCount64());char buf[RECV_BUF] = { 0 };while (1){//isleep(1);std::this_thread::sleep_for(std::chrono::milliseconds(1));ikcp_update(pkcp, GetTickCount64());//处理收消息n = recvfrom(clientfd, buf, RECV_BUF, 0, (struct sockaddr *)&CientAddr, &len);if (n > 0){printf("UDP recv[%d] size= %d \n", recv_count++, n);//预接收数据:调用ikcp_input将裸数据交给KCP,这些数据有可能是KCP控制报文,并不是我们要的数据。//kcp接收到下层协议UDP传进来的数据底层数据buffer转换成kcp的数据包格式ret = ikcp_input(pkcp, buf, n);if (ret < 0){continue;}//kcp将接收到的kcp数据包还原成之前kcp发送的buffer数据ret = ikcp_recv(pkcp, buf, n); //从 buf中 提取真正数据,返回提取到的数据大小if (ret < 0){ // 没有检测ikcp_recv提取到的数据//isleep(1);std::this_thread::sleep_for(std::chrono::milliseconds(1));std::cout << "ikcp_recv failed" << std::endl;continue;}int send_size = ret;//ikcp_send只是把数据存入发送队列,没有对数据加封kcp头部数据//应该是在kcp_update里面加封kcp头部数据//ikcp_send把要发送的buffer分片成KCP的数据包格式,插入待发送队列中。ret = ikcp_send(pkcp, buf, send_size);printf("Server reply -> bytes[%d], ret = %d, buf:%s\n", send_size, ret, buf);ikcp_flush(pkcp); // 快速flush一次 以更快让客户端收到数据number++;}else if (n == 0){printf("finish loop\n");break;}else{printf("n:%d\n", n);}}/getchar();return 0;
}
wireshark抓包如下
推荐一个零声学院免费公开课程,个人觉得老师讲得不错,分享给大家:Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等技术内容,立即学习
基于UDP的可靠性传输协议-KCP简介相关推荐
- UDT:基于UDP的可靠传输协议
基于UDP 上的UDT ,比TCP传输效率高 UDT:基于UDP的数据传输协议(初译) (译者:Jack) Status of this Memo This Internet-Draft is sub ...
- UDP可靠性传输协议(QUIC)
目录 UDP与TCP对比 可靠性机制 ACK机制 重传机制 流控控制 序号机制 重排机制 窗口机制 UDP可靠性设计 UDP窗口流控 KCP(出于实时性考虑) QUIC 简述 优点 缺点 报文格式 建 ...
- 网络协议之:基于 UDP 的高速数据传输协议 UDT
简介 简单就是美.在网络协议的世界中,TCP 和 UDP 是建立在 IP 协议基础上的两个非常通用的协议.我们现在经常使用的 HTTP 协议就是建立在 TCP 协议的基础上的.相当于 TCP 的稳定性 ...
- 网络协议之:基于UDP的高速数据传输协议UDT
文章目录 简介 UDT协议 UDT的缺点 总结 简介 简单就是美.在网络协议的世界中,TCP和UDP是建立在IP协议基础上的两个非常通用的协议.我们现在经常使用的HTTP协议就是建立在TCP协议的基础 ...
- 基于UDP的可靠传输——QUIC 协议
一.UDP协议 UDP用户数据报协议,非连接的协议,传输数据之前源端和终端不建立连接,当它想传送时直接去抓取来自应用程序的数据,并尽可能快地把它扔到网络上. UDP传输协议的特点 UDP无连接,时间上 ...
- 快速可靠网络传输协议 KCP
KCP 是一个快速可靠协议,能以比 TCP浪费10%-20%的带宽的代价,换取平均延迟降低30%-40%,且最大延迟降低三倍的传输效果.纯算法实现,并不负责底层协议(如UDP)的收发,需要使用者自己定 ...
- UDP的可靠性传输详解
文章目录 UDP和TCP的区别 TCP UDP 为什么要使用UDP传输可靠性数据 如何使用UDP传输可靠性数据 KCP的使用方式 kcp配置模式 kcp的协议头 UDP和TCP的区别 Tcp和udp都 ...
- 网络传输协议 kcp 原理解析
1.概述 对于游戏开发,尤其是 MOBA(多人在线竞技)游戏,延迟是需要控制的.但是对于传统的 TCP(网络友好,很棒),并不利于包的实时性传输,因为他的超时重传和拥塞控制都是网络友好,对于我们包的实 ...
- UDT协议-基于UDP的可靠数据传输协议
1. 介绍 随着网络带宽时延产品(BDP)的增加,通常的TCP协议开始变的低效.这是因为它的AIMD(additive increase multiplicative decrease)算法彻底减 ...
最新文章
- 你应该更新的Java知识
- 简单说明c语言程序步骤,C语言的入门简介和三个简单的C语言程序详细说明
- python三维数组切片_【NumPy学习指南】day4 多维数组的切片和索引
- kodi资源_kodi 展示播放 NAS 电影
- java 日本时区_java时区时间ZoneOffset, ZoneId,OffsetTime,OffsetDateTi
- 问题-[Delphi]用LoadLibrary加载DLL时返回0的错误
- web调试代理工具Whistle
- [黑科技] 使用Word和Excel自制题库自判断答题系统
- matlab作图函数的总结与分析.pdf,Matlab作图函数的总结与分析_黄琼湘
- VMware虚拟机安装Windows Server 2008 R2
- 经典企业文化书籍推荐,有了这6本书企业文化落地不再是难事
- matlab中提取公因子化简,利用MATLAB化简表达式或者多项式 | 望天博客
- 9、Linux文本处理三剑客之sed命令
- 暗刺,高并发五个利器
- micropython 进阶小实验 如何用单片机制作鞋码匹配仪
- 《日语综合教程》第七册 第六課 自然と人間
- linux添加扩展gpio,嵌入式Linux工控板的ISA总线GPIO扩展
- GCC 编译器警告——【-Wunused-variable】【-Wunused-parameter】
- SDOI R2 咕咕记
- tadf发光原理 热活化延迟荧光(TADF)原理是什么?