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

clipboard.png

如上圖所示,這篇文章咱們將介紹CSBattleMgr的狀況,可是咱們不會去研究這個服務器的特別細節的東西(這些細節咱們將在後面的文章中介紹)。閱讀一個未知的項目源碼若是咱們開始就糾結於各類細節,那麼咱們最終會陷入「橫當作嶺側成峯,遠近高低各不一樣」的尷尬境界,浪費時間不說,可能收穫也是事倍功半。因此,儘管咱們不熟悉這套代碼,咱們仍是儘可能先從總體來把我,先大體瞭解各個服務的功能,細節部分回頭咱們再針對性地去研究。mysql

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

mysql -uroot -p123321
鏈接成功之後,以下圖所示:git

clipboard.png

而後咱們輸入如下指令,查看咱們須要的數據庫是否建立成功:github

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

數據庫建立成功後以下圖所示:sql

clipboard.png

至於數據庫中的表是否建立成功,咱們這裏先不關注,後面咱們實際用到哪張數據表,咱們再去研究。數據庫

mysql沒問題了,接下來咱們要啓動一下redis,經過第二篇文章咱們知道redis須要啓動兩次,也就是一共兩個redis進程,咱們遊戲服務中分別稱爲redis-server和redis-login-server(它們的配置文件信息不同),咱們能夠在ServerBinx64Release目錄下手動cmd命令行執行下列語句:windows

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

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

clipboard.png

redis和redis-logic服務啓動後以下圖所示:

clipboard.png

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

在啓動好了mysql和redis後,咱們如今正式來看一下CSBattleMgr這個服務。讀者不由可能要問,那麼多服務,你怎麼知道要先看這個服務呢?咱們上一篇文章中也說過,咱們再start.bat文件中發現除了redis之外,這是第三個須要啓動的服務,因此咱們先研究它(start.bat咱們能夠認爲是源碼做者爲咱們留下的部署步驟「文檔」):

clipboard.png

咱們打開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;

}

clipboard.png

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

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的功能也是同樣,因此你在代碼中能夠看到這樣引入命名空間的語句:

clipboard.png

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鏈接成功:

clipboard.png

讀者看上圖會發現,這些日誌信息有三個顏色,出錯信息使用紅色,重要的正常信息使用綠色,通常的輸出信息使用灰色。這是如何實現的呢?咱們將在下一篇文章《從零學習開源項目系列(三) 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窗口顯示以下:

clipboard.png

上圖中咱們看到Mysql和redis服務均已連上,可是程序會一直提示鏈接127.0.0.1:1234端口連不上。由此咱們判定,這個使用1234端口的服務沒有啓動。這不是咱們介紹的重點,重點是說明這個服務會定時自動重連這個1234端口,自動重連機制是咱們作服務器開發必須熟練開發的一個功能。因此我建議你們好好看一看這一塊的代碼。咱們這裏帶着你們簡單梳理一遍吧。

首先,咱們根據提示找到INetSessionMgr::LogText的42行,並在那裏加一個斷點:

clipboard.png

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

clipboard.png

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

clipboard.png

說明是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,咱們經過搜索這個隊列的名字找到生產者,而後在生產者往隊列中加入數據那裏加上一個斷點:

clipboard.png

等斷點觸發之後,咱們看下此時的調用堆棧:

clipboard.png

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

clipboard.png

到這裏咱們基本上認識了,這裏鏈接使用的異步connect(),即在線程A中將鏈接socket,而後使用WSAEventSelect綁定該socket並設置該socket爲非阻塞模式,等鏈接有結果了(成功或失敗)使用Windows API WSAEnumNetworkEvents去檢測這個socket的鏈接事件(FD_CONNECT),而後將判斷結果加入隊列m_oEvtQueue中,另一個線程B從隊列中取出判斷結果打印出日誌。若是您不清楚這個流程,請學習一下異步connect的使用方法和WSAEventSelect、WSAEnumNetworkEvents的用法。那麼這個異步connect在哪裏呢?咱們搜索一下socket API connect函數(其實我能夠一開始就搜索connect函數的,可是我之因此不這麼作是想讓您瞭解一下我研究一個不熟悉的項目代碼的思路),獲得以下圖:

clipboard.png

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

clipboard.png

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

clipboard.png

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

clipboard.png

一樣的道理,咱們經過隊列名稱m_oReqQueue找到生產者:

clipboard.png

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

clipboard.png

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

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』(中文名:高性能服務器開發),關注公衆號後,在公衆號中回覆『最後一戰原始源碼』,便可獲得下載連接。(噴子和代碼販子請遠離!)

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

clipboard.png

相關文章
相關標籤/搜索