溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

從零學(xué)習(xí)游戲服務(wù)器開發(fā)(三) CSBattleMgr服務(wù)源碼研究

發(fā)布時間:2020-07-25 20:37:14 來源:網(wǎng)絡(luò) 閱讀:630 作者:張小方32 欄目:建站服務(wù)器

從零學(xué)習(xí)游戲服務(wù)器開發(fā)(三) CSBattleMgr服務(wù)源碼研究

如上圖所示,這篇文章我們將介紹CSBattleMgr的情況,但是我們不會去研究這個服務(wù)器的特別細(xì)節(jié)的東西(這些細(xì)節(jié)我們將在后面的文章中介紹)。閱讀一個未知的項目源碼如果我們開始就糾結(jié)于各種細(xì)節(jié),那么我們最終會陷入“橫看成嶺側(cè)成峰,遠(yuǎn)近高低各不同”的尷尬境界,浪費時間不說,可能收獲也是事倍功半。所以,盡管我們不熟悉這套代碼,我們還是盡量先從整體來把我,先大致了解各個服務(wù)的功能,細(xì)節(jié)部分回頭我們再針對性地去研究。

這個系列的第二篇文章《從零學(xué)習(xí)開源項目系列(二) 最后一戰(zhàn)概況》中我們介紹了,這套游戲的服務(wù)需要使用redismysql,我們先看下mysql是否準(zhǔn)備好了(mysql服務(wù)啟動起來,數(shù)據(jù)庫建表數(shù)據(jù)存在,具體細(xì)節(jié)請參考第二篇文章)。打開Windows的cmd程序,輸入以下指令連接mysql:

mysql -uroot -p123321
連接成功以后,如下圖所示:

從零學(xué)習(xí)游戲服務(wù)器開發(fā)(三) CSBattleMgr服務(wù)源碼研究

然后我們輸入以下指令,查看我們需要的數(shù)據(jù)庫是否創(chuàng)建成功:

show databases;
這些都是基本的sql語句,如果您不熟悉的話,可能需要專門學(xué)習(xí)一下。

數(shù)據(jù)庫創(chuàng)建成功后如下圖所示:

從零學(xué)習(xí)游戲服務(wù)器開發(fā)(三) CSBattleMgr服務(wù)源碼研究

至于數(shù)據(jù)庫中的表是否創(chuàng)建成功,我們這里先不關(guān)注,后面我們實際用到哪張數(shù)據(jù)表,我們再去研究。

mysql沒問題了,接下來我們要啟動一下redis,通過第二篇文章我們知道redis需要啟動兩次,也就是一共兩個redis進(jìn)程,我們游戲服務(wù)中分別稱為redis-server和redis-login-server(它們的配置文件信息不一樣),我們可以在Server\Bin\x64\Release目錄下手動cmd命令行執(zhí)行下列語句:

start /min "redis-server" "redis-server.exe" redis.conf

start /min "redis-Logicserver" "redis-server.exe" redis-logic.conf
但是這樣比較麻煩,我將這兩句拷貝出來,放入一個叫start-redis.bat文件中了,每次啟動只要執(zhí)行一下這個bat文件就可以:

從零學(xué)習(xí)游戲服務(wù)器開發(fā)(三) CSBattleMgr服務(wù)源碼研究

redis和redis-logic服務(wù)啟動后如下圖所示:

從零學(xué)習(xí)游戲服務(wù)器開發(fā)(三) CSBattleMgr服務(wù)源碼研究

我們常見的redis服務(wù)都是linux下的源碼,微軟公司對redis源碼進(jìn)行了改造,出了一個Windows版本,稍微有點不盡人意(例如:Windows下沒有完全與linux的fork()相匹配的API,所以只能用CreateProcess()去替代)。關(guān)于windows版本的redis源碼官方下載地址為:https://github.com/MicrosoftArchive/redis/releases。

在啟動好了mysql和redis后,我們現(xiàn)在正式來看一下CSBattleMgr這個服務(wù)。讀者不禁可能要問,那么多服務(wù),你怎么知道要先看這個服務(wù)呢?我們上一篇文章中也說過,我們再start.bat文件中發(fā)現(xiàn)除了redis以外,這是第三個需要啟動的服務(wù),所以我們先研究它(start.bat我們可以認(rèn)為是源碼作者為我們留下的部署步驟“文檔”):

