从零学习游戏服务器开发(三) 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

相关文章
相关标签/搜索