大多数实时网络游戏,将 server 的时间和 client 的时间校对一致是可以带来许多其他系统设计上的便利的。这里说的对时,并非去调整 client 的 os 中的时钟,而是把 game client 内部的逻辑时间调整跟 server 一致即可。
一个粗略的对时方案可以是这样的,client 发一个数据包给 server,里面记录下发送时刻。server 收到后,立刻给这个数据包添加一个server 当前时刻信息,并发还给 client 。因为大部分情况下,game server 不会立刻处理这个包,所以,可以在处理时再加一个时刻。两者相减,client 可以算得包在 server 内部耽搁时间。
client 收到 server 发还的对时包时,因为他可以取出当初发送时自己附加的时刻信息,并知道当前时刻,也就可以算出这个数据包来回的行程时间。这里,我们假定数据包来回时间相同,那么把 server 通知的时间,加上行程时间的一半,则可以将 client 时间和 server 时间校对一致。
这个过程用 udp 协议做比用 tcp 协议来的好。因为 tcp 协议可能因为丢包重发引起教大误差,而 udp 则是自己控制,这个误差要小的多。只是,现在网络游戏用 tcp 协议实现要比 udp 有优势的多,我们也不必为对时另起一套协议走 udp 。
一般的解决方法用多次校对就可以了。因为,如果双方时钟快慢一致的情况下,对时包在网络上行程时间越短,就一定表明误差越小。这个误差是不会超过包来回时间的一半的。我们一旦在对时过程中得到一个很小的行程时间,并在我们游戏逻辑的时间误差允许范围内,就不需要再校对了。
或者校对多次,发现网络比较稳定(虽然网速很慢),也可以认为校对准确。这种情况下,潜在的时间误差可能比较大。好在,一般,我们在时间敏感的包上都会携带时间戳。当双方时间校对误差很小的时候,client 发过来的时间戳是不应该早于 server 真实时刻的。(当时间校对准确后,server 收到的包上的时间戳加上数据包单行时间,应该等于 server 当前时刻)
一旦 server 发现 client 的包“提前”收到了,只有一种解释:当初校对时间时糟糕的网络状态带来了很多的时间误差,而现在的网络状态要明显优于那个时候。这时,server 应该勒令 client 重新对时。同理,client 发现 server 的数据包“提前”到达,也可以主动向 server 重新对时。
一个良好的对时协议的设定,在协议上避免 client 时间作弊(比如加速器,或者减速器)是可行的。这里不讨论也不分析更高级的利用游戏逻辑去时间作弊的方式,我们给数据包打上时间戳的主要目的也非防止时间作弊。
校对时间的一般用途是用来实现更流畅的战斗系统和位置同步。因为不依赖网络传输的统一时间参照标准可以使游戏看起来更为实时。
时间同步算法的应用非常广泛。
譬如在Unix系统里面,Make命令,只是用来编译新修改过的代码文件。Make命令使用运行的客户端的时钟来决定哪个文件是被修改过的。但是,如果把代码放到文件服务器上面,而运行make命令的主机与文件服务器的时间不同的时候,make命令就有可能工作不正常。
譬如玩dota的时候,几个客户端需要一个同步过的时钟来使每个人的画面保持一致。、再譬如PC电脑同步服务器上面的时间可以做到很高的同步精度。
时间同步算法
时间同步算法,有以下几个解决方案:
Cristian’s algorithm算法
Cristian's Algorithm算法的应用背景,主要是在一个进程P像一个服务器S请求时间:
1. P发送一个请求包到S请求时间。
2. S收到P的请求包以后,在包上面加上当前S的时间,然后回发过去。
3. P收到数据包之后,把当前时间设置为T+RTT/2。
RTT表示一个Round Trip Time,即P从发送到接受到数据包的时间。该算法假设发送数据包和接受数据包在网络上所用的时间是一样的。而且也假设S在处理请求的时候时间可以忽略不计。基于以上假设,改算法可以改进如下:
从P发送多个请求包到S,然后取RTT最小的做为RTT除以二加在此包包含的时间上。
算法精度分析:假设min为从S到P的最短时间,T为包含在上述定义的RTT中的时间。那么,P设置时间的范围应该是[T+min,T+RTT-min]。这样时间的偏差范围就在RTT-2min以内。改进后的算法精度应该为RTT/2-min。
Berkeley algorithm算法
Berkeley算法的使用环境与Cristian算法有所不同。Cristian算法是用在一个客户端向一个服务器请求正确时间的时候。而Berkeley算法是几个客户端之间同步时钟的算法。具体算法如下:
1. 首先通过Change and Robert’s Algorithm来从一个环里面选择一个节点做为Master。
2. 一个Master使用Cristian算法来请求各个节点的时间。
3. Master通过记录RTT的平均值,同时剔除偏差很大的RTT来评估出每个节点的时间偏差。
4. Master发送每个节点的时间偏差到每个节点,让节点自行校正。
客户端接受到了时间以后,一般来说不会把当前的时间往回调整。因为这会导致一些程序莫名奇妙的错误。因为在很多算法中,时间不会往回调整是一个基本假设。譬如make命令。
解决的方案有一个:让时钟走慢点就可以了。花费一些时间来调整到正确时间。
另外,还需讨论一下Change and Robert’s Algorithm这个算法。这个算法和时间同步算法一样,是玩dota的时候需要用到的。在dota初始化的时候,需要同步各个玩家的时钟。在掉线了之后,就要通过特定的算法来找一个新的主机:
Change and Robert’s Algorithm
Change and Robert’s Algorithm算法假设每个Process都有一个UID,同时在一个Ring状网络中可以有个没有方向的通讯信道。算法如下:
1. 首先ring中的每个节点把自个标识为non-participant。
2. 当一个process发现主机掉线了的时候,它首先把自个标识成为participant,然后发送给邻居一个包含了自个UID的一个选主机的数据包。
3. 当数据包达到邻居的时候,首先和自己的UID比较下,如果自己的UID比这个UID大,就把自己标识成为participant,同时修改数据包里面的UID,并且也往顺时针方向发送这个数据。
4. 当一个process接到一个数据包的时候发现这个数据包里面的UID和自己的UID一样的时候,就开始这个算法的第二阶段:
5. 这个process把自己标识成为non-participant,同时发送已经选择好了主机的信息到邻居,并且包含UID信息。
6. 如此循环,当回到被选中成为主机的Process的时候,整个过程结束。
这是在分布式系统里面选择一个主机的算法。当然,在特定的环境下,可以把选择的条件变化一下,譬如选择网络速度最快的或者是CPU最快的作为主机。同时,这个算法还可以避免多个Process同时发现主机掉线,几个process同时寻求主机的情况。
这个算法的伪码可以描述如下:
Start : M:= i:
Send to neighbor;
Upon receiving message
If M Send Elseif M=j then leader; Endif; 该算法详细的复杂度分析,数学模型和统计表可以参考这篇论文: http://www.vs.inf.ethz.ch/publ/papers/MsgCplxElecAlgo.pdf 本文仅分析了Centrilized System里面的几个时间同步算法,对于分布式系统里面的Network Time Protocal和Reference broadcast Synchronization算法并未做分析。以后有空研究研究NTP。