魔獸時間是暴雪著名的網絡遊戲,我之前也玩過一段時間的戰士,這款遊戲目前已進入晚年時期,不過裏面各類豐富的遊戲系統和遊戲內容都很是讓人印象深入。開源的Mangos項目模擬魔獸服務器端很是成功,目前國內外也有很多基於Mangos模擬器而搭建的私服,多數服務端運轉良好,很是穩定。國外有一個叫作MonsterWOW的魔獸私服,單服承載5000人,總共有幾組服務器,幾萬人同時在線,這是我在網站上親眼看到的實時數據,通常來說,若是對MMORPG遊戲服務端稍微熟悉都知道,5000人同服在線,並且容許遊戲邏輯的是一臺單獨的服務器,支撐這麼龐大一個遊戲世界,確定有很是過人之處,至少據我所知國內的單服性能與之相比都有較大差距,國內分佈式的服務端架構基本也是將遊戲邏輯分散到多臺服務器上,單一世界承載數量也不算很高。幾年前的Eve Online單一世界能夠承載兩萬多玩家同時在線、實時交互。我想國內多數MMORPG服務端的承載人數應該都是在七八百、一兩千這個數量級的。Mangos的源代碼下載下來很久了,一直沒時間研究,它目前是C++寫成的,個人主要方向是C#,不過我一直有將C#作遊戲服務端的打算,因此既然它有那麼多過人之處,就算不能掌握所有也應該研究學習一下。 python
今天粗略地看了一下,服務端主要又三大塊組成,數據庫、服務端邏輯、腳本。數據庫用的MySQL,這裏不是很關鍵暫且不說,腳本有本身的腳步引擎,簡單的任務、戰鬥等均可以經過數據庫配置相應條目來完成,複雜的戰鬥AI等在腳步庫中由C++直接寫成,這個腳本庫是要被編譯爲機器代碼的,執行效率至關高效,例如巫妖王的戰鬥比較複雜就用C++寫,其它簡單的就配置在數據庫中由腳步引擎來驅動執行。國內很多服務端都是很是老式的C++早期服務端結構,很多嵌入了lua解釋器,大量的寫lua腳本,甚至用lua寫邏輯。我我的很不理解這種方式,你說效率高吧,lua再快能多塊,解釋執行和編譯執行不是一個數量級的,看看服務端的承載人數就知道了,lua JIT即時編譯都不靠譜。或許有人會說lua簡單,策劃均可以學習以後寫腳本,事實上倒是寫腳本的人寫出一大堆的不敢說垃圾代碼,也算是低質量代碼,這樣更加拖累服務端的性能了。爲什麼不學學一些比較優秀的項目,也來想辦法搞一個腳本引擎,而後寫出工具就可讓策劃配置大量的任務、戰鬥這些遊戲內容,複雜的邏輯直接由遊戲程序員來編寫,用C++、C#多好,搞不懂爲何lua已經成爲好多公司的標準了,就算不是lua也是python。就說劍網3這個遊戲吧,我玩了兩年多的劍純陽,對這款遊戲的體驗有足夠的瞭解。咱們不和其它遊戲的遊戲比,至少在國內算優秀做品,也取得了必定的成功,雖說抄魔獸也有點多。之前玩遊戲的時候,二十多我的進個副本放些技能卡得要命,人多了在一個地圖直接卡到爆,後來一個好朋友和我說,劍網3服務端用lua寫了好多東西,能lua的多半都用lua了,一個天子峯老6,這個Boss的lua腳本竟有好幾個lua文件,每一個文件幾百行代碼,我想啊,服務端徹底充斥着這種低質量的腳本,還談什麼效率,談什麼承載人數,能跑起來就不錯了。關鍵是那個Boss的戰鬥並不複雜,和魔獸不少Boss比起來就算是很是簡單的Boss了,mangos服務端一個複雜Boss的代碼都比這個簡單不少,代碼總數也僅兩百多行,執行效率更不是一個數量級的。這裏發發牢騷,不用較真,言歸正傳。 程序員
Mangos服務端是一個多線程、邏輯單線程的服務端。每一個線程內部都採用循環結構,主線程啓動後將建立多個工做線程,主要包括負責遊戲世界運做的核心線程,具備處理用戶請求,執行定時器的能力。其它幾個工做線程還有網絡Io,該線程啓動後其內部將使用線程池進行網絡Io操做,不間斷地接收數據包,並存儲到相關玩家的消息隊列中,由世界線程進行處理,其它幾個工做線程先不討論,由於今天也是第一次看mangos的源代碼.務端啓動後這些線程將永不停息地工做。世界線程是服務器的核心,負責處理全部玩家操做請求,定時器、AI等。如下是世界線程啓動後執行的代碼: sql
/// Heartbeat for the World void WorldRunnable::run() { ///- Init new SQL thread for the world database WorldDatabase.ThreadStart(); // let thread do safe mySQL requests (one connection call enough) sWorld.InitResultQueue(); uint32 realCurrTime = 0; uint32 realPrevTime = WorldTimer::tick(); uint32 prevSleepTime = 0; // used for balanced full tick time length near WORLD_SLEEP_CONST ///- While we have not World::m_stopEvent, update the world while (!World::IsStopped()) { ++World::m_worldLoopCounter; realCurrTime = WorldTimer::getMSTime(); uint32 diff = WorldTimer::tick(); sWorld.Update(diff); realPrevTime = realCurrTime; // diff (D0) include time of previous sleep (d0) + tick time (t0) // we want that next d1 + t1 == WORLD_SLEEP_CONST // we can't know next t1 and then can use (t0 + d1) == WORLD_SLEEP_CONST requirement // d1 = WORLD_SLEEP_CONST - t0 = WORLD_SLEEP_CONST - (D0 - d0) = WORLD_SLEEP_CONST + d0 - D0 if (diff <= WORLD_SLEEP_CONST + prevSleepTime) { prevSleepTime = WORLD_SLEEP_CONST + prevSleepTime - diff; ACE_Based::Thread::Sleep(prevSleepTime); } else prevSleepTime = 0; #ifdef WIN32 if (m_ServiceStatus == 0) World::StopNow(SHUTDOWN_EXIT_CODE); while (m_ServiceStatus == 2) Sleep(1000); #endif } sWorld.CleanupsBeforeStop(); sWorldSocketMgr->StopNetwork(); MapManager::Instance().UnloadAll(); // unload all grids (including locked in memory) ///- End the database thread WorldDatabase.ThreadEnd(); // free mySQL thread resources }
由於是直接粘貼的,看上去比較亂,這裏先做一下說明,這是世界線程的根循環結構,在while(!World::IsStopped())內部只有一個核心函數調用,其餘都是一些控制更新時間之類的代碼,不用太關注: 數據庫
sWorld.Update(diff);
sWorld是單一實例的World對象,它表明了整個遊戲世界,和多數MMORPG同樣,啓動後進入根循環,在運行內部一直調用更新整個遊戲世界的Update函數,服務端不停的Update遊戲世界,每次Update能在100毫秒內完成,則客戶端會感到很是流暢。在根循環退出後,清理服務器相關資源,線程結束被回收。Mangos使用的是開源跨平臺的網絡、線程處理庫ACE,這個東西粗略的看了一下,比較複雜,若是要研究透徹是很困難的事,這裏提一下,不對ACE探討。到這裏咱們僅僅須要關注一個函數了,就是World的Update方法內部到底在幹什麼? 服務器
void World::Update(uint32 diff) { ///- Update the different timers for (int i = 0; i < WUPDATE_COUNT; ++i) { if (m_timers[i].GetCurrent() >= 0) m_timers[i].Update(diff); else m_timers[i].SetCurrent(0); } ///- Update the game time and check for shutdown time _UpdateGameTime(); ///-Update mass mailer tasks if any sMassMailMgr.Update(); /// Handle daily quests reset time if (m_gameTime > m_NextDailyQuestReset) ResetDailyQuests(); /// Handle weekly quests reset time if (m_gameTime > m_NextWeeklyQuestReset) ResetWeeklyQuests(); /// Handle monthly quests reset time if (m_gameTime > m_NextMonthlyQuestReset) ResetMonthlyQuests(); /// Handle monthly quests reset time if (m_gameTime > m_NextCurrencyReset) ResetCurrencyWeekCounts(); /// <ul><li> Handle auctions when the timer has passed if (m_timers[WUPDATE_AUCTIONS].Passed()) { m_timers[WUPDATE_AUCTIONS].Reset(); ///- Update mails (return old mails with item, or delete them) //(tested... works on win) if (++mail_timer > mail_timer_expires) { mail_timer = 0; sObjectMgr.ReturnOrDeleteOldMails(true); } ///- Handle expired auctions sAuctionMgr.Update(); } /// <li> Handle AHBot operations if (m_timers[WUPDATE_AHBOT].Passed()) { sAuctionBot.Update(); m_timers[WUPDATE_AHBOT].Reset(); } /// <li> Handle session updates UpdateSessions(diff); /// <li> Handle weather updates when the timer has passed if (m_timers[WUPDATE_WEATHERS].Passed()) { ///- Send an update signal to Weather objects for (WeatherMap::iterator itr = m_weathers.begin(); itr != m_weathers.end();) { ///- and remove Weather objects for zones with no player // As interval > WorldTick if (!itr->second->Update(m_timers[WUPDATE_WEATHERS].GetInterval())) { delete itr->second; m_weathers.erase(itr++); } else ++itr; } m_timers[WUPDATE_WEATHERS].SetCurrent(0); } /// <li> Update uptime table if (m_timers[WUPDATE_UPTIME].Passed()) { uint32 tmpDiff = uint32(m_gameTime - m_startTime); uint32 maxClientsNum = GetMaxActiveSessionCount(); m_timers[WUPDATE_UPTIME].Reset(); LoginDatabase.PExecute("UPDATE uptime SET uptime = %u, maxplayers = %u WHERE realmid = %u AND starttime = " UI64FMTD, tmpDiff, maxClientsNum, realmID, uint64(m_startTime)); } /// <li> Handle all other objects ///- Update objects (maps, transport, creatures,...) sMapMgr.Update(diff); sBattleGroundMgr.Update(diff); sOutdoorPvPMgr.Update(diff); ///- Delete all characters which have been deleted X days before if (m_timers[WUPDATE_DELETECHARS].Passed()) { m_timers[WUPDATE_DELETECHARS].Reset(); Player::DeleteOldCharacters(); } // execute callbacks from sql queries that were queued recently UpdateResultQueue(); ///- Erase corpses once every 20 minutes //每20分鐘清除屍體 if (m_timers[WUPDATE_CORPSES].Passed()) { m_timers[WUPDATE_CORPSES].Reset(); sObjectAccessor.RemoveOldCorpses(); } ///- Process Game events when necessary //處理遊戲事件 if (m_timers[WUPDATE_EVENTS].Passed()) { m_timers[WUPDATE_EVENTS].Reset(); // to give time for Update() to be processed uint32 nextGameEvent = sGameEventMgr.Update(); m_timers[WUPDATE_EVENTS].SetInterval(nextGameEvent); m_timers[WUPDATE_EVENTS].Reset(); } /// </ul> ///- Move all creatures with "delayed move" and remove and delete all objects with "delayed remove" sMapMgr.RemoveAllObjectsInRemoveList(); // update the instance reset times sMapPersistentStateMgr.Update(); // And last, but not least handle the issued cli commands ProcessCliCommands(); // cleanup unused GridMap objects as well as VMaps sTerrainMgr.Update(diff); }
這是World::Update函數的所有代碼,服務器循環執行這些代碼,每一次執行就能更新一次遊戲世界。這個函數看似比較長,實際上不算很長,其中的關鍵之處在於首先是根據定時器來執行特定的任務,而執行這些任務則是經過調用各個模塊的Manager來完成,好比遊戲世界裏面的屍體每20分鐘清除一次,就檢測相關的定時器是否超時,超時則清理屍體,而後重置定時器。經過這些定時器,來執行遊戲中由服務器主動完成的任務,這些任務基本上是經過定時器來啓動的。遊戲中的天氣系統、PvP系統、地形系統等等都根據定時器指定的頻率進行更新。除了更新各個模塊以外,其中還有個很是重要的調用: 網絡
UpdateSessions(diff);
若是翻譯過來就是更新全部會話,服務器端爲每個客戶端創建一個Session,即會話,它是客戶端與服務端溝通的通道,取數據、發數據都得經過這條通道,這樣客戶端和服務端才能溝通。在mangos的構架中,Session的做用很是重要,但其功能不只僅取客戶端發過來的數據、將服務端數據發給客戶端那麼簡單,後面會繼續結束這個Session,很關鍵的東西,下面是UpdateSessions的具體實現: session
void World::UpdateSessions(uint32 diff) { ///- Add new sessions WorldSession* sess; while (addSessQueue.next(sess)) AddSession_(sess); ///- Then send an update signal to remaining ones for (SessionMap::iterator itr = m_sessions.begin(), next; itr != m_sessions.end(); itr = next) { next = itr; ++next; ///- and remove not active sessions from the list WorldSession* pSession = itr->second; WorldSessionFilter updater(pSession); if (!pSession->Update(updater)) { RemoveQueuedSession(pSession); m_sessions.erase(itr); delete pSession; } } }
其內部結構很簡單,主要遍歷全部會話,移除不活動的會話,並調用每一個Session的Update函數,達到更新全部Session的目的,有1000玩家在線就會更新1000個會話,前面提到了Session,每一個會話的內部都掛載有一個消息隊列,這裏隊列存儲着從客戶端發過來的數據包,1000個會話就會有1000個數據包隊列,隊列是由網絡模塊收到數據包後,將其掛載到相應Sesson的接收隊列中,客戶端1發來的數據包被掛載到Session1的隊列,客戶端2的就掛載到Session2的隊列中。mangos的架構中Session不止是收發數據的入口,一樣也是處理客戶端數據的入口,即處理客戶端請求的調度中心。每次Update Session的時候,這個Update 函數的內部會取出隊列中全部的請求數據,循環地對每個數據包調用數據包對應的處理代碼,即根據數據包的類型(操做碼OpCode)調用相應的函數進行處理,而這些「相應的函數」是Session內部的普通成員函數,以HandleXXXXXX開頭,爲了便於理解,我將Session的Update函數主體核心代碼寫在這裏: 多線程
bool WorldSession::Update(PacketFilter& updater) { ///- Retrieve packets from the receive queue and call the appropriate handlers /// not process packets if socket already closed WorldPacket* packet = NULL; while (m_Socket && !m_Socket->IsClosed() && _recvQueue.next(packet, updater)) { OpcodeHandler const& opHandle = opcodeTable[packet->GetOpcode()]; ExecuteOpcode(opHandle, packet); } }
這樣看起了比較清楚了,Session在Update的時候,取出全部數據包,每一個數據包都有一個操做碼,opcode,魔獸模擬器有1600多個操做碼,玩家或者服務器的每一個操做都有一個對應的操做碼,好比攻擊某個目標、拾取一件東西、使用某個物品都有操做碼,被追加到數據包頭部,這樣每次取數據包的操做碼,就能夠查找相應的處理代碼來處理這個數據包。 架構
從代碼裏面能夠看到opHandle就是根據操做碼查找到的數據處理程序,內部有相應數據處理函數的指針,ExecuteOpcode便是經過這個函數指針調用該函數來處理數據包。而處理函數實際上都是 Session的普通成員函數,固然調度處理代碼的時候並不是根據操做碼進行switch判斷來調用相應處理函數,這樣會寫一個很是巨大的switch結構,mangos的方式是經過硬編碼將這些處理函數的地址存在opcodeTable這個全局的表結構中,使用OpCode做爲索引,迅速地定位到相應的處理函數,即找到改數據包對應的Handler,並執行他們。 app
void HandleGroupInviteOpcode(WorldPacket& recvPacket);
void HandleGroupInviteResponseOpcode(WorldPacket& recvPacket);
void HandleGroupUninviteOpcode(WorldPacket& recvPacket);
void HandleGroupUninviteGuidOpcode(WorldPacket& recvPacket);
void HandleGroupSetLeaderOpcode(WorldPacket& recvPacket);
void HandleGroupDisbandOpcode(WorldPacket& recvPacket);
void HandleOptOutOfLootOpcode(WorldPacket& recv_data);
void HandleSetAllowLowLevelRaidOpcode(WorldPacket& recv_data);
void HandleLootMethodOpcode(WorldPacket& recvPacket);
void HandleLootRoll(WorldPacket& recv_data);
void HandleRequestPartyMemberStatsOpcode(WorldPacket& recv_data);
void HandleRaidTargetUpdateOpcode(WorldPacket& recv_data);
void HandleRaidReadyCheckOpcode(WorldPacket& recv_data);
void HandleRaidReadyCheckFinishedOpcode(WorldPacket& recv_data);
void HandleGroupRaidConvertOpcode(WorldPacket& recv_data);
void HandleGroupChangeSubGroupOpcode(WorldPacket& recv_data);
void HandleGroupAssistantLeaderOpcode(WorldPacket& recv_data);
void HandlePartyAssignmentOpcode(WorldPacket& recv_data);
上面是極小部分的處理函數,他們都是Session的成員函數,這些函數並不是是最終處理數據的,每每一個函數對應一個邏輯模塊,與這個模塊相關的操做碼有不少,好比聊天系統客戶端發來的操做碼多是密聊、隊聊、地圖聊天,可是在Session收到數據包時,會將這個模塊的這些操做碼都調用HandleMessage函數,這些Handle函數內部會根據具體的操做碼再調用相應模塊的處理函數,就是說消息的調度是兩級的。先從入口點,經過查找OpCodeTabel找到一級調度函數、數據包傳過去後又進行二級調度,分發到更小的子模塊,直到分發的具體模塊爲止。
今天暫時寫到這裏,還有不少想說的,之後繼續慢慢吹,下次繼續今天沒完善的內容、談一談mangos的二進制協議、數據通訊機制等內容,長期研究下mangos,確定有好處的。