KCP简介
KCP是一个快速可靠的协议,能以比 TCP浪费10%-20%的带宽的代价,换取平均延迟降低 30%-40%,且最大延迟降低三倍的传输效果。纯算法实现,并不负责底层协议(如UDP)的收发,需要使用者自己定义下层数据包的发送方式,以 callback的方式提供给 KCP。 连时钟都需要外部传递进来,内部不会有任何一次系统调用。
整个协议只有 ikcp.h, ikcp.c两个源文件,可以方便的集成到用户自己的协议栈中。也许你实现了一个P2P,或者某个基于 UDP的协议,而缺乏一套完善的ARQ可靠协议实现,那么简单的拷贝这两个文件到现有项目中,稍微编写两行代码,即可使用。
KCP特性
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
TCP与UDP都是传输层的协议,比较两者的区别主要应该是说TCP比UDP多了什么?
- 面向连接:TCP接收方与发送方维持了一个状态(建立连接,断开连接),双方知道对方还在。
- 可靠的:发送出去的数据对方一定能够接收到,而且是按照发送的顺序收到的。
- 流量控制与拥塞控制:TCP靠谱通过滑动窗口确保,发送的数据接收方来得及收。TCP无私,发生数据包丢失的时候认为整个网络比较堵,自己放慢数据发送速度。
TCP协议的可靠与无私让使用TCP开发更为简单,同时它的这种设计也导致了慢的特点。UDP协议简单,所以它更快。但是,UDP毕竟是不可靠的,应用层收到的数据可能是缺失、乱序的。
KCP协议就是在保留UDP快的基础上,提供可靠的传输,应用层使用更加简单。TCP可靠简单,但是复杂无私,所以速度慢。KCP尽可能保留UDP快的特点下,保证可靠。
使用方式
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)来接收数据。KCP的流程图如下:
协议配置
协议默认模式是一个标准的 ARQ,需要通过配置打开各项加速开关:
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. 这个可以理解为 TCP的 SND_BUF 和 RCV_BUF,只不过单位不一样SND/RCV_BUF 单位是字节,这个单位是包。
3.最大传输单元:
纯算法协议并不负责探测 MTU,默认 mtu是1400字节,可以使用ikcp_setmtu来设置该值。该值将会影响数据包归并及分片时候的最大传输单元。
4.最小RTO:
不管是 TCP还是 KCP计算 RTO时都有最小 RTO的限制,即便计算出来RTO为40ms,由于默认的 RTO是100ms,协议只有在100ms后才能检测到丢包,快速模式下为30ms,可以手动更改该值:
kcp->rx_minrto = 10;
KCP原理
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进行分片。放入到名为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会把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超时计算是RTOx2,这样连续丢三次包就变成RTOx8了,而KCP非快速模式下每次+RTO,急速模式下+0.5RTO(实验证明1.5这个值相对比较好),提高了传输速度。
- 快速重传
发送端发送了1,2,3,4,5几个包,然后收到远端的ACK: 1, 3, 4, 5,当收到ACK3时,KCP知道2被跳过1次,收到ACK4时,知道2被跳过了2次,此时可以认为2号丢失,不用等超时,直接重传2号包,大大改善了丢包时的传输速度。TCP有快速重传算法,TCP包被跳过3次之后会进行重传。
注:可以通过统计错误重传(重传的包实际没丢,仅乱序),优化该设置。
- 选择重传
老的TCP丢包时会全部重传从丢的那个包开始以后的数据,KCP是选择性重传,只重传真正丢失的数据包。但是,目前大部分的操作系统,linux与android手机均是支持SACK选择重传的。
- 数据发送
通过判定,kcp包是否需要发送,如果需要发送的kcp包则通过,kcp_setoutput设置的发送接口进行发送,UDP通常为sendto。步骤3,会对较小的kcp包进行合并,一次性发送提高效率