KCP协议简介
2018-11-05 14:15:26 阿炯

Kcp 是一个非常简单和快速的,基于 KCP 协议的 UDP 隧道,它可以将 TCP 流转换为 KCP+UDP 流。而 KCP 是一个快速可靠协议,能以比 TCP 浪费10%-20%的带宽的代价,换取平均延迟降低 30%-40%,且最大延迟降低三倍的传输效果。纯算法实现,并不负责底层协议(如UDP) 的收发,需要使用者自己定义下层数据包的发送方式,以 callback的方式提供给 KCP。连时钟都需要外部传递进来,内部不会有任何一次系统调用。

整个协议只有 ikcp.h, ikcp.c两个源文件,可以方便的集成到用户自己的协议栈中。也许你实现了一个P2P,或者某个基于 UDP的协议,而缺乏一套完善的ARQ可靠协议实现,那么简单的拷贝这两个文件到现有项目中,稍微编写两行代码,即可使用。


技术特性

TCP是为流量设计的(每秒内可以传输多少KB的数据),讲究的是充分利用带宽。而KCP 是为流速设计的(单个数据包从一端发送到一端需要多少时间),以10%-20%带宽浪费 的代价换取了比 TCP快30%-40%的传输速度。TCP信道是一条流速很慢,但每秒流量很大的大运河,而KCP是水流湍急的小激流。KCP有正常模式和快速模式两种,通过以下策略达到提高流速的结果:

RTO翻倍vs不翻倍:

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

选择性重传 vs 全部重传:

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

快速重传:

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

延迟ACK vs 非延迟ACK:

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

UNA vs ACK+UNA:

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

非退让流控:

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

KCP因何存在

首先要看TCP与UDP的区别,TCP与UDP都是传输层的协议,比较两者的区别主要应该是说TCP比UDP多了什么?

面向连接:TCP接收方与发送方维持了一个状态(建立连接,断开连接),双方知道对方还在。
可靠的:发送出去的数据对方一定能够接收到,而且是按照发送的顺序收到的。
流量控制与拥塞控制:TCP靠谱通过滑动窗口确保,发送的数据接收方来得及收。TCP无私,发生数据包丢失的时候认为整个网络比较堵,自己放慢数据发送速度。

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应用层发送的数据,在接收缓存足够的情况下,要么收到全的,要么收不到。


协议简介

KCP是一个可靠的传输协议,UDP本身是不可靠的,所以需要额外信息来保证传输数据的可靠性。因此我们需要在传输的数据上增加一个包头。用于确保数据的可靠、有序。

    0                         4      5     6      8 (BYTE)

    +-------------------+----+----+----+

    |      conv      | cmd | frg |  wnd |

    +-------------------+----+----+----+   8

    |         ts         |             sn           |

    +-------------------+----------------+  16

    |       una       |             len          |

    +-------------------+----------------+   24

    |                                                   |

    |            DATA (optional)          |

    |                                                   |

    +-------------------------------------+

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

后面的讲解,主要以极速模式: ikcp_nodelay(kcp, 1, 10, 2, 1)为主,启用nodelay设置,刷新间隔控制在10ms,开启快速重传模式,关闭流量控制。


数据发送过程

数据发送准备


用户发送数据的函数为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时进行。发送过程如下图所示:


[ KCP 发送过程 ]

步骤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这个值相对比较好),提高了传输速度。


[ RTO算法对比图 ]

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包进行合并,一次性发送提高效率


数据接收过程

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

KCP数据包接收

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


[ 接收处理流程图 ]

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包数据进行组合返回给用户。


2、IKCP_CMD_ACK数据确认包
两个使命:1、RTO更新,2、确认发送包接收方已接收到。

正常情况:收到的sn为11,una为12。表示sn为11的已经确认,下一个等待接收的为12。发送队列中,待确认的一个包为11,这个时候snd_una向后移动一位,序列号为11的包从发送队列中删除。


[ 数据确认包处理流程 ]

异常情况:如下图所示,sn!=11的情况均为异常情况。sn<11表示,收到重复确认的包,如本来以为丢失的包重新又收到了,所以产生重复确认的包;sn>17,收到没发送过的序列号,概率极低,可能是conv没变重启程序导致的;112,则启动快速重传。


[ KCP快速确认 ]

确认包发送,接收到的包会全部放在acklist中,以IKCP_CMD_ACK包发送出去。


流量控制

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

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

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

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

拥塞控制(KCP可关闭)

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

网络中链路的带宽,与整条网络中的交换节点(路由器、交换机、基站等)有关。如果所有使用该链路的流量超出了,该链路所能提供的能力,就会发生拥塞。车多路窄,就会堵车,车越多堵的越厉害。因此,TCP作为一个大公无私的协议,当网络上发送拥堵的时候会降低自身发送数据的速度。拥塞控制是整个网络的事情,流量控制是发送和接收两方的事情。

当发送方没有按时接收到确认包,就认为网络发生了拥堵行为。TCP拥塞控制的方式,归结为慢开始、拥塞避免,如下图所示


[ 拥塞控制算法 ]

KCP发生丢包的情况下的拥塞控制策略与TCP Tahoe版本的策略一致。TCP Reno版本已经使用快恢复策略。因此在丢包的情况下,其实KCP拥塞控制策略比TCP更为苛刻。

KCP在发生快速重传,数据包乱序时,采用的是TCP快恢复的策略。控制窗口调整为已经发送没有接收到ack的数据包数目的一半+resent。

注:目前kernel 3.2以上的linux,默认采用google改进的拥塞控制算法,Proportional Rate Reduction for TCP。该算法的主要特点为,的cwnd如下图所示:



KCP协议是传输层的一个具有可靠性的传输层ARQ协议。它的设计是为了解决在网络拥堵情况下tcp协议的网络速度慢的问题。kcp力求在保证可靠性的情况下提高传输速度。kcp协议的关注点主要在控制数据的可靠性和提高传输速度上面,因此kcp没有规定下层传输协议,一般用udp作为下层传输协议,kcp层协议的数据包在udp数据报文的基础上增加控制头。当用户数据很大,大于一个udp包能承担的范围时(大于mss),kcp会将用户数据分片存储在多个kcp包中。因此每个kcp包称为一个分片。

为了提供可靠性,kcp采用了重传机制。为实现重传机制,kcp为每个分片分配一个唯一标识,接收方收到一个包后告知发送方接到的包的序号,发送方接到确认后再继续发送。而如果发送方在一定时间内(超时重传时间)没有接到确认,就说明数据包丢失了,发送方需要重传丢失的数据包,所以发送方会把待确认的数据缓存起来,方便重传。

停等的重传机制发送一个包后必须等待确认后再发下一个包,传输速度较慢,所以为了提高发送速度,发送方可以不必再每发送一个包后就进行等待确认,而是可以发送多个包出去,然后等待接收方一一确认。又由于接收方不可能同时处理无限多的数据,因此需要限制发送方往网络中发送的数据数量。因此接收方限制发送方在未收到确认之前只能发送wnd大小的数据,这个机制叫做滑动窗口机制。kcp采用滑动窗口机制来提高发送速度。由于UDP在网络中的传输是不可靠的,因此会出现丢包和包的乱序。kcp是可靠的保证数据有序的协议,所以为了纠正包的乱序。接收方维护一个接收窗口。接收窗口有一个起始序号rcv_nxt以及尾序号rcv_nxt+rcv_wnd。如果接收窗口收到序号为rcv_nxt的分片那么rcv_nxt就加一,形象一点的说法是滑动窗口右移,并把该数据放入接收队列供应用层取用。如果收到的数据在窗口范围内但不是rcv_nxt那么就把数据保存起来,等收到rcv_nxt序号的分片时再一并放入接收队列供应用层取用。

当网络拥堵严重时,会发生丢包,丢包发生时kcp为了保证可靠性需要重传数据。而发送方需要判断什么时候发生了丢包,以及丢了哪些包。为了解决这个问题,发送方为缓存队列中的每个包设置了包序号和超时重传时间。当检测到当前时间超过了分片的超时重传时间,该分片还没有得到确认时就会触发该分片的超时重传。

数据在网络中的传输时间是不固定的,因此超时重传时间比较长。而为了尽早地判断出数据包的丢失,kcp引入了快速重传机制。快速重传机制工作原理是,当发送方发送了n,n+1,n+2...等等包出去后,接收方没有接收到n,而接收到n+1,n+2..等等n号包之后的包,这时因为n号包之后的包都已经接收到了,而n号包还没有接收到,所以可以认为n号包已经丢失了,告知发送方可以进行快速重传。kcp为了支持快速重传,接收方需要告诉发送方,哪些包已经成功收到了,哪些包没有收到。因此接收方返回发送方的确认数据(ack)中包含以下信息:接收窗口左端的序号rcv_nxt,接收到的大于rcv_nxt的包序号sn。rcv_nxt的含义是接收方已经成功按顺序接收了rcv_nxt序号之前的所有包,大于rcv_nxt的序号sn表示的是在接收窗口内的不连续的包。发送方接收到接收方发过来的数据时,首先解析rcv_nxt,把发送缓存中所有小于rcv_nxt序号的包全部移除掉(因为这些包全都都已经正确接收了)。然后再解析sn,遍历发送缓存,找到所有序号小于sn的包,这些包就是可能在网络中已经丢掉了的包,只是可能,因为有可能这些包只是拥堵在了网络中,需要更长的时间到达,所以这里我们设置一个快速重传的门限,对每个分片维护一个快速重传的计数,每收到一个ack解析sn后找到了一个分片,就把该分片的快速重传的计数加一,如果该计数达到了快速重传门限,那么就认为该分片已经丢失,可以触发快速重传,该门限值在kcp中可以设置,tcp中是3。

丢包发生时,由于滑动窗口的存在,假设第n个包丢失了,但是此时n+1,n+2号包却已经传输成功了,此时最好只重传丢失的n号包,而不重传成功传输的n+1,n+2号包,这个机制叫做选择重传,选择重传的关键在于接收方要告知发送方哪些包已经收到了,哪些包没有收到,为了最小化数据量,接收方可以告诉发送方哪些包已经按序收到了,哪些包是收到的但是不连续。所以返回的ack中包含rcv_nxt和sn。rcv_nxt代表收到的所有连续的包,sn代表哪些不连续的包收到了,那么根据这两个参数可以计算出来没有收到的包的序号。

当网络实在很拥堵的时候(一般由于网络消息太多,堵车了),kcp会限制发送方发送的数据量,这叫做拥塞控制,拥塞控制就是告诉发送方,网络太堵了,应该少发一些数据,因此在滑动窗口的机制上引入了拥塞窗口,也就是说发送发发送的数据不得超过拥塞窗口,拥塞窗口的大小会随网络情况而变快,网络快拥塞窗口就大,反之同理。

那么拥塞窗口应该等于多少呢?解决这一问题的原则是,让网络充分被利用,但是不能堵塞,这里引入了慢启动机制,慢启动也就是控制拥塞窗口从0开始增长,随着数据不断地成功传输,拥塞窗口逐渐增大,直至达到饱和,也就是网络的收发平衡。为了快速达到网络的收发平衡,拥塞窗口采用倍数增长。也就是每成功发送一个数据拥塞窗口加一,举个例子,窗口大小为1时,发送一个数据,成功后窗口变成2,之后发送两个数据出去,成功接收后窗口大小变为4。为了方便让更多的用户连入网络时,网络能有足够的流量提供给用户,还可以设置拥塞门限,拥塞门限值就是当用户拥塞窗口快速增长到门限值后就减慢增加速度,缓慢增长,腾出流量给其它用户。

但是当网络很拥堵的情况下,导致发送数据出现重传时,这时说明网络中消息太多了,用户应该减少发送的数据,也就是拥塞窗口应该减小。怎么减小呢,在快速重传的情况下,有包丢失了但是有后续的包收到了,说明网络还是通的,这时采取拥塞窗口的退半避让,拥塞窗口减半,拥塞门限减半。减小网络流量,缓解拥堵。当出现超时重传的时候,说明网络很可能死掉了,因为超时重传会出现,原因是有包丢失了,并且该包之后的包也没有收到,这很有可能是网络死了,这时候,拥塞窗口直接变为1,不再发送新的数据,直到丢失的包传输成功。

在上述原理之下,kcp为了提高传输速度,还可以有许多选项供用户选择:
kcp的拥塞控制可以取消

ack回复可以设置成无延迟ack回复

kcp的快速重传门限可以控制

总之,kcp采取一系列措施尽量提高网络传输速率,在网络实时性和可靠性要求比较高的场景下可以考虑kcp协议代替tcp协议。

Kcptun 是 KCP 协议的一个简单应用,可以用于任意 TCP 网络程序的传输承载,以提高网络流畅度,降低掉线情况。由于 Kcptun 使用 Go 语言编写,内存占用低(经测试,在64M内存服务器上稳定运行),而且适用于所有平台,甚至 Arm 平台。

Kcptun 工作示意图:


总结:TCP可靠简单,但是复杂无私,所以速度慢。KCP尽可能保留UDP快的特点下,保证可靠。


KCP 协议:https://github.com/skywind3000/kcp

Kcptun 项目地址:https://github.com/xtaci/kcptun


参考来源:
可靠UDP-KCP协议快在哪

Kcp协议详解