距离上次写博客已经大半年了,这大半年中经历了很多的人和事,并且也收获了很多,所在的项目组游戏已经成功上线运营了,在此稍微花点时间记录一下这大半年在业余时间做的一些技术积累吧,在此就以堪称是“大魔兽私服”服务器框架——Mangos来做些框架设计上的分享吧,Mangos整体看来还是1:n模式的架构,底层的采用了多线程的IO,而游戏的主逻辑依旧是单线程的,Mangos服务器框架主要是基于ACE开源库的,很早之前就有接触过ACE,但是没有做个比较大的项目,正好借此机会来加深一下如何用ACE来架设服务器框架的能力,好了,下面简要的介绍一下Mangos的架构吧:在Mangos中,主要分为了两类服务器,一类服务器就是我们常说的登陆服务器,也叫大厅服务器,而第二类服务器就是我们常说的游戏逻辑服务器,下面我们就从大厅服务器说起吧,Mangos大厅服务器设计的其实很简单,采用的是单线程的方式,主要是负责接收登陆请求,验证请求以及针对验证通过的请求发送游戏逻辑服务器的地址(游戏区的信息),接下去来看看,大厅服务器的简要代码吧:
1.大厅服务器主流程:
extern int main(int argc,char** argv)
{
char const* cfg_file = "readmd.conf";
char const* options = ":c:s";
ACE_Get_Opt cmd_opts(argc,argv,options);
cmd_opts.long_option("version",'v');
char serviceDaemonMode = '\0';
int option;
while((option =cmd_opts())!=EOF)
{
switch (option)
{
case 'c':
cfg_file = cmd_opts.opt_arg();
break;
case 'p':
printf("loginServer config\n");
return 0;
case 's':
{
const char* mode = cmd_opts.opt_arg();
if(!strcmp(mode,"run"))
serviceDaemonMode = 'r';
#ifdef WIN32
else if(!strcmp(mode,"install"))
serviceDaemonMode = 'i';
else if(!strcmp(mode,"uninstall"))
serviceDaemonMode = 'u';
#else
#endif
else
{
printf("Runtime-Error: -%c unsupported argument %s",cmd_opts.opt_opt(),mode);
return 1;
}
break;
}
default:
break;
}
}
#ifdef WIN32
switch (serviceDaemonMode)
{
case 'i':
WinServiceInstall();
return 1;
case 'u':
WinServiceUnInstall();
return 1;
case 'r':
WinServiceRun();
break;
default:
break;
}
#endif
if(!sConfig.SetSource(cfg_file))
{
printf("Could not find configuration file %s.",cfg_file);
return 1;
}
ACE_Reactor::instance(new ACE_Reactor(new ACE_TP_Reactor(),true),true);
if(!StartDB())
return 1;
ACE_Acceptor acceptor;
uint16 port = sConfig.GetIntDefault("LoginServerPort",DEFAULT_LOGINSERVER_PORT);
std::string ip = sConfig.GetStringDefault("LoginServerIp","0.0.0.0");
ACE_INET_Addr bind_addr(port,ip.c_str());
if(acceptor.open(bind_addr,ACE_Reactor::instance(),ACE_NONBLOCK) == -1)
{
printf("SimpleMangos can not bind to %s:%d",ip.c_str(),port);
return 1;
}
LoginDatabase.AllowAsyncTransactions();
HookSignals();
uint32 numLoops = (sConfig.GetIntDefault("MaxPingTime",30) * (MINUTE * 1000000/100000));
uint32 loopCounter = 0;
while(!stopEvent)
{
ACE_Time_Value interval(0,100000);
if(ACE_Reactor::instance()->run_reactor_event_loop(interval) ==-1)
break;
if((++loopCounter) == numLoops)
{
loopCounter = 0;
LoginDatabase.Ping();
}
#ifdef WIN32
if(m_ServiceStatus ==0)
stopEvent = true;
while(m_ServiceStatus == 2)
Sleep(1000);
#endif
}
LoginDatabase.HaltDelayThread();
UnhookSignals();
return 0;
}
在上述代码中,主要关注两点,第一点就是那个Acceptor,另一个就是那个StartDB,Acceptor这货主要是用来接受客户端的请求到来,并通过AuthSocket来进一步的处理,至于AuthSocket这个后面会单独来说,在此我们把主要的精力放在那个while循环中,Acceptor中有多种i/o多路服用的方案,在此我们使用了ACE_TP_Reactor方式,这个方式就是最简单的select模型,Acceptor通过调用open函数,来将自己注册到reactor对象中,这样一来当有请求到来时,会回调Acceptor里面回调open函数,最终会调用到初始化它时,所使用的事件句柄(AuthSocket),这里面其实有些绕,多体会一下应该没有问题,之后所有的时间处理都是在while中进行的,也主要是通过run_reactor_loop中进行的,这个函数会调用handle_event函数,在这里面除此之外,其实还有一点需要体会的哦,就是那个startDB,在Mongos中,数据库主要有三种数据库:1.登陆数据库,2.角色数据库,3.世界数据库,在此我们主要使用到登陆数据库,这个数据库主要记录了玩家的一些登陆状态的信息,包括:一些账号的封闭以及解封等。接下去我们就来看看那个AuthSocket实现吧,这个很重要,因为之后所有的操作都是通过这个类来实现的,在此之前,我们需要知道ACE的一些基本知识,ACE其实就是有一些过度抽象的模块构造,每个模块的行为都是由初始化它时所使用的模板参数决定,在此,是使用AuthSocket作为这个时候的模板参数,在分析AuthSocket之前,我们先来看看它的父类,BufferSocket,这个类继承与ACE_Svc_Handler
class BufferSocket : public ACE_Svc_Handler
{
protected:
typedef ACE_Svc_Handler Base;
virtual void OnRead(){}
virtual void OnAccept(){}
virtual void OnClose(){}
public:
BufferSocket();
virtual ~BufferSocket();
size_t recv_len()const;
bool recv_soft(char* buf,size_t len);
bool recv(char* buf,size_t len);
void recv_skip(size_t len);
bool send(const char* buf,size_t len);
const std::string& get_remote_addrss()const;
virtual int open(void*)override;
void close_connection();
virtual int handle_input(ACE_HANDLE = ACE_INVALID_HANDLE) override;
virtual int handle_output(ACE_HANDLE = ACE_INVALID_HANDLE) override;
virtual int handle_close(ACE_HANDLE = ACE_INVALID_HANDLE,
ACE_Reactor_Mask = ACE_Event_Handler::ALL_EVENTS_MASK);
private:
ssize_t noblk_send(ACE_Message_Block& message_block);
private:
ACE_Message_Block input_buffer_;
protected:
std::string remote_address_;
};
在BufferSocket中,除了实现了ACE_Svc_Handlert提供的接口之外,他还为上层提供了三个接口:OnAccept,OnRead,OnClose函数,这三个就是主要供上层使用的,下面就来看看AuthSocket代码吧,至于BufferSocket代码在此就不贴了。
class AuthSocket : public BufferSocket
{
public:
const static int S_BYTE_SIZE = 32;
AuthSocket();
~AuthSocket();
void OnAccept() override;
void OnRead()override;
void LoadRealmList(ByteBuffer& buffer,uint32 acctid);
bool _HandleLoginTest();
//void _SetVSFields(const std::string& rI);
private:
bool _authed;
std::string _login;
std::string _safeLogin;
std::string _localizationName;
uint16 _build;
//AccoutTypes _accoutSecurityLevel;
ACE_HANDLE patch_;
void InitPatch();
};
typedef struct AuthHandler
{
eAuthCmd cmd;
uint32 status;
bool (AuthSocket::*handler)();
} AuthHandler;
const AuthHandler table[] =
{
{CMD_AUTH_LOGIN_TEST,STATUS_CONNECTED, &AuthSocket::_HandleLoginTest}
};
#define AUTH_TOTAL_COMMANDS sizeof(table)/sizeof(AuthHandler)
AuthSocket::AuthSocket()
{
_authed = false;
//_accountSecurityLevel = SEC_PLAYER;
_build = 0;
patch_ = ACE_INVALID_HANDLE;
}
AuthSocket::~AuthSocket()
{
if(patch_ != ACE_INVALID_HANDLE)
ACE_OS::close(patch_);
}
void AuthSocket::OnAccept()
{
printf("Acceptign connection from %s",get_remote_addrss().c_str());
}
void AuthSocket::OnRead()
{
uint8 _cmd;
while(1)
{
if(!recv_soft((char*)&_cmd,1))
return;
size_t i;
for(i=0;i buf;
buf.resize(4);
recv((char*)&buf[0],4);
EndianConvert(*((uint16*)(buf[0])));
uint16 remaining = ((sAuth_Login_Test_C*)&buf[0])->size;
printf("[AuthLogin] Get header, body is %#04x bytes",remaining);
if((remaining < sizeof(sAuth_Login_Test_C)-buf.size())||(recv_len() < remaining))
return false;
buf.resize(remaining+buf.size()+1);
buf[buf.size()-1] = 0;
sAuth_Login_Test_C* ch = (sAuth_Login_Test_C*)&buf[0];
recv((char*)&buf[4],remaining);
printf("[AuthLogin] Get full packet,#04x bytes",ch->size);
return true;
}