본문 바로가기

지식/Network

Linux Network Engine for Online Game

출처 : http://chonga.pe.kr/computer/linux/linux-netengine/
 
Linux Network Engine for Online Game
문서상태 작성: 이홍기(orinmir _at_ chonga _dot_ pe _dot_ kr)
등록일: 2003년 4월 20일
문서버전: 0.1.1
수정일:
   v0.1.2 ; 2003-04-21; 문서 저작권 수정
일러 두기 : 본 문서는 select와 rtsignal사이의 구현상의 차이점과 온라인 게임에 적합한 서버의 구조를 간단하게 정리한 문서이다. 실제로 구현을 해 본 바로는 /dev/epoll이 rtsignal보다는 좀더 안정적인 것 같았다. 그 부분에 대해서도 갱신해 나갈 예정이다.
받기   * linux network game engine for rtsignal (doc)
  * rtsignal patch: one-sig-perfd-2.4.19.patch, one-sig-perfd-2.4.13.patch
  * /dev/epoll patch : ep_patch-2.4.19-0.32.diff, ep_patch-2.4.18-0.32.diff
내용 Linux RealTime Signal 기반의 온라인 게임 서버 엔진의 구현

문서 위치: http://chonga.pe.kr/computer/linux/linux-netengine/

최초 작성일: 2002-11-05

최종 수정일: 2003-04-21


목차

0. 머리말

1. 개요 및 소개

2. 이론 및 배경

3. 구현 과정

4. 결론 및 검토

5. 참고 문헌

 

 

0. 머리말

 본 문서의 저작자는 이홍기(genius(at)chonga.pe.kr)이며, 이 곳에 사용된 자료의 내용과 자료의 그림에 대한 발췌 및 수정 후 재 배포시 저자에 대한 정보를 포함해야하며 그 때에는 저자의 허락을 받도록 한다.

 

1. 개요 소개

  Linux는 OS 개발의 역사에 있어서 Open source project의 견인차 역할을 하고 있는 OS로 embedded system에서부터 고성능 서버의 OS에 이르기까지 주목 받고 있다.

  온라인 게임은 현대 게임 산업의 화두다. 지역 네트웍이나 30명 미만의 동시 접속으로만 이루어진 형태의 초기 온라인 게임에서 지금은 동시접속 1만명 이상을 처리하는 대용량 온라인 게임의 등장으로 관련 분야에 대한 관심이 높다. 실제로 온라인 게임 서버는 일반적인 네트웍 기술을 기반으로 특화된 형태의 독립서버라 볼 수 있다.

 

  온라인 게임 서버의 범주에서 볼 때, 고성능이면서 대용량의 처리를 하려면 패킷의 송수신 처리부와 데이터 처리부의 최적화가 핵심이며 이에 대한 여러가지 구현 방법이 존재한다. TCP/IP 기반의 네트웍 환경에서 일반적으로 사용되는 BSD socket의 고전적인 구현 방법으로는 select/poll을 이용한 io multiplexing(혹은 polling 방식)이 오래된 만큼 안정적이다. 하지만 이 방법은 말 그대로 kernel에서 처리되는 network io를 주기적인 polling에 의해 감시하므로 고성능 대용량 서버에서는 불필요한 CPU 부하를 유발하며 분명한 한계가 존재한다. 이에 대한 대안으로 여러 가지 vendor별 개선안이 나와있으며, linux에서는 RT Signal을 사용한 개선방법이 제공된다. 본 글에서는 이 RT Signal 방식을 이용한 네트웍 처리부를 통해 기존 방식에 비해 개선된 소켓 서버를 이용해 게임 서버의 기본 엔진을 구현에 대해 정리하고자 한다. 성능에 대한 평가는 추후 작업으로 남겨둔다.

 

2. 이론 배경

  BSD socket의 태동과 함께 하나 이상의 소켓 접속을 처리하는 서버에서 사용된 방법은 io multiplexing 방식으로 select()와 poll() 시스템 콜을 통한 polling 방식이었다. 이 방법은 지금까지도 unix 서버 프로그램의 대부분에서 사용되는 방식 중의 하나이며 전통적인 unix 프로그램은 이 방식과 함께 프로그램의 구조 역시 single thread을 선호하였다. Unix kernel은 초기에 multi threaded application에 대한 지원이 없이 설계되었기 때문이기도 하다.

  현재에 이르러서는 unix에서도 SMP(Symmetric Multiprocessing) 시스템의 범용화와 더불어 posix thread를 대부분 지원하고 있고, 이와 더불어 전통적인 polling 방식이 시스템 자원의 낭비를 초래하고 있어 개선점을 찾으려는 시도가 많아졌다. 이를 해결하기 위해 표 1)과 같이 여러가지 개선 방식을 vendor 별로 지원하고 있으며, 최근에 들어서 linux에서도 여러 가지 패치를 통해 새로운 socket 처리 방식을 지원하고 있다.

 

