您的位置:首页 > 编程语言 > Go语言

Mangos服务器框架设计分析(一)

2015-01-22 20:41 369 查看
距离上次写博客已经大半年了,这大半年中经历了很多的人和事,并且也收获了很多,所在的项目组游戏已经成功上线运营了,在此稍微花点时间记录一下这大半年在业余时间做的一些技术积累吧,在此就以堪称是“大魔兽私服”服务器框架——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<AuthSocket,ACE_SOCK_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<ACE_SOCK_STREAM,ACE_NULL_SYNCH>,这个类很重要,它基本上已经实现了整个底层数据处理,发送,接收流程,只是向它的上层提供了几个比较重要的接口,作为使用ACE的程序员,我们只需要稍加实现这些接口就OK了,是不是很方便呀,呵呵,在BufferSocket中,主要实现了数据最基础的发送和接受,而至于如何处理都留给了我们的上层,在BufferSocket中,实现了ACE_Svc_Handler继承而来的handle_input,handle_output以及handle_close,这三个函数基本上就囊括了我们的socket最本质的操作了,至于如何进行后续处理,我们只需要提供相应的实现即可,接下来看看BufferSocket代码吧:

class BufferSocket : public ACE_Svc_Handler<ACE_SOCK_STREAM,ACE_NULL_SYNCH>
{
protected:
typedef ACE_Svc_Handler<ACE_SOCK_STREAM,ACE_NULL_SYNCH> 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代码中,除了我们刚说的三个基本函数之外,还有一些很值得我们学习的地方就是采用了缓存队列的方式来处理发送或接收的数据包,在BufferSocket中,其实有两种用来处理数据包的方式,一种是直接发送的方式,一种就是具有延时效应的方式,对于延时发送实现其实就是使用了ACE的缓存队列,这部分内容大家可以好好学习一下,很有用,另外,在这里还是提一下吧,其实除了我们刚刚说的那三个最基本的函数之外,ACE_Acceptor还提供了一个很重要的函数open,这个函数就是用来接收新连接的函数,当成功接收之后,对应的返回的连接句柄就可以通过peer()函数来获取到,这样就可以相互通信了,是不是很方便。

在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();
};


这个类就是我们说的正真处理数据包的地方,下面就来看看AuthSocket实现吧,由于Mangos中AuthSocket代码中有大量的处理事件函数,为了简洁期间,我自己都改写了,这样看起来就明了很多,代码如下:

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<AUTH_TOTAL_COMMANDS;++i)
{
if((uint8)table[i].cmd == _cmd &&
(table[i].status == STATUS_CONNECTED ||
(_authed && table[i].status == STATUS_AUTHED)))
{
printf("[AUTH] Get data from cmd %u recv length %u",(uint32)_cmd,(uint32)recv_len());

if(!(*this.*table[i].handler)())
{
printf("Command handler failed for cmd %u recv length %u",(uint32)_cmd,(uint32)recv_len());
return;
}
break;
}
}
if(i == AUTH_TOTAL_COMMANDS)
printf("[AUTH] Get unknown packet %u",(uint32)_cmd);
}
}

bool AuthSocket::_HandleLoginTest()
{
printf("Entering _HandleLoginTest");
if(recv_len() < sizeof(sAuth_Login_Test_C))
return false;

std::vector<uint8> 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;
}


其实最主要就是那个while循环了,通过获取到客户端发送过来的一些消息指令,进行后续的处理,类似一个事件分发器,道理很简单,好好地体会一下吧,所有的操作都可以在AuthHandler中进行注册,至于后续对消息的处理,可以单独实现函数来实现,只要在事件数组中注册即可,呵呵,好了,至于Mangos大厅服务器的主体基本上就是这些了,就是一个Acceptor,一个Reactor,然后一个ACE_Svc_Hanlder,还是比较的简单,接下去会针对世界服务器以及相关的数据库进行简要的分析,谢谢

如果需要,请注明转载,多谢
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: