Mangos魔兽世界服务端初探(1)--游戏服务端主体结构与消息分发
魔獸時(shí)間是暴雪著名的網(wǎng)絡(luò)游戲,我以前也玩過一段時(shí)間的戰(zhàn)士,這款游戲目前已進(jìn)入晚年時(shí)期,不過里面各種豐富的游戲系統(tǒng)和游戲內(nèi)容都非常讓人印象深刻。開源的Mangos項(xiàng)目模擬魔獸服務(wù)器端非常成功,目前國內(nèi)外也有不少基于Mangos模擬器而搭建的私服,多數(shù)服務(wù)端運(yùn)轉(zhuǎn)良好,非常穩(wěn)定。國外有一個(gè)叫做MonsterWOW的魔獸私服,單服承載5000人,總共有幾組服務(wù)器,幾萬人同時(shí)在線,這是我在網(wǎng)站上親眼看到的實(shí)時(shí)數(shù)據(jù),一般來講,如果對(duì)MMORPG游戲服務(wù)端稍微熟悉都知道,5000人同服在線,而且允許游戲邏輯的是一臺(tái)單獨(dú)的服務(wù)器,支撐這么龐大一個(gè)游戲世界,肯定有非常過人之處,至少據(jù)我所知國內(nèi)的單服性能與之相比都有較大差距,國內(nèi)分布式的服務(wù)端架構(gòu)基本也是將游戲邏輯分散到多臺(tái)服務(wù)器上,單一世界承載數(shù)量也不算很高。幾年前的Eve Online單一世界可以承載兩萬多玩家同時(shí)在線、實(shí)時(shí)交互。我想國內(nèi)多數(shù)MMORPG服務(wù)端的承載人數(shù)應(yīng)該都是在七八百、一兩千這個(gè)數(shù)量級(jí)的。Mangos的源代碼下載下來好久了,一直沒時(shí)間研究,它目前是C++寫成的,我的主要方向是C#,不過我一直有將C#做游戲服務(wù)端的打算,所以既然它有那么多過人之處,就算不能掌握全部也應(yīng)該研究學(xué)習(xí)一下。
???? 今天粗略地看了一下,服務(wù)端主要又三大塊組成,數(shù)據(jù)庫、服務(wù)端邏輯、腳本。數(shù)據(jù)庫用的MySQL,這里不是很關(guān)鍵暫且不說,腳本有自己的腳步引擎,簡單的任務(wù)、戰(zhàn)斗等都可以通過數(shù)據(jù)庫配置相應(yīng)條目來完成,復(fù)雜的戰(zhàn)斗AI等在腳步庫中由C++直接寫成,這個(gè)腳本庫是要被編譯為機(jī)器代碼的,執(zhí)行效率相當(dāng)高效,例如巫妖王的戰(zhàn)斗比較復(fù)雜就用C++寫,其它簡單的就配置在數(shù)據(jù)庫中由腳步引擎來驅(qū)動(dòng)執(zhí)行。國內(nèi)不少服務(wù)端都是非常老式的C++早期服務(wù)端結(jié)構(gòu),不少嵌入了lua解釋器,大量的寫lua腳本,甚至用lua寫邏輯。我個(gè)人很不理解這種方式,你說效率高吧,lua再快能多塊,解釋執(zhí)行和編譯執(zhí)行不是一個(gè)數(shù)量級(jí)的,看看服務(wù)端的承載人數(shù)就知道了,lua JIT即時(shí)編譯都不靠譜?;蛟S有人會(huì)說lua簡單,策劃都可以學(xué)習(xí)之后寫腳本,事實(shí)上卻是寫腳本的人寫出一大堆的不敢說垃圾代碼,也算是低質(zhì)量代碼,這樣更加拖累服務(wù)端的性能了。為何不學(xué)學(xué)一些比較優(yōu)秀的項(xiàng)目,也來想辦法搞一個(gè)腳本引擎,然后寫出工具就可以讓策劃配置大量的任務(wù)、戰(zhàn)斗這些游戲內(nèi)容,復(fù)雜的邏輯直接由游戲程序員來編寫,用C++、C#多好,搞不懂為什么lua已經(jīng)成為好多公司的標(biāo)準(zhǔn)了,就算不是lua也是python。就說劍網(wǎng)3這個(gè)游戲吧,我玩了兩年多的劍純陽,對(duì)這款游戲的體驗(yàn)有足夠的了解。我們不和其它游戲的游戲比,至少在國內(nèi)算優(yōu)秀作品,也取得了一定的成功,雖然說抄魔獸也有點(diǎn)多。以前玩游戲的時(shí)候,二十多個(gè)人進(jìn)個(gè)副本放些技能卡得要命,人多了在一個(gè)地圖直接卡到爆,后來一個(gè)好朋友和我說,劍網(wǎng)3服務(wù)端用lua寫了好多東西,能lua的多半都用lua了,一個(gè)天子峰老6,這個(gè)Boss的lua腳本竟有好幾個(gè)lua文件,每個(gè)文件幾百行代碼,我想啊,服務(wù)端完全充斥著這種低質(zhì)量的腳本,還談什么效率,談什么承載人數(shù),能跑起來就不錯(cuò)了。關(guān)鍵是那個(gè)Boss的戰(zhàn)斗并不復(fù)雜,和魔獸很多Boss比起來就算是非常簡單的Boss了,mangos服務(wù)端一個(gè)復(fù)雜Boss的代碼都比這個(gè)簡單很多,代碼總數(shù)也僅兩百多行,執(zhí)行效率更不是一個(gè)數(shù)量級(jí)的。這里發(fā)發(fā)牢騷,不用較真,言歸正傳。
???? Mangos服務(wù)端是一個(gè)多線程、邏輯單線程的服務(wù)端。每個(gè)線程內(nèi)部都采用循環(huán)結(jié)構(gòu),主線程啟動(dòng)后將創(chuàng)建多個(gè)工作線程,主要包括負(fù)責(zé)游戲世界運(yùn)作的核心線程,具有處理用戶請(qǐng)求,執(zhí)行定時(shí)器的能力。其它幾個(gè)工作線程還有網(wǎng)絡(luò)Io,該線程啟動(dòng)后其內(nèi)部將使用線程池進(jìn)行網(wǎng)絡(luò)Io操作,不間斷地接收數(shù)據(jù)包,并存儲(chǔ)到相關(guān)玩家的消息隊(duì)列中,由世界線程進(jìn)行處理,其它幾個(gè)工作線程先不討論,因?yàn)榻裉煲彩堑谝淮慰磎angos的源代碼.務(wù)端啟動(dòng)后這些線程將永不停息地工作。世界線程是服務(wù)器的核心,負(fù)責(zé)處理所有玩家操作請(qǐng)求,定時(shí)器、AI等。以下是世界線程啟動(dòng)后執(zhí)行的代碼:
?///?Heartbeat?for?the?World void?WorldRunnable::run() {///-?Init?new?SQL?thread?for?the?world?databaseWorldDatabase.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?worldwhile?(!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?-?D0if?(diff?<=?WORLD_SLEEP_CONST?+?prevSleepTime){prevSleepTime?=?WORLD_SLEEP_CONST?+?prevSleepTime?-?diff;ACE_Based::Thread::Sleep(prevSleepTime);}elseprevSleepTime?=?0; #ifdef?WIN32if?(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?threadWorldDatabase.ThreadEnd();??????????????????????????????//?free?mySQL?thread?resources }因?yàn)槭侵苯诱迟N的,看上去比較亂,這里先作一下說明,這是世界線程的根循環(huán)結(jié)構(gòu),在while(!World::IsStopped())內(nèi)部只有一個(gè)核心函數(shù)調(diào)用,其他都是一些控制更新時(shí)間之類的代碼,不用太關(guān)注:
sWorld.Update(diff);sWorld是單一實(shí)例的World對(duì)象,它代表了整個(gè)游戲世界,和多數(shù)MMORPG一樣,啟動(dòng)后進(jìn)入根循環(huán),在運(yùn)行內(nèi)部一直調(diào)用更新整個(gè)游戲世界的Update函數(shù),服務(wù)端不停的Update游戲世界,每次Update能在100毫秒內(nèi)完成,則客戶端會(huì)感到非常流暢。在根循環(huán)退出后,清理服務(wù)器相關(guān)資源,線程結(jié)束被回收。Mangos使用的是開源跨平臺(tái)的網(wǎng)絡(luò)、線程處理庫ACE,這個(gè)東西粗略的看了一下,比較復(fù)雜,如果要研究透徹是很困難的事,這里提一下,不對(duì)ACE探討。到這里我們僅僅需要關(guān)注一個(gè)函數(shù)了,就是World的Update方法內(nèi)部到底在干什么?
void?World::Update(uint32?diff) {///-?Update?the?different?timersfor?(int?i?=?0;?i?<?WUPDATE_COUNT;?++i){if?(m_timers[i].GetCurrent()?>=?0)m_timers[i].Update(diff);elsem_timers[i].SetCurrent(0);}///-?Update?the?game?time?and?check?for?shutdown?time_UpdateGameTime();///-Update?mass?mailer?tasks?if?anysMassMailMgr.Update();///?Handle?daily?quests?reset?timeif?(m_gameTime?>?m_NextDailyQuestReset)ResetDailyQuests();///?Handle?weekly?quests?reset?timeif?(m_gameTime?>?m_NextWeeklyQuestReset)ResetWeeklyQuests();///?Handle?monthly?quests?reset?timeif?(m_gameTime?>?m_NextMonthlyQuestReset)ResetMonthlyQuests();///?Handle?monthly?quests?reset?timeif?(m_gameTime?>?m_NextCurrencyReset)ResetCurrencyWeekCounts();///?<ul><li>?Handle?auctions?when?the?timer?has?passedif?(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?auctionssAuctionMgr.Update();}///?<li>?Handle?AHBot?operationsif?(m_timers[WUPDATE_AHBOT].Passed()){sAuctionBot.Update();m_timers[WUPDATE_AHBOT].Reset();}///?<li>?Handle?session?updatesUpdateSessions(diff);///?<li>?Handle?weather?updates?when?the?timer?has?passedif?(m_timers[WUPDATE_WEATHERS].Passed()){///-?Send?an?update?signal?to?Weather?objectsfor?(WeatherMap::iterator?itr?=?m_weathers.begin();?itr?!=?m_weathers.end();){///-?and?remove?Weather?objects?for?zones?with?no?player//?As?interval?>?WorldTickif?(!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?tableif?(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?beforeif?(m_timers[WUPDATE_DELETECHARS].Passed()){m_timers[WUPDATE_DELETECHARS].Reset();Player::DeleteOldCharacters();}//?execute?callbacks?from?sql?queries?that?were?queued?recentlyUpdateResultQueue();///-?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?processeduint32?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?timessMapPersistentStateMgr.Update();//?And?last,?but?not?least?handle?the?issued?cli?commandsProcessCliCommands();//?cleanup?unused?GridMap?objects?as?well?as?VMapssTerrainMgr.Update(diff); }這是World::Update函數(shù)的全部代碼,服務(wù)器循環(huán)執(zhí)行這些代碼,每一次執(zhí)行就能更新一次游戲世界。這個(gè)函數(shù)看似比較長,實(shí)際上不算很長,其中的關(guān)鍵之處在于首先是根據(jù)定時(shí)器來執(zhí)行特定的任務(wù),而執(zhí)行這些任務(wù)則是通過調(diào)用各個(gè)模塊的Manager來完成,比如游戲世界里面的尸體每20分鐘清除一次,就檢測相關(guān)的定時(shí)器是否超時(shí),超時(shí)則清理尸體,然后重置定時(shí)器。通過這些定時(shí)器,來執(zhí)行游戲中由服務(wù)器主動(dòng)完成的任務(wù),這些任務(wù)基本上是通過定時(shí)器來啟動(dòng)的。游戲中的天氣系統(tǒng)、PvP系統(tǒng)、地形系統(tǒng)等等都根據(jù)定時(shí)器指定的頻率進(jìn)行更新。除了更新各個(gè)模塊之外,其中還有個(gè)非常重要的調(diào)用:
UpdateSessions(diff);如果翻譯過來就是更新所有會(huì)話,服務(wù)器端為每一個(gè)客戶端建立一個(gè)Session,即會(huì)話,它是客戶端與服務(wù)端溝通的通道,取數(shù)據(jù)、發(fā)數(shù)據(jù)都得通過這條通道,這樣客戶端和服務(wù)端才能溝通。在mangos的構(gòu)架中,Session的作用非常重要,但其功能不僅僅取客戶端發(fā)過來的數(shù)據(jù)、將服務(wù)端數(shù)據(jù)發(fā)給客戶端那么簡單,后面會(huì)繼續(xù)結(jié)束這個(gè)Session,很關(guān)鍵的東西,下面是UpdateSessions的具體實(shí)現(xiàn):
void?World::UpdateSessions(uint32?diff) {///-?Add?new?sessionsWorldSession*?sess;while?(addSessQueue.next(sess))AddSession_(sess);///-?Then?send?an?update?signal?to?remaining?onesfor?(SessionMap::iterator?itr?=?m_sessions.begin(),?next;?itr?!=?m_sessions.end();?itr?=?next){next?=?itr;++next;///-?and?remove?not?active?sessions?from?the?listWorldSession*?pSession?=?itr->second;WorldSessionFilter?updater(pSession);if?(!pSession->Update(updater)){RemoveQueuedSession(pSession);m_sessions.erase(itr);delete?pSession;}} }其內(nèi)部結(jié)構(gòu)很簡單,主要遍歷所有會(huì)話,移除不活動(dòng)的會(huì)話,并調(diào)用每個(gè)Session的Update函數(shù),達(dá)到更新所有Session的目的,有1000玩家在線就會(huì)更新1000個(gè)會(huì)話,前面提到了Session,每個(gè)會(huì)話的內(nèi)部都掛載有一個(gè)消息隊(duì)列,這里隊(duì)列存儲(chǔ)著從客戶端發(fā)過來的數(shù)據(jù)包,1000個(gè)會(huì)話就會(huì)有1000個(gè)數(shù)據(jù)包隊(duì)列,隊(duì)列是由網(wǎng)絡(luò)模塊收到數(shù)據(jù)包后,將其掛載到相應(yīng)Sesson的接收隊(duì)列中,客戶端1發(fā)來的數(shù)據(jù)包被掛載到Session1的隊(duì)列,客戶端2的就掛載到Session2的隊(duì)列中。mangos的架構(gòu)中Session不止是收發(fā)數(shù)據(jù)的入口,同樣也是處理客戶端數(shù)據(jù)的入口,即處理客戶端請(qǐng)求的調(diào)度中心。每次Update Session的時(shí)候,這個(gè)Update 函數(shù)的內(nèi)部會(huì)取出隊(duì)列中所有的請(qǐng)求數(shù)據(jù),循環(huán)地對(duì)每一個(gè)數(shù)據(jù)包調(diào)用數(shù)據(jù)包對(duì)應(yīng)的處理代碼,即根據(jù)數(shù)據(jù)包的類型(操作碼OpCode)調(diào)用相應(yīng)的函數(shù)進(jìn)行處理,而這些“相應(yīng)的函數(shù)”是Session內(nèi)部的普通成員函數(shù),以HandleXXXXXX開頭,為了便于理解,我將Session的Update函數(shù)主體核心代碼寫在這里:
bool?WorldSession::Update(PacketFilter&?updater){///-?Retrieve?packets?from?the?receive?queue?and?call?the?appropriate?handlers///?not?process?packets?if?socket?already?closedWorldPacket*?packet?=?NULL;while?(m_Socket?&&?!m_Socket->IsClosed()?&&?_recvQueue.next(packet,?updater)){OpcodeHandler?const&?opHandle?=?opcodeTable[packet->GetOpcode()];ExecuteOpcode(opHandle,?packet);}}這樣看起了比較清楚了,Session在Update的時(shí)候,取出所有數(shù)據(jù)包,每個(gè)數(shù)據(jù)包都有一個(gè)操作碼,opcode,魔獸模擬器有1600多個(gè)操作碼,玩家或者服務(wù)器的每個(gè)操作都有一個(gè)對(duì)應(yīng)的操作碼,比如攻擊某個(gè)目標(biāo)、拾取一件東西、使用某個(gè)物品都有操作碼,被追加到數(shù)據(jù)包頭部,這樣每次取數(shù)據(jù)包的操作碼,就可以查找相應(yīng)的處理代碼來處理這個(gè)數(shù)據(jù)包。
從代碼里面可以看到opHandle就是根據(jù)操作碼查找到的數(shù)據(jù)處理程序,內(nèi)部有相應(yīng)數(shù)據(jù)處理函數(shù)的指針,ExecuteOpcode即是通過這個(gè)函數(shù)指針調(diào)用該函數(shù)來處理數(shù)據(jù)包。而處理函數(shù)實(shí)際上都是 Session的普通成員函數(shù),當(dāng)然調(diào)度處理代碼的時(shí)候并非根據(jù)操作碼進(jìn)行switch判斷來調(diào)用相應(yīng)處理函數(shù),這樣會(huì)寫一個(gè)非常巨大的switch結(jié)構(gòu),mangos的方式是通過硬編碼將這些處理函數(shù)的地址存在opcodeTable這個(gè)全局的表結(jié)構(gòu)中,使用OpCode作為索引,迅速地定位到相應(yīng)的處理函數(shù),即找到改數(shù)據(jù)包對(duì)應(yīng)的Handler,并執(zhí)行他們。
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);上面是極小部分的處理函數(shù),他們都是Session的成員函數(shù),這些函數(shù)并非是最終處理數(shù)據(jù)的,往往一個(gè)函數(shù)對(duì)應(yīng)一個(gè)邏輯模塊,與這個(gè)模塊相關(guān)的操作碼有很多,比如聊天系統(tǒng)客戶端發(fā)來的操作碼可能是密聊、隊(duì)聊、地圖聊天,但是在Session收到數(shù)據(jù)包時(shí),會(huì)將這個(gè)模塊的這些操作碼都調(diào)用HandleMessage函數(shù),這些Handle函數(shù)內(nèi)部會(huì)根據(jù)具體的操作碼再調(diào)用相應(yīng)模塊的處理函數(shù),就是說消息的調(diào)度是兩級(jí)的。先從入口點(diǎn),通過查找OpCodeTabel找到一級(jí)調(diào)度函數(shù)、數(shù)據(jù)包傳過去后又進(jìn)行二級(jí)調(diào)度,分發(fā)到更小的子模塊,直到分發(fā)的具體模塊為止。
今天暫時(shí)寫到這里,還有很多想說的,以后繼續(xù)慢慢吹,下次繼續(xù)今天沒完善的內(nèi)容、談一談mangos的二進(jìn)制協(xié)議、數(shù)據(jù)通信機(jī)制等內(nèi)容,長期研究下mangos,肯定有好處的。
轉(zhuǎn)載于:https://my.oschina.net/u/1024573/blog/412232
總結(jié)
以上是生活随笔為你收集整理的Mangos魔兽世界服务端初探(1)--游戏服务端主体结构与消息分发的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Windows2003系统csrss.e
- 下一篇: 利用计算机辅助设计,一种利用计算机辅助设