从零学习开源项目系列(三) 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; } ...