UDP可靠性传输-KCP

 

一、 KCP协议

 

1. 简介

KCP是一个基于UDP的快速可靠协议,能以比 TCP浪费10%-20%的带宽的代价,换取平均延迟降低 30%-40%的效果。

KCP官方:https://github.com/skywind3000/kcp

KCP纯算法实现,并不负责底层协议(如UDP)的收发,需要使用者自己定义下层数据包的发送方式,以 callback的方式提供给 KCP。 连时钟都需要外部传递进来,内部不会有任何一次系统调用。

1.1 名称说明

用户数据:应用层发送的数据,如一张图片2Kb的数据

MTU:数据链路层最大传输单元。即每次发送的最大数据

RTO:Retransmission TimeOut,超时重传时间

cwnd:congestion window,拥塞窗口,标识发送方可以发送多少个KCP数据包。与接收方窗口有关,与网络状况(拥塞控制)有关,与发送窗口大小有关。

rwnd:receiver window,接收窗口大小,标识接收方还可接收多少个KCP数据包

snd_queue:待发送KCP数据包队列

snd_nxt:下一个即将发送的kcp数据包序列号

snd_una:下一个待确认的序列号

2. UDP是如何做到可靠,KCP协议的优势

RTO翻倍vs不翻倍

TCP超时计算是RTOx2,这样连续丢三次包就变成RTOx8了,十分恐怖,而KCP启 动快速模式后不x2,只是x1.5(实验证明1.5这个值相对比较好),提高了传输 速度。

延迟ACK和非延迟ACK

TCP为了充分利用带宽,延迟发送ACK(NODELAY都没有用),这样超时计算会算出较大RTT时间,延长了丢包时的判断。KCP的ACK是否延迟发送可以调节。

UNA vs ACK+UNA

ARQ模型响应有两种,UNA(此编号前所有包已收到,如TCP)和ACK(该编号包已收到),光用UNA将导致全部重传,光用ACK作为丢失成本太高,以往协议都是二选其一,而KCP协议中,除去单独的ACK包外,所有的包都有UNA信息。

选择性重传vs全部重传

TCP丢包时会全部重传从丢的那个包开始以后的数据,KCP是选择性重传,只重传真正丢失的数据包。

快速重传(跳过多少个包马上重传)

发送端发送了1,2,3,4,5几个包,然后收到远端的ACK: 1, 3, 4, 5,当收到ACK3时, KCP知道2被跳过1次,收到ACK4时,知道2被跳过了2次,此时可以认为2号丢失, 不用等超时,直接重传2号包,大大改善了丢包时的传输速度。fastresend = 2

非退让流控

KCP正常模式同TCP一样使用公平退让法则,即发送窗口大小由:发送缓存大小、 接收端剩余接收缓存大小、丢包退让及慢启动这四要素决定。但传送及时性要 求很高的小数据时,可选择通过配置跳过后两步,仅用前两项来控制发送频率。 以牺牲部分公平性及带宽利用率之代价,换取了开着BT都能流畅传输的效果。

3. KCP使用方式

3.1 KCP源码使用

  1. 创建KCP对象:

 

  2. 设置传输回调函数(如UDP的send函数):

 

 

  3. 循环调用update:

 

  4. 输入一个应用层数据包(如UDP收到的数据包):

 

处理了下层协议的输出/输入后 KCP协议就可以正常工作了,使用 ikcp_send 来向远端发送数据。而另一端使用 ikcp_recv(kcp, ptr, size)来接收数据。

3.2 KCP源码流程图

 

 

 

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