1) 소켓 서버의 vendor별 개선 방식

 

개선 방법

설명

안정성

MS

Windows

IOCP(io completion port)[iocp]

GetQueuedCompletionStatus()

호출을 통해 커널로부터 직접 socket event queue를 받음.

대부분의 상용

게임서버에서 채용

Solaris

/dev/poll

[devpoll1]

Poll()방식의 완벽한 대체, 장치 개념으로 처리

sun에서 지원

FreeBSD

kqueue/kevent

[kqueue]

event poll과 유사

안정적이라는 평

Linux

Realtime signal[rtsig1]

Sigwaitinfo()호출을 통한 POLL_IN event 받음

Signal overflow

단점

Realtime signal/fd

Realtime signal 방식과 같으나

Signal overflow 문제 해결

몇가지 웹서버에서

테스트 됨

/dev/poll

Solaris /dev/poll과 유사

Sun의 /dev/poll

보다 성능 월등히 떨어짐[devpoll2]

/dev/epoll [rtsig2]

Solaris /dev/poll과 유사

현재 테스트 코드 나옴 /dev/poll보다 성능이 뛰어남.

 

  각 vendor별 지원 방식은 대동소이하며 방식은 커널로부터 관심 있는 socket 중, 이벤트가 있는 것만 받아오는 식이다. 기존의 polling 방식이 갖는 문제점은 single thread 기반이어서 timeout 시간을 두면서 looping을 하도록 되어 있다는 점이다. timeout 이후의 시간에 다른 processing을 할 수 있도록 하는 것이 일반적이다. 이와 더불어 최대 1000개의 소켓 접속을 받을 수 있다면, 1000개의 소켓에 대해 매 시간 polling을 하여 event의 발생 유무를 확인하도록 되어 있다.

  그림 1)과 같이 polling 방식은 Kernel space에서 가진 1000개의 소켓 정보를 그대로 User space로 복사해 온 후, 전체 목록을 들면서 비교하는 방식을 취한다. 높은 성능을 내기 위해서는 timeout 시간을 적게하여 검사시간을 짧게하여 Packet 전송, 수신 효율을 높여야하는 문제점과 사용하지 않는 소켓 정보를 모두 검사해야하는 문제점이 있다. 따라서 그림 2)와 같이 일반적으로 이 구현을 고성능 서버에서 사용하는 경우 1개의 소켓 접속만  이루어져도 이 프로세스에 의해 CPU 사용률이 90% 이상으로 증가하며, 소켓 접속이 많아지면 많아질수록 응답 속도도 떨어질 가능성이 있다.

 

그림 1) polling 방식(select() )과 RTsignal 방식의 비교

 

그림 2) polling 방식의 구현 예

  본 글에서 Linux의 RTSignal을 도입하게 된 것은 기존 방식의 대안 중 하나이면서 linux kernel 2.2부터 RealTime Signal(Posix RealTime Signal)을 지원하기 시작했기 때문이다. Linux의 RT signal방식은 과부하 상황에서 overflow가 일어나는 기존 signal 기반 프로그램들보다는 명확하게 polling 방식을 대체할 수 있는 방법이다. (poll()에서 사용하는 POLL_IN, POLL_OUT, POLL_ERR 등을 그대로 사용할 수 있다.) RT Signal의 장점이라면, signal을 등록해놓고 해당하는 이벤트가 발생하기 전까지는 sigwaitinfo system call을 통해 대기하고 있다가, 이벤트가 발생하면, 정해둔 signal handler를 통해 처리를 하며 timeout도 지원한다. 기존의 구현 방식에 비해 낮은 CPU점유율과 빠른 응답 속도, high load에서도 안정되고 일관된 성능을 유지하고 있다. 이러한 방식을 또한 event-dispatch 방식이라고도 한다. [rtsig3]

  하지만 기본적으로 제공되는 RT Signal의 단점은 제한된 signal resource로 인해, 고부하에서 signal overflow가 되면 더이상 새로운 접속을 처리할 수 없게 된다. 이 때에는 기존의 poll()쪽으로 처리를 돌려 해결하는 혼합된 구현 방법을 함께 쓰기도 한다. 기본적으로 linux에서 지원하는 방법은 기존의 polling 방식과 병행해야하는 문제점이 있다.[phhttpd] 최근에 이르러 이를 극복한 방법으로 1개의 file descriptor당 하나씩 signal을 할당하고 그 개수 만큼 가지게 하여, 자원 만큼을 수용할 수 있는 방식의 패치가 Luban에 의해 제안되었다. 이 프로젝트에서는 이 방식을 사용하여 작성하기도 결정하였다.

  타 OS의 개선 방법에 비해 linux에서 새로 도입된 이 방식은 EWOULDBLOCK 등에 대한 에러 처리를 지원하지 않기 때문에 이에 대한 것을 설계 시점에서 같이 해주어야 했다. 따라서 그림 3)과 같은 producer/consumer구조를 도입했다. 우선 IO Event Queue를 두어 같은 형태로 POLL_IN, POLL_OUT, POLL_REMOVE를 처리하도록 하였는데, 이 구조는 INPUT event는 sigwaitinfo()의 호출에서 POLL_IN에 해당하는 socket read event가 발생하였을 경우에 wake up이 되고 IO Event Queue에 push함으로써 thread의 기반 구조에서 producer의 역할을 하게 된다. 마찬가지로 게임 서버가 실행되면서 이벤트가 발생하는데 이때 POLL_OUT이라면 이에 해당하는 socket write event로 정의하고 객체의 object가 제거되면 POLL_REMOVE에 해당하는 socket close event를 발생시켜 IOEvent Queue에 producer thread의 역할을 하게 된다. Consumer thread는 Event Dispatching Thread를 통해 IO Event Queue를 pop을 하여 각각의 경우를 read(), write(), close()에 대응시켜 이벤트를 처리하며, 각각의 시스템 콜에서 EWOULDBLOCK과 같은 socket error를 만나면 또 다시 IO Event Queue에 그 상황에 맞는 Event를 만들어 다시 넣어주는 과정을 통해 소켓의 reading/ writing buffer 제어를 해준다.

그림 3) RealTime Signal I/O의 구현

  결론적으로 RTSignal 방식을 사용함으로써 기존 방식에 비해, 시스템 자원을 효율적인 활용 할 수 있고, event dispatch방식으로 threaded application에 적합한 구조로 쓰여질 수 있다. 예상되는 문제점으로는 threaded application이 갖는 성능 공유 객체에 대한 thread간 경쟁에 대한 성능 배분을 어떻게 최적화하느냐에 따라 성능이 크게 좌우될 가능성이 높다.


3. 구현과정

 

  앞서 설명한 RT signal을 이용한 소켓 서버의 개선 점을 토대로 polling 방식과 RealTime Signal을 이용한 I/O 부분을 각각 구현하여 기본 소켓 서버를 구현한다. 게임 서버의 기본이 되는 게임 객체 처리와 Packet 분석 및 처리 과정을 통한 게임 서버 엔진을 만들고 엔진 성능 테스트를 위한 더미 클라이언트를 만든다.

 

개발 환경

 

Server

Client

비 고

OS

Linux

Kernel 2.4.19

Microsoft Windows

 

Compiler

Gnu c/c++ 2.95.2

Borland C++ Builder 5

 

 

 Server 의 설정

 Server에서 linux kernel은 2002년 11월 25일 현재 최신 안정 버전인 2.4.19를 사용하였다.[liinuxknldn] 대부분의 network server는 socket을 기반으로 하고 있으며, kernel에서 설정하는 socket 개수은 file descriptor의 한계에 따라 제한된다. 따라서 이에 대한 kernel 설정 작업은 필수적이다. 또한 Realtime signal을 사용하기 위해서는 Realtime signal per File descriptor 에 대한 패치를 해야한다. Kernel의 설정과 함께 /dev/epoll 에 대한 세팅도 할 수 있으며 참고적으로 추가했다.

 

 

내 용

Max socket 설정

에 대한

Kernel 수정

 

include/linux/fs.h:

  increase NR_FILE from 4096 to 65536 

  increase NR_RESERVED_FILES from 10 to 128

 

fs/inode.c:

 increase MAX_INODE from 16384 to 262144 

참고: MAX_INODE는 NR_FILE의 3배정도 (2.4.19에서 max없음)

 

Include/linux/limits.h

  NR_OPEN->15000

Rtsignal per fd

Luban ‘s patch 적용 [rtsigdn]

dev/epoll

Eventpoll 적용 [epolldn]

Realtime Signal과 더불어 eventpoll도 함께 패치를

적용하고 아래와 같이 char 장치를 하나 만든다.

mknod /dev/epoll c 10 124

Sysctl 설정

echo 20 > /proc/sys/net/ipv4/tcp_fin_timeout

echo 8192 >  /proc/sys/net/ipv4/tcp_max_syn_backlog

echo 1200 > /proc/sys/net/ipv4/tcp_keepalive_time

echo 1 > /proc/sys/net/ipv4/tcp_syncookies

Shell 설정

Shell에서 설정한 socket max에 대해 재설정을 해주어야한다

$ ulimit –n 15000

 

 

  프로젝트는 크게 Server Engine과 ClientEngine 부분으로 나눌 수 있다. (표2) 참조.) 프로젝트의 이름은 물고기 등을 몰면서 논다는 의미의 몰이와 놀이의 합성어로 MoriNori(몰이놀이)로 지었다.

 

2) 프로젝트 모듈 설명

프로젝트 폴더

내용

파일 명

ServerEngine

자료구조 및 Packet 정보 등 의 공유 라이브러리

Queue* , DLinkList*, BST*

MAP은 n x n 2차원 좌표계 구조로 지원

Map*

Packet 처리부

PacketParser*

소켓과 소켓서버에 대한

라이브러리

Socket*

SocketServer*

Network*

게임 기본 객체의 처리

ServerBasic*

ClientEngine

서버와 네트웍 쪽의 기본라이브러리는 공유하며 더미 접속클라이언트 기능과 객체 동기 테스트 기능

*

 

 그림 3)의 Realtime signal의 I/O를 Network 라이브러리에 구현을 한 상태로, 사용자 접속을 받아 소켓 버퍼를 제어하게 되면, 그 다음으로는 Game에서 쓰이는 Packet 형태로 가공을 하게 되는데, 그림 4)에서 Network Module에서 Input Packet과 Output Packet의 형태로 CPacket class를 통해 게임으로 들어오고 나가는 Packet이 관리된다. 이 때, 사용자로부터 입력된 Packet을 실제로 소비하는 부분이 Packet Parser이며 Game Object Loop에서 다시 사용자에게 보내지는 Output Packet을 발생시킨다. Packet Parser를 통해 유입된 Packet은 Game Object Loop를 통해 PC(Playerable Character, 사용자가 조종하는 객체) 객체의 행동을 담당하게 되며, 컴퓨터 인공지능으로 PC가 아닌 다른 MONSTER 객체를 처리하게 된다. 또한 Game Global Loop에서 게임내에서의 날씨 변화 등에 대해 처리하며, Output Packet에 영향을 미친다. 여기까지 2가지 Game Loop가 등장하는데 이 것에는 tick이라는 단위가 도입되는데, 이 tick은 한 Loop당 1/10 sec 와 같은 현실적인 Loop 수행에 관련된 응답 시간이 요구된다. 이 tick을 통해 게임내 객체는 tick단위의 동기화가 일어난다.

그림 4) 게임 서버의 구조

  게임 클라이언트의 구조는 게임 서버의 구조와 어느 정도는 일치하지만, 대용량의 사용자를 처리하는 개념이 아닌 나 자신이 조종하는 PC 객체의 시야 범위 내에서의 객체 이동에만 관심이 있으므로 네트웍 부분과 Packet 처리에 있어서는 간소화 된다.

 

그림 5) 게임 클라이언트의 구조

 

서버 엔진의 구현

 

[기본 라이브러리]

  프로젝트 초기에 기본적인 자료구조와 쓰레드 관련 함수들은 c++기반으로 작성하였다. 기본적으로 class기반의 SGI에서 나온 standard template library(stl)에서 자료구조를 제공하고 있지만 코드의 간명함과 threaded application에서의 저수준에서의 최적화, 실행 바이너리 사이즈를 줄일 수 있는 장점이 있어 대부분을 직접 구현 하였다. Thread에 관련된 라이브러리는 기존의 c thread library로 정의되어 있는 pthread_* 부분을 class로 감싸주었다.[pthread]

 

클래스 이름

설명

비고

CMutex

객체 상호배제 lock/unlock

 

CRWLock

Read/Write Lock

 

CCondition

Condition 동기

 

CThread

Posix thread의 create

Join, schedule 등을 감싼

Thread 라이브러리

 

CThreadWrapper

Thread의 startup 동기 지원

CThread를 감쌌다.

CQueue

Locked Queue

CMutex

Condition

CQueueLockedPBuffer

Multiple pop queue를

지원한 Locked Queue

기존 CQueue에 array로

여러개를 pop

CQueueBuffer

BYTE Buffer처리를 위한

Queue

Socket Buffer로 사용

CDLinkedList

기본 적인 Locked List

 

BST

Locked Binary Searching Tree

 

 

 

[네트웍 엔진의 구현]

  게임 서버에서의 네트웍 엔진은 일반적인 네트웍 서버의 형태를 따르며, 몇가지 측면에서 성능 개선 위한 방식을 사용한다. 기본적으로 본 프로젝트에서는 비교하고자하는 polling방식과 RTSignal 방식의 큰 차이가 있고, 의사코드에서 보는 바와 같이 client별로 socket을 Add/Delete해주는 Do_AddSocket(), Do_CloseSocket()과 buffering을 해주는 Do_Read(), Do_Write와 같은 부분은 서로 공유하게된다. socket을 accept하는 부분의 부하를 줄이기 위해 하나의 독립된 thread로 처리하였다.

 

 accept thread의 의사코드(pseudo-code)

while(1)

{

    s = accept ( serversocket)

    if (is_valid_socket(s) ) {

        Do_AddSocket(s);

    }

}

 

 Polling 방식은 관심이 있는 Socket 정보를 서버에서 처리할 수 있는 MAXSOCKET의 FD list에 FD_SET 명령을 사용하여 세팅하고 select() 시스템호출을 통해 넘겨받은 결과 개수에 따라 다시 MAXSOCKET 만큼을 반복하면서 FD_ISSET으로 어떤 종류의 event인지 검사하는 구조다.

 

 select polling의 의사코드

while(1)

{

     // socket fdset recompute

FD_ZERO(INPUTFD); FD_ZERO(OUTPUTFD); FD_ZERO(EXCEPTFD)

    For ( s=0 ;s<= MAXSOCKET;s++) {

         If (is_valid_client_socket(s) )  {

             FD_SET(s,INPUTFD);

FD_SET(s,OUTPUTFD);

FD_SET(s,EXCEPTFD);

         }

    }

    resultcount = select (MAXSOCKET+1, INPUTFD, OUTPUTFD, EXCEPTFD, TIMEOUT{sec=0,usec=1000});

 

For ( s=0 ;s<= MAXSOCKET && resultcount>0 ;s++) {

         If (is_valid_client_socket(s) )  {

             If (FD_ISSET(s,INPUTFD)) {

resultcount --;

                  Do_read(s); // 실패시 socket 삭제

             }

If (FD_ISSET(s,OUTPUTFD)) {

resultcount --;

                  Do_write(s); // 실패시 socket 삭제

             }

If (FD_ISSET(s,EXCEPTFD)) {

resultcount --;

                  Do_ReadOOB(s); // Out of Bound 긴급 패킷(거의 쓰이지 않음)

             }

     } // for

} // while

 

 RTSignal은 socket의 생성과 소멸시에 추가적인 작업이 필요하며 Signal 처리 thread에서Socket IO Event Dispatch에서 사용될 POLLIN/POLLOUT/POLLREMOVE에 해당하는 IOEvent를 생성하며, 게임 루프에서 클라이언트로 패킷을 만들어 내거나 강제로 삭제해야하는 경우 그때 마다 POLLOUT/POLLREMOVE IOEvent를 생성하게된다. 이렇게 생성된 IOEvent는 Event Dispatch Thread 의사코드와 같이 이를 통해 소비된다. 모든 IO Event Queue에서 pop 처리는 CQueueLockedPBuffer를 사용하며 array로 query하여 한꺼번에 처리해여 함수 호출에 대한 overhead를 줄이도록 하였다.

 

 RTSignal에서의 Do_AddSocket 호출시에 추가해야하는 정보

fcntl(SOCKETFD, F_SETFL, O_RDWR | O_NONBLOCK | O_ASYNC);
fcntl(SOCKETFD, F_SETSIG, SIGRTMIN);
fcntl(SOCKETFD, F_SETOWN, getpid()); // pid sigwaitinfo 일어나는 thread pid

fcntl(SOCKETFD, F_SETAUXFL, O_ONESIGFD);

 

RTSignal에서의 Do_CloseSocket 호출시에 추가해야하는 정보

fcntl(SOCKETFD,F_SETFL,O_RDWR);

 

RTSignal에서의 Signal 처리 Thread 의사코드

While (1) {

sigemptyset(&signalset);

    sigaddset(&signalset,SIGRTMIN);

    pthread_sigmask(SIG_BLOCK, &signalset, NULL); // pthread signal block 무시하기

signum=sigwaitinfo(&signalset,&siginfo);

if (signum==SIGRTMIN) {

     SOCKETFD=siginfo.si_fd;

     revents=siginfo.si_band;

     si_code=siginfo.si_code;

if (si_code==POLL_IN) {

       if (revents & (POLLIN)) {

            insertIOEvent(SOCKETFD,RTSIG_EVENT_POLLIN)

       }

     else if (si_code==POLL_OUT) {

if (revents & (POLLOUT)) {

            insertIOEvent(SOCKETFD,RTSIG_EVENT_POLLOUT)

       }

     }

     else {

insertIOEvent(SOCKETFD,RTSIG_EVENT_POLLREMOVE)

     }

}

} // while

 

RTSignal에서의 IO Event Dispatch Thread 의사코드

While (1) {

IOEvent = POP_IOEVENT_FROM_QUEUE();

    If (IOEvent.event == RTSIG_EVENT_POLLIN) {

        Do_Read(IOEvent.SOCKETFD);

    }

   else if (IOEvent.event==RTSIG_EVENT_POLLOUT) {

       Do_Write(IOEvent.SOCKETFD);

   } else {

       Do_CloseSocket(IOEvent.SOCKETFD);

   }

} // while

 

 

[서버의 객체 처리]

 빠른 응답 속도를 위해 UserID를 기반으로하는 BST(Binary Searching Tree)와 SocketID와 UserID에 대응하는 array를 통해 searching 최적화를 시도하였다. 2진 트리로 구성하면 검색 시간이 2분 검색과 같은 0(log2n)이 되고 자료의 집합이 유동적일 때 행렬이나 리스트를 이용하는 것보다 효율적이므로 사용자의 접속이 순차적인 순서가 아닌 유동적인 접속이 이루어지므로 사용자 객체의 목록 관리에 BST를 사용하였다.[ds]

 사용자의 객체를 찾기 위해서는 다음과 같은 과정을 거친다

 

순서

내용

속도

1

SocketID로 UserID를 찾음

배열검색(O(1))

2

UserID의 객체 검색

BST검색(O(log2n))

 

 

[패킷의 정의와 처리]

  여기서 정의한 게임 패킷은 3 byte의 Header part와 가변 길이의 Data part로 구성되며 structure 기반의 Packet 정의와 메모리 복사를 통한 빠른 Packet 생성을 고려하여 작성되었다. 서버상에서의 패킷은 CPacket 클래스를 통해 socket 정보를 포함한 Packet 정보를 사용한다.

 

패킷의 정의

Header

Data

CMD(1B)

LENGTH(2B)

BODY (Header LENGTH BYTE)

 

 

Packet 처리 Thread의 의사코드

While (1) {

PacketArray = POP_PACKET_FROM_INPUTQUEUE();

For (PacketArray loop) {

    Packet = PacketArray(NowIndex)

    Switch (GETPKTNO(Packet)) {

        Case ADD: processAddPacket(); break;

Case DEL: processDelPacket(); break;

Case MOVE: processMovePacket(); break;

Case CHAT : procesChatPsacket(); break;

        }

} // for

} // while

 

 

[맵에서의 게임 객체 동기화 처리]

 게임 객체는 게임상에서 자신의 등장을 주변에 알릴 필요가 있으며, 움직임이나 채팅 메시지와 더불어 다른 객체들과 동기를 맞출 필요가 있다. 일반적으로 게임내에서의 객체 동기는 시야범위 내로 한정하며 여기서는 240x240 평면을 하나의 시야범위로 처리하였다. 실제적으로 동기의 범위가 아래 그림과 같이 4x4 평면에서 검은색 좌표를 중심으로 1의 거리 만큼을 가진다면, 동기되어야할 객체는 맵 상에서 자신의 영역인 0을 포함한 9개의 블럭내에 있는 객체와의 동기가 이루어져야 한다.

 

2

2

2

2

1

1

1

2

1

0

1

2

1

1

1

2

 

 서버에서의 메시지 패킷의 전달 체계를 2가지로 나누어 놓았는데, sendSinglecast와 sendMulticast의 함수로 MessageManager에 포함되어 있다. sendSinglecast는 메시지를 받는 객체가 하나인 경우이고 sendMulticast인 경우는 시야범위내 동기 객체가 있을 경우에 사용하게 된다. 위의 시야 범위 동기를 예로 든다면 맵 상에서 영역 내에 등록된 객체의 목록(보통 UserID의 List)을 가져와 sendMulticast에서 이 목록을 참조해 정보 전송을 통해 객체 동기화를 한다. 현재는 전체 맵을 대상으로 시야 범위 동기하도록 구현되어 있다.

 

[서버의 실행]

  ServerProject를 빌드하면 serverengine 이라는 binary 파일이 생성된다. 서버의 실행

방법은 다음과 같다.

 

testclient# ./serverengine

MoriNori Game Server Engine : ver. 2002

 

Usage : serverengine svrtype [maxsocket]

  svrtype

    1 : SELECT

    2 : RTSIG

    3 : DEV/EPOLL (test in progress)

 

  maxsocket default value is 1024

 

testclient# ./serverengine 1

  (SELECT모드의 서버로 동작)

 

 

더미 클라이언트의 구현

 더미 클라이언트는 스트레스 테스트 클라이언트와 게임 클라이언트의 두 가지 기능을 지원하는 프로그램으로 구성하였다. 클라이언트를 빌드하면 MoriNoriClient.exe 파일이 생성되는데 Windows 운영체제에서 이를 실행하면 더미 클라이언트 윈도가 나타난다.

 

[스트레스 테스트 클라이언트]

  스트레스 테스트 클라이언트는 다수의 접속된 클라이언트를 다루기 위해 select를 이용하였다. select의 동작 방법은 이미 설명한 바 있으며, 여기서는 Packet의 송수신 속도를 측정하기 위해, 패킷을 보낼 때, 시간 태그를 설정하고 그에 대한 응답을 받았을 때 Packet 송수신 시간을 측정 한 후 계산하도록 하였다. Packet의 송신 량은 아래 그림과 같이 SendTick(ms)에서 기본값 1000ms(1초)을 주도록 하였고 테스트시에 1ms에서 2000ms 사이의 값을 넣어 주게 하였다. 이 와 더불어 select의 polling timeout값을 조절해주어야하는데, send tick보다는 낮은 값을 대입해야한다. 적절한 테스트 성능을 내기 위해서는 select timeout을 sec/usec 설정을 0/1000으로 설정한다.

 

그림 6) 스트레스 테스트 클라이언트

 

[객체 동기 테스트 클라이언트]

 본 프로젝트의 게임 서버 엔진으로서의 기능을 테스트하기 위한 엔진 테스트용 클라이언트를 만들었다. 기본적인 객체의 추가 삭제와 시야범위 내 이동 테스트를 통한 객체 동기와 채팅 메시지를 주고 받는 기능을 갖고 있다. 그림 5)의 클라이언트 엔진의 구조를 토대로 구현하였다. 게임 클라이언트에서 사용된 Socket은 blocking mode로 packet reading을 위한 read socket thread 를 두어 Packet Parser와 합친 상태로 서버로부터의 packet 송수신을 처리하게 하였고 검은 색 바탕을 가진 게임 Stage는 1초에 한번씩 화면을 갱신하도록 하였다.

 

 그림 7)은 2개의 Client를 띄워서 객체 동기를 테스트 한 예다.

그림 7) 객체 동기 테스트 클라이언트

 

4. 결론 검토

 지금까지 polling방식과 RTSignal 방식을 지원하는 네트웍 엔진을 구현하고 그것을 토대로 게임 서버의 엔진 구현과 더미 테스트 클라이언트를 작성하였다. 그리고 더미 테스트 클라이언트의 스트레스 테스트 기능을 이용한 네트웍 엔진의 성능 평가는 충분치 않아 보류하였다.

 

 네트웍 엔진으로서의 관점에서 볼 때 Polling방식은 RTSignal방식에 비해 시스템 자원의 대부분을 소비하고 있었으며, 1CPU 서버 시스템에서는 이 프로세스에 의해 거의 100%에 가까운 사용량을 보이며 다른 처리 Thread의 task 수행 시간이 현저히 줄어들어 어느정도 한계가 있음을 확인 할 수 있다. RTSignal의 경우는 단위 시간당 클라이언트의 접속에도 CPU 사용률에는 변화가 없었으며, 부하 클라이언트의 접속 후, 패킷의 송수신 부하 시에 5Mbps수준에서 25%정도의 CPU사용률을 보였으며 접속자 수와 송수신 부하량에 따라 서서히 증가함을 볼 수 있다. 게임 서버 엔진의 관점에서 보더라도 객체의 처리에서 오는 게임 프로세스상의 부하 점이 존재하기 때문에 SMP시스템이라면 성능이 어느정도 보장되지만 UP(UniProcessor)에서는 그 성능을 보장하기가 힘들다는 결론이 나온다. 따라서 RTSignal이 더 구현상으로는 유리하다고 볼 수 있다. Event 처리에 관련된 부분을 적용해본 결과 이 것에 적용한 Event Dispatching 구조는 특히 Multi threaded application의 thread간 메시지 전달 구조에 알맞다는 것을 알 수 있었다. 물론 Multi thread 구조가 Single thread 구조에 비해 더 나은 것은 테스크의 효율적인 할당과 여러 개의 CPU에 대한 스케쥴링의 효율성 때문이다. 하지만 객체를 공유하기 위해 공유 객체에 대한 locking 객체의 경쟁과 이벤트 발생에 드는 메모리 생성 속도 최적화에 따르는 설계의 어려움이 따르기 때문에 성능 비교에 있어 객관성을 떨어트릴 수 있는 부분이라고 생각한다.

 

  테스트 시에 발견한 RTSignal의 문제점은 아직 버그가 존재하기 때문에 안정성에서 기존 방식에 비해 부족함을 확인하였다. Socket 객체에 대한 Signal을 등록하는 시점과 등록된 시점 사이에 일어나는 event를 감지하지 못하기 때문에 그 사이에 들어온 이벤트는 무시된다. 이는 accept()에 관련된 새로운 시스템 호출을 도입하는 것으로 해결이 가능할 것 같다. 그리고 socket close시에 그에 대한 event가 종종 무시되어 CLOSE_WAIT으로 무한 대기를 하게 되는 경우도 있다. 이 경우에 대해서는 현재는 주기적인 socket의 유효성을 검사하는 코드를 추가하여 해결할 수 있지만 추후 개선이 필요한 부분이다.

 

 그럼에도 불구하고 고전적인 polling 방식을 벗어나 RTSignal과 같은 새로운 방식이 이미 다른 OS에서도 지원되거나 요구되고 있다. Linux에서 지원하는 RTSignal이 안정성이 확보된다면 자원의 효율적인 사용은 하드웨어의 비용적인 측면과 Multi threaded application에 적합한 구조의 개발 측면에서도 부합한다는 결론을 내렸다.

 

  이번 구현을 통해 네트웍 엔진에 대한 성능 평가와 게임 서버 엔진으로서의 기본적인 토대를 마련할 수 있었다. 네트웍 엔진에 대한 성능 평가에 초점을 두었기 때문에 실제적으로 중요한 게임 객체 동기화에 따르는 수행속도 최적화와 그에 대한 부하 조절 부분에 대한 구현과 성능 평가는 좀 더 고민해야할 과제다.

 


