行文前先安利下《再深谈TCP/IP三步握手&四步挥手原理及衍生问题—长文解剖IP 》、《再谈UDP协议—浅入理解深度记忆》
KCP协议科普KCP是一个快速可靠协议,能以比 TCP浪费10%-20%的带宽的代价,换取平均延迟降低 30%-40%,且最大延迟降低三倍的传输效果。
纯算法实现,并不负责底层协议(如UDP)的收发,需要使用者自己定义下层数据包的发送方式,以 callback的方式提供给 KCP。 连时钟都需要外部传递进来,内部不会有任何一次系统调用。本文传输协议之考虑UDP的情况。
整个KCP协议主要依靠一个循环ikcp_update来驱动整个算法的运转,所有的数据发送,接收,状态变化都依赖于此,所以如果有操作占用每一次update的周期过长,或者设置内部刷新的时间间隔过大,都会导致整个算法的效率降低。在ikcp_update中最终调用的是ikcp_flush,这是协议中的一个核心函数,将数据,确认包,以及窗口探测和应答发送到对端。
KCP使用ikcp_send发送数据,该函数调用ikcp_output发送数据,实际上最终调用事先注册的发送回调发送数据。KCP通过ikcp_recv将数据接收出来,如果被分片发送,将在此自动重组,数据将与发送前保持一致。
KCP为什么存在?首先要看TCP与UDP的区别,TCP与UDP都是传输层的协议,比较两者的区别主要应该是说TCP比UDP多了什么?
-
面向连接:TCP接收方与发送方维持了一个状态(建立连接,断开连接),双方知道对方还在。
-
可靠的:发送出去的数据对方一定能够接收到,而且是按照发送的顺序收到的。
-
流量控制与拥塞控制:TCP靠谱通过滑动窗口确保,发送的数据接收方来得及收。TCP无私,发生数据包丢失的时候认为整个网络比较堵,自己放慢数据发送速度。
-
TCP协议的可靠性让使用TCP开发更为简单,同时它的这种设计也导致了慢的特点。
-
TCP是为流量设计的(每秒内可以传输多少KB的数据),讲究的是充分利用带宽。
-
TCP为了实现网络通信的可靠性,使用了复杂的拥塞控制算法,建立了繁琐的握手过程以及重传策略。由于TCP内置在系统协议栈中,极难对其进行改进。
-
UDP协议简单,所以它更快。但是,UDP毕竟是不可靠的,应用层收到的数据可能是缺失、乱序的。
-
UDP协议以其简单、传输快的优势,在越来越多场景下取代了TCP,如网页浏览、流媒体、实时游戏、物联网。
随着网络技术飞速发展,网速已不再是传输的瓶颈,CDN服务商Akamai报告从2008年到2015年7年时间,各个国家网络平均速率由1.5Mbps提升为5.1Mbps,网速提升近4倍。网络环境变好,网络传输的延迟、稳定性也随之改善,UDP的丢包率低于5%,如果再使用应用层重传,能够完全确保传输的可靠性。
KCPKCP协议就是在保留UDP快的基础上,提供可靠的传输,应用层使用更加简单——TCP可靠简单,但是复杂无私,所以速度慢。KCP尽可能保留UDP快的特点下,保证可靠。
-
TCP是为流量设计的(每秒内可以传输多少KB的数据),讲究的是充分利用带宽。
-
KCP是为流速设计的(单个数据包从一端发送到一端需要多少时间),以10%-20%带宽浪费的代价换取了比 TCP快30%-40%的传输速度。
TCP信道是一条流速很慢,但每秒流量很大的大运河,而KCP是水流湍急的小激流。
MOBA类和“吃鸡”游戏多使用帧同步为主要同步算法,竞技性也较高,无论从流畅性,还是从公平性要求来说,对响应延迟的要求都最高,根据业内经验,当客户端与服务器的网络延迟超过150ms时,会开始出现卡顿,当延迟超过250ms时,会对玩家操作造成较大影响,游戏无法公平进行。类似地,“吃鸡”游戏(如《绝地求生》)玩法对玩家坐标、动作的同步要求极高,延迟稍大导致的数据不一致对体验都会造成较大影响,其实时性要求接近MOBA类游戏。而对于传统mmorpg来说,多采用状态同步算法,以属性养成和装备获取为关注点,也有一定竞技性,出于对游戏流畅性的要求,对延迟也有一定要求,同步算法的优化程度不一样,这一要求也不一样,一般情况下为保证游戏正常进行,需要响应延迟保持在300ms以下。相比之下,对于炉石传说、斗地主、梦幻西游等回合制游戏来说,同时只有一个玩家在操作双方数据,无数据竞争,且时间粒度较粗,甚至可通过特效掩盖延迟,因此对网络延迟的要求不高,即便延迟达到500ms~1000ms,游戏也能正常进行
不同传输层协议在可靠性、流量控制等方面都有差别,而这些技术细节会对延迟造成影响。
tcp追求的是完全可靠性和顺序性,丢包后会持续重传直至该包被确认,否则后续包也不会被上层接收,且重传采用指数避让策略,决定重传时间间隔的RTO(retransmission timeout)不可控制,linux内核实现中最低值为200ms,这样的机制会导致丢包率短暂升高的情况下应用层消息响应延迟急剧提高,并不适合实时性高、网络环境复杂的游戏。
基于udp定制传输层协议,引入顺序性和适当程度或者可调节程度的可靠性,修改流控算法。适当放弃重传,如:设置最大重传次数,即使重传失败,也不需要重新建立连接。比较知名的tcp加速开源方案有:quic、enet、kcp、udt。
kcp/quic/enet协议的区别先安利下《浅谈QUIC协议原理与性能分析及部署方案》,
-
quic 是一个完整固化的 http 应用层协议,目前已经更名 http/3,指定使用 udp(虽然本质上并不一定需要 udp)。其主要目的是为了整合TCP协议的可靠性和udp协议的速度和效率,其主要特性包括:避免前序包阻塞、减少数据包、向前纠错、会话重启和并行下载等,然而QUIC对标的是TCP+TLS+SPDY,相比其他方案更重,目前国内用于网络游戏较少
-
kcp 只是一套基于无连接的数据报文之上的连接和拥塞控制协议,对底层【无连接的数据报文】没有具体的限制,可以基于 udp,也可以基于伪造的 tcp/icmp 等,也可以基于某些特殊环境的非 internet 网络(比如各种现场通信总线)
-
enet: 有ARQ协议。收发不用自己实现,提供连接管理,心跳机制。支持人数固定。自己实现跨平台。支持可靠无序通道。没有拥塞控制。线程不安全
其实kcp不能和quic对比(quic vs enet),只是讲到UDP的时候,顺带搭上QUIC协议,类似的还有WebRTC
为什么采用UDP,而不是其他的协议呢?比如SCTP天生就具备TCP/UDP所不具备的各种优点(支持多宿主多流分帧可无序抗syn flooding),但是就比如Windows系统,各种路由器、网关都不支持,无法铺开(除非在私有网络或者专用网络中用)。况且,TCP/UDP的各种问题很多都已经通过技术或技巧给解决了。
KCP的配置模式在网络中,我们认为传输是不可靠的,而在很多场景下我们需要的是可靠的数据,所谓的可靠,指的是数据能够正常收到,且能够顺序收到,于是就有了ARQ协议,TCP之所以可靠就是基于此。
ARQ协议(Automatic Repeat-reQuest),即自动重传请求,是传输层的错误纠正协议之一,它通过使用确认和超时两个机制,在不可靠的网络上实现可靠的信息传输。
ARQ协议有两种模式: 停等ARQ协议同步请求响应模式,基于超时重传保证可靠。
-
A会为每个即将发送的数据编号,编号的目的是为了标识数据和给数据排序
-
A发送完数据之后,会给这次发送的数据设置一个超时计时器
-
B收到数据,将会返回一个确认,该确认也有自己的编号
-
A收到确认,将删除副本且取消超时计时器,保留副本的原因是传输可能出错
-
B收到错误的数据,或者数据在传输过程中出错,总之就是说B没有收到想要的数据
-
A在超时计时器的设置时间内没有收到确认,此时重发数据
所以可靠的TCP有32位序列号和32位确认号,TCP和UDP都有16位校验和。
连续ARQ协议可以连续发送多个分组,而不必每发完一个分组就停下来等待对方确认。
是不是想到了HTTP1.1中的管道模式与HTTP1.0停等模式,但这里有些许区别,HTTP1.1是中服务器按照顺序响应客户端请求,但连续ARQ协议不会响应每个数据段,而是仅仅响应编号最大的这个数据段,表示之前的数据都收到了,这个叫做UNA模式,而停等ARQ协议可以看作是ACK模式。
现在已经能够在不可靠的网络中传输可靠的数据,但这不意味着可以随意发送数据,带宽是有限的,接收方的负载也是有限的,所以引入了窗口协议,做流量控制。
窗口协议中有两种: 拥塞窗口防止过多的数据注入到网络中,这样可以使网络中的路由器 和链路不至于过载。
与拥塞控制相关的有慢启动、退半避让、快重传、快恢复等。
慢启动是在刚开始发送数据时让窗口缓慢扩张,退半避让是在网络拥堵时窗口大小减半,快重传是在网络恢复时及时给予响应,与之配合的就是快恢复。
滑动窗口接收方告知发送方自己可以接收缓冲区的大小,通常与连续ARQ协议配合使用。
TCP协议中的16位窗口大小就是为窗口协议提供支持的。而UDP协议的目标是尽最大努力交付,不管你收到没有,所以没有该字段。
TCP协议是面向连接的协议,在数据传输前通过三次握手建立连接,传输完成后通过四次挥手断开连接,整个过程表示一次完整的数据传输,所以需要4位头长告知哪些是正在传输的数据。
UDP协议是无连接的,两次数据传输没有任何联系,所以需要16位长度告知本次传输的数据有多少。同时注意,UDP协议每次传输的数据量并不是2^16 - 1 - 8 - 20(8表示UDP头长,20表示IP头长),而是与MTU有关,即数据链路层的最大传输单元(Maximum Transmission Unit),值是1500。
TCP协议中的8位标志位表示不同的功能,例如当SYN = 1时表示建立连接时让ack = seq + 1而不做任何验证,当URG = 1时16位紧急指针生效,紧急指针表示正常数据的起始位置,而之前的数据则表示额外的紧要数据,可以被尽快处理。
当清楚TCP和UDP的工作流程,KCP就很容易理解了。
KCP工作模式:
KCP协议默认模式是一个标准的 ARQ,需要通过配置打开各项加速开关:
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代表关闭。
KCP有正常模式和快速模式两种,通过以下策略达到提高流速的结果:
-
普通模式/正常模式: ikcp_nodelay(kcp, 0, 40, 0, 0);
-
极速模式/快速模式: ikcp_nodelay(kcp, 1, 10, 2, 1)
int ikcp_wndsize(ikcpcb *kcp, int sndwnd, int rcvwnd);
该调用将会设置协议的最大发送窗口和最大接收窗口大小,默认为32. 这个可以理解为 TCP的 SND_BUF 和 RCV_BUF,只不过单位不一样 SND/RCV_BUF 单位是字节,这个单位是包。
最大传输单元:纯算法协议并不负责探测 MTU,默认 mtu是1400字节,可以使用ikcp_setmtu来设置该值。该值将会影响数据包归并及分片时候的最大传输单元。
最小RTO:TCP超时计算是RTOx2,这样连续丢三次包就变成RTOx8了,十分恐怖,而KCP启动快速模式后不x2,只是x1.5(实验证明1.5这个值相对比较好),提高了传输速度
KCP对比TCP配置 RTO翻倍vs不翻倍:-
TCP超时计算是RTOx2,这样连续丢三次包就变成RTOx8了,十分恐怖
-
KCP启动快速模式后不x2,只是x1.5(实验证明1.5这个值相对比较好),提高了传输速度
-
TCP丢包时会全部重传从丢的那个包开始以后的数据
-
KCP是选择性重传,只重传真正丢失的数据包。(TCP同样有选择重传SACK,但有区别,后续文章再介绍)。
与TCP相同,都是通过累计确认实现的,发送端发送了1,2,3,4,5几个包,然后收到远端的ACK:1,3,4,5,当收到ACK = 3时,KCP知道2被跳过1次,收到ACK = 4时,知道2被跳过了2次,此时可以认为2号丢失,不用等超时,直接重传2号包,大大改善了丢包时的传输速度。1字节cmd = 81时,sn相当于TCP中的seq,cmd = 82 时,sn相当于TCP中的ack。cmd相当于WebSocket协议中的openCode,即操作码。
延迟ACK vs 非延迟ACK:TCP在连续ARQ协议中,不会将一连串的每个数据都响应一次,而是延迟发送ACK,即上文所说的UNA模式,目的是为了充分利用带宽,但是这样会计算出较大的RTT时间,延长了丢包时的判断过程,而KCP的ACK是否延迟发送可以调节。
-
TCP为了充分利用带宽,延迟发送ACK(NODELAY都没用),这样超时计算会算出较大 RTT时间,延长了丢包时的判断过程。
-
KCP的ACK是否延迟发送可以调节。
ARQ模型响应有两种,UNA(此编号前所有包已收到,如TCP)和ACK(该编号包已收到),光用UNA将导致全部重传,光用ACK则丢失成本太高,以往协议都是二选其一,而 KCP协议中,除去单独的 ACK包外,所有包都有UNA信息。
非退让流控:KCP正常模式同TCP一样使用公平退让法则,即发送窗口大小由:发送缓存大小、接收端剩余接收缓存大小、丢包退让及慢启动这四要素决定。但传送及时性要求很高的小数据时,可选择通过配置跳过后两步,仅用前两项来控制发送频率。以牺牲部分公平性及带宽利用率之代价,换取了开着BT都能流畅传输的效果
在传输及时性要求很高的小数据时,可以通过配置忽略上文所说的窗口协议中的拥塞窗口机制,而仅仅依赖于滑动窗口。2字节wnd与TCP协议中的16位窗口大小意义相同,值得一提的是,KCP协议的窗口控制还有其它途径,当cmd = 83时,表示询问远端窗口大小,当cmd = 84时,表示告知远端窗口大小。
4字节conv表示会话匹配数字,为了在KCP基于UDP实现时,让无连接的协议知道哪个是哪个,相当于WEB系统HTTP协议中的SessionID。
1字节frg表示拆数据时的编号,4字节len表示整个数据的长度,相当于WebSocket协议中的len。
IKCPCB结构
IKCPCB是KCP中最重要的结构,也是在会话开始就创建的对象,代表着这次会话,所以这个结构体体现了一个会话所需要涉及到的所有组件。其中一些参数在IKCPSEG中已经描述,不再多说。
-
conv:标识这个会话;
-
mtu:最大传输单元,默认数据为1400,最小为50;
-
mss:最大分片大小,不大于mtu;
-
state:连接状态(0xFFFFFFFF表示断开连接);
-
snd_una:第一个未确认的包;
-
snd_nxt:下一个待分配的包的序号;
-
rcv_nxt:待接收消息序号。为了保证包的顺序,接收方会维护一个接收窗口,接收窗口有一个起始序号rcv_nxt(待接收消息序号)以及尾序号 rcv_nxt + rcv_wnd(接收窗口大小);
-
ssthresh:拥塞窗口阈值,以包为单位(TCP以字节为单位);
-
rx_rttval:RTT的变化量,代表连接的抖动情况;
-
rx_srtt:smoothed round trip time,平滑后的RTT;
-
rx_rto:由ACK接收延迟计算出来的重传超时时间;
-
rx_minrto:最小重传超时时间;
-
snd_wnd:发送窗口大小;
-
rcv_wnd:接收窗口大小;
-
rmt_wnd:远端接收窗口大小;
-
cwnd:拥塞窗口大小;
-
probe:探查变量,IKCP_ASK_TELL表示告知远端窗口大小。IKCP_ASK_SEND表示请求远端告知窗口大小;
-
interval:内部flush刷新间隔,对系统循环效率有非常重要影响;
-
ts_flush:下次flush刷新时间戳;
-
xmit:发送segment的次数,当segment的xmit增加时,xmit增加(第一次或重传除外);
-
rcv_buf:接收消息的缓存;
-
nrcv_buf:接收缓存中消息数量;
-
snd_buf:发送消息的缓存;
-
nsnd_buf:发送缓存中消息数量;
-
rcv_queue:接收消息的队列
-
nrcv_que:接收队列中消息数量;
-
snd_queue:发送消息的队列;
-
nsnd_que:发送队列中消息数量;
-
nodelay:是否启动无延迟模式。无延迟模式rtomin将设置为0,拥塞控制不启动;
-
updated:是否调用过update函数的标识;
-
ts_probe:下次探查窗口的时间戳;
-
probe_wait:探查窗口需要等待的时间;
-
dead_link:最大重传次数,被认为连接中断;
-
incr:可发送的最大数据量;
-
acklist:待发送的ack列表;
-
ackcount:acklist中ack的数量,每个ack在acklist中存储ts,sn两个量;
-
ackblock:2的倍数,标识acklist最大可容纳的ack数量;
-
user:指针,可以任意放置代表用户的数据,也可以设置程序中需要传递的变量;
-
buffer:存储消息字节流;
-
fastresend:触发快速重传的重复ACK个数;
-
nocwnd:取消拥塞控制;
-
stream:是否采用流传输模式;
-
logmask:日志的类型,如IKCP_LOG_IN_DATA,方便调试;
-
output udp:发送消息的回调函数;
-
writelog:写日志的回调函数。
参考文章:
在网络中狂奔:KCP协议 https://zhuanlan.zhihu.com/p/112442341
可靠UDP,KCP协议快在哪? https://wetest.qq.com/lab/view/391.html
KCP 协议与源码分析(一) https://github.com/skywind3000/kcp
网络编程懒人入门(五):快速理解为什么说UDP有时比TCP更有优势 http://www.52im.net/thread-1277-1-1.html
转载本站文章《KCP协议:从TCP到UDP家族QUIC/KCP/ENET》,
请注明出处:https://www.zhoulujun.net/html/theory/ComputerScienceTechnology/network/2016_0106_387.html