一、简介
云真机已经支持手机投屏。云真机的实时运行对时延的要求比远程视频对话(100ms以内)更高。在无线网络中,如何实时、更可靠地传输视频流成为一个挑战。通过websocket、RTMP、UDP的对比,最终选择了可靠的UDP协议KCP进行实时音视频传输。
二、KCP简介
KCP 是一种快速可靠的协议,与 TCP 相比,它可以减少 30%-40% 的平均延迟,并将最大延迟减少三倍,代价是浪费 10%-20% 的带宽。纯算法实现不负责底层协议(如UDP)的收发。用户需要定义底层数据包的发送方式,以回调的形式提供给KCP。连时钟都需要从外面传入,里面不会有系统调用。本文的传输协议考虑了UDP的情况。
名词说明(源代码字段):
三、如何使用KCP 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.循环调用更新
// 以一定频率调用 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)接收数据。
总结:UDP收到的数据包通过kcp_input源源不断地馈送到KCP,KCP会把这部分数据(KCP协议数据)解包,重新打包成应用层用户数据,应用层可以通过kcp_recv获取。应用层通过kcp_send发送数据,KCP会将用户数据拆分成kcp包,通过kcp_output以UDP(send)的形式发送出去。
4. KCP的使用方法
KCP文档这部分介绍,大家不用太花心思去了解KCP协议。协议默认模式为标准ARQ,需要配置开启各种加速开关:
1、工作方式
int ikcp_nodelay(ikcpcb *kcp, int nodelay, int interval, int resend, int nc)
普通模式: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的单位是byte,单位是packet。
3、最大传输单元
纯算法协议不负责检测MTU,默认mtu为1400字节,可以使用ikcp_setmtu设置该值。该值将影响合并和分片数据包时的最大传输单元。
4. 最低 RTO
TCP和KCP在计算RTO时都有一个最小RTO限制。即使计算出的 RTO 为 40ms,由于默认的 RTO 为 100ms,协议只能在 100ms 后检测到丢包。在快速模式下,它是 30ms。您可以手动更改此值:
kcp->rx_minrto = 10;
相关视频推荐
腾讯端提问:UDP是如何实现可靠传输的?
10篇网络八卦文,每一篇都非常经典,让你在面试中充满力量
网络原理tcp/udp、网络编程epoll/reactor、各大厂商面试高频技术点
学习地址:c/c++ linux服务器开发/后台架构师
需要C/C++ Linux服务器架构师学习资料加qun812855908获取(资料包括C/C++、Linux、golang技术、Nginx、ZeroMQ、MySQL、Redis、fastdfs、MongoDB、ZK、流媒体、CDN、P2P、K8S、Docker、 TCP/IP、协程、DPDK、ffmpeg等),免费分享
5. 为什么会存在KCP
首先,我们需要看一下TCP和UDP的区别。TCP和UDP都是传输层协议。两者的区别应该说TCP比UDP多:
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应用层发送的数据要么全接收,要么在接收缓冲区足够时不接收。
总结:TCP可靠简单,但复杂无私,所以速度慢。KCP尽可能保留UDP的快速特性,保证可靠性。
六、KCP原理 一、协议简介
KCP是一种可靠的传输协议,而UDP本身是不可靠的,所以需要额外的信息来保证传输数据的可靠性。因此,我们需要为传输的数据添加一个头部。用于保证数据的可靠性和有序性。
下面的讲解主要针对极速模式:ikcp_nodelay(kcp, 1, 10, 2, 1),开启nodelay设置,控制刷新间隔10ms,开启快速重传模式,关闭流控。
2.数据发送流程
1.数据发送准备
用户发送数据的函数是ikcp_send。
ikcp_send(ikcpcb kcp,常量字符缓冲区,int len)
这个函数的功能很简单。它根据MSS划分用户发送的数据。如上图,用户发送1900字节数据,MTU为1400字节。因此,这个函数会将1900byte的用户数据分成两个包,一个数据大小为1400,header frg设置为1,len设置为1400;在第二个数据包中,header frg设置为0,len设置为500。将KCP数据包切割后放入名为snd_queue的队列中发送。
注意:在streaming模式下,kcp会将两次发送的数据连接成一个完整的kcp数据包。在非流模式下,用户数据 %MSS 数据包也将作为数据包发送。
MTU,数据链路层规定的每帧的最大长度,超过这个长度的数据将被分片。通常MTU的长度为1500字节,IP协议规定所有路由器都应该能够转发(512个数据+60个IP报头+4个保留=576字节)数据。MSS,最大输出大小(双方约定),KCP的大小为MTU-kcp header 24字节。IP 数据报越短,路由器转发速度越快,但资源利用率越低。当传输链路上的所有MTU都相同时,效率最高,应尽量避免数据传输项目,重新划分。UDP再次划分后(一般是1分为2),只要其中任何一份丢失,都必须重传两份。所以数据包mtu,
以太网的MTU通常为1500字节-IP头(20字节固定+40字节可选)-UDP头8字节=1472字节。KCP 考虑了多种传输协议,但在 UDP 的情况下,设置为 1472 字节更为合理。
2.实际发送
KCP会持续更新更新最新情况,实际发送数据会在更新期间进行。发送流程如下图所示:
第一步:等待队列移动到发送队列:
第二步:发送发送队列中的数据:
发送队列包含两种数据,一种是已经发送但还没有被接收方确认的数据,另一种是还没有发送的数据。没有发送的数据比较好处理,直接发送即可。重点是已经发送但未被接收方确认的数据。这部分的策略直接决定了协议是否快速高效。KCP主要使用两种策略来决定是否重传KCP数据包,超时重传、快速重传和选择性重传。
超时重传:
TCP超时计算是RTOx2,所以连续丢3个包就变成RTOx8,KCP非fast模式每次+RTO,fast模式+0.5RTO(实验证明1.5这个值比较好),提高了传输速度。
快速重传:
发送端发送了几个1,2,3,4,5的数据包,然后收到了远端的ACK:1,3,4,5。当收到ACK3的时候,KCP知道2被跳过了一次,收到ACK4的时候,知道2已经跳过了两次,此时可以认为2号丢包,不等待超时,直接重传2号包,大大提高了丢包时的传输速度。TCP有一个快速重传算法,TCP包被跳过3次后会重传。注意:您可以通过计算错误重新传输来优化此设置(重新传输的数据包实际上并没有丢失,只是乱序)。
选择重传:
当旧的TCP丢失一个数据包时,它会重传从丢失的数据包开始的所有数据。KCP是选择性重传,只重传真正丢失的数据包。不过目前大部分操作系统,linux和android手机都支持SACK选择重传。
第三步:数据发送:
通过第2步判断是否需要发送kcp包,如果需要发送kcp包,则通过kcp_setoutput设置的发送接口发送,一般为UDP sendto。在第3步中,将较小的kcp数据包合并一次发送,以提高效率。
3、数据接收流程
KCP的接收过程是将UDP接收到的数据进行解包,重新组合成时序可靠的数据交付给用户。
1、KCP包接收
kcp_input 输入 UDP 收到的数据包。kcp包解压前24个字节,包括conv、frg、cmd、wnd、ts、sn、una、len。根据una,snd_buf中una之前的所有kcp包都会被删除,因为这些包的接收者已经确认。根据wnd更新接收端的接收窗口大小。根据不同的命令字分别处理。接收到数据后,更新过程如下:
1、IKCP_CMD_PUSH数据发送命令:
一个。KCP会将接收到的数据包的sn和ts放在acklist中,相邻的两个节点组成一个group分别存储sn和ts。更新时,将读取确认列表,并使用命令 IKCP_CMD_ACK 返回确认数据包。如下图,收到两个kpc包,acklist中会分别存入10、123、11、124。
b. kcp数据包放在rcv_buf队列中。丢弃接收窗口外的数据包并重复。然后将rcv_buf中的数据包移动到rcv_queue中。原来的rcv_buf已经有sn=10和sn=13的包数据包mtu,而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的情况是异常情况。sn17,收到一个没有发送的序列号的概率极低,可能是没有改conv就重启程序造成的;112,开始快速重传。
为了确认包的发送,所有收到的包都会被放入acklist中,并与IKCP_CMD_ACK包一起发送出去。
七、流量控制和拥塞控制 1、RTO计算(和TCP完全一样) 2、流量控制
流量控制是对点到点流量的控制,是一个端到端的问题。综上所述,发送方的速度必须与接收方接收(处理)数据的速度相匹配。发送方应该抑制自己的发送速率,以便接收方能够及时接收。
KCP的发送机制采用了TCP的滑动窗口方式,可以非常方便地控制流量。KCP头包含wnd,这是接收方当前可以接收的大小。可以发送的数据是snd_una和snd_una+wnd之间的数据。接收方每次都会告诉发送方我能接收多少,发送方控制它保证自己发送的数据不超过接收方可以接收的大小。
KCP默认为32,即最多可以接收32*MTU=43.75kB。KCP采用更新方式,更新间隔为10ms。然后KCP限制你的最大传输速率为4375kB/s。在大内容高速传输的情况下,需要调用ikcp_wndsize来调整接收和发送窗口。
KCP的主要特点是实时性高。对于实时性高的应用,如果发生数据堆积,延迟会不断增加。建议最好控制应用端的发送流量与网速相等,避免缓存堆积延迟。
3.拥塞控制(可以关闭KCP)
KCP的优点是可以完全关闭拥塞控制,非常自私的发送。KCP采用的拥塞控制策略是TCP最古老的策略,没有任何优势。完全关闭拥塞控制不是最优策略,它都会导致更多的拥塞。
网络中链路的带宽与整个网络中的交换节点(路由器、交换机、基站等)有关。如果使用链路的所有流量都超过了链路可以提供的容量,就会发生拥塞。如果车多,路窄,就会堵车,而且车越多,堵车就越严重。因此,作为一种无私的协议,TCP在网络拥塞时会降低发送数据的速度。拥塞控制是整个网络的事情,流量控制是发送方和接收方的事情。
当发送方没有按时收到确认包时,就认为网络拥塞了。TCP拥塞控制的方式归结为慢启动和拥塞避免,如下图所示:
KCP丢包情况下的拥塞控制策略与TCP Tahoe版本的策略一致。TCP Reno 版本已经使用了快速恢复策略。所以在丢包的情况下,KCP的拥塞控制策略其实比TCP更严格。
当发生快速重传和数据包乱序时,KCP采用TCP快速恢复策略。控制窗口调整为已发送但未收到ack+resent的数据包数量的一半。
注意:目前内核3.2以上的Linux默认采用Google改进的拥塞控制算法Proportional Rate Reduction for TCP。该算法的主要特点是cwnd如下图所示: