当mangos开始执行到 sMaster.Run()时,开始运行Master对象 ,至此,Mangos开始所构建的游戏服务器才算真正开始运行。
sMaster其实是Master类的一个实例,sMaster MaNGOS::Singleton<Master>::Instance(),应该是使用了单例模式来实现的,设计模式这块不是很清楚。
Master类的主要作用是启动Server服务器,Run()函数为该类的执行入口。
该函数主要处理如下工作:
1,输出一些版本等信息;
2,开始启动数据库的连接,连接三个数据库,这三个数据库就是在main函数中所声明的三个全局DatabaseMysq,包括WorldDatabase游戏世界数据库,CharacterDatabase游戏角色数据库,loginDatabase登录信息数据库.
3,进行游戏世界的初始化,即sWorld (#define sWorld MaNGOS::Singleton<World>::Instance())的初始化。
4,从配置中获取通讯端口,生成一个socket句柄,并将监听Socket与该端口进行绑定, 这一步是做通讯的初始化处理 。
5,对程序的中止信号进行捕捉 ,捕捉中断信号,其实主要是设置World::m_stopEvent,该参数非常重要,主要用于世界更新中判断是否需要更新服务器。
6,启动WorldRunnable线程,并设置Server的登录状态,该线程会调用WorldRunnable::run(),该函数用于世界信息的更新,服务器检查从客户端发送来的信息,并且根据信息进行游戏世界的更新,应该是是核心中的核心。
7,启动CliRunnable线程 // 如果配置中启动了命令行模式,则使用该线程,用于在服务器执行过程中处理命令行信息
8,启动RAListenSocket // 作用暂时未知
9 ,主服务器的主循环,判断当World::m_stopEvent不为真时,即服务器未收到中止信号时,进行如下几个处理
10,当服务器World::m_stopEvent为真时,开始结束服务做各种断开服务连接的准备工作等等,并且清空各种ScriptingModule。
代码较长,贴出部分代码如下:
/// Master 开始执行入口
void Master::Run()
{
// 输出版本信息和其他一些信息
sLog.outString( "MaNGOS daemon %s", _FULLVERSION );
sLog.outString( "<Ctrl-C> to stop.\n\n" );
sLog.outTitle( "MM MM MM MM MMMMM MMMM MMMMM");
sLog.outTitle( "MM MM MM MM MMM MMM MM MM MMM MMM");
sLog.outTitle( "MMM MMM MMM MM MMM MMM MM MM MMM");
sLog.outTitle( "MM M MM MMMM MM MMM MM MM MMM");
sLog.outTitle( "MM M MM MMMMM MM MMMM MMM MM MM MMM");
sLog.outTitle( "MM M MM M MMM MM MMM MMMMMMM MM MM MMM");
sLog.outTitle( "MM MM MMM MM MM MM MMM MM MM MMM");
sLog.outTitle( "MM MM MMMMMMM MM MM MMM MMM MM MM MMM MMM");
sLog.outTitle( "MM MM MM MMM MM MM MMMMMM MMMM MMMMM");
sLog.outTitle( " MM MMM http://www.mangosproject.org");
sLog.outTitle( " MMMMMM\n\n");
// 开始启动数据库连接,若数据库启动失败,则退出run(),mangosd运行结束
if (!_StartDB()) //
return;
///初始化游戏世界 其中 sWorld定义如下: 也是使用的MaNGOS::Singleton<T>进行实例化
// #define sWorld MaNGOS::Singleton<World>::Instance() sWorld 被定义为一个这样的宏,挺方便的
sWorld.SetInitialWorldSettings();
//获取世界通讯端口号
port_t wsport = sWorld.getConfig(CONFIG_PORT_WORLD);
SocketHandler h; // 实例化一个Socket句柄
ListenSocket<WorldSocket> worldListenSocket(h); // 创建 监听Socket类
// 将该Socket监听类绑定到世界通讯的端口上 若绑定无错误,则返回0 否则返回错误代码
if (worldListenSocket.Bind(wsport))
{
clearOnlineAccounts();
sLog.outError("MaNGOS cannot bind to port %d", wsport);
return; // 套接字绑定出错,程序返回
}
h.Add(&worldListenSocket); // 将绑定好的套接字增加到之前声明的Socket句柄上
//捕捉程序中止信号,判断是否中止服务
_HookSignals();
//启动WorldRunnable线程 该线程会调用WorldRunnable::run(),该函数用于世界信息的更新,服务器检查从客户端发送来的信息,
//并且根据信息进行游戏世界的更新,应该是是核心中的核心。
ZThread::Thread t(new WorldRunnable);
t.setPriority ((ZThread::Priority )2);
// 然后设置Server的登录状态,设置分区号码realmID
loginDatabase.PExecute("UPDATE `realmlist` SET `color` = 0, `population` = 0 WHERE `id` = '%d'",realmID);
#ifdef WIN32
if (sConfig.GetBoolDefault("Console.Enable", 1) && (m_ServiceStatus == -1)/* need disable console in service mode*/)
#else
if (sConfig.GetBoolDefault("Console.Enable", 1))
#endif
{
//若配置文件中设置了命令行模式,则启动命令行处理进程,用于处理在服务器运行过程中的命令输入,类似于处理终端输入的GM命令
ZThread::Thread td1(new CliRunnable);
}
///启动RAlisten 这部分没有想明白是做什么,以后在细看
ListenSocket<RASocket> RAListenSocket(h);
if (sConfig.GetBoolDefault("Ra.Enable", 0))
{
port_t raport = sConfig.GetIntDefault( "Ra.Port", 3443 );
std::string stringip = sConfig.GetStringDefault( "Ra.IP", "0.0.0.0" );
ipaddr_t raip;
if(!Utility::u2ip(stringip, raip))
sLog.outError( "MaNGOS RA can not bind to ip %s", stringip.c_str());
else if (RAListenSocket.Bind(raip, raport))
sLog.outError( "MaNGOS RA can not bind to port %d on %s", raport, stringip.c_str());
else
{
h.Add(&RAListenSocket);
sLog.outString("Starting Remote access listner on port %d on %s", raport, stringip.c_str());
}
}
uint32 realCurrTime, realPrevTime;
realCurrTime = realPrevTime = getMSTime();
uint32 socketSelecttime = sWorld.getConfig(CONFIG_SOCKET_SELECTTIME);
uint32 numLoops = (sConfig.GetIntDefault( "MaxPingTime", 30 ) * (MINUTE * 1000000 / socketSelecttime));
uint32 loopCounter = 0;
///- 主循环,当世界信息中m_stopEvent不为真,即未接到中止信号时,执行更新循环,判断当前时序,根据监听端口获得到的客户端信息,对游戏世界进行更新,并且通过心跳机制保证与数据库的连接。
while (!World::m_stopEvent)
{
if (realPrevTime > realCurrTime)
realPrevTime = 0;
// 判断游戏更新时间,根据时间进行服务器更新
realCurrTime = getMSTime();
sWorldSocketMgr.Update( realCurrTime - realPrevTime );
realPrevTime = realCurrTime;
// 核心中的核心函数处理Socket信息,使用WorldSocket::OnRead函数
h.Select(0, socketSelecttime);
// 应该属于心跳机制,保持与数据库的连接处于激活状态
if( (++loopCounter) == numLoops )
{
loopCounter = 0;
sLog.outDetail("Ping MySQL to keep connection alive");
delete WorldDatabase.Query("SELECT 1 FROM `command` LIMIT 1");
delete loginDatabase.Query("SELECT 1 FROM `realmlist` LIMIT 1");
delete CharacterDatabase.Query("SELECT 1 FROM `bugreport` LIMIT 1");
}
}
// 当接收到中止信号之后,即m_stopEvent为真以后,处理服务器关闭相关事宜。
// 设置服务器状态为离线状态 之前设置 color为0 为在线
loginDatabase.PExecute("UPDATE `realmlist` SET `color` = 2 WHERE `id` = '%d'",realmID);
_UnhookSignals();
// when the main thread closes the singletons get unloaded
// since worldrunnable uses them, it will crash if unloaded after master
t.wait();
clearOnlineAccounts(); ///- 服务器关闭,清除在线的帐号
CharacterDatabase.HaltDelayThread(); // 角色数据库延迟处理
WorldDatabase.HaltDelayThread(); // 世界数据库
loginDatabase.HaltDelayThread(); // 登录数据库
sLog.outString( "Halting process..." );
UnloadScriptingModule();
return;
}
主线就是这样的,以后会根据主线慢慢的读进去,自己也是刚刚开始接触这代码,感觉挺有意思的。
在run()中,输出相关信息后,要做的第一件事情就是连接数据库,使用_StartDB()方法对数据库进行连接,数据库的地址和用户名密码信息均从配置文件中读取,可能这也是单机版的问题,实际中的不清楚应该怎么做。
bool Master::_StartDB()函数代码如下:
// 进行数据库连接的初始化
bool Master::_StartDB()
{
// 读Config文件,从中获得到世界数据数据库的配置信息
// 我下到的源码自带的配置文件中世界数据字符串描述文件如下:
//"127.0.0.1;3306;mangos;mangos;mangos" 可以用来配置单机测试
std::string dbstring; // 记录数据库字符串
if(!sConfig.GetString("WorldDatabaseInfo", &dbstring))
{
// 无法获取到数据库配置信息则 连接出错返回
sLog.outError("Database not specified in configuration file");
return false;
}
sLog.outString("World Database: %s", dbstring.c_str());
///- 连接worldData数据库,其中全局变量WorldDatabase为之前在main函数中定义的DatabaseMysql类
if(!WorldDatabase.Initialize(dbstring.c_str()))
{
sLog.outError("Cannot connect to world database %s",dbstring.c_str());
return false;
}
// 获取角色数据库 默认配置为"127.0.0.1;3306;mangos;mangos;characters"
if(!sConfig.GetString("CharacterDatabaseInfo", &dbstring))
{
sLog.outError("Character Database not specified in configuration file");
return false;
}
sLog.outString("Character Database: %s", dbstring.c_str());
//连接到角色数据库,可以获取到角色的信息 使用的是之前main中定义的CharacterDatabase全局变量
if(!CharacterDatabase.Initialize(dbstring.c_str()))
{
sLog.outError("Cannot connect to Character database %s",dbstring.c_str());
return false;
}
///从配置文件中获取登录信息数据库的信息
if(!sConfig.GetString("LoginDatabaseInfo", &dbstring))
{
sLog.outError("Login database not specified in configuration file");
return false;
}
//连接到登录信息数据库,这里应该是记录用户的账户信息,具体数据库的内容以后会详细分析的
sLog.outString("Login Database: %s", dbstring.c_str() );
if(!loginDatabase.Initialize(dbstring.c_str()))
{
sLog.outError("Cannot connect to login database %s",dbstring.c_str());
return false;
}
//从配置文件中获取到游戏分区的信息
realmID = sConfig.GetIntDefault("RealmID", 0);
if(!realmID)
{
sLog.outError("Realm ID not defined in configuration file");
return false;
}
sLog.outString("Realm running as realm ID %d", realmID); //输出一些分区信息 ?
//该函数用来清楚数据库中的角色在线信息
//当一个帐号中拥有角色时,连接数据库后设置该分区内该帐号下的角色在线标志为0,此处暂时为弄明白0代表什么
clearOnlineAccounts();
// 从世界数据库中获取到版本信息 并且检测版本是否正确
QueryResult* result = WorldDatabase.Query("SELECT `version` FROM `db_version` LIMIT 1");
if(result)
{
Field* fields = result->Fetch();
sLog.outString("Using %s", fields[0].GetString());
delete result;
}
else
sLog.outString("Using unknown world database."); // 若版本错误则只输出提示信息
return true;
}
连接数据库之后是游戏世界的初始化。以后会顺着这个思路一点一点分析其实现的。