1. 程式人生 > >從零學習遊戲伺服器開發(三) CSBattleMgr服務原始碼研究

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


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

這個系列的第二篇文章《從零學習開源專案系列(二) 最後一戰概況》中我們介紹了,這套遊戲的服務需要使用redis和mysql,我們先看下mysql是否準備好了(mysql服務啟動起來,資料庫建表資料存在,具體細節請參考第二篇文章)。開啟Windows的cmd程式,輸入以下指令連線mysql:

mysql -uroot -p123321

連線成功以後,如下圖所示:


然後我們輸入以下指令,檢視我們需要的資料庫是否建立成功:

show databases;

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

資料庫建立成功後如下圖所示:

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

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檔案就可以:


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


我們常見的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服務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;
}

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

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的功能也是一樣,所以你在程式碼中可以看到這樣引入名稱空間的語句:


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連線成功:


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


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

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


很快,由於重連機制,觸發這個斷點,我們看下此時的呼叫堆疊:


我們切換到如圖箭頭所示的堆疊處程式碼:


說明是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,我們通過搜尋這個佇列的名字找到生產者,然後在生產者往佇列中加入資料那裡加上一個斷點:


等斷點觸發以後,我們看下此時的呼叫堆疊:


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


到這裡我們基本上認識了,這裡連線使用的非同步connect(),即線上程A中將連線socket,然後使用WSAEventSelect繫結該socket並設定該socket為非阻塞模式,等連線有結果了(成功或失敗)使用Windows API WSAEnumNetworkEvents去檢測這個socket的連線事件(FD_CONNECT),然後將判斷結果加入佇列m_oEvtQueue中,另外一個執行緒B從佇列中取出判斷結果打印出日誌。如果您不清楚這個流程,請學習一下非同步connect的使用方法和WSAEventSelect、WSAEnumNetworkEvents的用法。那麼這個非同步connect在哪裡呢?我們搜尋一下socket API connect函式(其實我可以一開始就搜尋connect函式的,但是我之所以不這麼做是想讓您瞭解一下我研究一個不熟悉的專案程式碼的思路),得到如下圖:


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


通過上圖中的埠資訊1234,我們驗證了的確是上文說的流程。然後我們觀察一下這個呼叫堆疊:


發現這裡又是一個消費者,又存在一個佇列!


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


我們看下這個時候的生產者的呼叫堆疊:


切換到如圖所示的程式碼處:

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