当前位置: 首页 > 工具软件 > MaNGOS Game > 使用案例 >

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

包谭三
2023-12-01

在上一篇博文中,我们分析了Mangos服务器的登陆服务器,Mangos登陆服务器主要就是验证用户的合法性,并且针对通过验证的用户发送游戏列表服务器列表,用户选择了相关游戏服务器时所涉及到的流程就是今天本博文需要分析的,Mangos游戏服务器的主要架构就是一对多的关系,下层的I/O是多线程的,而上层的游戏主逻辑是单线程的,两者的胶合部分就是已经封装好的session模块,好了,下面就来看看具体的流程细节吧,首先来看看游戏服务器(世界服务器)的主流程吧,代码如下:

extern int main(int argc,char** argv)
{
	const char* cfg_file = _MANGOSD_CONFIG;
	const char* options = ":a:c:s";

	ACE_Get_Opt cmd_opts(argc,argv,options);
	cmd_opts.long_option("version",'v',ACE_Get_Opt::NO_ARG);
	cmd_opts.long_option("ahbot",'a',ACE_Get_Opt::ARG_REQUIRED);

	char serviceDaemonMode = '\0';

	int option;
	while((option = cmd_opts())!= EOF)
	{
		switch (option)
		{
		case 'a':
			break;
		case 'c':
			cfg_file = cmd_opts.opt_arg();
			break;
		case 'v':
			return 0;
		case 's':
			break;
		default:
			break;
		}
	}

	if(!sConfig.SetSource(cfg_file))
	{
		printf("Could not find configuration file %s\n",cfg_file);
		return 1;
	}
	return sMaster.Run();
}
上述代码可能跟Mangos代码有部分出入,主要是为了看清整个流程而进行了精简,在上述代码中主要就是那个sMaster.run了,其他的就是一些配置方面的加载功能,好了,我们就顺着sMaster往下看看,代码如下:

int Master::Run()
{
	std::string pidfile = sConfig.GetStringDefault("PidFile","");
	if(!pidfile.empty())
	{
		uint32 pid = CreatePIDFile(pidfile);
		if(!pid)
		{
			printf("Cannot create pid file %s\n",pidfile.c_str());
			return 1;
		}
	}
	if(!_StartDB())
	{
		return 1;
	}

	//sWorld.SetInitialWorldSettings();

	CharacterDatabase.AllowAsyncTransactions();
	WorldDatabase.AllowAsyncTransactions();
	LoginDatabase.AllowAsyncTransactions();

	_HookSignals();

	ACE_Based::Thread world_thread(new WorldRunnable());
	world_thread.setPriority(ACE_Based::Highest);

	ACE_Based::Thread* cliThread = NULL;

#ifdef WIN32
	if(sConfig.GetBoolDefault("Console.Enable",true) && (m_ServiceStatus == -1))
#else
	if(sConfig.GetBoolDefault("Console.Enable",true))
#endif
	{
		cliThread = new ACE_Based::Thread(new CliRunnable());
	}

	ACE_Based::Thread* rar_thread = NULL;
	if(sConfig.GetBoolDefault("Ra.Enable",false))
	{
		rar_thread = new ACE_Based::Thread(new RARunnable());
	}

	//uint16 port = sWorld.getConfig(CONFIG_UINT32_PORT_WORLD);
	uint16 port = 17777;
	std::string ip = sConfig.GetStringDefault("BindIP","0.0.0.0");
	if(sWorldSocketMgr->StartNetwork(port,ip) == -1)
	{
		printf("Failed to start network\n");
		World::StopNow(ERROR_EXIT_CODE);
	}
	sWorldSocketMgr->Wait();

	_UnHookSignals();

	world_thread.wait();

	if(rar_thread)
	{
		rar_thread->wait();
		rar_thread->destroy();
		delete rar_thread;
	}

	//clearOnlineAccounts();

	//sMassMailMgr.Update(true);

	CharacterDatabase.HaltDelayThread();
	WorldDatabase.HaltDelayThread();
	LoginDatabase.HaltDelayThread();

#ifdef WIN32
	cliThread->wait();
#else
	cliThread->destroy();
#endif
	delete cliThread;

	return World::GetExitCode();
}
在上面的代码中,我们主要关注三点东西:1.数据库的初始化操作,2.世界主线程(游戏主线程)的初始化以及启动,3.底层的I/O线程的初始化以及启动,好了,我们就按照这三点来稍微看看,首先看第一点(1.数据库的初始化操作),代码如下:

bool Master::_StartDB()
{
	std::string dbstring = sConfig.GetStringDefault("WorldDatabaseInfo","");
	int nConnections = sConfig.GetIntDefault("WorldDatabaseConnections",1);
	if(dbstring.empty())
	{
		printf("Database not specified in configuration file\n");
		return false;
	}
	printf("World Database total connection:%d\n",nConnections+1);

	if(!WorldDatabase.Initialize(dbstring.c_str(),nConnections))
	{
		printf("Cannot connect to world database %s\n",dbstring.c_str());
		return false;
	}

	dbstring = sConfig.GetStringDefault("CharacterDatabaseInfo","");
	nConnections = sConfig.GetIntDefault("CharacterDatabaseConnections",1);
	if(dbstring.empty())
	{
		printf("Character Database not specified in Configuration file\n");
		return false;
	}

	printf("Character Database total connections:%d\n",nConnections+1);
	if(!CharacterDatabase.Initialize(dbstring.c_str(),nConnections))
	{
		printf("Cannot connect to Character database %s\n",dbstring.c_str());
		WorldDatabase.HaltDelayThread();
		return false;
	}

	dbstring = sConfig.GetStringDefault("LoginDatabaseInfo","");
	nConnections = sConfig.GetIntDefault("LoginDatabaseConnections",1);
	if(dbstring.empty())
	{
		printf("Login Database not specified in Configuration file\n");
		WorldDatabase.HaltDelayThread();
		CharacterDatabase.HaltDelayThread();
		return true;
	}

	printf("Login Database total connections:%d\n",nConnections+1);
	if(!LoginDatabase.Initialize(dbstring.c_str(),nConnections))
	{
		printf("Cannot connect to login database\n");
		WorldDatabase.HaltDelayThread();
		CharacterDatabase.HaltDelayThread();
		return false;
	}

	//clearOnlineAccounts();
	return true;
}
在这段代码中,我们主要看下数据库的初始化操作,在mangos中主要涉及到三个数据库,通过上面的代码也可以看出来,初始化数据库的接口时Initialize,传入的参数是创建数据的sql语句以及数据库连接数,针对数据库部分我们会在后面的博文中进行详细的分析,在此只需要记住数据库初始化的大致流程以及传入的参数即可,好了,接着上面,我们来看看第二点(游戏主线程的初始化以及启动),这段代码如下:

	ACE_Based::Thread world_thread(new WorldRunnable());
	world_thread.setPriority(ACE_Based::Highest);
这段代码中我们需要关注一下几点:1.ACE_Based::Thread,2.WorldRunnable,3.设置游戏主线程的优先级,我们先来看看ACE_Based::Thread这个吧,代码如下:

Thread::Thread(Runnable* instance) : m_iThreadId(0),m_hThreadHandle(0),m_task(instance)
{
	if(m_task)
		m_task->incReference();
	bool _start = start();
	assert(_start);
}
bool Thread::start()
{
	if(m_task ==0 || m_iThreadId !=0)
		return false;
	bool res = (ACE_Thread::spawn(&Thread::ThreadTask,(void*)m_task,THREADFLAG,&m_iThreadId,&m_hThreadHandle)==0);
	if(res)
		m_task->incReference();
	return res;
}
上面代码只是从Threading.cpp文件中提取出来的,在这里面可以看出通过ACE_Thread::spawn接口创建了相应的线程,并且设置线程执行函数,我们再来看看这个线程的执行函数吧(m_task),代码如下:

	class Runnable
	{
	public:
		virtual ~Runnable(){}
		virtual void run() = 0;

		void incReference()
		{
			++m_refs;
		}
		void decReference()
		{
			if(!--m_refs)
				delete this;
		}
	private:
		ACE_Atomic_Op<ACE_Thread_Mutex,long> m_refs;
	};
在Thread中引用了Runnable对象,也就是我们的m_task,从上面的代码中可以看出我们的m_task具有引用技术效果,并且该类只是一个充当接口的抽象函数,我们来看看具体的实例吧,代码如下:

class WorldRunnable : public ACE_Based::Runnable
{
public:
	void run() override;
};

void WorldRunnable::run()
{
	WorldDatabase.ThreadStart();
	//sWorld.InitResultQueue();

	uint32 realCurrTime = 0;
	uint32 realPrevTime = WorldTimer::tick();

	uint32 prevSleepTime = 0;

	while(!World::IsStopped())
	{
		++World::m_worldLoopCounter;
		realCurrTime = WorldTimer::getMSTime();

		uint32 diff = WorldTimer::tick();
		sWorld.Update(diff);
		realPrevTime = realCurrTime;

		if(diff <= WORLD_SLEEP_CONST + prevSleepTime)
		{
			prevSleepTime = WORLD_SLEEP_CONST + prevSleepTime - diff;
			ACE_Based::Thread::Sleep(prevSleepTime);
		}
		else
			prevSleepTime = 0;

#ifdef WIN32
		if(m_ServiceStatus ==0) World::StopNow(SHUTDOWN_EXIT_CODE);
		while(m_ServiceStatus ==2) Sleep(1000);
#endif
	}
	//sWorld.KickAll();

	sWorld.UpdateSessions(1);

	//sBattleGroundMgr.DeleteAllBattlenGrounds();

	sWorldSocketMgr->StopNetwork();

	//MapManager::Instance()->UnloadAll();

	WorldDatabase.ThreadEnd();
}
从上述代码中可以看出,我们的游戏主线程的主要的流程,首先肯定是一个大的无限循环函数了,并且定时更新我们的游戏服务器相关状态(通过sWorld.update接口来实现),之后针对游戏主线程关闭时进行了相关的清理工作,大致的流程就是这样,代码如下:
void World::Update(uint32 diff)
{
    ///- Update the different timers
    for (int i = 0; i < WUPDATE_COUNT; ++i)
    {
        if (m_timers[i].GetCurrent() >= 0)
            m_timers[i].Update(diff);
        else
            m_timers[i].SetCurrent(0);
    }

    ///- Update the game time and check for shutdown time
    _UpdateGameTime();

    ///-Update mass mailer tasks if any
    sMassMailMgr.Update();

    /// Handle daily quests reset time
    if (m_gameTime > m_NextDailyQuestReset)
        ResetDailyQuests();

    /// Handle weekly quests reset time
    if (m_gameTime > m_NextWeeklyQuestReset)
        ResetWeeklyQuests();

    /// Handle monthly quests reset time
    if (m_gameTime > m_NextMonthlyQuestReset)
        ResetMonthlyQuests();

    /// <ul><li> Handle auctions when the timer has passed
    if (m_timers[WUPDATE_AUCTIONS].Passed())
    {
        m_timers[WUPDATE_AUCTIONS].Reset();

        ///- Update mails (return old mails with item, or delete them)
        //(tested... works on win)
        if (++mail_timer > mail_timer_expires)
        {
            mail_timer = 0;
            sObjectMgr.ReturnOrDeleteOldMails(true);
        }

        ///- Handle expired auctions
        sAuctionMgr.Update();
    }

    /// <li> Handle AHBot operations
    if (m_timers[WUPDATE_AHBOT].Passed())
    {
        sAuctionBot.Update();
        m_timers[WUPDATE_AHBOT].Reset();
    }

    /// <li> Handle session updates
    UpdateSessions(diff);

    /// <li> Handle weather updates when the timer has passed
    if (m_timers[WUPDATE_WEATHERS].Passed())
    {
        ///- Send an update signal to Weather objects
        for (WeatherMap::iterator itr = m_weathers.begin(); itr != m_weathers.end();)
        {
            ///- and remove Weather objects for zones with no player
            // As interval > WorldTick
            if (!itr->second->Update(m_timers[WUPDATE_WEATHERS].GetInterval()))
            {
                delete itr->second;
                m_weathers.erase(itr++);
            }
            else
                ++itr;
        }

        m_timers[WUPDATE_WEATHERS].SetCurrent(0);
    }
    /// <li> Update uptime table
    if (m_timers[WUPDATE_UPTIME].Passed())
    {
        uint32 tmpDiff = uint32(m_gameTime - m_startTime);
        uint32 maxClientsNum = GetMaxActiveSessionCount();

        m_timers[WUPDATE_UPTIME].Reset();
        LoginDatabase.PExecute("UPDATE uptime SET uptime = %u, maxplayers = %u WHERE realmid = %u AND starttime = " UI64FMTD, tmpDiff, maxClientsNum, realmID, uint64(m_startTime));
    }

    /// <li> Handle all other objects
    ///- Update objects (maps, transport, creatures,...)
    sMapMgr.Update(diff);
    sBattleGroundMgr.Update(diff);
    sOutdoorPvPMgr.Update(diff);

    ///- Delete all characters which have been deleted X days before
    if (m_timers[WUPDATE_DELETECHARS].Passed())
    {
        m_timers[WUPDATE_DELETECHARS].Reset();
        Player::DeleteOldCharacters();
    }

    // execute callbacks from sql queries that were queued recently
    UpdateResultQueue();

    ///- Erase corpses once every 20 minutes
    if (m_timers[WUPDATE_CORPSES].Passed())
    {
        m_timers[WUPDATE_CORPSES].Reset();

        sObjectAccessor.RemoveOldCorpses();
    }

    ///- Process Game events when necessary
    if (m_timers[WUPDATE_EVENTS].Passed())
    {
        m_timers[WUPDATE_EVENTS].Reset();                   // to give time for Update() to be processed
        uint32 nextGameEvent = sGameEventMgr.Update();
        m_timers[WUPDATE_EVENTS].SetInterval(nextGameEvent);
        m_timers[WUPDATE_EVENTS].Reset();
    }

    /// </ul>
    ///- Move all creatures with "delayed move" and remove and delete all objects with "delayed remove"
    sMapMgr.RemoveAllObjectsInRemoveList();

    // update the instance reset times
    sMapPersistentStateMgr.Update();

    // And last, but not least handle the issued cli commands
    ProcessCliCommands();

    // cleanup unused GridMap objects as well as VMaps
    sTerrainMgr.Update(diff);
}

这个函数包括了游戏主线程的主要工作流程了,在这里面我们需要关注一下UpdateSessoin和ProcessCliCommand这两个函数,UpdateSession主要更新目前处在游戏中角色的一些游戏状态(主要是一些收发消息以及相关的处理),这个函数很重要,游戏中角色的所有的操作都跟session有关系,这部分内容会在后续的博文中进行分析,在此只需要其大致的作用即可,另一个ProcessCliCommand函数主要是接收和处理客户端发送过来的一些GM指令之类的消息,好了,基本上我们的游戏主逻辑线程大致的流程就是这样,首先创建线程,设置线程的执行函数并执行,在执行函数中囊括了游戏主线程的基本的一些操作,这部分内容表面上其实很简单,但是如果具体仔细分析的话,有些地方还是需要好好地体会一下的,好了,游戏主线程就分析到这里,下面我们来看看底层的I/O线程吧,代码如下:

	uint16 port = 17777;
	std::string ip = sConfig.GetStringDefault("BindIP","0.0.0.0");
	if(sWorldSocketMgr->StartNetwork(port,ip) == -1)
	{
		printf("Failed to start network\n");
		World::StopNow(ERROR_EXIT_CODE);
	}
	sWorldSocketMgr->Wait();

int WorldSocketMgr::StartReactiveIO(ACE_UINT16 port,const char* address)
{
	m_UseNoDelay = sConfig.GetBoolDefault("Network.TcpNodelay",true);
	int num_threads = sConfig.GetIntDefault("Network.Threads",1);

	if(num_threads <0)
	{
		printf("Network.Threads is wrong in you config file\n");
		return -1;
	}

	m_NetThreadCount = static_cast<size_t>(num_threads+1);
	m_NetThreads = new ReactorRunnable[m_NetThreadCount];
	printf("Max allowed socket connections %d\n",ACE::max_handles());

	m_SockOutKBuff = sConfig.GetIntDefault("Network.OutKBuff",-1);
	m_SockOutUBuff = sConfig.GetIntDefault("Network.OutUBuff",65535);

	if(m_SockOutUBuff<0)
	{
		printf("Network.OUTBUFF is wrong in your config file\n");
		return -1;
	}

	WorldSocket::Acceptor* acc = new WorldSocket::Acceptor();
	m_Acceptor = acc;

	ACE_INET_Addr listen_addr(port,address);
	if(acc->open(listen_addr,m_NetThreads[0].GetReactor(),ACE_NONBLOCK) == -1)
	{
		printf("Failed to open acceptor,check if the port is free\n");
		return -1;
	}
	for(size_t i =0;i<m_NetThreadCount;++i)
		m_NetThreads[i].Start();
	return 0;
}

int WorldSocketMgr::StartNetwork(ACE_UINT16 port,std::string& address)
{
	m_port = port;
	m_addr = address;

	if(StartReactiveIO(port,address.c_str()) ==-1)
		return -1;
	return 0;
}
首先是在Master.cpp中调用WorldSocketMgr.cpp中的StartNetwork函数,其实这个函数主要是作为一个封装函数而存在,真正起作用的是StartReactiveIO函数,在这个函数中根据配置创建了多个线程,并且每个线程的创建流程类似于我们的游戏主逻辑线程,而其中一个线程主要作为监听和接收客户端的请求而存在,其他的线程会处理相应的消息事件,我们来稍微看看reactorRunnable这个类,这个就是我们的I/O线程主要流程,代码如下:

class ReactorRunnable : protected ACE_Task_Base
{
public:
	ReactorRunnable():
		m_Reactor(0),
		m_Connections(0),
		m_ThreadId(-1)
	{
		ACE_Reactor_Impl* imp = NULL;
#if defined (ACE_HAS_EVENT_POLL) || defined(ACE_HAS_DEV_POLL)
		imp = new ACE_Dev_Poll_Reactor();
		imp->max_notify_iterations(128);
		imp->restart(1);
#else
		imp = new ACE_TP_Reactor();
		imp->max_notify_iterations(128);
#endif
		m_Reactor = new ACE_Reactor(imp,1);
	}

	virtual ~ReactorRunnable()
	{
		Stop();
		Wait();
		delete m_Reactor;
	}

	void Stop()
	{
		m_Reactor->end_reactor_event_loop();
	}

	int Start()
	{
		if(m_ThreadId != -1)
			return -1;
		return (m_ThreadId = activate());
	}

	void Wait()
	{
		ACE_Task_Base::wait();
	}

	long Connections()
	{
		return static_cast<long>(m_Connections.value());
	}

	int AddSocket(WorldSocket* sock)
	{
		ACE_GUARD_RETURN(ACE_Thread_Mutex,Guard,m_NewSockets_lock,-1);
		++m_Connections;
		sock->AddReference();
		sock->reactor(m_Reactor);
		m_NewSockets.insert(sock);
		return 0;
	}

	ACE_Reactor* GetReactor()
	{
		return m_Reactor;
	}

protected:
	void AddNewSockets()
	{
		ACE_GUARD(ACE_Thread_Mutex,Guard,m_NewSockets_lock);
		if(m_NewSockets.empty())
			return;

		for(SocketSet::const_iterator iter = m_NewSockets.begin();iter != m_NewSockets.end();iter++)
		{
			WorldSocket* sock = *iter;
			if(sock->IsClosed())
			{
				sock->RemoveReference();
				--m_Connections;
			}
			else
				m_Sockets.insert(sock);
		}
		m_NewSockets.clear();
	}

	virtual int svc()
	{
		printf("NetWork Thread starting\n");
		WorldDatabase.ThreadStart();
		assert(m_Reactor);

		SocketSet::iterator i,t;
		while(!m_Reactor->reactor_event_loop_done())
		{
			ACE_Time_Value interval(0,10000);
			if(m_Reactor->run_reactor_event_loop(interval) == -1)
				break;

			AddNewSockets();

			for(i=m_Sockets.begin();i != m_Sockets.end();i++)
			{
				if((*i)->Update() == -1)
				{
					t = i;
					++i;
					(*t)->CloseSocket();
					(*t)->RemoveReference();
					m_Sockets.erase(t);
				}
				else
					++i;
			}
		}
		WorldDatabase.ThreadEnd();
		printf("NetWork Thread Exiting\n");
		return 0;
	}
	private:
		typedef ACE_Atomic_Op<ACE_SYNCH_MUTEX,long> AtomicInt;
		typedef std::set<WorldSocket*> SocketSet;

		ACE_Reactor* m_Reactor;
		AtomicInt m_Connections;
		int m_ThreadId;

		SocketSet m_Sockets;
		SocketSet m_NewSockets;

		ACE_Thread_Mutex m_NewSockets_lock;
};
上述代码中可以看出,我们的每个I/O线程都包含了一个reactor对象,这个对象的作用想必大部分人应该还是很清楚的,当有事件到来时,负责响应在其上注册的事件,并通知执行有关事件回调函数,在这里面我们需要关注一下activate这函数,这个函数的作用就是创建i/o线程,并且调用svc函数,这个函数就是本i/o线程的主体了,负责处理在其reactor对象注册的I/O事件,在这个函数里面我们需要注意一下,里面其实包含了两类操作,第一类就是数据包的接收,第二类就是数据报的发送,第一类主要是通过我们的

run_reactor_event_loop函数来实现的,其底层通过调用handle_event接口,并最终会调用注册在本线程的reactor对象事件句柄也就是WorldSocket对象,其中会分别调用handle_input函数,第二类数据包的发送就是靠WorldSocket->update函数来触发,有人可能会有疑惑就是当有客户端请求来时,是如何处理的呢,其实很简单,由于我们已经在第一个I/O线程中注册了acceptor事件,所有当有客户端请求来时,会在第一个线程中进行处理,处理完之后,会将已经建立的新WorldSocket注册到最小连接数的I/O线程上,而各自的线程会负责处理每个注册在自己的reactor对象上的I/O事件了,至于如何处理的流程请参考WorldSocket.cpp文件,在这个流程中我们需要注意的是这里面会有将收到发送给上层消息转存在每个session队列里面,原理很简单,在此不再多说了,好了,接下去就是我们的第三点:设置游戏主线程的优先级了,设置这个的目的就是确保我们的游戏逻辑线程能够以较高的效率来处理游戏相关消息以及事件,具体的代码请参见Threading.cpp,

好了针对mangos的游戏服务器(世界服务器)介绍就到这里了,其实主要是依靠ACE提供好的一套事件处理框架,我们只需要通过继承和引用的方式即可,例如在mangos里面的Acceptor以及我们的I/O线程,基本上理解了这两个对象,mangos底层的一些消息通信机制基本就掌握了,其他的基本就是消息的末端处理了以及相关的时间同步更新之类的,这些都应该算是上层应用了,在I/O层我们只需要关注消息的走向即可,好了,说的有点多,大家可以自行分析一下,还是蛮有收获的,下一篇我们会针对mangos内使用的数据库进行简要的分析研究,谢谢了。

如果需要,请注明转载,多谢






 类似资料: