udp可靠性传输

udp数据包传输快,但是相对于tcp来说不可靠;而tcp可靠,但是传输速度相比udp没有那么快。所以有些需求需要中和它们的优缺点来中和一下,就出现了一些中间件,比较有名的KCP和QUIC

1 kcp介绍

    KCP是一个快速可靠协议,能以比TCP浪费10%-20%的带宽的代价,换取平均延迟降低30%-40%,且最大延迟降低三倍的传输效果。纯算法实现,并不负责底层协议(如UDP)的收发,需要使用者自己定义下层数据包的发送方式,以callback的方式提供给KCP。连时钟都需要外部传递进来,内部不会有任何一次系统调用。本文传输协议之考虑UDP的情况。
    TCP协议的可靠与无私让使用TCP开发更为简单,同时他的这种设计也导致慢的特点。UDP协议简单,所以它更快。但是,UDP毕竟不可靠的,应用层收到的数据可能是缺失,乱序的。KCP协议就是再保留UDP快的基础上,提供可靠的传输,应用层使用更加简单。
    其它差别,TCP是以字节流的形式,UDP以数据包的形式很多人以为,udp是不可靠的,所以sendto(1000),接受端recvfrom(1000),可能会收到900。这个是错误的。所谓数据报文,就是说UDP是有界的,sendto(300),sendto(500);接受到,recvfrom(1000),recvfrom(1000),那么可能会收到300,500或者其中一个或者都没有收到。
   UDP应用层发送的数据,再接受缓存足够的情况下,要么收到全的,要么收不到。
快速重传会统计之前被跳过包的次数,然后进行重传
TCP慢启动,使得带宽浪费

2 kcp使用方式

1. 创建 KCP 对象:
// 初始化 kcp 对象,conv 为一个表示会话编号的整数,和 tcp 的 conv 一样,通信双
// 方需保证 conv 相同,相互的数据包才能够被认可,user 是一个给回调函数的指针

ikcpcb *kcp = ikcp_create(conv, user);

2. 设置传输回调函数(如 UDP 的 send 函数):
// KCP 的下层协议输出函数,KCP 需要发送数据时会调用它
// buf/len 表示缓存和长度
// user 指针为 kcp 对象创建时传入的值,用于区别多个 KCP 对象

int udp_output(const char *buf, int len, ikcpcb *kcp, void *user)
{
....
}

// 设置回调函数

kcp->output = udp_output;

3. 循环调用 update:
// 以一定频率调用 ikcp_update 来更新 kcp 状态,并且传入当前时钟(毫秒单位)
// 如 10ms 调用一次,或用 ikcp_check 确定下次调用 update 的时间不必每次调用

ikcp_update(kcp, millisec);

4. 输入一个应用层数据包(如 UDP 收到的数据包):
// 收到一个下层数据包(比如 UDP 包)时需要调用:ikcp_input(kcp,received_udp_packet,received_udp_size);
处理了下层协议的输出/输入后 KCP 协议就可以正常工作了,使用 ikcp_send 来向
远端发送数据。而另一端使用 ikcp_recv(kcp, ptr, size)来接收数据。

总结:UDP 收到的包,不断通过 kcp_input 喂给 KCP,KCP 会对这部分数据(KCP 协议数据)进行解包,重新封装成应用层用户数据,应用层通过 kcp_recv 获取。应用层通过 kcp_send 发送数据,KCP 会把用户数据拆分 kcp 数据包,通过 kcp_output,以 UDP(send)的方式发送。

3 kcp源码分析

kcp源码流程图如下:
udp可靠性传输_第1张图片

3.1 数据发送过程

   数据发送准备用户发送数据的函数为 ikcp_send。ikcp_send(ikcpcb kcp, const char buffer, int len)
   该函数的功能非常简单,把用户发送的数据根据 MSS 进行分片。如上图,用户发送 1900 字节的数据,MTU 为 1400byte。因此,该函数会把 1900byte 的用户数据分成两个包,一个数据大小为 1400,头 frg 设置为 1,len 设置为 1400;第二个包,头 frg 设置为 0,len 设置为 500。
   切好 KCP 包之后,放入到名为 snd_queue 的待发送队列中。

