泉州网站制作方案,衣服商业网站建设策划书,潍坊网站推广排名,代理赚钱网络游戏服务器的主要作用是模拟整个游戏世界#xff0c;客户端用过网络连接把一些信息数据发给服务器#xff0c;在操作合法的情况下#xff0c;更新服务器上该客户端对应的player实体、所在场景等#xff0c;并把这些操作及其影响广播出去。让别的客户端能显示这些操作。…网络游戏服务器的主要作用是模拟整个游戏世界客户端用过网络连接把一些信息数据发给服务器在操作合法的情况下更新服务器上该客户端对应的player实体、所在场景等并把这些操作及其影响广播出去。让别的客户端能显示这些操作。 在这个模拟过程中需要解决的一个重要问题是多长时间处理更新一次该服务器上的待处理事件体现在实际开发中这就是一个服务器端的心跳设计问题tick。 在网络游戏服务器端的心跳设计里主要面临以下几个问题
心跳函数的处理间隔往往是固定的因为需要模拟现实世界中时间的性质不能让游戏世界表现得忽快忽慢。但处理间隔固定不代表一定要和真实时间一致有可能有快放慢放的需求。固定间隔的心跳间隔多少长50ms100ms500ms由于服务器每次心跳处理的事件数量和复杂度不一样每次处理所需的时间也会不同服务器繁忙时和闲置时相差很远应该使用什么策略来应对编码实现时应该怎么设计是和游戏主循环在同一个线程里还是把心跳写到一个单独的timer线程里或者干脆做成一台心跳服务器心跳指令定期通过TCP发出或者通过同步卡逻辑服务器都由心跳服务器控制tick的频率。心跳必须和逻辑程序写在一个进程空间里吗有没有以独立运行的心跳服务 为了解决以上问题本文将对心跳进行分类从不同角度进行讨论。 一、按照策略分类 就心跳间隔策略而言现在的网游服务器端主要分为两种。分别是固定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 Sleep2Run1和Run2不一定相等同时满足等式Tick1 Run1 Sleep1Tick2 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) 25: // we want that next d1 t1 WORLD_SLEEP_CONST 26: // we cant know next t1 and then can use (t0 d1) WORLD_SLEEP_CONST requirement 27: // d1 WORLD_SLEEP_CONST - t0 WORLD_SLEEP_CONST - (D0 - d0) WORLD_SLEEP_CONST d0 - D0 28: if (diff WORLD_SLEEP_CONSTprevSleepTime) //----------------4 29: { 30: prevSleepTime WORLD_SLEEP_CONSTprevSleepTime-diff; 31: ACE_Based::Thread::Sleep(prevSleepTime); 32: } 33: else 34: prevSleepTime 0; 35: 36: #ifdef WIN32 37: if (m_ServiceStatus 0) World::StopNow(SHUTDOWN_EXIT_CODE); 38: while (m_ServiceStatus 2) Sleep(1000); 39: #endif 40: } 41: 42: sWorld.KickAll(); // save and kick all players 43: sWorld.UpdateSessions( 1 ); // real players unload required UpdateSessions call 44: 45: // unload battleground templates before different singletons destroyed 46: sBattleGroundMgr.DeleteAllBattleGrounds(); 47: 48: sWorldSocketMgr-StopNetwork(); 49: 50: sMapMgr.UnloadAll(); // unload all grids (including locked in memory) 51: 52: ///- End the database thread 53: WorldDatabase.ThreadEnd(); // free mySQL thread resources 54: } 以上代码是游戏世界的主循环看while循环里的代码主要干下面几件事
1从WorldTimer::getMSTime()得到一个uint32的值realCurrTimerealCurrTime是循环的到增加到0xFFFFFFFF后在增加就变成0表示当前时间单位是毫秒是一个相对前一次tick的时间。
2使用WorldTimer::tick();计算上次tick到这次tick的时间差diff该值理论上等于realCurrTime – realPrevTime
3sWorld.Update( diff );就是tick里的处理函数游戏逻辑在这里得到更新处理。
4这里就是图1-2中的“图C”所描述的如果运行时间大于固定的tick时间则不sleep继续占用CPU来处理更新直到能在一个tick处理所有操作为止这个时候才会sleep让出CPU时间片。
5WORLD_SLEEP_CONST就是固定的tick的时间长度在这里是50ms 总结现在可以回答本节前面的两个问题在高负荷情况下mangos采用图1-2中“图C”的方式提高服务器的响应速度每个tick时间长度为50ms也就是每秒钟更新20次能满足更新的需求。 timer_thread 出于模块化的考虑固定tick时间策略还有一种实现方式使用单独的线程做timer周期性产生心跳信号或者心跳task。工作原理如下图 图 1-3 图1-3也是比较常见的设计方案服务器进程采用多线程方式主循环线程、timer线程及其他非工作线程向任务队列task queue中添加task而工作线程不断的从任务队列中取出任务执行相应的处理。这里提到的timer thread就是用来产生心跳任务的timer thread会每隔50ms产生一个heartbeat task放入任务队列中。一般来说队列中的heartbeat task的数量会远远大于其他task所以这种策略也可以称为固定tick时间的心跳策略。在服务器高负荷运行的情况下近似于mangos所采用的图1-2中“图C”的方式进行处理。多个worker thread的情况下还需要对heartbeat task加锁。 1.2 固定Sleep时间 固定Sleep也是一种比较常见的心跳函数间隔处理策略如下图每次心跳处理函数执行完毕后sleep固定长度时间。 图 1-4 如图1-4Sleep1 Sleep2Run1和Run2不一定相等同时满足等式Tick1 Run1 Sleep1Tick2 Run2 Sleep2。下面结合实例进行说明 天龙 根据网上流出的天龙源代码GameServer工程的主循环至线程的心跳函数的调用过程如下 1: BOOL Server::Loop( ) 2: { 3: ........ 4: 5: ret g_pThreadManager-Start( ) ; //--------1 6: 7: ........ 8: 9: return TRUE ; 10: } 11: | 12: | 13: \|/ 14: BOOL ThreadManager::Start( ) 15: { 16: ........ 17: 18: BOOL ret ; 19: m_pServerThread-start() ; //--------2 20: MySleep( 500 ) ; 21: ret m_pThreadPool-Start( ) ; 22: 23: ........ 24: } 25: | 26: | 27: \|/ 28: VOID Thread::start () 29: { 30: ........ 31: 32: #if defined(__LINUX__) 33: pthread_create( m_TID, NULL , MyThreadProcess , this ); 34: #elif defined(__WINDOWS__) 35: m_hThread ::CreateThread( NULL, 0, MyThreadProcess , this, 0, m_TID ) ; 36: #endif 37: 38: ........ 39: } 40: | 41: | 42: \|/ 43: VOID ServerThread::run( ) 44: { 45: ........ 46: 47: _MY_TRY 48: { 49: g_pServerManager-m_ThreadID getTID() ; 50: 51: while( IsActive() ) 52: { 53: if( g_pServerManager ) 54: { 55: BOOL ret g_pServerManager-Tick( ) ; //--------3 56: Assert( ret ) ; 57: } 58: 59: ........ 60: } 61: } 62: _MY_CATCH 63: { 64: ........ 65: } 66: 67: ........ 68: } 如上主循环启动一个“用来处理服务器之间数据通讯的线程”m_pServerThread以及一个线程池m_pThreadPool。首先看m_pServerThread的run ()函数调用g_pServerManager-tick ()函数代码如下 1: BOOL ServerManager::Tick( ) 2: { 3: ........ 4: 5: BOOL ret ; 6: 7: _MY_TRY 8: { 9: ret Select( ) ; //--------1 10: Assert( ret ) ; 11: } 12: _MY_CATCH 13: { 14: SaveCodeLog( ) ; 15: } 16: 17: ........ 18: } 19: | 20: | 21: \|/ 22: BOOL ServerManager::Select( ) 23: { 24: __ENTER_FUNCTION 25: 26: MySleep(50) ; //--------2 27: if( m_MaxFDINVALID_SOCKET m_MinFDINVALID_SOCKET ) 28: { 29: return TRUE ; 30: } 31: 32: m_Timeout[SELECT_USE].tv_sec m_Timeout[SELECT_BAK].tv_sec; 33: m_Timeout[SELECT_USE].tv_usec m_Timeout[SELECT_BAK].tv_usec; 34: 35: m_ReadFDs[SELECT_USE] m_ReadFDs[SELECT_BAK]; 36: m_WriteFDs[SELECT_USE] m_WriteFDs[SELECT_BAK]; 37: m_ExceptFDs[SELECT_USE] m_ExceptFDs[SELECT_BAK]; 38: 39: _MY_TRY 40: { 41: INT iRet SocketAPI::select_ex( (INT)m_MaxFD1 , 42: m_ReadFDs[SELECT_USE] , 43: m_WriteFDs[SELECT_USE] , 44: m_ExceptFDs[SELECT_USE] , 45: m_Timeout[SELECT_USE] ) ; //--------3 46: if( iRetSOCKET_ERROR ) 47: { 48: Assert(FALSE) ; 49: } 50: } 51: _MY_CATCH 52: { 53: Log::SaveLog( SERVER_LOGFILE, ERROR: ServerManager::Select( )... ) ; 54: } 55: 56: return TRUE ; 57: 58: __LEAVE_FUNCTION 59: 60: return FALSE ; 61: 62: } 如上在Tick ()函数里首先调用ServerManager::Select ()在Select函数中2调用MySleep(50)让出CPU时间片50ms然后给select_ex设置100us的超时可以认为每次执行完处理后会固定sleep 50ms。 再来看看Threadpool里线程的tick函数 1: BOOL Scene::Tick( ) 2: { 3: ........ 4: 5: //网络处理 6: _MY_TRY 7: { 8: ret m_pScenePlayerManager-Select( ) ; //--------1 9: Assert( ret ) ; 10: } 11: _MY_CATCH 12: { 13: SaveCodeLog( ) ; 14: } 15: 16: ........ 17: } 18: | 19: | 20: \|/ 21: BOOL ScenePlayerManager::Select( ) 22: { 23: { 24: MySleep( 50 ) ; //--------2 25: } 26: 27: if( m_MaxFDINVALID_SOCKET m_MinFDINVALID_SOCKET ) 28: return TRUE ; 29: 30: m_Timeout[SELECT_USE].tv_sec m_Timeout[SELECT_BAK].tv_sec; 31: m_Timeout[SELECT_USE].tv_usec m_Timeout[SELECT_BAK].tv_usec; 32: 33: m_ReadFDs[SELECT_USE] m_ReadFDs[SELECT_BAK]; 34: m_WriteFDs[SELECT_USE] m_WriteFDs[SELECT_BAK]; 35: m_ExceptFDs[SELECT_USE] m_ExceptFDs[SELECT_BAK]; 36: 37: _MY_TRY 38: { 39: INT ret SocketAPI::select_ex( (INT)m_MaxFD1 , 40: m_ReadFDs[SELECT_USE] , 41: m_WriteFDs[SELECT_USE] , 42: m_ExceptFDs[SELECT_USE] , 43: m_Timeout[SELECT_USE] ) ; //--------3 44: if( ret SOCKET_ERROR ) 45: { 46: Assert(FALSE) ; 47: } 48: } 49: _MY_CATCH 50: { 51: SaveCodeLog( ) ; 52: } 53: 54: ........ 55: } 根据以上代码中的1、2、3可以看到场景的tick函数中采用同样的方法每次调用时先调用MySleep(50)让出CPU时间片50ms然后再执行相应的处理代码。 总结
a固定Sleep时间在高负荷情况下由于每次都会强制让出CPU时间片会不会导致响应不及时
b在每次运行执行处理函数的时间比较稳当的情况下这种策略还是能提供比较稳定的tick间隔。
c这样的设计目前为止本人还没有看出有什么好处请各位博友指教。 二、按照物理位置分类 按照物理位置分类分为两种一种是运行在同一物理主机上另一种是运行在不同的物理主机上。该分类是受到云风一篇blog的启发见《心跳服务器》。
1心跳服务和逻辑程序运行在同一物理主机上据我所知大部分的网游服务器程序都采用这种方式而且心跳服务往往和逻辑程序写在同一个进程里。心跳服务和逻辑程序不在一个进程里但运行在同一物理主机上的情况本人没有见过也不敢妄加臆断。
2心跳服务运行在一台独立的物理主机上a按照云风的说法这样做最大的好处就是方便实现服务器的快放慢放功能从而很容易重现bug。博文中指出使用使用10Hz的心跳间隔以局域网内0.1~0.2ms的ping值不知道能不能容忍而且使用TCP连接多两次网络IO不知道效率如何b另一种独立的心跳服务器是本人读研时参与过的大屏幕显示同步时遇到的当时一块大屏幕有24台主机每台主机负责绘制图像中的一块区域。使用一台单独的心跳服务器用同步卡向着24台主机的串口周期性的发信号这24台主机收到从串口来的心跳信号后同时开始绘制这一帧。不过据说精确度有待提高……… 三、总结 综上所述最常见的心跳服务是这样设计的使用固定tick时间策略与逻辑处理程序写在同一个进程里tick时间50ms100ms。