溫馨提示×

溫馨提示×

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

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

從零學習游戲服務器開發(三) CSBattleMgr服務源碼研究

發布時間:2020-07-25 20:37:14 來源:網絡 閱讀:668 作者:張小方32 欄目:建站服務器

從零學習游戲服務器開發(三) CSBattleMgr服務源碼研究

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

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

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

從零學習游戲服務器開發(三) CSBattleMgr服務源碼研究

然后我們輸入以下指令,查看我們需要的數據庫是否創建成功:

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

數據庫創建成功后如下圖所示:

從零學習游戲服務器開發(三) CSBattleMgr服務源碼研究

至于數據庫中的表是否創建成功,我們這里先不關注,后面我們實際用到哪張數據表,我們再去研究。

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

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

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

從零學習游戲服務器開發(三) CSBattleMgr服務源碼研究

redis和redis-logic服務啟動后如下圖所示:

從零學習游戲服務器開發(三) CSBattleMgr服務源碼研究

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

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

從零學習游戲服務器開發(三) CSBattleMgr服務源碼研究

我們打開CSBattleMgr服務main.cpp文件,找到入口main函數,內容如下:

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();
}

通過調試,我們發下這個函數大致做了以下任務:

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

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

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

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

INT32   CCSKernel::Initialize()
{
    //JJIAZ加載配置的時候 不要隨便調整順序
    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;
}

從零學習游戲服務器開發(三) CSBattleMgr服務源碼研究

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

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的一些相關信息,由于redis是做服務的緩存的,所以我們一般在項目中看到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);
    } 
}

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

從零學習游戲服務器開發(三) CSBattleMgr服務源碼研究

GetCSKernelInstance()->Start();是初始化所有的網絡連接的Session管理器,所謂Session,中文譯為“會話”,其下層對應網絡通信的連接,每一路連接對應一個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;
}

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

從零學習游戲服務器開發(三) CSBattleMgr服務源碼研究

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

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

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);
    }
}

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

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

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

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

接著我們就正式進入了一個while循環:

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);
}

循環具體做了啥,我們先看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();
    }
}

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

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

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

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

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循環中第三個函數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();
    }
}

這也是一個與時間有關的操作。具體細節我們也在后面文章中介紹。

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

從零學習游戲服務器開發(三) CSBattleMgr服務源碼研究

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

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

從零學習游戲服務器開發(三) CSBattleMgr服務源碼研究

很快,由于重連機制,觸發這個斷點,我們看下此時的調用堆棧:

從零學習游戲服務器開發(三) CSBattleMgr服務源碼研究

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

從零學習游戲服務器開發(三) CSBattleMgr服務源碼研究

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

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)
            {
                /// 溢出了,發生了數據回繞 \///
                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();時,看到這里我們大致可以看出這又是一個生產者消費者模型,只不過這里是消費者——從隊列中取出數據,對應的switch-case分支是:

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

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

通過這段代碼我們發現隊列的名字叫m_oEvtQueue,我們通過搜索這個隊列的名字找到生產者,然后在生產者往隊列中加入數據那里加上一個斷點:

從零學習游戲服務器開發(三) CSBattleMgr服務源碼研究

等斷點觸發以后,我們看下此時的調用堆棧:

從零學習游戲服務器開發(三) CSBattleMgr服務源碼研究

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

從零學習游戲服務器開發(三) CSBattleMgr服務源碼研究

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

從零學習游戲服務器開發(三) CSBattleMgr服務源碼研究

我們在上述標紅的地方加個斷點:

從零學習游戲服務器開發(三) CSBattleMgr服務源碼研究

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

從零學習游戲服務器開發(三) CSBattleMgr服務源碼研究

發現這里又是一個消費者,又存在一個隊列!

從零學習游戲服務器開發(三) CSBattleMgr服務源碼研究

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

從零學習游戲服務器開發(三) CSBattleMgr服務源碼研究

我們看下這個時候的生產者的調用堆棧:

從零學習游戲服務器開發(三) CSBattleMgr服務源碼研究

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

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;

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

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

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

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

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

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

從零學習游戲服務器開發(三) CSBattleMgr服務源碼研究

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

亚洲午夜精品一区二区_中文无码日韩欧免_久久香蕉精品视频_欧美主播一区二区三区美女