在这个模拟过程中,需要解决的一个重要问题是:多长时间处理(更新)一次该服务器上的待处理事件,体现在实际开发中,这就是一个服务器端的心跳设计问题(tick)。
在网络游戏服务器端的心跳设计里主要面临以下几个问题:
- 心跳函数的处理间隔往往是固定的,因为需要模拟现实世界中时间的性质,不能让游戏世界表现得忽快忽慢。但处理间隔固定不代表一定要和真实时间一致,有可能有快放慢放的需求。
- 固定间隔的心跳,间隔多少长?50ms,100ms,500ms?
- 由于服务器每次心跳处理的事件数量和复杂度不一样,每次处理所需的时间也会不同,服务器繁忙时和闲置时相差很远,应该使用什么策略来应对?
- 编码实现时应该怎么设计?是和游戏主循环在同一个线程里,还是把心跳写到一个单独的timer线程里,或者干脆做成一台心跳服务器(心跳指令定期通过TCP发出,或者通过同步卡),逻辑服务器都由心跳服务器控制tick的频率。
- 心跳必须和逻辑程序写在一个进程空间里吗?有没有以dandu运行的心跳服务?
为了解决以上问题,本文将对心跳进行分类,从不同角度进行讨论。
一、按照策略分类
就心跳间隔策略而言,现在的网游服务器端主要分为两种。分别是固定tick时间和固定sleep时间,可以通过下图进行具体的说明:
图 1-1
如上图1-1中,画出两种间隔策略的示意图,渐变颜色的横条代表时间,Tick1、Tick2代表程序两次不同的更新操作,Run1、Run2代表在心跳函数里处理更新操作所需的时间,Sleep1、Sleep2代表让出CPU时间片的时间。
(1)固定Tick时间:顾名思义就是指程序每次心跳的时间都是等长的、固定的。如图中的“图A”,Tick1和Tick2的时间是相等的,如果实际执行的比上次执行时间长(Run2 > Run1),则Sleep2 < Sleep1,同时满足等式:Tick1 = Tick2 = Run1 + Sleep1 = Run2 + Sleep2
(2)固定Sleep时间:每次心跳,更新操作执行完成后,sleep固定的时间。如图中的“图B”,Sleep1 = Sleep2,Run1和Run2不一定相等,同时满足等式:Tick1 = Run1 + Sleep1,Tick2 = Run2 + Sleep2
下面结合具体的代码对比说明这两种策略
1.1 固定Tick时间
使用固定tick时间的心跳策略的一大好处就是,在负荷不高的情况下,由于相邻两次tick的时间一定,所以开始执行Run1到开始执行Run2的时间间隔一定。tick时间固定带来的另一个好处就是容易实现逻辑服务器运行时快放慢放功能(见??),当然固定tick时间同样带来一些问题,如下图:
图 1-2
如图1-2,在负荷不高的情况下,心跳函数可以按照上图中“图A”的时间线正常的运行,如果在服务器运行的过程中遇到一些突发事件(开新服、做活动、大世界内大范围的帮战),会导致服务器CPU负荷变高,从而使得一次tick无法处理完当前所有事件,出现“图B”中的情况Run1 > Tick1,这时Sleep1不管取什么值都不能满足等式Tick1 = Run1 + Sleep1,
这样一来就带来第一个问题:高负荷情况下如何保证CPU能充分利用的情况下,tick1和tick2两次心跳互相不干扰?伴随而来的另一个问题是tick时间设为多长才能满足低负荷时固定间隔的要求,同时不能经常出现“图B”的情况?
下面结合实例讲解固定tick时间的心跳如何编写,以及如何处理以上两个问题。
Mangos-Zero
mangos-zero项目中的逻辑服务进程mangosd的心跳函数采用如图1-2中的“图C”的方法,当更新的处理时间Run1大于固定大小的tick时间时,下一个tick到来时不sleep直接执行Run2,实现代码如下:
1: /// Heartbeat for the World
2: void WorldRunnable::run()
3: {
4: ///- Init new SQL thread for the world database
5: WorldDatabase.ThreadStart(); // let thread do safe mySQL requests (one connection call enough)
6: sWorld.InitResultQueue();
7:
8: uint32 realCurrTime = 0;
9: uint32 realPrevTime = WorldTimer::tick();
10:
11: uint32 prevSleepTime = 0; // used for balanced full tick time length near WORLD_SLEEP_CONST
12:
13: ///- While we have not World::m_stopEvent, update the world
14: while (!World::IsStopped())
15: {
16: ++World::m_worldLoopCounter;
17: realCurrTime = WorldTimer::getMSTime(); //----------------(1)
18:
19: uint32 diff = WorldTimer::tick(); //--------------(2)
20:
21: sWorld.Update( diff ); //--------------(3)
22: realPrevTime = realCurrTime;
23:
24: // diff (D0) include time of previous sleep (d0) + tick time (t0)