• 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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    选择性重传 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;
    	}
    	......
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    延迟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为11
        IUINT32 len;    // 数据长度
        IUINT32 resendts;   // 下次超时重传时间戳
        IUINT32 rto;        //该分片的超时等待时间,其计算方法同TCP
        IUINT32 fastack;    // 收到ack时计算该分片被跳过的累计次数,此字段用于快速重传,自定义需要几次确认开始快速重传
        IUINT32 xmit;       // 发送分片的次数,每发一次加1.发送的次数对RTO的计算有影响,但是比TCP来说,影响会小一些.
        char data[1];
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    非退让流控:
    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包中

    1. 拆包
      假定这个消息是个图片消息,比较大,大小为3252 bytes
      在这里插入图片描述
      Msg被拆成了3部分,包含在3个KCP包中。注意, frg 的序号是从大到小的,一直到0为止。这样接收端
      收到KCP包时,只有拿到 frg 为0的包,才会进行组装并交付给上层应用程序。由于 frg 在header中占1
      个字节,也就是最大能支持(1400 – 24) * 256 / 1024 = 344kB的消息

    总结:在消息模式下,每个KCP包最多包含一个上层应用的消息。

    1. 流模式
      消息模式减少了上层应⽤从流中拆解出消息的麻烦,但是它对⽹络的利⽤率较低。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 
    #include 
    #include 
    #include 
    #include 
    
    #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 1500
    
    std::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, 1
    	ikcp_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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123

    server代码:

    #include "ikcp.h"
    #include 
    #include 
    #include 
    #include 
    #include 
    
    #pragma comment(lib,"ws2_32.lib")
    
    #define RECV_BUF 1500
    
    std::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, 1
    	ikcp_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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139

    wireshark抓包如下
    在这里插入图片描述

    推荐一个零声学院免费公开课程,个人觉得老师讲得不错,分享给大家:Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等技术内容,立即学习

  • 相关阅读:
    java 抽象类与接口——接口
    测试工程师需要具备哪些“技能”?
    删除所有出现次数最少的字符
    K8S使用开源CEPH作为后端StorageClass
    千人优学 | GBase 8s数据库2022年6月大学生专场实训圆满结束
    C51之智能感应垃圾桶
    自动驾驶仿真:Carsim基于车辆后轴中心输出参数
    Docker+Jenkins(blueocean)+Gitee构建CICD流水线
    Libcurl 学习与使用
    如果重复定义宏,两个值不同,最终的值是哪一个?
  • 原文地址:https://blog.csdn.net/qq_23350817/article/details/126577319