注:本文的锁步同步(Lockstep)特指只同步操作的确定性锁步同步。本文将快照同步等同于状态同步。
网络传输协议
目前游戏中常用的网络传输协议主要是指UDP协议和TCP协议。
在TCP/IP协议族中,TCP和UDP位于传输层,它俩都独立依赖于IP协议。
Berkeley套接字(Berkeley Sockets)是进行UDP或TCP通信的标准API,属于POSIX标准,所以Socket、Berkeley Socket、POSIX Socket、BSD Socket,都指同一个东西。
TCP的报头如下
除了也有端口、校验外,TCP主要额外提供了:
- 保序可靠传输(Sequenced Reliable Transmission),通过报头Sequence number、Acknowledgement Number、Window Size等字段来实现;
- 流量控制(Flow Control),通过报头的滑动窗口实现,接受者把自己的期待流量提示给发送者,防止发送方以过快的速度发送数据给接收方;
- 拥塞控制 ,通过报头的Sequence number、Acknowledgement number等字段,以及通过Timer机制实现,据此收发双方可判断数据包丢包,而推断出当前网络是否拥塞,并采取一些措施来减缓数据发送。
TCP本身足够复杂,本文并不能细究他们的具体原理,仅简述和网络游戏同步相关的特性(更多详细内容查阅历史文章:深入浅出TCPIP专栏 ):
由于TCP提供保序可靠传输,所以TCP很适用于回合制游戏、RPG等通信频率不太高的游戏类型;
由于TCP本身对所有发出的数据包,都考虑保序可靠、流量控制、拥塞控制(包含了慢启动(Slow-Start),而这些功能对于大部分游戏类型,特别是快节奏(Fast Paced)游戏是不适用的,因为这类游戏通信频率高,要求低时延,新发的数据包可能比已发送的数据包更重要(比如游戏状态数据包,游戏状态经常改变,与其重传旧状态数据包,倒不如将其抛弃,赶快传输新的状态数据包);
- 报头大,不适用于数据包数量多的情况。
- 建立连接的过程慢,首次通信需要经过三次握手,
- 复杂对外不透明,难扩展;
那么我们再看下UDP报头:
UDP在IP之上,增加了端口(Port)的概念,收发双方从而能通过约定好的几个端口来进行不同功能的通信;增加了校验和以增加了一定的校验能力。
所以也能看到UDP其对于网络游戏来说,和IP几乎有很相似的特性:
不可靠不保序传输,即传输不保序,也不确保可达,某个具体数据包在传输的过程中,是可能丢失的;也可能数据包到达了,但数据出错从而校验失败,也被Socket丢弃掉;
不过相比TCP,UDP又有如下几个优势:
- 1.传输快,多亏了不可靠,所以Socket不会浪费时间和带宽,对丢失的数据包进行重传;
- 2.连接快,因为根本就无连接。多亏了不可靠,所以收发双方每次通信都是无状态的,所以通信前不需建立连接;
- 3.应用通过Socket成功拿到的UDP数据包保证是正确的;
- 4.简单,上层应用能按需实现额外的传输特性(QoS),比如保序不可靠、可靠不保序、可靠保序、最新状态传输(Most Recent State)、快速冗余传输(Quickest Possible Delivery),等;
- 5.报头小,节省带宽。
因此很多MMORPG网络游戏采用UDP,TCP更加适用于回合制等慢节奏游戏。
网络同步模型
锁步同步(Lockstep)和状态同步(State Synchronization)。
本文针对“帧”有以下两种术语定义:
- 逻辑帧,在本文会被称为Tick,游戏在逻辑层面是离散的过程,即可以认为是一个逻辑帧一个逻辑帧地进行逻辑运算,逻辑帧号是指游戏逻辑层面当前处于第几帧;
- 渲染帧,在本文会被称为Frame,游戏在画面呈现层面也是离散的过程,即可以认为是一幅画面一幅画面地呈现给玩家的,渲染帧号是指游戏当前呈现的是第几幅画面;
游戏的逻辑帧率和渲染帧率是互相独立的,比如一个游戏可以是20帧每秒的逻辑帧率、60帧每秒的渲染帧率。
游戏逻辑是一个逻辑帧一个逻辑帧地持续离散进行的,可以抽象为:
游戏在第0个逻辑帧时,根据玩家信息P和游戏配置C,进行初始化运算g,得出初始化状态集合S0;游戏在第k个逻辑帧时,根据前一个状态集合Sk-1和游戏配置C,根据第k帧收到的外部变化原因集合Ik,进行逻辑t运算,得出第k个逻辑帧新的游戏状态集合Sk。
I是游戏状态变化的根本原因的集合,往往是各个玩家操作。
S是游戏状态的集合,由众多状态子集组成,其中有以下2个重要子集定义:
其中O为一些能被玩家所明显观察到的对象的状态集合,M为一些可用于推导最终状态的中间状态集合。
在网络同步时,称从客户端发出信息进行网络传输的过程为上行,称客户端经过网络传输收到信息的过程为下行,则锁步同步的本质是,上下行都仅包含游戏外部变化原因集合Ik;状态同步的本质是,下行仅包含游戏运算得出的结果状态子集Ok,上行包含Ik和/或状态子集Mk。
注:本文的锁步同步特指只同步操作的确定性锁步同步(Deterministic Lockstep)。本文不讨论快照同步(Snapshot Synchronization、Snapshot Interpolation),认为其等同于状态同步。
状态同步
因为状态同步只同步游戏运算得出的结果状态Sk,所以需要有机器来进行权威(Authoritative)的状态计算,并传输给其他机器,其他机器都将采纳接收到的状态。
本文只讨论权威机器只有一部的情况。视乎具体网络拓扑结构,这部权威机器会被称为服务器(Server)或主机(Host),其他没有权威的机器称为客户端(Client)。
所以,状态同步,口头交流中也常不太精确地被称为“CS同步(Client-Server)”。
包括Halo、Unreal、Unity、Overwatch等,几乎所有著名状态同步的技术实现,都最终参考了Mark Frohnmayer和Tim Gift于1998年发布的The TRIBES Engine Networking Model[1]一文进行实现。
状态同步开发过程中最基础也最重要的是,不管客户端网络对象当前处于什么状态,它都要做到能正确地完全退出旧状态,退出后不能残留旧状态的逻辑层效果,并正确地进入服务器告知的新权威状态,从而带来新状态的逻辑层效果。
也要避免以非状态同步的方式同步权威状态,比如服务器只传输Ik给客户端(而不传输Sk),让客户端在本地计算出网络对象的新状态Sk。这可能会带来服务器客户端之间状态不一致。比如某个网络对象在第k个逻辑帧发生了Ik从而进入Sk,且之后S都不再改变,那么服务器只会在第k帧才会发送Ik;之后若客户端因断线重连或实时死亡重播等原因导致该网络对象状态在客户端被重置为S0,但当前服务器逻辑帧已大于k,服务器不会再传输Ik给客户端,那么该网络对象在客户端里就错误地一直停留在S0了。
之前在锁步同步讨论到的确定性指的是“严格的”确定性。在讨论状态同步时,偶尔也会提及“不严格的”确定性(比如[4]的Q/A阶段最后一个问题),此类确定性只要求客户端服务器之间满足“足够的”确定性,以便客户端能够比较准确地进行预表现即可,因为客户端最终会采纳服务器的状态,修正累计的误差。
锁步同步
因为锁步同步只同步变化的原因,所以要求各个客户端的运算逻辑是严格确定性的,所有客户端才能算出严格一致的结果。如果在计算过程中包含了一丝不确定的因素,即会导致各个客户端运算时有一丝的误差,那么接下来的逻辑帧误差会越来越大,导致蝴蝶效应,从而最终各个客户端看到的结果状态完全不一样了。
以著名锁步游戏王者荣耀为例,假设你的客户端和其他正确客户端已经发生不一致,其他玩家在正确客户端作出的合理操作,到达你已经状态不一致的客户端做逻辑演算时,却变得不一样的状态结果,这个结果很可能是不合理的,所以你很可能会看到其他玩家英雄都奇怪地乱跑,甚至跑到塔下送死,或者对着空气放技能,等。
游戏要做到严格确定性,须做好一些事情:
不使用浮点数而使用定点数,或限定各客户端所运行的硬件及操作系统从而浮点数的运算是一致的;
确定性的随机数机制;
确定性的容器及算法(增加、移除、排序等);
隔离和封装逻辑层,以防止其他不确定性的调用;
如需,则也须做到确定性的物理机制、导航机制、动画骨骼机制等;
排查所有引起异常(exception)的逻辑。
对锁步同步游戏来说,不同步造成的游戏体验是极差的,偶现的不同步问题是极为头痛的, 因此制定检测不同步的管线流程对锁步同步游戏来说是至关重要的,比如帧状态哈希对比、静态代码扫描分析、帧级别甚至函数级别的高性能日志、外网不同步率统计,等。
锁步同步和状态同步对比
锁步同步 | 状态同步 | |
流量 | 一般情况下较低,决定于网络 | 一般情况下较高,决定于当前 |
预表现 | 难,客户端需本地进行状态序 | 较易,客户端进行预表现,服 |
确定性 | 须要严格确定性 | 须要不严格确定性 |
对弱网络的 | 较低,因为较难做到预表现 | 较高,因为较易做到预表现 |
断线重连 | 较难,需比较耗时地进行快播 | 较易,服务器下发当前实时游 |
高线重播 | 较易,且重播文件大小较小 | 较易,但重播文件较大(和流 |
实时重播 | 难,视乎需求,客户端可能需 | 较易,服务器下发历史 |
网络逻辑性 | 较难,因为客户端需要运算所 | 较易,大部分逻辑默认是在服 |
大量网络实 | 好,因为流量只决定于网络玩 | 如果客户端可观察到的网络实 |
大量网络实 体时的性能 情况 | 较差,因为客户端需要运算所 | 如果客户端可观察到的网络实 |
外挂 | 因为客户端拥有所有信息,所 | 也会有透视类外挂,但服务器 |
开发特征 | 平时开发起来很高效,不需前 | 平时开发起来效率一般,需要 |
采用第三方库 | 较难,因为第三方库需要确保确定性 | 较容易,因为第三方库不须确 |
网络拓扑结构
网络拓扑结构,是指参与游戏的设备之间网络连接方式,主要包括对等结构(Peer-to-Peer,P2P结构)和主从结构(Client-Server,CS结构)。
P2P结构是网状结构(Mesh Topology),游戏中的P2P一般是全连接(Full Connected)的网状结构,如下所示。P2P结构中所有客户端两两相连,连接数为O(n2),地位平等,功能一致。
如下所示,CS结构有至少一部机器为服务器,本文只讨论只有一部服务器或主机的情况,其和其他客户端为主从关系,客户端只和服务器连接,客户端之间不会相连,连接数为O(n)。
两种网络拓扑结构的对比如下:
P2P结构 | CS结构 | |
连接数 | o(n2) | o(n) |
流量 | 各客户端相等,都为O(nz) | 服务器为O(n),客户端为o(1) |
客户端之间 时延 | 较小,为RTT/2 | 较大,为RTT |
网络同步模型和网络拓扑结构是不同的概念,所以它们的组合情况如下表所示:
P2P结构 | cs结构 | |
锁步同步 | 把本地机器玩家的操作广播给 其他机器 | 把本地客户端玩家的操作发送 给服务器,服务器再广播给所 有客户端 |
状态同步 | 各个机器只对自己控制的角色 有权威,向其他机器广播自己 控制角色的权威状态 | 把本地客户端玩家的操作发送 给服务器,服务器再把根据客 户端的情况发送需要的网络实 体的权威状态 |
网络质量
评估网络环境质量,主要包括时延(Latency)、丢包(Packet Loss)、带宽(Bandwidth)。
环境类 型 | 平均时 延 ( ms) | 抖动时 延 (ms) | 丢包率 | 上行带 宽 | 下行带 宽 |
正常网络 | 20 | 20 | 2% | 9o% | 9o% |
普通弱网 络 | 30 | 100~30 o | 12% | 8o% | 60% |
超低网络 | 5o | 100~5o o | 30% | 60% | 40% |
每忙网络 | 5o~10o | 30~5o | 5% | 25% | 25% |
交通工具 行驶中 | 200~40 o | 200~20 oo | 5% | 60% | 60% |
地铁中 | 200~40 o | 200~20 oo | 129% | 609% | 60% |
基站切换 中 | 3000~7 ooo | 2000 | 5% | 60% | 60% |
网络时延主要有2种评估数值,Ping和RTT(Round Trip Time)。
Ping,指网络连接的两个端之间的信号在网络传输所花费的时间,Ping描述了“两部机器之间的网络传输时延是多少?”
RTT 一般情况下可认为等于Ping,但在本文中,RTT = Ping + WaitTime + ProcessTime,即RTT包含了Ping、两个端的处理信号前的等待时间、两个端处理信号的时间 。比如从A端游戏逻辑发出信号开始计时,在A端可能等待一段时间后,也可能处理一些其他逻辑后,方调用操作系统发出信号,经网络传输到达B端操作系统后,B端也可能有类似的等待时间和处理其他逻辑时间,也包括处理该信号本身的时间,然后才发出响应信号,响应信号经过网络传输到A端操作系统后,再来一些类似的等待处理时间,最终A端游戏逻辑接收到响应信号方结束计时,该时长为RTT。
Ping值和RTT值,对一般网络游戏而言,20ms为优秀,50ms为正常,100ms为一般,200ms为差。
4G网络自身时延约30ms~40ms,5G网络自身时延为6~10ms,骨干网在大陆内部互连时延约20ms。即4G时代4G接入本身是网络时延瓶颈,5G时代骨干网为网络时延瓶颈。
上海与各国际城市的骨干网时延(ms)如下表,详情可参阅[17]。
上海 | |
北京 | 27 |
香港 | 33 |
东京 | 57 |
旧金山 | 168 |
悉尼 | 254 |
马德里 | 300 |
孟买 | 578 |
巴黎 | 256 |
纽约 | 237 |
伦敦 | 258 |
法兰克福 | 248 |
蒙特利尔 | 227 |
网络丢包直接原因主要是因为无线网络和/或拥塞控制,但根本原因比较多元和复杂,故统计略。
对比传统应用,网络同步涉及到的带宽较小,正常情况下带宽不会成为网络游戏同步中的瓶颈。
一般锁步同步游戏需要2~4KB/s,快节奏状态同步游戏需要5~10KB/s。
对一般网络游戏而言,网络丢包率2%以下为优秀,5%为一般,10%以上为差。
注意传统网络中网络带宽常见为b/s(Bit per Second),但本文采用B/s(Byte per Second)。