最近在做帧同步的PVP时遇到一个问题。在当初设计的帧同步方案中,服务器是以恒定每秒30帧的速率向客户端广播当前的双方的操作信息的。客户端也是按照每秒30帧的速率来处理这些信息,从而推进游戏逻辑的。这个方案双方速率一致是很重要的,如果服务器快了,客户端就要快进,如果服务器慢了,客户端就要等待。服务器方面我们是通过设置一个定时器来驱动这一部分逻辑的,这个定时器的间隔就是1000 / 30 ms,取值33或者34在个人电脑(Win7)环境下,都是能正常1秒能执行30次的。但部署到服务器环境(WinServer2012)下,定时器间隔无论设置33ms还是34ms,最终1秒都只能触发21次左右,如果把值设得更小,例如30,就会变成1秒执行32次,但无论设什么值都无法准确的1秒执行30次。
一开始在外网虚拟的服务器下发现这个问题,以为是虚拟机导致的,后面换到自己的物理服务器上(WinServer2008) 还是有这个问题,就开始考虑是不是服务器底层调度有问题,但后来通过一些测试发现是系统的问题。
这个测试程序很简单,就是在一个循环中每次计数器nCount加1,然后Sleep(1),每1秒输出计数器在这1秒内累加了的次数。只要系统调度足够快,这个数值是可以达到1000的。在个人电脑(Win7)环境下,输出的确也是1000,但换到服务器环境下(WinServer2008/2012),输出就只有64。
通过这个测试我们知道,在服务器环境下,系统调度的最小间隔是1000 / 64 = 15.625ms,所以最初我们设置服务器定时器间隔为33ms或34ms时,服务器只会在第三次轮询时触发定时器,调用间隔是最小轮询间隔15.625 * 3 = 46.875ms,所以频率就是1秒21次。而如果设置定时器间隔为30ms,服务器就会在第二次轮询时触发定时器,调用间隔就是15.625 * 2 = 31.25ms,频率就是32。所以受系统调用的最小间隔限制,服务器的定时器是无法很准确执行的。
问题的原因找到了,怎么解决这个问题呢?其实系统调度频率是有API可以设置的:timeBeginPeriod(),详细的用法可以查阅MSDN。在服务器程序启动的时候通过调用 timeBeginPeriod(1);可以让WinServer环境下也像Win7一样得到更准确的定时器。通过这个方法可以让服务器准确的1秒30次执行定时逻辑。
到了这里其实服务器调度频率的问题已经解决了。接下来是一些额外的思考,首先为什么Win7跟WinServer在默认调度频率上面会有这么大的差距呢?仔细想想Win7作为个人使用的操作系统,平常使用的环境就是存在大量的任务,并且大部分任务需要渲染交互。所以采用调度频率高而相对时间片小的调度策略。作为服务器操作系统的WinServer正好相反,同时存在的任务相对较少而每个任务每次调度时处理的数据可能会相对较多,所以采用调度频率低而时间片长的调度策略。如果是单核服务器,我们直接把调度频率改高,频繁的切换带来的上下文保存,堆栈恢复等开销是会增大的,从而影响服务器性能。在多核服务器上这个问题也是存在的,但影响有多少,就不确定的了。
还有一个问题是对于帧同步模式的思考,开始说了服务器是以30FPS(
1秒30帧 )的恒定频率来下发指令,从而推动客户端逻辑的,这个做法在客户端也能以30FPS的频率更新的情况下是能表现良好的。但客户端可能受限于不同的机器性能或者不同的运行环境从而没法保证一直是30FPS,如果当前连接的两个客户端都达不到30FPS,服务器再以30FPS来下发指令的话,就会导致双方都需要快进,这样的体验是不好的。
所以帧同步服务器下发指令的频率应该要根据当前连接的客户端情况而定动态变化的。例如客户端A能跑28FPS,客户端B跑24FPS,那么服务端就应该以28FPS来下发指令,推进逻辑,这样A是能保证流畅的,B是偶尔会快进的。随着AB客户端的帧率变化,服务器下发指令的频率也要相应调整。利用客户端上行信息与服务器当前逻辑信息,是可以做到这一点的。关于帧同步的细节就不在这里展开了。
最终的处理方式是,不修改服务器调度的频率,定时器间隔设为30ms,让定时器以每秒32次的频率触发,然后通过逻辑的处理让服务器真正派发到客户端的指令频率能动态变化。这样一来不需要依赖特别准确的定时器(但频率还是不能低于30FPS),二来在客户端都跑不满帧的情况下能照顾到跑得比较快的客户端,保证一方能有流畅的体验。