对于网络游戏来说,从物体的移动、攻击到最基础的计时等等,都需要客户端与服务器保持时间的相对一致,那么服务器与客户端同步便是一个必须要解决的问题。通常,网络游戏都会利用心跳来进行同步,那么当客户端并不需要如此精度的同步时,有没有其他方法呢?这里主要讨论低精度的时间同步(精确到秒)。
工作中接触过3种简单的时间同步方法:
首先,定义时间同步类
/// 32位操作系统 typedef unsigned int64_t QWORD; typedef unsigned long DWORD; class TimeSynchronize() { public: void ServerTimeSync(QWORD ServerTime); QWORD GetLocalTime(); private: QWORD initServerTime; QWORD initClientTime; DWORD initClientTimeFromStartup; }; /// 服务器下发同步消息 void TimeSynchronize::ServerTimeSync(unsigned int64_t serverTime) { initClientTime = time(NULL); initServerTime = serverTime; initClientTimeFromStartup = timeGetTime(); }
1.当客户端启动的时候,服务器向客户端下发服务器当前的时间,当客户端需要获取当前时间时,只需用校正过的时间加上本地时间即可。
DWORD TimeSynchronize::GetLocalTime() { return initServerTime - initClientTime + time(NULL); ///< initServerTime - initClientTime为了消除客户端与服务器的时间误差 }
看到这里,细心的同学要笑了,这种做法修改本地的系统时间,不就修改了该函数的返回值么。确实,接口中只要取用了本地时间,便将修改本地时间的风险带入了接口。
2.服务器定时向客户端发送同步消息,就是所谓的心跳机制,类似TCP中的心跳,服务器定时发送一个自定义的结构体(心跳包或心跳帧),让对方知道自己“在线”。 以确保链接的有效性。当服务器超过一定时间没有收到来自客户端的回复,则当做玩家掉线,服务器关闭socket链接。不同的游戏也会根据游戏类型的不同,根据自己的需要设计心跳机制,间隔从几十ms到几s不等。
那么当客户端在大部分时间中并不需要高精度的时间同步时,有没有其他办法以降低对服务器性能的消耗?
3.利用timeGetTime接口,与同步时获取的服务器时间模拟当前时间。
// timeGetTime:函数以毫秒计的系统时间。该时间为从系统开启算起所经过的时间。 // DWORD timeGetTime(VOID); // 参数:无参数。 // 返回值:以毫秒值返回系统时间。 DWORD TimeSynchronize::GetLocalTime() { return initServerTime + timeGetTime() - initClientTimeFromStartup + initServerTime - initClientTime; }
可以看到timeGetTime的返回值精度为(ms),对于精度要求为(s)级的应用来说,已然够用了。
当然利用timeGetTime()要注意返回值为32位,取值范围为0~2^32,约为49.71天,要避免溢出。如果服务端利用该接口,又不便于重启服务器,建议利用精度更高的接口,如:QueryPerformanceFrequency(),QueryPerformanceCounter()。