#网络基础知识 ## 网络字节序 大端字节序:Big Endian,高位字节存在起始位置 小端字节序:Little Endian,低位字节存放在起始位置 ``` int32 1234 16进制为 0x4d2 大端序为 0x00 0x00 0x04 0xd2 小端序为 0xd2 0x04 0x00 0x00 let uint8s = new Uint8Array([0x00,0x00,0x04,0xd2]); let uint32s = new Uint32Array(uint8s.buffer); console.log(uint32s); uint8s = new Uint8Array([0xd2,0x04,0x00,0x00]); uint32s = new Uint32Array(uint8s.buffer); console.log(uint32s); ``` ### 为什么存在两种字节序? 计算机处理低位字节的效率比较高,因为计算都是从低位开始的。所以,计算机的内部处理都是小端字节序。 但是,人类还是习惯读写大端字节序。所以,除了计算机的内部处理,其他的场合几乎都是大端字节序,比如网络传输和文件储存。 ### 为什么要处理字节序? 网络传输过程中,传输的是字节流,计算机收到数据以后进行处理的时候也是安装字节流顺序依次写入内存,如果不做处理,在不同的计算机系统上字节序不一样,还原生成的字节流数据就是错误的。 ### 什么时候需要处理字节序 跨机器通信 多字节编解码(int16,int32,float等) 二进制文件流 ## 网络传输协议 ## TCP ### RTT(Round-Trip Time) 往返时延。在计算机网络中它是一个重要的性能指标,表示从发送端发送数据开始,到发送端收到来自接收端的确认(接收端收到数据后便立即发送确认),总共经历的时延。 RTT=传播时延(往返)+排队时延(路由器和交换机的)+数据处理时延(应用层) ### 区分MSS与MTU 最大报文段长度(MSS)与最大传输单元(Maximum Transmission Unit, MTU)均是协议用来定义最大长度的。不同的是,MTU应用于OSI模型的第二层数据链接层,并无具体针对的协议。MTU限制了数据链接层上可以传输的数据包的大小,也因此限制了上层(网络层)的数据包大小。例如,如果已知某局域网的MTU为1500字节,则在网络层的因特网协议(Internet Protocol, IP)里,最大的数据包大小为1500字节(包含IP协议头)。MSS针对的是OSI模型里第四层传输层的TCP协议。因为MSS应用的协议在数据链接层的上层,MSS会受到MTU的限制。 ![](/api/file/getImage?fileId=628f2513a625330015000007) ### MTU最大传输单元(Maximum Transmission Unit) MTU是数据链路层的概念。MTU限制的是数据链路层的payload,假设你要传一个数据帧到服务器,路径为 1500 1500 1500 1500 笔记本-wifi路由器-公司出口路由器-服务器 每个节点都有一个MTU值 如果我改变笔记本的mtu值2000,然后发送一个2500的数据包,这时候这个就会产生分包的概念,这个怎么分的呢? 笔记本会分拆2000和500 到wifi路由器的时候2000又被分拆成1500和500 如果这个包的IP协议里面DF标志位为1,表示不支持分拆,这个包就直接丢弃了。 ![](/api/file/getImage?fileId=628f2513a625330015000001) #### 为什么是1500? 其实一个标准的以太网数据帧大小是:1518,头信息有14字节,尾部校验和FCS占了4字节,所以真正留给上层协议传输数据的大小就是:1518 - 14 - 4 = 1500,那么,1518这个值又是从哪里来的呢? 1518这个值是考虑到传输效率以及传输时间而折中选择的一个值,并且由于目前网络链路中的节点太多,其中某个节点的MTU值如果和别的节点不一样,就很容易带来拆包重组的问题,甚至会导致无法发送。 以太网帧结构由四个字段组成,各字段含义为: dest|src|type|data|crc 4 4 4 4 目的地址:该地址指的是MAC地址,指该数据要发送至哪里 源地址:MAC地址,填本地MAC地址,指该数据从哪里来 类型:指数据要交给上层(网络层)的那个协议(IP协议,ARP协议…) 数据:要传输的数据,不过该数据有长度的要求,是在46–1500字节之间,该长度称为最大传输单元即MTU 若数据长度不够46字节,则需要填充内容;若数据长度超过1500字节,则需要分片传输。 MTU对IP协议的影响 (1)IP报文在超过MTU后需要分片,接收端需要组装; (2)一旦分片后的IP报文有一部分丢失,则接收端组装会失败,对于整个P报文而言相当于传输失败,而IP协议不会负责重新传输数据; (3)由于MTU影响的IP报文的分片和组装会加大报文丢失的可能性; (4)报文的分片和组装由IP层自己做,会加大传输的成本,降低性能。 4 MTU对UDP协议的影响 (1)UDP协议的报头为固定的20字节; (2)若UDP数据的长度超过(1500-20)1480字节,则数据在网络层会分片; (3)数据的分片会加大数据丢失的可能性。 5 MTU对TCP协议的影响 (1)TCP协议的报头长度为20–60字节; (2)若TCP报文的总长度超过1500字节,则数据同样在网络层会分片; (3)TCP单个数据报的最大长度称为最大段尺寸MSS; (4)在TCP三次握手建立连接的时候,双方会商量传输中MSS的大小; (5)与UDP相同的是,分片越多数据丢包的可能性越大,可靠性也就越差。 ### 慢启动 最初的TCP的实现方式是,在连接建立成功后便会向网络中发送大尺寸的数据包,假如网络出现问题,很多这样的大包会积攒在路由器上,很容易导致网络中路由器缓存空间耗尽,从而发生拥塞。因此现在的TCP协议规定了,新建立的连接不能够一开始就发送大尺寸的数据包,而只能从一个小尺寸的包开始发送,在发送和数据被对方确认的过程中去计算对方的接收速度,来逐步增加每次发送的数据量(最后到达一个稳定的值,进入高速传输阶段。相应的,慢启动过程中,TCP通道处在低速传输阶段),以避免上述现象的发生。这个策略就是慢启动。 ![](/api/file/getImage?fileId=628f2513a625330015000002) 由于需要考虑拥塞控制和流量控制两个方面的内容,因此TCP的真正的发送窗口=min(rwnd, cwnd)。但是rwnd是由对端确定的,网络环境对其没有影响,所以在考虑拥塞的时候我们一般不考虑rwnd的值,我们暂时只讨论如何确定cwnd值的大小。关于cwnd的单位,在TCP中是以字节来做单位的,我们假设TCP每次传输都是按照MSS大小来发送数据的,因此你可以认为cwnd按照数据包个数来做单位也可以理解,所以有时我们说cwnd增加1也就是相当于字节数增加1个MSS大小 ### 拥塞避免 从慢启动可以看到,cwnd可以很快的增长上来,从而最大程度利用网络带宽资源,但是cwnd不能一直这样无限增长下去,一定需要某个限制。TCP使用了一个叫慢启动门限(ssthresh)的变量,当cwnd超过该值后,慢启动过程结束,进入拥塞避免阶段。对于大多数TCP实现来说,ssthresh的值是65536(同样以字节计算)。拥塞避免的主要思想是加法增大,也就是cwnd的值不再指数级往上升,开始加法增加。此时当窗口中所有的报文段都被确认时,cwnd的大小加1,cwnd的值就随着RTT开始线性增加,这样就可以避免增长过快导致网络拥塞,慢慢的增加调整到网络的最佳值. ### 拥塞 TCP认为网络拥塞的主要依据是它重传了一个报文段。TCP对每一个报文段都有一个定时器,称为重传定时器(RTO),当RTO超时且还没有得到数据确认,那么TCP就会对该报文段进行重传,当发生超时时,那么出现拥塞的可能性就很大,某个报文段可能在网络中某处丢失,并且后续的报文段也没有了消息,在这种情况下,TCP反应比较强烈 1.把ssthresh降低为cwnd值的一半 2.把cwnd重新设置为1 3.重新进入慢启动过程。 TCP拥塞控制窗口变化的原则是AIMD原则,即加法增大,乘法减小。 ``` 乘法减小:无论在慢启动阶段还是在拥塞控制阶段,只要网络出现超时,就是将cwnd置为1,ssthresh置为cwnd的一半,然后开始执行慢启动算法(cwnd<ssthresh)。 加法增大:当网络频发出现超时情况时,ssthresh就下降的很快,为了减少注入到网络中的分组数,而加法增大是指执行拥塞避免算法后,是拥塞窗口缓慢的增大,以防止网络过早出现拥塞。 这两个结合起来就是AIMD算法,是使用最广泛的算法。拥塞避免算法不能够完全的避免网络拥塞,通过控制拥塞窗口的大小只能使网络不易出现拥塞。 ``` ![](/api/file/getImage?fileId=628f2513a625330015000008) ### 选择重传 ![](/api/file/getImage?fileId=628f2513a625330015000004) ``` 发送窗口的大小为m,接收窗口的大小为n 接收方先接收序号不连续的分组,并发送ACK确认,然后等待发送方重发丢失的分组(发送方每收到一个ACK确认就会关闭相应的定时器,最终没有收到ACK确认的分组的定时器超时,发送方会再次重发) 收到重发的分组后给予ACK确认,再对全部分组进行排序,最后交给上层应用。 ``` ### 快速重传 收到3个相同的ACK。TCP在收到乱序到达包时就会立即发送ACK,TCP利用3个相同的ACK来判定数据包的丢失,此时进行快速重传. 快速重传做的事情有: ``` 1.把ssthresh设置为cwnd的一半 2.把cwnd再设置为ssthresh的值(具体实现有些为ssthresh+3) 3.重新进入拥塞避免阶段。 ``` ![](/api/file/getImage?fileId=628f2513a625330015000003) 快速重传机制要求接收方每收到一个失序的TCP报文段后就立即发出重复确认(为了使发送方及早知道没有到达对方)而不要等待自己发送数据时才进行确认。 快重传算法规定:发送方只要连续收到3个重复确认就应当立即重传未被确认的报文段。 快速重传算法是针对一个包的重传情况的,然而在实际中,一个重传超时可能导致许多的数据包的重传,因此当多个数据包从一个数据窗口中丢失时并且触发快速重传和快速恢复算法时,问题就产生了. ### 快恢复 当发送端收到连续三个重复的确认时,就执行“乘法减小”算法,把慢开始门限 ssthresh 减半。但接下去不执行慢开始算法。 由于发送方现在认为网络很可能没有发生拥塞,因此现在不执行慢开始算法,即拥塞窗口 cwnd 现在不设置为 1,而是设置为慢开始门限 ssthresh 减半后的数值,然后开始执行拥塞避免算法(“加法增大”),使拥塞窗口缓慢地线性增大。 ### TCP状态 |状态 | 描述| |:----:| :----: | |CLOSED| 呈阻塞,关闭状态,表示当前主机没有活动的传输连接或没有正在进行传输连接| |LISTEN |呈监听状态,表示服务器正在等待新的传输连接进入| |SYNRCVD| 表示服务器已经收到一个传输连接请求,但尚未确认| |SYNSENT| 表示客户端已经发出一个传输连接请求,等待服务器的确认| |ESTABLISHED| 传输连接建立| |FIN_WAIT_1| 主动关闭方的主机已经发送关闭连接请求,等待对方确认| |CLOSE_WAIT| 被动关闭方的主机收到主动关闭方的关闭连接请求,并已确认| |FIN_WAIT_2| 主动关闭方的主机已经收到对方对主动关闭连接请求的确认,等待对方发送关闭传输连接请求| |LAST_ACT| 被动关闭方的主机已经发送关闭连接请求,等到主动方确认| |TIME_WAIT| 主动关闭方的主机收到对方发送的关闭连接请求| ![](/api/file/getImage?fileId=628f2513a625330015000006) ## UDP TCP协议的优势在于有序、可靠、有连接,但是由于较为保守的超时重传方案,慢启动,滑动窗口变化,过大的包头,会给帧同步带来比较高的延迟和较大的数据流量,而UDP是较为精简的网络协议,不保证时序、不保证可靠性、无连接,但是这些都可以自行实现、定制,并且包头较小。 ## 数据协议 ### Protobuf优化 对于这类收发频繁的消息,如果使用Protobuf,会造成每个逻辑帧的GC,这是非常不好的,解决方案,要么对Protobuf做无GC改造,要么就自己实现一个简单的byte[]读写。无GC改造工程太大,感觉无必要,我们只是在战斗的几个频繁发送的消息,需要自己处理一下byte[]读写即可 ### 自定义数据协议 #帧同步定义 传统的帧同步,使用锁帧技术(LockStep),是指服务器按帧转发客户端的操作,客户端进行确定性运算和一致性模拟(同步操作两边客户端通过完全一致的操作计算出完全一致的状态)目的是为了实现高实时性,高同步性。这个技术的要点,是在进行广播某一帧时,在这之前必须收到所有对等端前置的通知应答,这么做目的是保证公平性(大家进程一致,要卡一起卡)。 #帧同步优化 根据不同项目类型、对操作手感的要求、联机玩家的个数等,会有不同的难点和痛点。不同的优化方向,优化手法的差异,可能导致一些争论 ##乐观帧锁定 > 某些游戏中,我们更关注的是一个玩家卡了,不能把其他玩家也卡住。因此我们采用乐观帧锁定方式。玩家操作以command形式,发送给帧同步服务器,服务器会把command插入某一个逻辑帧frame,然后在合适的时机把frame广播给所有玩家,游戏世界一致执行。在这个结构上,帧同步服务器,是以固定间隔把frame进行广播,某一个玩家卡了或者挂了,对于其他玩家是完全不可见,也是无影响的。 #帧同步和状态同步 状态同步:发送操作给服务端,服务端处理计算逻辑,更新客户端数据。 帧同步:发送操作给服务端。服务端中转所有数据,通过随机因子保证每次得出结果一样。 最大的区别就是战斗核心逻辑写在哪,状态同步的战斗逻辑在服务端,帧同步的战斗逻辑在客户端。 ![帧同步状态同步区别](/api/file/getImage?fileId=628f2513a625330015000005) ![帧同步参考点](/api/file/getImage?fileId=628f2513a625330015000009) #预测回滚 > 一个好的网络同步系统必须实现预测、有预测就有预测失败的情况,发生后要解决冲突,回滚状态是必须支持的。而状态回滚还包括了只回滚部分状态,而不能简单回滚整个世界。 # 常用udp方案 ## 基于可靠传输的UDP > 基于可靠传输的UDP,是指在UDP上加一层封装,自己去实现丢包处理,消息序列,重传等类似TCP的消息处理方式,保证上层逻辑在处理数据包的时候,不需要考虑包的顺序,丢包等。类似的实现有Enet,KCP等。 ## 冗余信息的UDP > 是指需要上层逻辑自己处理丢包,乱序,重传等问题,底层直接用原始的UDP,或者用类似Enet的Unsequenced模式。常见的处理方式,就是两端的消息里面,带有确认帧信息,比如客户端(C)通知服务器(S)第100帧的数据,S收到后通知C,已收到C的第100帧,如果C一直没收到S的通知(丢包,乱序等原因),就会继续发送第100帧的数据给S,直到收到S的确认信息。 # 注意事项 ## 游戏数据变化一致性 > 游戏数据类型,包括int,bool,float,string。其中int,bool,string运算不会有一致性问题。 > 对于需要确定性的帧同步,浮点数是一个坑。在不同的CPU架构,不同编译器版本,在乘法,除法,精度控制上,都可能具有一定程度的不一致。虽然理论上,只要编译器及处理器都遵循IEEE754标准,就能够确保浮点数的一致性。但在手游环境下,芯片种类繁多,执行的标准,尤其是对浮点数运算器的优化,都有一定的差异。 > 另外浮点数相关的函数库,也是一个问题。数学库,如cos,sin等,甚至浮点数fmod,都需要自己实现一套,与环境编译器无关的实例。另外开方等高阶数学计算,因为存在cpu及数学库的依赖性,我们也需要自己手动实现相关函数,并保证结果的一致性。 > 浮点数计算无法保证一致性,比较好的实现方式是转换为定点数。关于定点数的实现,比较简单的方式是,在原来浮点数的基础上乘1000或10000,对应地方除以1000或10000,这种做法最为简单,再辅以三角函数查表,能解决一些问题,减少计算不一致的概率,但是,这种做法是治标不治本的方式,存在一些隐患(举个例子,例如一个int和一个float做乘法,如果原数值就要*1000,那最后算出来的数值,可能会非常大,有越界的风险。)。 ## 多线程或者协程 多线程或者协程在运算过程中逻辑是不确定的 ## 稳定型算法 dict,hash,sort,search等使用稳定性算法,不要使用结果不确定的算法 ## 随机种子下发 需要保证每次随机的数字都相同,所以需要自己实现一套随机数,不能用引擎自带的那个随机数接口,而且需要服务端发送相同的随机种子,保证计算结果一致。 ## 逻辑和显示分离 最基本,我们动作切换的逻辑,是基于自己抽象的逻辑帧,而不是基于animator中一个clip的播放。比如一个攻击动作,当第10帧的时候,开始出现攻击框,并开始检测和敌人受击框的碰撞,这个时候的第10帧,必须是独立的逻辑,不能依赖于animator播放的时间,或者AnimatorStateInfo的normalizedTime等。甚至,当我们不加载角色的模型,一样可以跑战斗的逻辑。如果抽离得好,还可以放到服务器跑,做为战斗的验证程序,王者荣耀就是这样做的。 #网络优化 > 帧同步对于网络优化,是一个大问题。这里涉及几个点: > 网络延迟及抖动处理,网络同步量及服务器承载优化,玩家操作延迟优化。游戏世界由逻辑帧,直接驱动,在不做优化的情况下,如果网络发生抖动,逻辑帧的到来时快时慢,对于玩家体验而言是致命的。在这里,先直接给出我们的做法: > 帧聚合及帧延后执行技术。 > > 帧聚合可以简单理解为把多个帧,集合起来发送。帧延后执行,大致是本地缓冲部分逻辑帧,按照玩家本地速率执行,把网络抖动抹平。 > > 关于网络优化,帧同步使用UDP还是TCP, > 这个问题是被很多人所讨论的,这里也给大家一个参考结论。帧同步,需要可靠传输,因此网络传输层一定要具有可靠性。另外,部分基于帧同步制作的游戏,还希望要能有最短的用户操作反馈延迟,比如100ms以内。综上,我们简单分析下两个协议。TCP是可靠传输协议,但是TCP协议,对于网络丢包重传机制,是基于网络公平性,总流量较少来设计的,其RTO机制,在发生丢包时,需要较长时间恢复。TCP底层实现过于复杂,无有效参数来直接设置RTT或者RTO超时性,对于减少丢包后的恢复时间,基本无解。UDP是不可靠传输协议,甚至可以认为只是在IP层上做了一个基于分包的简单处理,不对链接管理,传输可靠性进行任何处理。这个也是UDP的优点,纯净无添加。在进行帧同步的底层开发时,也可以使用UDP协议,自己开发链接管理及可靠性传输(这里不需要开发NAT穿透等功能,因此难度一般),并把RTO,RTT按照最快速度恢复策略,定制编码即可。最终实现一个简化的,基于最快速度恢复丢包的,TCP like协议。 > > 最终,帧同步网络底层,因为都是使用可靠传输协议,使用TCP还是UDP,在框架库中,只是一个初始化参数的问题。接口层可以完全一致,因此对于最终开发用户,TCP还是UDP,只是运行期配置参数的问题。至于到底选哪个,要根据项目情况而定。如果是希望玩家操作延迟最低,则可以配置为使用UDP。如果是希望帧同步服务器负载能力最大化,则配置为使用TCP(用UDP封装简化的类TCP网络库,系统运行期网络消耗及CPU消耗,都会大于原生的TCP接口)。 # cocos帧同步实现方案 > 实现帧同步最重要的是保证所有客户端每一帧计算结果一致,而且当前帧要保证与服务器同步。在服务器端,我们每隔固定时间间隔发送帧信息f,在客户端的onMessage中收到并处理帧信息。在收到帧信息之前需要停止客户端渲染,等待网络接收到新的帧信息之后再进行渲染。 ## 基本逻辑(采用乐观锁策略) > 1. 客户端连接服务器 > 2. 服务器下发当前服务器帧信息 > 3. 客户端上报操作 > 4. 服务器统一收集每个客户端的输入 > 5. 服务器等下一帧的时候同步给所有客户端 怎么控制fps,玩家反映时间大概 50-100ms,服务端帧率至少控制在15-20左右 > 6. 客户端通过服务器的操作指令执行游戏逻辑 > 7. 客户端上报自己的操作指令 ## web端帧同步方案 ## web端tcp传输(websocket) ## 研发方向:web端实现udp传输 ### quic(httpV3) ### webrtc #Cocos简单demo #参考文献 [TCP协议详解](https://blog.csdn.net/rock_joker/article/details/76769404) [帧锁定同步算法](http://www.skywind.me/blog/archives/131) [再谈网游同步技术](http://www.skywind.me/blog/archives/1343) [网游帧同步的分析与设计](https://zhuanlan.zhihu.com/p/105390563) [六:帧同步联机战斗(预测,快照,回滚)](https://zhuanlan.zhihu.com/p/38468615) [实时对战网络游戏--基于帧同步的最佳实践](https://www.gameres.com/694649.html) 重入性、反外挂、游戏录像 [浅谈《守望先锋》中的 ECS 构架](https://blog.codingnow.com/2017/06/overwatch_ecs.html) [io类游戏快速开发 2](https://indienova.com/u/cyclegtx/blogread/11322) [关于帧同步和网游游戏开发的一些心得](https://www.kisence.com/2017/11/12/guan-yu-zheng-tong-bu-de-xie-xin-de/) [Unity帧同步解决方案(一)](https://zhuanlan.zhihu.com/p/66582899) [帧同步和状态同步该怎么选(上)](https://zhuanlan.zhihu.com/p/104932624) [帧同步技术目标总结](https://zhuanlan.zhihu.com/p/31618785) Promise的顺序逻辑 Docker镜像配置
No Leanote account? Sign up now.