從零學(xué)習(xí)游戲服務(wù)器開發(fā)(三) CSBattleMgr服務(wù)源碼研究

我們打開CSBattleMgr服務(wù)main.cpp文件,找到入口main函數(shù),內(nèi)容如下:

int main(){
    DbgLib::CDebugFx::SetExceptionHandler(true);
    DbgLib::CDebugFx::SetExceptionCallback(ExceptionCallback, NULL);

    GetCSKernelInstance();
    GetCSUserMgrInstance();
    GetBattleMgrInstance();
    GetCSKernelInstance()->Initialize();
    GetBattleMgrInstance()->Initialize();
    GetCSUserMgrInstance()->Initialize();

    GetCSKernelInstance()->Start();
    mysql_library_init(0, NULL, NULL);
    GetCSKernelInstance()->MainLoop();
}

通過調(diào)試,我們發(fā)下這個函數(shù)大致做了以下任務(wù):

//1. 設(shè)置程序異常處理函數(shù)
//2. 初始化一系列單例對象
//3. 初始化mysql
//4. 進(jìn)入一個被稱作“主循環(huán)”的無限循環(huán)

步驟1設(shè)置程序異常處理函數(shù)沒有好介紹的,我們看一下步驟2初始化一系列單例對象,總共初始化了三個類的對象CCSKernel、CCSUserMgr和CCSBattleMgr。單例模式本身沒啥好介紹的,但是有人要提單例模式的線程安全性,所以出現(xiàn)很多通過加鎖的單例模式代碼,我個人覺得沒必要;認(rèn)為要加鎖的朋友可能認(rèn)為單例對象如果在第一次初始化時同時被多個線程調(diào)用就會有問題,我覺得加鎖帶來的開銷還不如像上面的代碼一樣,在整個程序初始化初期獲取一下單例對象,讓單例對象生成出來,后面即使多個線程獲取這個單例對象也都是讀操作,無需加鎖。以GetCSKernelInstance();為例:

CCSKernel* GetCSKernelInstance(){
    return &CCSKernel::GetInstance();
}
CCSKernel& CCSKernel::GetInstance(){
    if (NULL == pInstance){
        pInstance = new CCSKernel;
    }
    return *pInstance;
}

GetCSKernelInstance()->Initialize()的初始化動作其實是加載各種配置信息和事先設(shè)置一系列的回調(diào)函數(shù)和定時器:

INT32   CCSKernel::Initialize()
{
    //JJIAZ加載配置的時候 不要隨便調(diào)整順序
    CCSCfgMgr::getInstance().Initalize(); 

    INT32 n32Init = LoadCfg();   
    if (eNormal != n32Init)
    {
        ELOG(LOG_ERROR," loadCfg()............failed!");
        return n32Init;
    }

    if(m_sCSKernelCfg.un32MaxSSNum > 0 )
    {
        m_psSSNetInfoList = new SSSNetInfo[m_sCSKernelCfg.un32MaxSSNum];
        memset(m_psSSNetInfoList, 0, sizeof(SSSNetInfo) * m_sCSKernelCfg.un32MaxSSNum);

        m_psGSNetInfoList = new SGSNetInfo[m_sCSKernelCfg.un32MaxGSNum];
        memset(m_psGSNetInfoList, 0, sizeof(SGSNetInfo) * m_sCSKernelCfg.un32MaxGSNum);

        m_psRCNetInfoList = new SRCNetInfo[10];
    }

    m_GSMsgHandlerMap[GSToCS::eMsgToCSFromGS_AskRegiste] = std::bind(&CCSKernel::OnMsgFromGS_AskRegiste, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3);
    m_GSMsgHandlerMap[GSToCS::eMsgToCSFromGS_AskPing] = std::bind(&CCSKernel::OnMsgFromGS_AskPing, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3);
    m_GSMsgHandlerMap[GSToCS::eMsgToCSFromGS_ReportGCMsg] = std::bind(&CCSKernel::OnMsgFromGS_ReportGCMsg, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3);

    m_SSMsgHandlerMap[SSToCS::eMsgToCSFromSS_AskPing] = std::bind(&CCSKernel::OnMsgFromSS_AskPing, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3);

    AddTimer(std::bind(&CCSKernel::ProfileReport, this, std::placeholders::_1, std::placeholders::_2), 5000, true);

    return eNormal;
}

