游戏在弱网情况下会变得体验很差,玩家的直观感受就是我的操作怎么没有反应,整个游戏世界都是一卡一顿的。这个就是因为网络问题导致了游戏体验变差。
那什么是弱网环境?弱网环境就是指网络不好的环境,尤其是移动网络下,因为移动网络下,我们经常会进出电梯或者去到一些信号不好的区域,又或者wifi和移动网络处于切换过程中,导致网络突然变坏,此时网络的时延和丢包率会增大。但是当走出这块区域后,网络又正常起来,这就是移动网络面临的问题,这个也是手游网络传输需要解决的技术难题。
从TCP的时延谈起
网络丢包有随机性丢包和相关性丢包两种。
相关性丢包:
链路阻塞
路由器负载过高
无线信号衰减
基站,场景切换
随机性丢包:原因是二进制信道噪声,无规律随机出现,89%的丢包都属于这种。
连续K次丢包的概率是很低的,因此重传是能解决丢包问题,如果2次重传不够,那就发3次。
重传机制
通信的可靠性始终是最需要考虑的指标,而这又是TCP的优点。不少游戏也是在用TCP做游戏通信。在网络良好的情况下,TCP的通信速度其实很快,但到了网络不好的情况下,网络时延会因为TCP的一些机制导致时延进一步增大。比如TCP的三次握手产生的延迟,当网络不佳时,TCP的长链接有可能断开,此时需要多次进行三次握手,增加了不少时间延迟。
TCP的哪些机制还导致了通信时延进一步增大呢?其实是TCP的重传机制。
当发生网络发生丢包时,重传机制是保证传输可靠性的重要手段,但是重传的机制会引入大量的延迟。
重传方式分为三类:定时重传,请求重传和FEC选择重传。
定时重传
发送端如果在发出数据包后一个RTO之后还未收到这个数据包的ACK消息,那么就重传这个数据包。这种方式依赖于接收端的ACK和RTO,容易产生误判,主要有两种情况:
对方收到了数据包,但是ACK发送途中丢失。
ACK在途中,但是发送端的时间已经超过了一个RTO。
所以超时重传的方式主要集中在RTO的计算上,如果你的场景是一个对延迟敏感但对流量成本要求不高的场景,就可以将RTO的计算设计比较小,这样能尽最大可能保证你的延时足够小,但缺点就是很可能对方ACK没来得及到达发送方时,发送方已经开始重传了,使得冗余发送了。例如:实时操作类网游、教育领域的书写同步,是典型的用expense换latency和Quality的场景,适合用于小带宽低延迟传输。如果是大带宽实时传输,定时重传对带宽的消耗是很大的,极端情况会用20%的重复重传率,所以在大带宽模式下一般会采用请求重传模式。
上面提到了RTO,什么是RTO呢?
RTO(Retransmission Time Out):重传超时时间,即从数据发送时刻算起,超过这个时间便执行重传, RTO协议实现值最小1s。
一般而言,RTO时间是需要大于RTT的,RTT又是什么意思呢?
RTT(Round Trip Time):一个连接的往返时间,即数据发送时刻到接收到确认的时刻的差值。
由于网络波动的不确定性,每个RTT都是动态变化的,所以 RTO 也应随着 RTT 动态变化。
当 RTO < RTT 时, 将会触发大量的重传, 当 RTO > RTT 时候, 如果频繁出现丢包, 重传不及时, 又会造成网络的反应慢, 最好的结果是 RTO 略大于 RTT.
请求重传
请求重传就是接收端在发送ACK的时候携带自己丢失报文的信息反馈,发送端接收到ACK信息时根据丢包反馈进行报文重传。
这个反馈过程最关键的步骤就是回送ACK的时候应该携带哪些丢失报文的信息。请求重传这种方式比定时重传方式的延迟会大,一般适合于带宽较大的传输场景,例如:视频、文件传输、数据同步等。
FEC选择重传
FEC(Forward Error Correction)是一种前向纠错技术,一般是通过XOR类似的算法来实现,也有多层的EC算法和raptor涌泉码技术,其实是一个解方程的过程,核心思想是冗余发送来减少重传概率。
在发送方发送报文的时候,会根据FEC方式把几个报文进行FEC分组,通过XOR的方式得到若干个冗余包,然后一起发往接收端,如果接收端发现丢包但能通过FEC分组算法还原,就不向发送端请求重传,如果分组内包是不能进行FEC恢复的,就请求想发送端请求原始的数据包。FEC分组方式适合解决要求延时敏感且随机丢包的传输场景,在一个带宽不是很充裕的传输条件下,FEC会增加多余的冗余包,可能会使得网络更加不好。FEC方式不仅可以配合请求重传模式,也可以配合定时重传模式。
TCP重传机制带来的时延
回顾TCP的重传机制,TCP依靠ARQ进行重传。ARQ协议(Automatic Repeat-reQuest),即自动重传请求,是传输层的错误纠正协议之一,它通过使用确认和超时两个机制,在不可靠的网络上实现可靠的信息传输。
ARQ协议有3种模式:
1.停等ARQ协议
发送M1包后没有收到ACK,那就等到超时才重发M1,然后才能发M2。缺点就是带宽没有利用好,过于空闲了,而且等待时间有点长,包的发送依赖于上一个数据包的成功发送。
2.连续ARQ协议:可以连续发送好几个数据包,M3的包没有收到ack,因此触发超时重传,此时重传的是M3开始的M3,M4,M5,接收方前面正确收到的M4,M5都会被丢弃(M3以后的包都被丢弃),等待重传的M4,M5。缺点是冗余发送了,丢的是M3,却要重传M3后的包,网络带宽的利用变低了(浪费)。此外,因为网络不好的情况下,重传的M4,M5还是有可能发送丢包,也就是触发了更多重试的可能,也就是网络时延增大了。在出现丢包的时候,一般是网络拥塞,大量的重传又可能进一步加剧拥塞。
3.SACK:Selective-Ack,选择重传。连续ARQ协议暴露的缺点是,哪些包成功送达、哪些包丢了对于发送端而言并不知情,因此一个思路是在给发送端回ack包时,是否可以把【哪些包成功收到】的信息带回给发送端呢?收到这个信息后的发送端,就知道该重传具体哪一个包了。比如还是上面的例子,M3丢失,但M4,M5成功送达接收端。M4,M5会被缓存起来,发送端只需对M3重传,不必重传M4,M5了。SACK是需要额外开启的。
上面的问题是由于单纯以时间驱动来进行重传的,都必须等待一个超时时间,不能快速对当前网络状况做出响应,如果加入以数据驱动呢?TCP引入了一种叫Fast Retransmit(快速重传 )的算法,就是在连续收到3次相同确认号的ACK,那么就进行重传。这个算法基于这么一个假设,连续收到3个相同的ACK,那么说明当前的网络状况变好了,可以重传丢失的包了,这就大大减少了重传等待时间。
了解到TCP的重传机制带来的时延增大的原因后,有几个方法来减少时延:
RTO不翻倍。TCP是基于ARQ协议实现的可靠性,TCP每发生一次重传,RTO就加倍,即RTO=2*RTO,该方法称为指数退避。所以当丢包严重时,重传等待时间会越来越长。那么为了减少时延,我们可以减少这个RTO为1.5RTO,假设超时计算是RTO1.5,也就是说假如连续丢包3次,TCP是RTO8,那么新的机制RTO3.375,意味着可以更快地重新传输数据。
使用SACK机制,也就是只重传真正丢失的那个包。
使用快速重传,在连续收到3次相同确认号的ACK,那么就进行重传。
TCP分段、IP分组带来的问题
以太网的链路层对数据帧的长度会有一个限制,其最大值默认是1500字节,链路层的这个特性称为MTU,即最大传输单元。
数据链路层的有效数据,最小46byte,最大一般1500byte,这里的最大就是MTU,MTU表示网络层必须将发给网卡API的包 <= 1500byte,否则调用失败,所以在IP层就必须对大包先分好组,保证传递到数据链路层的数据不会超过一个1500字节。tcp是可靠传输协议,TCP在报文段中发送MSS选项的终端利用该选项来对端TCP实体通告本端点在一个报文段中所能够接受的最大数据长度。若没有指定这个选项意味着本终端能够接受任何长度的报文段。值得注意的是,MSS是为了约束对方发送数据的大小,而不是自己,所以假设自己发送大于MSS的数据,将会在IP层进行分片。如果要传输的数据大于 1500- 20(ip头部) - 20(tcp头部) =1460Byte时,那MSS(最大报文段长度)就是1460字节,当应用层传输的数据超过MSS时,就会触发分片。在ip层会被分片,而 ip层分片会导致,如果其中的某一个分片丢失,因为tcp层不知道哪个ip数据片丢失,所以 就需要重传整个数据段,这样就造成了很大空间和时间资源的浪费。所以一个解决时延的思路,就是控制发送的数据要小于一个MSS,这样就避免了数据分片的发生。
Nagle算法和delay ack机制带来的问题
Nagle算法和delay ack机制是减少发送端和接收端包量的两个机制,可以有效减少网络包量,避免拥塞。但是,在特定场景下,Nagle算法要求网络中只有一个未确认的包, 而delay ack机制需要等待更多的数据包, 再发送ACK回包, 导致发送和接收端等待对方发送数据, 造成死锁, 只有当delay ack超时或者发送方等待超时后才能解开死锁,进而导致应用侧对外的延时高。
这里简单介绍下Nagle算法和delay ack出现的背景:考虑发送一个字节的的情景,每次发送一个字节的有用数据,就会产生41个字节长的分组,20个字节的IP Header 和 20个字节的TCP Header,这就导致了1个字节的有用信息要浪费掉40个字节的头部信息,这是一笔巨大的字节开销,而且这种Small packet在广域网上会增加拥塞的出现。Nagle算法可以在发送端收集小包后一次发送,delay ack就是在接收端等待一会儿对多个ack包一次发送。
在默认的情况下,Nagle算法和延迟ACK是默认开启,也就是说TCP_NODELAY和TCP_QUICKACK默认关闭。如果服务器开启延迟ACK、客户端开启Nagle算法 ,就很容易导致网络延迟增大。所以为了减少网络时延,可以开启TCP_NODELAY 和TCP_QUICKACK。
可靠UDP
对于游戏开发,尤其是MOBA(多人在线竞技)游戏,延迟是需要控制的。但是对于传统的TCP,并不利于包的实时性传输,因为他的超时重传和拥塞控制都是网络友好,对于我们包的实时性或者在弱网下的传输,没有优势。所以一般都是需要基于UDP去实现一套自己的网络协议,保证包的实时,以及可靠,这种协议我们统称为RUDP(Reliable-UDP)。本质上就是以空间(更多的带宽)换时间(更小的时延)。
RUDP技术要点
结合TCP可靠传输的策略,再根据我们时延的需求大于带宽的需求,RUDP的实现要点有以下几点:
1.使用定时重传和选择重传的机制来对抗丢包,这个重传策略可以在应用层实现,底层传输层用的是UDP。
2.UDP传输是无序的,需要在应用层利用序列号标记每个包,实现包的保序。
3.定时重传的RTO不再选择TCP那种二进制退避的RTO增长策略,而是采用更短的RTO来进行重传,比如1.5RTO,即缺点是冗余重传,浪费了带宽,好处是减少了重传时间,优化了网络时延。
4.使用选择重传,即在接收方发起ACK时,需要带上丢包的序列号,发送端知道具体丢的包后,只重传丢失的包,其他的包不再重传,因为接收端成功接收到的包都被缓存起来了。重传的包越少,丢包的概率就越低。
5.快速重传,模拟TCP快重传的策略,发送端发送了1,2,3,4,5几个包,然后收到远端的ACK: 1, 3, 4, 5,当收到ACK3时,发送端知道2被跳过1次,收到ACK4时,知道2被跳过了2次,此时可以认为2号丢失,不用等超时,直接重传2号包,大大改善了丢包时的传输速度。
6.冗余发送,比如一次发送可以携带着多个之前还未收到确认的包一起发送,即使还未超时。比如发送端需要发送1,2,3,4,5几个包,那冗余发送的策略就是:1,12, 23, 34, 45。通过冗余包,来避免重传。
7.UDP分组优化:IP包过大会分组,IP分片越多,UDP包丢包概率就越大。策略就是将一个UDP大包分为几个UDP小包来发送。以太网:1500-20-8=1472;英特网:576-20-8=548。因为还存在各种子网,所以经验总结最佳包大小应限制在470字节以下。
7.流量控制可以采取与TCP的滑动窗口机制一样;拥塞控制也采取TCP的拥塞控制机制(慢开始、拥塞避免),但这个需要做成可关闭的选项,因为在追求极致的时延时,此时会非常自私的进行发送,不再考虑网络拥塞的情况。
TCP和RUDP在网络良好和网络丢包严重下的性能对比:
KCP
这里介绍一个游戏界常用的基于UDP的可靠传输协议KCP,在一些强调实时性的竞技游戏,多数采用KCP的来做网络传输。
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号包,大大改善了丢包时的传输速度。