Mangos 서버 프레임워크 설계 분석(二)
18561 단어 오픈 소스 항목(라이브러리)
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에서 사용하는 데이터베이스에 대해 간단명료한 분석 연구를 하겠습니다. 감사합니다.
필요하다면 전재를 명기해 주십시오. 감사합니다.