從零學(xué)習(xí)游戲服務(wù)器開發(fā)(三) CSBattleMgr服務(wù)源碼研究

如上圖所示,這些配置信息都是游戲術(shù)語,包括各種技能、英雄、模型等信息。

GetBattleMgrInstance()->Initialize()其實是幫CSKernel對象啟動一個定時器:

INT32   CCSBattleMgr::Initialize(){
    GetCSKernelInstance()->AddTimer(std::bind(&CCSMatchMgr::Update, m_pMatchMgr, std::placeholders::_1, std::placeholders::_2), c_matcherDelay, true);
    return eNormal;
}

GetCSUserMgrInstance()->Initialize()是初始化mysql和redis的一些相關(guān)信息,由于redis是做服務(wù)的緩存的,所以我們一般在項目中看到cacheServer這樣的字眼指的都是redis:

void CCSUserMgr::Initialize(){
    SDBCfg cfgGameDb = CCSCfgMgr::getInstance().GetDBCfg(eDB_GameDb);
    SDBCfg cfgCdkeyDb=CCSCfgMgr::getInstance().GetDBCfg(eDB_CdkeyDb); 
    m_UserCacheDBActiveWrapper = new DBActiveWrapper( std::bind(&CCSUserMgr::UserCacheDBAsynHandler, this, std::placeholders::_1), cfgGameDb, std::bind(&CCSUserMgr::DBAsyn_QueryWhenThreadBegin, this) );
    m_UserCacheDBActiveWrapper->Start();

    m_CdkeyWrapper = new DBActiveWrapper( std::bind(&CCSUserMgr::UserAskDBAsynHandler, this, std::placeholders::_1), cfgCdkeyDb, std::bind(&CCSUserMgr::CDKThreadBeginCallback, this) );
    m_CdkeyWrapper->Start();

    for (int i = 0; i < gThread ; i++)
    {
        DBActiveWrapper* pThreadDBWrapper(new DBActiveWrapper(std::bind(&CCSUserMgr::UserAskDBAsynHandler, this, std::placeholders::_1), cfgGameDb));
        pThreadDBWrapper->Start();
        m_pUserAskDBActiveWrapperVec.push_back(pThreadDBWrapper);
    } 
}

注意一點哈,不知道大家有沒有發(fā)現(xiàn),我們代碼中大量使用C++11中的std::bind()這樣函數(shù),注意由于我們使用的Visual Studio版本是2010,2010這個版本是不支持C++11的,所以這里的std::bind不是C++11的,而是C++11發(fā)布之前的草案tr1中的,所以全部的命名空間應(yīng)該是tr1::std::bind,其他的類似C++11的功能也是一樣,所以你在代碼中可以看到這樣引入命名空間的語句:

從零學(xué)習(xí)游戲服務(wù)器開發(fā)(三) CSBattleMgr服務(wù)源碼研究

GetCSKernelInstance()->Start();是初始化所有的網(wǎng)絡(luò)連接的Session管理器,所謂Session,中文譯為“會話”,其下層對應(yīng)網(wǎng)絡(luò)通信的連接,每一路連接對應(yīng)一個Session,而管理這些Session的對象就是Session Manager,在我們的代碼中是CSNetSessionMgr,它繼承自接口類INetSessionMgr:

class CSNetSessionMgr : public INetSessionMgr
{
public:
    CSNetSessionMgr();
    virtual ~CSNetSessionMgr();
public:
    virtual ISDSession* UCAPI CreateSession(ISDConnection* pConnection) { return NULL; /*重寫*/}
    virtual ICliSession* UCAPI CreateConnectorSession(SESSION_TYPE type);
    virtual bool CreateConnector(SESSION_TYPE type, const char* ip, int port, int recvsize, int sendsize, int logicId);

private:
    CSParser m_CSParser;
};
初始化CSNetSessionMgr的代碼如下:

INT32   CCSKernel::Start()
{
    CSNetSessionMgr* pNetSession = new CSNetSessionMgr;

    GetBattleMgrInstance()->RegisterMsgHandle(m_SSMsgHandlerMap, m_GSMsgHandlerMap,  m_GCMsgHandlerMap, m_RCMsgHandlerMap);
    GetCSUserMgrInstance()->RegisterMsgHandle(m_SSMsgHandlerMap, m_GSMsgHandlerMap,  m_GCMsgHandlerMap, m_RCMsgHandlerMap);

    ELOG(LOG_INFO, "success!");

    return 0;
}

