Mangos魔獸世界服務端初探(1)--遊戲服務端主體結構與消息分發

魔獸時間是暴雪著名的網絡遊戲,我之前也玩過一段時間的戰士,這款遊戲目前已進入晚年時期,不過裏面各類豐富的遊戲系統和遊戲內容都很是讓人印象深入。開源的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等。如下是世界線程啓動後執行的代碼:web

 /// 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())內部只有一個核心函數調用,其餘都是一些控制更新時間之類的代碼,不用太關注:sql

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的具體實現:網絡

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函數主體核心代碼寫在這裏:session

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,並執行他們。架構

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,確定有好處的。

相關文章
相關標籤/搜索