注:流模式情况下,kcp 会把两次发送的数据衔接为一个完整的 kcp 包。非流模式下,用户数据%MSS 的包,也会作为一个包发送出去。
   MTU,数据链路层规定的每一帧的最大长度,超过这个长度数据会被分片。通常 MTU 的长度为 1500字节,IP 协议规定所有的路由器均应该能够转发(512 数据+60IP 首部+4 预留=576 字节)的数据。 MSS,最大输出大小(双方的约定),KCP 的大小为 MTU-kcp 头 24 字节。IP 数据报越短,路由器转发越快,但是资源利用率越低。传输链路上的所有 MTU 都一至的情况下效率最高,应该尽可能的避免数据传输的工程中,再次被分。UDP 再次被分的后(通常 1 分为 2),只要丢失其中的任意一份,两份都要重新传输。因此,合理的 MTU 应该是保证数据不被再分的前提下,尽可能的大。

以太网的 MTU 通常为 1500 字节-IP 头(20 字节固定+40 字节可选)-UDP 头 8 个字节=1472 字节。KCP 会考虑多传输协议,但是在 UDP 的情况下,设置为 1472 字节更为合理。

实际发送:
KCP 会不停的进行 update 更新最新情况,数据的实际发送在 update 时进行。发送过程如下图所示:
udp可靠性传输_第2张图片
步骤 1:待发送队列移至发送队列
KCP 会把 snd_queue 待发送队列中的 kcp 包,移至 snd_buf 发送队列。移动的包的数量不会超过snd_una+cwnd-snd_nxt,确保发送的数据不会让接收方的接收队列溢出。该功能类似于 TCP 协议中的滑动窗口。cwnd=min(snd_wnd,rmt_wnd,kcp->cwnd)的最小值决定,snd_wnd,rmt_wnd 比较好理解可发送的数据,可发送的数据最大值,应该是发送方可以发送的数据和接收方可以接收的数据的最小值。kcp->cwnd 是拥塞控制的一个值,跟网络状况相关,网络状况差的时候,KCP 认为应该降低发送的数据,后面会有详细的介绍。
如上图中,snd_queue 待发送队列中有 4 个 KCP 包等待发送,这个时候 snd_nxt 下一个发送的 kcp 包序列号为 11,snd_una 下一个确认的 KCP 包为 9(8 已经确认,9,10 已经发送但是还没得到接收方的确认)。因为 cwnd=5,发送队列中还有 2 个发送了但是还未得到确认,所以可以从待发送队列中取前面的 3 个 KCP包放入到发送队列中,序列号分别设置为 11,12,13。
步骤 2:发送发送队列的数据
发送队列中包含两种类型的数据,已发送但是尚未被接收方确认的数据,没被发送过的数据。
没发送过的数据比较好处理,直接发送即可。
重点在于已经发送了但是还没被接收方确认的数据,该部分的策略直接决定着协议快速、高效与否。KCP主要使用两种策略来决定是否需要重传 KCP 数据包,超时重传、快速重传、选择重传。
1、超时重传
TCP 超时计算是 RTOx2,这样连续丢三次包就变成 RTOx8 了,而 KCP 非快速模式下每次+RTO,急速模式下+0.5RTO(实验证明 1.5 这个值相对比较好),提高了传输速度。
udp可靠性传输_第3张图片
2、快速重传
发送端发送了 1,2,3,4,5 几个包,然后收到远端的 ACK: 1, 3, 4, 5,当收到 ACK3 时,KCP 知道 2 被跳过 1次,收到 ACK4 时,知道 2 被跳过了 2 次,此时可以认为 2 号丢失,不用等超时,直接重传 2 号包,大大改善了丢包时的传输速度。TCP 有快速重传算法,TCP 包被跳过 3 次之后会进行重传。
注:可以通过统计错误重传(重传的包实际没丢,仅乱序),优化该设置。
3、选择重传
老的 TCP 丢包时会全部重传从丢的那个包开始以后的数据,KCP 是选择性重传,只重传真正丢失的数据包。但是,目前大部分的操作系统,linux 与 android 手机均是支持 SACK 选择重传的。

步骤 3:数据发送
通过步骤 2 判定,kcp 包是否需要发送,如果需要发送的 kcp 包则通过,kcp_setoutput 设置的发送接口进行
发送,UDP 通常为 sendto。步骤 3,会对较小的 kcp 包进行合并,一次性发送提高效率

3.2 数据接受过程

KCP 的接收过程是将 UDP 收到的数据进行解包,重新组装顺序的、可靠的数据后交付给用户。
KCP 数据包接收
kcp_input 输入 UDP 收到的数据包。kcp 包对前面的 24 个字节进行解压,包括 conv、 frg、 cmd、 wnd、ts、 sn、 una、 len。根据 una,会删除 snd_buf 中,所有 una 之前的 kcp 数据包,因为这些数据包接收者已经确认。根据 wnd 更新接收端接收窗口大小。根据不同的命令字进行分别处理。数据接收后,更新流程
如下所示:
udp可靠性传输_第4张图片
1、IKCP_CMD_PUSH 数据发送命令
a、KCP 会把收到的数据包的 sn 及 ts 放置在 acklist 中,两个相邻的节点为一组,分别存储 sn 和 ts。update时会读取 acklist,并以 IKCP_CMD_ACK 的命令返回确认包。如下图中,收到了两个 kpc 包,acklist 中会分别存放 10,123,11,124。
b、kcp 数据包放置 rcv_buf 队列。丢弃接收窗口之外的和重复的包。然后将 rcv_buf 中的包,移至 rcv_queue。原来的 rcv_buf 中已经有 sn=10 和 sn=13 的包了,sn=10 的 kcp 包已经在rcv_buf 中了,因此新收到的包会直接丢弃掉,sn=11 的包放置至 rcv_buf 中。
c、把 rcv_buf 中前面连续的数据 sn=11,12,13 全部移动至 rcv_queue,rcv_nxt 也变成 14。rcv_queue 的数据是连续的,rcv_buf 可能是间隔的
d、kcp_recv 函数,用户获取接收到数据(去除 kcp 头的用户数据)。该函数根据 frg,把 kcp 包数据进行组合返回给用户。
udp可靠性传输_第5张图片
2、IKCP_CMD_ACK 数据确认包
两个使命:1、RTO 更新,2、确认发送包接收方已接收到。
正常情况:收到的 sn 为 11,una 为 12。表示 sn 为 11 的已经确认,下一个等待接收的为 12。发送队列中,待确认的一个包为 11,这个时候 snd_una 向后移动一位,序列号为 11 的包从发送队列中删除。
udp可靠性传输_第6张图片
[ 数据确认包处理流程 ]
异常情况:如下图所示,sn!=11 的情况均为异常情况。sn<11 表示,收到重复确认的包,如本来以为丢失的包重新又收到了,所以产生重复确认的包;sn>17,收到没发送过的序列号,概率极低,可能是 conv 没变重启程序导致的;112,则启动快速重传
udp可靠性传输_第7张图片
[ KCP 快速确认 ]
确认包发送,接收到的包会全部放在 acklist 中,以 IKCP_CMD_ACK 包发送出去
总结:
udp可靠性传输_第8张图片
udp可靠性传输_第9张图片
udp可靠性传输_第10张图片

4 代码解读

代码主要分为客户端和服务器
客户端代码解读:
udp可靠性传输_第11张图片

你可能感兴趣的:(网络编程,udp,tcpip,tcpdump)