連接數(shù)據(jù)庫成功以后,我們的CSBattleMgr程序的控制臺會顯示一行提示mysql連接成功:

從零學(xué)習(xí)游戲服務(wù)器開發(fā)(三) CSBattleMgr服務(wù)源碼研究

讀者看上圖會發(fā)現(xiàn),這些日志信息有三個顏色,出錯信息使用紅色,重要的正常信息使用綠色,一般的輸出信息使用灰色。這是如何實現(xiàn)的呢?我們將在下一篇文章《從零學(xué)習(xí)開源項目系列(三) LogServer服務(wù)源碼研究》中介紹具體實現(xiàn)原理,個人覺得這是比使用日志級別標(biāo)簽更醒目的一種方式。

介紹完了初始化流程,我們介紹一下這個服務(wù)的主體部分MainLoop()函數(shù),先看一下整體代碼:

void CCSKernel::MainLoop(){
    TIME_TICK   tHeartBeatCDTick = 10;

    //偵聽端口10002
        INetSessionMgr::GetInstance()->CreateListener(m_sCSKernelCfg.n32GSNetListenerPort,1024000,10240000,0,&gGateSessionFactory);
        //偵聽端口10001
    INetSessionMgr::GetInstance()->CreateListener(m_sCSKernelCfg.n32SSNetListenerPort,1024000,10240000,1,&gSceneSessionFactory);
    //偵聽端口10010
        INetSessionMgr::GetInstance()->CreateListener(m_sCSKernelCfg.n32RCNetListenerPort,1024000,10240000,2,&gRemoteConsoleFactory);
        //連接LogServer 1234端口
    INetSessionMgr::GetInstance()->CreateConnector(ST_CLIENT_C2Log, m_sCSKernelCfg.LogAddress.c_str(), m_sCSKernelCfg.LogPort, 102400,102400,0);

        //連接redis 6379
    if (m_sCSKernelCfg.redisAddress != "0"){
        INetSessionMgr::GetInstance()->CreateConnector(ST_CLIENT_C2R, m_sCSKernelCfg.redisAddress.c_str(), m_sCSKernelCfg.redisPort,102400,102400,0);
    }
        //連接redis 6380,也是redis-logic
    if (m_sCSKernelCfg.redisLogicAddress != "0"){
        INetSessionMgr::GetInstance()->CreateConnector(ST_CLIENT_C2LogicRedis, m_sCSKernelCfg.redisLogicAddress.c_str(), m_sCSKernelCfg.redisLogicPort,102400,102400,0);
    }
    while(true)
    {
        if (kbhit())
        {
            static char CmdArray[1024] = {0};
            static int CmdPos = 0;
            char CmdOne = getche();
            CmdArray[CmdPos++] = CmdOne;
            bool bRet = 0;
            if (CmdPos>=1024 || CmdOne==13) { CmdArray[--CmdPos]=0; bRet = DoUserCmd(CmdArray); CmdPos=0; if (bRet) break; }
        }

        INetSessionMgr::GetInstance()->Update();

        GetCSUserMgrInstance()->OnHeartBeatImmediately();

        ++m_RunCounts;

        m_BattleTimer.Run();

        Sleep(1);
    }
}

這個函數(shù)雖然叫MainLoop(),但是實際MainLoop()只是后半部分,前半部分總共創(chuàng)建三個偵聽端口和三個連接器,也就是所謂的Listener和Connector,這些對象都是由上文提到的CSNetSessionMgr管理,所謂Listener就是這個服務(wù)使用socket API bind()和listen()函數(shù)在某個地址+端口號的二元組上綁定,供其他程序連接(其他程序可能是其他服務(wù)程序也可能是客戶端,具體是哪個,我們后面的文章再進(jìn)一步挖掘),偵聽端口統(tǒng)計如下:

偵聽端口10002
偵聽端口10001
偵聽端口10010
連接器(Connector)也有三個,分別連接的服務(wù)和端口號是:

連接redis的6379號端口
連接redis-logic的6380端口
連接某服務(wù)的1234端口
這個1234端口到底是哪個服務(wù)的呢?通過代碼我們可以看出是LogServer的,那么到底是不是LogServer的呢,我們后面具體求證一下。