5. 참고 문헌

 

[iocp] Writing Windows NT Server Applications in MFC Using I/O Completion Ports

    http://chonga.pe.kr/computer/programming/ms/windows_iocp.html

 

[devpoll1] A scalable and explicit event delivery mechanism for UNIX

    Gaurav Banga gaurav@netapp.com

    Network Appliance Inc., 2770 San Tomas Expressway, Santa Clara, CA 95051

    Jeffrey C. Mogul mogul@pa.dec.com

    Compaq Computer Corp. Western Research Lab., 250 University Ave., Palo Alto, CA,

94301

    Peter Druschel druschel@cs.rice.edu

    Department of Computer Science, Rice University, Houston, TX, 77005

 

[rtsig1] CITI Technical Report 00-4 : Scalable Network I/O in Linux

    Niels Provos, University of Michigan <provos@citi.umich.edu>

    Chuck Lever, Sun-Netscape Alliance <chuckl@netscape.com>

 

[kqueue] Kqueue: A generic and scalable event notification facility

    Jonathan Lemon jlemon@FreeBSD.org FreeBSD Project

    http://people.freebsd.org/~jlemon/papers/kqueue.pdf

 

[rtsig2] Analysis of RealTime Signals for Highly Concurrent Network I/O

    http://squid.visolve.com/developments/squid_rt_whitepaper.htm

 

[c10k] Client 10000 project

    http://www.kegel.com/c10k.html

 

[rtsig3] Scalability of Linux Event-Dispatch Mechanisms

    http://www.hpl.hp.com/techreports/2000/HPL-2000-174.html

 

[phhttpd] zach brown's phhttpd using rtsignal

    http://www.zabbo.net/phhttpd/

 

[devpoll2] Microbenchmark comparing poll, kqueue, and /dev/poll - 24 Oct 2000

    http://www.kegel.com/dkftpbench/Poller_bench.html

 

[ds]

  Functionmental of Datastructure in C++ , HOROWITZ 외, Computer Science Press.

 

[pthread]

  David R. Butenhof, Programming with POSIX Threads. Addison-Wesley, ISBN 0-201-63392-2.

 

[linuxknldn]

  linux kernel 2.4.19 download

http://www.kernel.org/pub/linux/kernel/v2.4/linux-2.4.19.tar.gz

 

[epolldn]

  http://www.xmailserver.org/linux-patches/nio-improve.html#patches

 

[rtsigdn]

  http://squid.visolve.com/developments/one-sig-perfd-2.4.x.patch