3.3 KCP配置模式

  1. 工作模式:int ikcp_nodelay(ikcpcb *kcp, int nodelay, int interval, int resend, int nc)

    nodelay:是否启用 nodelay模式,0不启用,1启用。

    interval:协议内部工作的interval,单位毫秒,比如10ms或者20ms。

    resend:快速重传模式,默认0关闭,可以设置2(2此ACK跨越将会之直接重传)

    nc:是否关闭流控,默认是0代表不关闭,1代表关闭

    普通模式:ikcp_nodelay(kcp,0,40,0,0);

    极速模式:ikcp_nodelay(kcp,1,10,2,1)

  2. 最大窗口:int ikcp_wndsize(ikcpcb *kcp, int sndwnd, int rcvwnd)

    该调用将会设置协议的最大发送窗口和最大接收窗口大小,默认为32,单位为包。

  3. 最大传输单元:int ikcp_setmtu(ikcpcb *kcp, int mtu)

    纯算法协议并不负责探测 MTU,默认 mtu是1400字节,可以使用ikcp_setmtu来设置该值。该值将会影响数据包归并及分片时候的最大传输单元。

  4. 最小RTO:kcp->rx_minrto

    不管是 TCP还是 KCP计算 RTO时都有最小 RTO的限制,即便计算出来RTO为40ms,由于默认的 RTO是100ms,协议只有在100ms后才能检测到丢包,快速模式下为30ms,可以手动更改该值。kcp->rx_minrto = 10。

4. KCP原理

4.1 KCP协议头

 

  • conv:连接号。UDP是无连接的,conv用于表示来自于哪个客户端。对连接的一种替代
  • cmd:命令字。如,IKCP_CMD_PUSH数据推送命令,IKCP_CMD_ACK确认命令,IKCP_CMD_WASK接收窗口大小询问命令,IKCP_CMD_WINS接收窗口大小告知命令
  • frg:分片,用户数据可能会被分成多个KCP包,发送出去
  • wnd:接收窗口大小,发送方的发送窗口不能超过接收方给出的数值
  • ts:时间序列
  • sn:序列号
  • una:下一个可接收的序列号。其实就是确认号,收到sn=10的包,una为11
  • len:数据长度
  • data:用户数据

4.2 数据发送过程

4.2.1 数据发送

用户发送数据的函数为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的待发送队列中。

MTU,数据链路层规定的每一帧的最大长度,超过这个长度数据会被分片。MSS,最大输出大小(双方的约定),KCP的大小为MTU-24字节(kcp头)。IP数据报越短,路由器转发越快,但是资源利用率越低。传输链路上的所有MTU都一致的情况下效率最高,应该尽可能的避免数据传输在传输的过程中再次被分。UDP再次被分的后(通常1分为2),只要丢失其中的任意一份,两份都要重新传输。因此,合理的MTU应该是保证数据不被再分的前提下,尽可能的大。Interbet的标准MTU值为576字节,而目前大多数路由器设备的MTU都为1500字节。

4.2.2 实际发送

 

步骤一:将待发送队列移至发送队列

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。

步骤二:发送发送队列的数据

发送队列中包含两种类型的数据,已发送但是尚未被接收方确认的数据,没被发送过的数据。没发送过的数据比较好处理,直接发送即可。重点在于已经发送了但是还没被接收方确认的数据,该部分的策略直接决定着协议快速、高效与否。KCP主要使用三种策略来决定是否需要重传KCP数据包,超时重传、快速重传、选择重传。

① 超时重传

与TCP的超时重传机制类似,KCP非快速模式下每次+RTO,这样子急速模式下+0.5RTO(实验证明1.5这个值相对比较好),提高了传输速度。KCP普通模式下和TCP一样,RTO呈指数增长,3次后就是RTO*8.

②快速重传

当设置了fastresend =2时,发送端发送了1,2,3,4,5几个包,然后收到远端的ACK: 1, 3, 4, 5,当收到ACK3时,KCP知道2被跳过1次,收到ACK4时,知道2被跳过了2次,此时可以认为2号丢失,不用等超时,直接重传2号包,大大改善了丢包时的传输速度。TCP有快速重传算法,TCP包是在接收到三次冗余ACK后立即重传。

③选择重传

老的TCP丢包时会全部重传从丢的那个包开始以后的数据,KCP是选择性重传,只重传真正丢失的数据包。但是,目前大部分的操作系统,linux与android手机均是支持SACK选择重传的。

步骤三:数据发送

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

4.3 数据接收过程

KCP的接收过程是将UDP收到的数据进行解包,重新组装顺序的、可靠的数据后交付给用户。

4.3.1 KCP数据包接收

kcp_input输入UDP收到的数据包。kcp包对前面的24个字节进行解压,包括conv、 frg、 cmd、 wnd、 ts、 sn、 una、 len。根据una,会删除snd_buf中,所有una之前的kcp数据包,因为这些数据包接收者已经确认。根据wnd更新接收端接收窗口大小。根据不同的命令字进行分别处理。数据接收后,更新流程如下所示:

cmd = IKCP_CMD_PUSH/IKCP_CMD_WASK/IKCP_CMD_ACK的处理分别如上图。

①KCP会把收到的数据包的sn及ts放置在acklist中,两个相邻的节点为一组,分别存储sn和ts。update时会读取acklist,并以IKCP_CMD_ACK的命令返回确认包。如下图中,收到了两个kpc包,acklist中会分别存放10,123,11,124。
②kcp数据包放置rcv_buf队列。丢弃接收窗口之外的和重复的包。然后将rcv_buf中的包,移至rcv_queue。原来的rcv_buf中已经有sn=10和sn=13的包了,sn=10的kcp包已经在rcv_buf中了,因此新收到的包会直接丢弃掉,sn=11的包放置至rcv_buf中。

③把rcv_buf中前面连续的数据sn=11,12,13全部移动至rcv_queue,rcv_nxt也变成14。rcv_queue的数据是连续的,rcv_buf可能是间隔的。

④kcp_recv函数,用户获取接收到数据(去除kcp头的用户数据)。该函数根据frg,把kcp包数据进行组合返回给用户。

 

4.4 RTO计算

跟TCP完全一样。

RTT:一个报文段发送出去,到收到对应确认包的时间差。
SRTT(kcp->rx_srtt):RTT的一个加权RTT平均值,平滑值。
RTTVAR(kcp->rx_rttval):RTT的平均偏差,用来衡量RTT的抖动。

4.5 流量控制-探测对方接收窗口

流量控制是点对点的通信量的控制,是一个端到端的问题。总结起来,就是发送方的速度要匹配接收方接收(处理)数据的速度。发送方要抑制自身的发送速率,以便使接收端来得及接收。

KCP的发送机制采用TCP的滑动窗口方式,可以非常容易的控制流量。KCP的头中包含wnd,即接收方目前可以接收的大小。能够发送的数据即为snd_una与snd_una+wnd之间的数据。接收方每次都会告诉发送方我还能接收多少,发送方就控制下,确保自己发送的数据不多于接收端可以接收的大小。

KCP默认为32,即可以接收最大为32*MTU=43.75kB。KCP采用update的方式,更新间隔为10ms,那么KCP限定了你最大传输速率为4375kB/s,在高网速传输大内容的情况下需要调用ikcp_wndsize调整接收与发送窗口。

KCP的主要特色在于实时性高,对于实时性高的应用,如果发生数据堆积会造成延迟的持续增大。建议从应用侧更好的控制发送流量与网络速度持平,避免缓存堆积延迟。

 4.6 拥塞控制

KCP的优势在于可以完全关闭拥塞控制,非常自私的进行发送。KCP采用的拥塞控制策略为TCP最古老的策略,无任何优势。完全关闭拥塞控制,也不是一个最优策略,它全是会造成更为拥堵的情况。

KCP发生丢包的情况下的拥塞控制策略是慢开始,在发生快速重传的情况下,采用的是快恢复策略。

4.7 项目集成

参考asio_kcp

 

posted @ 2022-03-18 13:13  幻cat  阅读(392)  评论(0编辑  收藏  举报