INetSessionMgr::GetInstance()->CreateConnector(ST_CLIENT_C2Log, m_sCSKernelCfg.LogAddress.c_str(), m_sCSKernelCfg.LogPort, 102400,102400,0);

接著我們就正式進(jìn)入了一個while循環(huán):

while(true)
{
    if (kbhit())
    {
        static char CmdArray[1024] = {0};
        static int CmdPos = 0;
        char CmdOne = getche();
        CmdArray[CmdPos++] = CmdOne;
        bool bRet = 0;
        if (CmdPos>=1024 || CmdOne==13) { CmdArray[--CmdPos]=0; bRet = DoUserCmd(CmdArray); CmdPos=0; if (bRet) break; }
    }

    INetSessionMgr::GetInstance()->Update();

    GetCSUserMgrInstance()->OnHeartBeatImmediately();

    ++m_RunCounts;

    m_BattleTimer.Run();

    Sleep(1);
}

循環(huán)具體做了啥,我們先看INetSessionMgr::GetInstance()->Update();代碼:

void INetSessionMgr::Update()
{
    mNetModule->Run();

    vector<char*> tempQueue;
    EnterCriticalSection(&mNetworkCs);
    tempQueue.swap(m_SafeQueue);
    LeaveCriticalSection(&mNetworkCs);

    for (auto it=tempQueue.begin();it!=tempQueue.end();++it){
        char* pBuffer = (*it);
        int nType = *(((int*)pBuffer)+0);
        int nSessionID = *(((int*)pBuffer)+1);
        Send((SESSION_TYPE)nType,nSessionID,pBuffer+2*sizeof(int));
        delete []pBuffer;
    }

    auto &map = m_AllSessions.GetPointerMap();
    for (auto it=map.begin();it!=map.end();++it)
    {
        (*it)->Update();
    }
}

通過這段代碼我們看出,這個函數(shù)先是使用std::vector對象的swap()方法把一個公共隊列中的數(shù)據(jù)倒換到一個臨時隊列中,這是一個很常用的技巧,目的是減小鎖的粒度:由于公共的隊列需要被生產(chǎn)者和消費者同時使用,我們?yōu)榱藴p小加鎖的粒度和時間,把當(dāng)前隊列中已有的數(shù)據(jù)一次性倒換到消費者本地的一個臨時隊列中來,這樣消費者就可以使用這個臨時隊列了,從而避免了每次都要通過加鎖從公共隊列中取數(shù)據(jù)了,提高了效率。接著,我們發(fā)現(xiàn)這個隊列中的數(shù)據(jù)是一個個的Session對象,遍歷這些Session對象個每個Session對象的連接的對端發(fā)數(shù)據(jù),同時執(zhí)行Session對象的Update()方法。具體發(fā)了些什么數(shù)據(jù),我們后面的文章再研究。

我們再看一下循環(huán)中的第二個函數(shù)GetCSUserMgrInstance()->OnHeartBeatImmediately();,其代碼如下:

INT32 CCSUserMgr::OnHeartBeatImmediately()
{
    OnTimeUpdate();
    SynUserAskDBCallBack();
    return eNormal;
}

這些名字都是自解釋的,先是同步時間,再同步數(shù)據(jù)庫的一些操作:

INT32 CCSUserMgr::SynUserAskDBCallBack(){
    while (!m_DBCallbackQueue.empty()){
        Buffer* pBuffer = NULL;
        m_DBCallbackQueue.try_pop(pBuffer);

        switch (pBuffer->m_LogLevel)
        {
        case DBToCS::eQueryUser_DBCallBack:
            SynHandleQueryUserCallback(pBuffer);
            break;
        case DBToCS::eQueryAllAccount_CallBack:
            SynHandleAllAccountCallback(pBuffer);
            break;
        case DBToCS::eMail_CallBack:
            SynHandleMailCallback(pBuffer);
            break;
        case  DBToCS::eQueryNotice_CallBack:
            DBCallBack_QueryNotice(pBuffer);
            break;
        default:
            ELOG(LOG_WARNNING, "not hv handler:%d", pBuffer->m_LogLevel);
            break;
        }

        if (pBuffer){
            m_DBCallbackQueuePool.ReleaseObejct(pBuffer);
        }
    }

    return 0;
}

