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


     魔獸時間是暴雪著名的網絡游戲,我以前也玩過一段時間的戰士,這款游戲目前已進入晚年時期,不過里面各種豐富的游戲系統和游戲內容都非常讓人印象深刻。開源的Mangos項目模擬魔獸服務器端非常成功,目前國內外也有不少基於Mangos模擬器而搭建的私服,多數服務端運轉良好,非常穩定。國外有一個叫做MonsterWOW的魔獸私服,單服承載5000人,總共有幾組服務器,幾萬人同時在線,這是我在網站上親眼看到的實時數據,一般來講,如果對MMORPG游戲服務端稍微熟悉都知道,5000人同服在線,而且允許游戲邏輯的是一台單獨的服務器,支撐這么龐大一個游戲世界,肯定有非常過人之處,至少據我所知國內的單服性能與之相比都有較大差距,國內分布式的服務端架構基本也是將游戲邏輯分散到多台服務器上,單一世界承載數量也不算很高。幾年前的Eve Online單一世界可以承載兩萬多玩家同時在線、實時交互。我想國內多數MMORPG服務端的承載人數應該都是在七八百、一兩千這個數量級的。Mangos的源代碼下載下來好久了,一直沒時間研究,它目前是C++寫成的,我的主要方向是C#,不過我一直有將C#做游戲服務端的打算,所以既然它有那么多過人之處,就算不能掌握全部也應該研究學習一下。

     今天粗略地看了一下,服務端主要又三大塊組成,數據庫、服務端邏輯、腳本。數據庫用的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等。以下是世界線程啟動后執行的代碼:

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

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

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM