写点什么

KCP 协议:从 TCP 到 UDP 家族 QUIC/KCP/ENET

作者:zhoulujun
  • 2022 年 3 月 27 日
  • 本文字数:6499 字

    阅读完需:约 21 分钟

行文前先安利下《再深谈TCP/IP三步握手&四步挥手原理及衍生问题—长文解剖IP 》、《再谈UDP协议—浅入理解深度记忆

KCP 协议科普

KCP 是一个快速可靠协议,能以比 TCP 浪费 10%-20%的带宽的代价,换取平均延迟降低 30%-40%,且最大延迟降低三倍的传输效果。

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


KCP协议传输流程图解


整个 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/UDP/KCP

TCP

  • TCP 协议的可靠性让使用 TCP 开发更为简单,同时它的这种设计也导致了慢的特点。

  • TCP 是为流量设计的(每秒内可以传输多少 KB 的数据),讲究的是充分利用带宽

  • TCP 为了实现网络通信的可靠性,使用了复杂的拥塞控制算法,建立了繁琐的握手过程以及重传策略。由于 TCP 内置在系统协议栈中,极难对其进行改进。 

UDP

  • UDP 协议简单,所以它更快。但是,UDP 毕竟是不可靠的,应用层收到的数据可能是缺失、乱序的。

  • UDP 协议以其简单、传输快的优势,在越来越多场景下取代了 TCP,如网页浏览、流媒体、实时游戏、物联网。

随着网络技术飞速发展,网速已不再是传输的瓶颈,CDN 服务商 Akamai 报告从 2008 年到 2015 年 7 年时间,各个国家网络平均速率由 1.5Mbps 提升为 5.1Mbps,网速提升近 4 倍。网络环境变好,网络传输的延迟、稳定性也随之改善,UDP 的丢包率低于 5%,如果再使用应用层重传,能够完全确保传输的可靠性。

KCP

KCP 协议就是在保留 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 协议

同步请求响应模式,基于超时重传保证可靠。


停等ARQ协议传输过程


  1. A 会为每个即将发送的数据编号,编号的目的是为了标识数据和给数据排序

  2. A 发送完数据之后,会给这次发送的数据设置一个超时计时器

  3. B 收到数据,将会返回一个确认,该确认也有自己的编号

  4. A 收到确认,将删除副本且取消超时计时器,保留副本的原因是传输可能出错

  5. B 收到错误的数据,或者数据在传输过程中出错,总之就是说 B 没有收到想要的数据

  6. A 在超时计时器的设置时间内没有收到确认,此时重发数据

所以可靠的 TCP 有 32 位序列号和 32 位确认号,TCP 和 UDP 都有 16 位校验和。

连续 ARQ 协议

可以连续发送多个分组,而不必每发完一个分组就停下来等待对方确认。


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

选择性重传 vs 全部重传:

  • 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 是否延迟发送可以调节。

UNA vs ACK+UNA:

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

用户头像

zhoulujun

关注

还未添加个人签名 2021.06.25 加入

还未添加个人简介

评论

发布
暂无评论
KCP协议:从TCP到UDP家族QUIC/KCP/ENET_网络加速_zhoulujun_InfoQ写作平台