再看一下while循環(huán)中第三個函數(shù)m_BattleTimer.Run();其代碼如下:

void CBattleTimer::Run(){
    TimeKey nowTime = GetInternalTime();

    while(!m_ThreadTimerQueue.empty()){
        ThreadTimer& sThreadTimer = m_ThreadTimerQueue.top();
        if (!m_InvalidTimerSet.empty()){
            auto iter = m_InvalidTimerSet.find(sThreadTimer.sequence);
            if (iter != m_InvalidTimerSet.end()){
                m_InvalidTimerSet.erase(iter);
                m_ThreadTimerQueue.pop();
                continue;
            }
        }

        if (nowTime >=  sThreadTimer.nextexpiredTime){
            m_PendingTimer.push_back(sThreadTimer);
            m_ThreadTimerQueue.pop();
        }
        else{
            break;
        }
    }

    if (!m_PendingTimer.empty()){
        for (auto iter = m_PendingTimer.begin(); iter != m_PendingTimer.end(); ++iter){
            ThreadTimer& sThreadTimer = *iter;
            nowTime = GetInternalTime();
            int64_t tickSpan = nowTime - sThreadTimer.lastHandleTime;
            sThreadTimer.pHeartbeatCallback(nowTime, tickSpan);

            if (sThreadTimer.ifPersist){
                TimeKey newTime = nowTime + sThreadTimer.interval;
                sThreadTimer.lastHandleTime = nowTime;
                sThreadTimer.nextexpiredTime = newTime;
                m_ThreadTimerQueue.push(sThreadTimer);
            }
        }

        m_PendingTimer.clear();
    }

    if (!m_ToAddTimer.empty()){
        for (auto iter = m_ToAddTimer.begin(); iter != m_ToAddTimer.end(); ++iter){
            m_ThreadTimerQueue.push(*iter);
        }

        m_ToAddTimer.clear();
    }
}

這也是一個與時間有關(guān)的操作。具體細(xì)節(jié)我們也在后面文章中介紹。

CSBattleMgr服務(wù)跑起來之后,cmd窗口顯示如下:

從零學(xué)習(xí)游戲服務(wù)器開發(fā)(三) CSBattleMgr服務(wù)源碼研究

上圖中我們看到Mysql和redis服務(wù)均已連上,但是程序會一直提示連接127.0.0.1:1234端口連不上。由此我們斷定,這個使用1234端口的服務(wù)沒有啟動。這不是我們介紹的重點,重點是說明這個服務(wù)會定時自動重連這個1234端口,自動重連機(jī)制是我們做服務(wù)器開發(fā)必須熟練開發(fā)的一個功能。所以我建議大家好好看一看這一塊的代碼。我們這里帶著大家簡單梳理一遍吧。

首先,我們根據(jù)提示找到INetSessionMgr::LogText的42行,并在那里加一個斷點:

從零學(xué)習(xí)游戲服務(wù)器開發(fā)(三) CSBattleMgr服務(wù)源碼研究

很快,由于重連機(jī)制,觸發(fā)這個斷點,我們看下此時的調(diào)用堆棧:

從零學(xué)習(xí)游戲服務(wù)器開發(fā)(三) CSBattleMgr服務(wù)源碼研究

我們切換到如圖箭頭所示的堆棧處代碼:

從零學(xué)習(xí)游戲服務(wù)器開發(fā)(三) CSBattleMgr服務(wù)源碼研究

說明是mNetModule->Run();調(diào)用產(chǎn)生的日志輸出。我們看下這個的調(diào)用:

bool CUCODENetWin::Run(INT32 nCount)
{
    CConnDataMgr::Instance()->RunConection();
    do
    {
// #ifdef UCODENET_HAS_GATHER_SEND       
// #pragma message("[preconfig]sdnet collect buffer, has a internal timer")        
//         if (m_pTimerModule)        
//         {
//             m_pTimerModule->Run();
//         }        
// #endif
#ifdef UCODENET_HAS_GATHER_SEND 
        static INT32 sendCnt = 0;
        ++sendCnt;
        if (sendCnt == 10)
        {
            sendCnt = 0;
            UINT32 now = GetTickCount();
            if (now < m_dwLastTick)
            {
                /// 溢出了,發(fā)生了數(shù)據(jù)回繞 \///
                m_dwLastTick = now;
            }

            if ((now - m_dwLastTick) > 50)
            {
                m_dwLastTick = now;            
                FlushBufferedData();
            }
        }       
#endif // 
        //SNetEvent stEvent; 
        SNetEvent *pstEvent  = CEventMgr::Instance()->PopFrontNetEvt();
        if (pstEvent == NULL)
        {
            return false;
        }
        SNetEvent & stEvent = *pstEvent; 

        switch(stEvent.nType)
        {
        case NETEVT_RECV:
            _ProcRecvEvt(&stEvent.stUn.stRecv);
            break;
        case NETEVT_SEND:
            _ProcSendEvt(&stEvent.stUn.stSend); 
            break; 
        case NETEVT_ESTABLISH:
            _ProcEstablishEvt(&stEvent.stUn.stEstablish);
            break;
        case NETEVT_ASSOCIATE:
            _ProcAssociateEvt(&stEvent.stUn.stAssociate);
            break;
        case NETEVT_TERMINATE:
            _ProcTerminateEvt(&stEvent.stUn.stTerminate);
            break;
        case NETEVT_CONN_ERR:
            _ProcConnErrEvt(&stEvent.stUn.stConnErr);
            break;
        case NETEVT_ERROR:
            _ProcErrorEvt(&stEvent.stUn.stError);
            break;
        case NETEVT_BIND_ERR:
            _ProcBindErrEvt(&stEvent.stUn.stBindErr);
            break;
        default:
            SDASSERT(false);
            break;
        }
        CEventMgr::Instance()->ReleaseNetEvt(pstEvent); 
    }while(--nCount != 0);
    return true;
}

我們看到SNetEvent *pstEvent = CEventMgr::Instance()->PopFrontNetEvt();時,看到這里我們大致可以看出這又是一個生產(chǎn)者消費者模型,只不過這里是消費者——從隊列中取出數(shù)據(jù),對應(yīng)的switch-case分支是:

case NETEVT_CONN_ERR:
_ProcConnErrEvt(&stEvent.stUn.stConnErr);
即連接失敗。那么在哪里連接的呢?我們只需要看看這個隊列的生產(chǎn)者在哪里就能找到了,因為連接不成功,往隊列中放入一條連接出錯的數(shù)據(jù),我們看一下CEventMgr::Instance()->PopFrontNetEvt()的實現(xiàn),找到具體的隊列名稱:

/**
 * @brief 獲取一個未處理的網(wǎng)絡(luò)事件(目前為最先插入的網(wǎng)絡(luò)事件)
 * @return 返回一個未處理的網(wǎng)絡(luò)事件.如果處理失敗,返回NULL
 * @remark 由于此類只有在主線程中調(diào)用,所以,此函數(shù)內(nèi)部并未保證線程安全
 */
inline SNetEvent*  PopFrontNetEvt()
{
    return  (SNetEvent*)m_oEvtQueue.PopFront();
}

通過這段代碼我們發(fā)現(xiàn)隊列的名字叫m_oEvtQueue,我們通過搜索這個隊列的名字找到生產(chǎn)者,然后在生產(chǎn)者往隊列中加入數(shù)據(jù)那里加上一個斷點:

從零學(xué)習(xí)游戲服務(wù)器開發(fā)(三) CSBattleMgr服務(wù)源碼研究

等斷點觸發(fā)以后,我們看下此時的調(diào)用堆棧:

從零學(xué)習(xí)游戲服務(wù)器開發(fā)(三) CSBattleMgr服務(wù)源碼研究

我們切換到上圖中箭頭所指向的代碼處:

從零學(xué)習(xí)游戲服務(wù)器開發(fā)(三) CSBattleMgr服務(wù)源碼研究

