基于UDP的可靠性传输协议-KCP简介

简介: 基于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为11
    IUINT32 len;    // 数据长度
    IUINT32 resendts;   // 下次超时重传时间戳
    IUINT32 rto;        //该分片的超时等待时间,其计算方法同TCP
    IUINT32 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包中

  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 <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 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;
}

server代码:

#include "ikcp.h"
#include <iostream>
#include <chrono>
#include <thread>
#include <atomic>
#include <WinSock2.h>
#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;
}

wireshark抓包如下


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


相关实践学习
深入解析Docker容器化技术
Docker是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的Linux机器上,也可以实现虚拟化,容器是完全使用沙箱机制,相互之间不会有任何接口。Docker是世界领先的软件容器平台。开发人员利用Docker可以消除协作编码时“在我的机器上可正常工作”的问题。运维人员利用Docker可以在隔离容器中并行运行和管理应用,获得更好的计算密度。企业利用Docker可以构建敏捷的软件交付管道,以更快的速度、更高的安全性和可靠的信誉为Linux和Windows Server应用发布新功能。 在本套课程中,我们将全面的讲解Docker技术栈,从环境安装到容器、镜像操作以及生产环境如何部署开发的微服务应用。本课程由黑马程序员提供。 &nbsp; &nbsp; 相关的阿里云产品:容器服务 ACK 容器服务 Kubernetes 版(简称 ACK)提供高性能可伸缩的容器应用管理能力,支持企业级容器化应用的全生命周期管理。整合阿里云虚拟化、存储、网络和安全能力,打造云端最佳容器化应用运行环境。 了解产品详情: https://www.aliyun.com/product/kubernetes
相关文章
|
存储 网络协议 算法
UDP 协议和 TCP 协议
本文介绍了UDP和TCP协议的基本结构与特性。UDP协议具有简单的报文结构,包括报头和载荷,报头由源端口、目的端口、报文长度和校验和组成。UDP使用CRC校验和来检测传输错误。相比之下,TCP协议提供更可靠的传输服务,其结构复杂,包含序列号、确认序号和标志位等字段。TCP通过确认应答和超时重传来保证数据传输的可靠性,并采用三次握手建立连接,四次挥手断开连接,确保通信的稳定性和完整性。
647 1
UDP 协议和 TCP 协议
|
11月前
|
网络协议 开发者
探讨UDP协议中connect函数的作用及影响
总结来看,虽然UDP是无连接的,`connect()` 函数的使用在UDP编程中是一种可选的技术,它可以带来编程上的便利和某些性能上的改进,同时它改变的是程序逻辑上的行为,而非UDP协议本身的无连接特性。在实际应用中,根据通信模式和需求的不同,开发者可以根据情况选择是否调用 `connect()` 函数。
432 8
|
监控 网络协议 视频直播
UDP协议(特点与应用场景)
UDP(用户数据报协议)是传输层的一种无连接协议,具有简单高效、低延迟的特点。其主要特点包括:无连接(无需握手)、不可靠传输(不保证数据完整性)、面向数据报(独立传输)。尽管UDP不如TCP可靠,但在实时通信(如语音通话、视频会议)、在线游戏、多媒体流媒体(如直播、点播)及网络监控等领域广泛应用,满足了对速度和实时性要求较高的需求。
1697 19
|
网络协议
为何UDP协议不可靠?DNS为何选择UDP?
总的来说,UDP和TCP各有优势,选择哪种协议取决于应用的具体需求。UDP可能不如TCP可靠,但其简单、快速的特性使其在某些场景下成为更好的选择。而DNS就是这样的一个例子,它利用了UDP的优势,以实现快速、高效的名字解析服务。
685 14
|
网络协议 Java 开发工具
全平台开源即时通讯IM框架MobileIMSDK:7端+TCP/UDP/WebSocket协议,鸿蒙NEXT端已发布,5.7K Stars
全平台开源即时通讯IM框架MobileIMSDK:7端+TCP/UDP/WebSocket协议,鸿蒙NEXT端已发布,5.7K Stars
751 1
|
缓存 网络协议
Jmeter如何对UDP协议进行测试?
`jmeter-plugins`是JMeter的插件管理器,用于管理和组织所有插件。访问[官网](https://jmeter-plugins.org/install/Install/)下载并放置于`lib/ext`目录下,重启JMeter后可在“选项”中看到插件管理器。
626 1
Jmeter如何对UDP协议进行测试?
|
XML JSON 算法
【JavaEE】——自定义协议方案、UDP协议
自定义协议,序列化,xml方案,json方案,protobuffer方案,UDP协议,校验和,比特翻转,CRC算法,md5算法
|
存储 网络协议 安全
用于 syslog 收集的协议:TCP、UDP、RELP
系统日志是从Linux/Unix设备及网络设备生成的日志,可通过syslog服务器集中管理。日志传输支持UDP、TCP和RELP协议。UDP无连接且不可靠,不推荐使用;TCP可靠,常用于rsyslog和syslog-ng;RELP提供可靠传输和反向确认。集中管理日志有助于故障排除和安全审计,EventLog Analyzer等工具可自动收集、解析和分析日志。
1290 2
|
监控 网络协议 网络性能优化
网络通信的核心选择:TCP与UDP协议深度解析
在网络通信领域,TCP(传输控制协议)和UDP(用户数据报协议)是两种基础且截然不同的传输层协议。它们各自的特点和适用场景对于网络工程师和开发者来说至关重要。本文将深入探讨TCP和UDP的核心区别,并分析它们在实际应用中的选择依据。
883 3
|
网络协议 SEO
TCP连接管理与UDP协议IP协议与ethernet协议
TCP、UDP、IP和Ethernet协议是网络通信的基石,各自负责不同的功能和层次。TCP通过三次握手和四次挥手实现可靠的连接管理,适用于需要数据完整性的场景;UDP提供不可靠的传输服务,适用于低延迟要求的实时通信;IP协议负责数据包的寻址和路由,是网络层的重要协议;Ethernet协议定义了局域网的数据帧传输方式,广泛应用于局域网设备之间的通信。理解这些协议的工作原理和应用场景,有助于设计和维护高效可靠的网络系统。
504 4