Mangos 서버 프레임워크 설계 분석(二)

지난 논문에서 우리는 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
",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
",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
"); 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
"); return false; } printf("World Database total connection:%d
",nConnections+1); if(!WorldDatabase.Initialize(dbstring.c_str(),nConnections)) { printf("Cannot connect to world database %s
",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
"); return false; } printf("Character Database total connections:%d
",nConnections+1); if(!CharacterDatabase.Initialize(dbstring.c_str(),nConnections)) { printf("Cannot connect to Character database %s
",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
"); WorldDatabase.HaltDelayThread(); CharacterDatabase.HaltDelayThread(); return true; } printf("Login Database total connections:%d
",nConnections+1); if(!LoginDatabase.Initialize(dbstring.c_str(),nConnections)) { printf("Cannot connect to login database
"); 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 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();

    /// 
  • 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(); } ///
  • Handle AHBot operations if (m_timers[WUPDATE_AHBOT].Passed()) { sAuctionBot.Update(); m_timers[WUPDATE_AHBOT].Reset(); } ///
  • Handle session updates UpdateSessions(diff); ///
  • 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); } ///
  • 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)); } ///
  • 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(); } ///
///- 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
"); 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
"); return -1; } m_NetThreadCount = static_cast(num_threads+1); m_NetThreads = new ReactorRunnable[m_NetThreadCount]; printf("Max allowed socket connections %d
",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
"); 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
"); return -1; } for(size_t i =0;i
우선 마스터입니다.cpp에서 WorldSocketMgr을 호출합니다.cpp의 Start Network 함수, 사실 이 함수는 주로 봉인 함수로 존재한다. 진정으로 작용하는 것은 Start Reactive IO 함수이다. 이 함수에서 설정에 따라 여러 개의 라인을 만들었고 각 라인의 창설 절차는 우리의 게임 논리 라인과 유사하다. 그 중 한 라인은 클라이언트의 요청을 감청하고 수신하는 것으로 존재하고 다른 라인은 해당하는 메시지 이벤트를 처리한다.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(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
"); 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
"); return 0; } private: typedef ACE_Atomic_Op AtomicInt; typedef std::set 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_ 호출을 통해이벤트 인터페이스, 그리고 최종적으로 이 라인에 등록된reactor 대상 이벤트 핸들, 즉 WorldSocket 대상을 호출합니다. 그 중에서 각각handle_put 함수, 두 번째 종류의 데이터 패키지의 발송은 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에서 사용하는 데이터베이스에 대해 간단명료한 분석 연구를 하겠습니다. 감사합니다.
필요하다면 전재를 명기해 주십시오. 감사합니다.

좋은 웹페이지 즐겨찾기