到這里我們基本上認(rèn)識了,這里連接使用的異步connect(),即在線程A中將連接socket,然后使用WSAEventSelect綁定該socket并設(shè)置該socket為非阻塞模式,等連接有結(jié)果了(成功或失?。┦褂肳indows API WSAEnumNetworkEvents去檢測這個socket的連接事件(FD_CONNECT),然后將判斷結(jié)果加入隊列m_oEvtQueue中,另外一個線程B從隊列中取出判斷結(jié)果打印出日志。如果您不清楚這個流程,請學(xué)習(xí)一下異步connect的使用方法和WSAEventSelect、WSAEnumNetworkEvents的用法。那么這個異步connect在哪里呢?我們搜索一下socket API connect函數(shù)(其實我可以一開始就搜索connect函數(shù)的,但是我之所以不這么做是想讓您了解一下我研究一個不熟悉的項目代碼的思路),得到如下圖:

從零學(xué)習(xí)游戲服務(wù)器開發(fā)(三) CSBattleMgr服務(wù)源碼研究

我們在上述標(biāo)紅的地方加個斷點:

從零學(xué)習(xí)游戲服務(wù)器開發(fā)(三) CSBattleMgr服務(wù)源碼研究

通過上圖中的端口信息1234,我們驗證了的確是上文說的流程。然后我們觀察一下這個調(diào)用堆棧:

從零學(xué)習(xí)游戲服務(wù)器開發(fā)(三) CSBattleMgr服務(wù)源碼研究

發(fā)現(xiàn)這里又是一個消費者,又存在一個隊列!

從零學(xué)習(xí)游戲服務(wù)器開發(fā)(三) CSBattleMgr服務(wù)源碼研究

同樣的道理,我們通過隊列名稱m_oReqQueue找到生產(chǎn)者:

從零學(xué)習(xí)游戲服務(wù)器開發(fā)(三) CSBattleMgr服務(wù)源碼研究

我們看下這個時候的生產(chǎn)者的調(diào)用堆棧:

從零學(xué)習(xí)游戲服務(wù)器開發(fā)(三) CSBattleMgr服務(wù)源碼研究

切換到如圖所示的代碼處:

bool ICliSession::Reconnect()
{
if (IsHadRecon() && mReconnectTag)
{
UINT32 curTime = GetTickCount();

    if (curTime>mReconTime)
    {
        mReconTime = curTime+10000;

        if (m_poConnector->ReConnect())
        {
            //printf("client reconnect server(%s)...\n",mRemoteEndPointer.c_str());
            ResetRecon();
            return true;
        }
    }
}

return false;

}
在這里我們終于可以好好看一下重連的邏輯如何設(shè)計了。具體代碼讀者自己分析哈,限于篇幅這里就不介紹了。

看到這里,可能很多讀者在對照我提供的代碼時,會產(chǎn)生一個困難:同樣的代碼為啥在我手中可以這樣分析,但是到你們手中可能就磕磕絆絆了?只能說經(jīng)驗和自我學(xué)習(xí)這是相輔相成的過程,例如上文中說的生產(chǎn)者消費者模式、任務(wù)隊列,我曾經(jīng)也和你們一樣,也不熟悉這些東西,但是當(dāng)我知道這些東西時我就去學(xué)習(xí)這些我認(rèn)為的“基礎(chǔ)”知識,并且反復(fù)練習(xí),這樣也就慢慢積累經(jīng)驗了。所以,孔子說的沒錯:學(xué)而不思則罔,思而不學(xué)則殆。什么時候該去學(xué)習(xí),什么時候該去思考,古人誠不欺我也。

到這里我們也大致清楚了CSBattleMgr做了哪些事情。后面我們把所有的服務(wù)都過一遍之后再從整體來介紹。下一篇文章我們將繼續(xù)研究這個偵聽1234端口的LogServer,敬請期待。

限于作者經(jīng)驗水平有限,文章中可能有錯漏的地方,歡迎批評指正。

另外有朋友希望我提供未經(jīng)我修改之前的源碼,這里也提供一下,×××方法:微信搜索公眾號『easyserverdev』(中文名:高性能服務(wù)器開發(fā)),關(guān)注公眾號后,在公眾號中回復(fù)『最后一戰(zhàn)原始源碼』,即可得到下載鏈接。(噴子和代碼販子請遠(yuǎn)離?。?/strong>

歡迎關(guān)注公眾號『easyserverdev』。如果有任何技術(shù)或者職業(yè)方面的問題需要我提供幫助,可通過這個公眾號與我取得聯(lián)系,此公眾號不僅分享高性能服務(wù)器開發(fā)經(jīng)驗和故事,同時也免費為廣大技術(shù)朋友提供技術(shù)答疑和職業(yè)解惑,您有任何問題都可以在微信公眾號直接留言,我會盡快回復(fù)您。

從零學(xué)習(xí)游戲服務(wù)器開發(fā)(三) CSBattleMgr服務(wù)源碼研究

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI