Thread 예제

QueryPerformanceCounter를 사용한 Thread 예제

#include <stdlib.h>
#include <stdio.h>
#include <Windows.h>
#define UPDATE_RATE 50 //thread가 50Hz 단위로 작업한다
//update sampling time을 측정하기 위한 변수
LARGE_INTEGER frq;
LARGE_INTEGER currentTick;
LARGE_INTEGER lastTick;

DWORD WINAPI ThreadFunction(LPVOID pvoid) ;
HANDLE threadID;

void main()
{
      //고상도 타이머를 지원하는지 여부 확인
      QueryPerformanceFrequency(&frq);
      char  buf[100] ;

       // 1 부터 상당히 큰수까지 더하는 쓰레드 생성
       threadID = CreateThread( NULL , 0 , ThreadFunction , NULL , 0 , NULL ) ;
      //Thread의 우선순위 선택
      //SetThreadPriority(thread1, THREAD_PRIORITY_HIGHEST);
      //SetThreadPriority(thread1, THREAD_PRIORITY_ABOVE_NORMAL);
      printf( "process Started\n" ) ;

       // 프로그램이 종료해버리면 쓰레드도 강제 종료 된다. 
       // 따라서 쓰레드가 완료 할때까지 기다리게 한다.
       gets(buf) ;

      //thread를 닫는다
      CloseHandle(threadID);
}

// 쓰레드 함수
DWORD WINAPI ThreadFunction(LPVOID pvoid)
{
      double elapsed = 0;
      while(true){
             QueryPerformanceCounter(&currentTick);
             elapsed = (double)(currentTick.QuadPart-              lastTick.QuadPart)/frq.QuadPart*1000; //milisecond 단위로 지난 시간이 나온다
            if(elapsed >= 1000.0 / UPDATE_RATE){
                    //printf("%.1f\n", 1000 / elapsed);                  
                   //작업할 내용을 삽입
                   //함수를 부르거나, 아니면 뭐 알아서..
                   lastTick = currentTick;
             }
      }
      return 0;
}



CreateThread에 들어가는 parameter는 아래 참조.
HANDLE WINAPI CreateThread(  
LPSECURITY_ATTRIBUTES lpThreadAttributes,
SIZE_T dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId);

Parameters

lpThreadAttributes
[in] A pointer to a SECURITY_ATTRIBUTES structure that determines whether the returned handle can be inherited by child processes. If lpThreadAttributes is NULL, the handle cannot be inherited.

The lpSecurityDescriptor member of the structure specifies a security descriptor for the new thread. If lpThreadAttributes is NULL, the thread gets a default security descriptor. The ACLs in the default security descriptor for a thread come from the primary token of the creator. Windows XP/2000/NT:  The ACLs in the default security descriptor for a thread come from the primary or impersonation token of the creator. This behavior changed with Windows XP SP2 and Windows Server 2003.

dwStackSize
[in] The initial size of the stack, in bytes. The system rounds this value to the nearest page. If this parameter is zero, the new thread uses the default size for the executable. For more information, see Thread Stack Size.
lpStartAddress
[in] A pointer to the application-defined function to be executed by the thread and represents the starting address of the thread. For more information on the thread function, see ThreadProc.
lpParameter
[in] A pointer to a variable to be passed to the thread.
dwCreationFlags
[in] The flags that control the creation of the thread. If the CREATE_SUSPENDED flag is specified, the thread is created in a suspended state, and will not run until the ResumeThread function is called. If this value is zero, the thread runs immediately after creation.

If the STACK_SIZE_PARAM_IS_A_RESERVATION flag is specified, the dwStackSize parameter specifies the initial reserve size of the stack. Otherwise, dwStackSize specifies the commit size.

Windows 2000/NT and Windows Me/98/95:  The STACK_SIZE_PARAM_IS_A_RESERVATION flag is not supported.
lpThreadId
[out] A pointer to a variable that receives the thread identifier. If this parameter is NULL, the thread identifier is not returned. Windows Me/98/95:  This parameter may not be NULL.

Return Value

If the function succeeds, the return value is a handle to the new thread.

If the function fails, the return value is NULL. To get extended error information, call GetLastError.

Note that CreateThread may succeed even if lpStartAddress points to data, code, or is not accessible. If the start address is invalid when the thread runs, an exception occurs, and the thread terminates. Thread termination due to a invalid start address is handled as an error exit for the thread's process. This behavior is similar to the asynchronous nature of CreateProcess, where the process is created even if it refers to invalid or missing dynamic-link libraries (DLLs).

Windows Me/98/95:  CreateThread succeeds only when it is called in the context of a 32-bit program. A 32-bit DLL cannot create an additional thread when that DLL is being called by a 16-bit program.

아쉽게도 한글판은 없는 듯.. 시간이 나면 번역을 해보자.

아래는 MSDN에서 찾은 몇 가지 팁들

개발 환경 내에서 BOUNCE.C 다중 스레드 프로그램을 컴파일하고 링크하려면

  1. 파일 메뉴에서 새로 만들기를 클릭한 다음 프로젝트 탭을 클릭합니다.
  2. 프로젝트 탭에서 콘솔 응용 프로그램을 클릭하고 프로젝트 이름을 지정합니다.
  3. C 소스 코드를 포함하는 파일을 프로젝트에 추가합니다.
  4. 보기 메뉴에서 속성 페이지를 클릭합니다. 속성 페이지 대화 상자에서 C/C++ 폴더를 클릭하고 코드 생성 페이지를 선택합니다. 런타임 라이브러리 드롭다운 상자에서 다중 스레드를 선택합니다. 확인을 클릭합니다.
  5. 빌드 메뉴에서 빌드 명령을 클릭하여 프로젝트를 빌드합니다.

영문판에서는, Multi-threaded Debug DLL (/MDd) 혹은 Multi-threaded DLL (/MD)에 체크해줘야 한다.

그외 읽어보면 좋은 MSDN 글 들
http://msdn.microsoft.com/library/kor/default.asp?url=/library/KOR/vccore/html/_core_multithreading_with_c_and_win32.asp


그리고 아래는 쓰레드를 이용해서 소켓프로그래밍을 하는 예제이다. 읽어보면 유용할 듯 하다.

0. 들어가는 글

소켓 프로그래밍 관련 책을 여러 권을 봤는데도, 간단한 게임 서버 하나조차 제대로 만들기가 힘들었다. 많은 예제를 봐도 너무 간단한 코드로 이루어져 있어서 실제로 네트워크 프로그래밍에 적용시키기에 부족한 점이 많았다. 하지만 게임 서버는 여러 개의 클라이언트 소켓을 관리해야 되기 때문에 소켓 기본 개념을 확실히 알아야 하며, I/O 모델, 쓰레드 등 필요한 배경 지식이 많다. 따라서 단순히 한 개의 소켓을 관리하는 예제만으로는 게임 서버를 구현하기가 어렵다. 이러한 한계를 극복하고, 본 강좌는 게임 서버를 개발하기 위해 필요한 배경지식을 알아보고, 간단한 게임 서버 예제를 작성해 보기로 한다. 또한 Overlapped I/O 라는 입출력 모델을 적용시켜, 보다 견고한 게임 서버로 확장시켜 본다. 본 강좌를 읽기 전에 필요한 지식은 기본 소켓 API를 이해하고 있는 정도면 되겠다.


1. 게임 서버의 기본 구조

게임 서버는 여러 개의 클라이언트가 한 개의 서버에 관리를 받으면서 필요한 데이터를 전송하거나 수신하는 구조를 가진다. 여러 개의 클라이언트들이 서버에게 데이터를 보냈을 때 자신에게 적절한 응답이 와야 할 것이다. 또한 서버는 적절한 지연시간 내에 클라이언트에게 응답해주어야 할 것이다. 서버에 데이터를 보냈는데 너무 늦게 응답해주거나, 클라이언트가 자신이 보낸 데이터와 전혀 다른 내용의 응답을 받아서는 안될 것이다.

이를 위해서는 접속하는 클라이언트들을 적절히 관리 할 수 있는 I/O 모델과 작업을 적절히 분배 할 수 있는 쓰레드 구조가 필요하다.



2. 게임 서버를 위한 배경지식

게임서버를 만들기 위해서는 어떤 지식들이 필요한지 알아보자.

본 강좌에서는 구현할 게임 서버는 윈속2를 사용한다. 따라서 윈속을 사용할 줄 알아야 한다. 윈속에 있는 기본적인 함수들의 사용법과 개념들을 익히면 될 것이다. 윈속에서는 유닉스 소켓에서 사용되는 대부분의 함수들을 사용할 수 있다. 따라서 유닉스 소켓을 사용할 줄 알면 윈속을 사용하는데 큰 무리가 없을 것이다. 하지만 윈속에는 확장된 개념들과 윈도우 개발 환경에 최적화된 API 들이 추가가 되있기 때문에, 윈도우 환경에 적합한 게임 서버를 개발하고자 한다면 윈속에 대한 공부가 추가로 필요하다.

여러 개의 클라이언트 소켓을 관리하고자 한다면, 각각의 소켓을 관리하는 작업을 할당하는 작업이 필요하다. 이를 구현하기 위해서는 Thread를 알아야 한다. Thread에 대한 개념과 Thread 사용법을 알면 되겠다. 사실 Thread에 대한 내용은 실제 코드에서 2~3줄 정도의 비중 정도 밖에 차지하지 않는다. 하지만 해당 Thread 코드로 인해 프로그램이 어떻게 동작하게 되는지에 대한 원리정도는 파악하고 있는게 좋다.

추가적으로 게임 서버를 좀더 견고하게 개발하기 위해 Overlapped I/O 모델을 적용시킨다. 이 입출력 모델은 게임 서버의 성능을 좀더 향상시킬 수 있다. Overlapped I/O 라는 것이 어떠한 개념인지 알아보고, 네트워크 프로그래밍에서 적용되는 방법에 대하여 알아본다.


3. 소켓의 기본 개념

소켓의 기본적인 개념을 살펴보자. (각 함수를 사용하는 방법은 설명을 생략한다.) 각각의 소켓함수의 기본적인 역할은 꼭 알고 넘어가자. 함수 사용법은 도움말을 찾아보면 쉽게 알 수 있지만 개념을 이해하는 것은 쉽게 알 수 없다고 생각한다. 이 기회에 소켓에 대한 기본 개념을 제대로 이해하도록 하자.

우선 비유를 하나 들기로 한다. 전화를 예로 들겠다. 내가 친구한테 전화를 건다고 하자. 그럼 우선 전화기를 든다. 그러면 전화기에서 친구 목소리가 들리는가? 당연히 아니다. 전화기에서 버튼을 눌러야만 전화가 걸린다. 따라서 다음 단계에서는 전화번호를 누른다. 전화번호를 누르면 바로 친구 목소리가 들리는가? 당연히 아니다. 상대편이 전화를 받아야 목소리가 들리는 것이다. 그 이후에야 서로 말을 주고받는다. 이 시점부터 비로소 상대방과 커뮤니케이션이 이루어지는 것이다.

이 예를 든 이유는, 이것은 네트워크가 구현되는 가장 통상적인 방법이기 때문이다. 전화기를 드는 것은 네트워크를 시작하기 위한 초기화 과정이라고 생각 할 수 있다. 전화기를 들면 뚜~ 하는 소리와 함께 초기화 된다. 이와 같이 컴퓨터 네트워크도 통신하기 전에 초기화를 해준다. (소켓을 생성한다.) 그리고 전화번호를 누른다. 이것은 어디와 통신 할 것인지 목적지를 정해주는 과정이다. 전화번호를 눌러서 상대방에게 전화를 거는 것처럼, 컴퓨터 네트워크도 목적지를 정해줘야 한다. (전화번호=아이피 주소, 번호 누르기=connect() 함수 실행) 목적지에 전화를 걸었으면 상대방이 수화기를 든다. 이는 내가 정해준 목적지에서 나와 통신하기를 허락하는 과정이다. (목적지에서 accept() 함수 실행) 그리고 서로 말을 주고 받는 것처럼 컴퓨터 네트워크에서는 데이터를 주고 받는다. (send(), receive() 함수 실행)

위와 같은 과정을 거쳐서 컴퓨터 네트워크가 성립된다. 이로서 네크워크가 성립되는 기본적인 과정들을 살펴보았다. 다음은 예제를 보면서 실제 프로그램을 작성해 보자. 각각의 과정이 실제 코드에서 어떻게 구현이 되었는지 자세히 짚어보고 넘어가자.

소켓 프로그램의 예제(클라이언트)



#include <winsock.h>
#include <stdio.h>
#include <stdlib.h>

void main()
{
       WSADATA        wsaData ;              // 윈속 초기화를 위한 구조체
       SOCKET         s ;     // 클라이언트 소켓
       SOCKADDR_IN    ServerAddr ;   // 소켓 초기화를 위한 구조체
       int            port = 8034 ;  // 포트
       char           buf[500] ;             // 버퍼

       // 윈속 초기화 
       WSAStartup( MAKEWORD(2,2) , &wsaData ) ;

       // 소켓 생성
       s = socket( AF_INET , SOCK_STREAM , IPPROTO_TCP ) ;

       // 소켓 설정 

       // 로컬에서 서버가 돌아간다고 가정한다.
       ServerAddr.sin_family = AF_INET ;
       ServerAddr.sin_port = htons(port) ;
       ServerAddr.sin_addr.s_addr = inet_addr( "127.0.0.1" ) ;

       // 서버에 연결
       connect( s , (SOCKADDR *)&ServerAddr , sizeof(ServerAddr) ) ;

       // 키보드 입력을 받는다.
       gets(buf) ;

       // 입력 받은 데이터를 보낸다.
       send( s , buf , strlen(buf) , 0 ) ;

       // 데이터를 받는다.
       recv( s , buf , 500 , 0 ) ;

       // 받은 데이터를 표시한다. 
       printf( "\nReceived Data\n" ) ;
       printf( "%s\n" , buf ) ;
}


소켓 프로그램의 예제(서버)



#include <winsock.h>
#include <stdio.h>
#include <stdlib.h>

void main()
{
       WSADATA        wsaData ;      // 윈속 초기화를 위한 구조체
       SOCKET         ListenSocket ; // 리슨 소켓
       SOCKET         AcceptSocket ; // 클라이언트 소켓
       SOCKADDR_IN    ServerAddr ;   // 소켓 초기화를 위한 구조체
       int            port = 8034 ;  // 포트
       char           buf[500] ;     // 버퍼

       // 윈속 초기화 
       WSAStartup( MAKEWORD(2,2) , &wsaData ) ;

       // 서버 소켓 생성
       ListenSocket = socket( AF_INET , SOCK_STREAM , IPPROTO_TCP ) ;

       // 소켓 주소, 타입 설정
       ServerAddr.sin_family = AF_INET ;
       ServerAddr.sin_port = htons(port) ;
       ServerAddr.sin_addr.s_addr = htonl(INADDR_ANY) ;

       // 바인드
       bind( ListenSocket , (SOCKADDR *)&ServerAddr , sizeof(SOCKADDR_IN) ) ;

       // 리슨
       listen( ListenSocket , 5 ) ;

       // 접속 요청 수락
       AcceptSocket = accept( ListenSocket , NULL , NULL ) ;

       // 데이터 받기
       recv( AcceptSocket , buf , 500 , 0 ) ;

       // 데이터 보내기
       send( AcceptSocket , buf , strlen(buf) , 0 ) ;
}


위의 예제가 컴파일을 성공적으로 수행하기 위해서는 ws2_32.lib를 프로젝트 세팅에 추가시켜 줘야 한다. 결과 확인 방법은 서버를 실행하고 클라이언트를 실행한뒤 클라이언트에서 키입력을 한다. 이 예제에서 구현된 서버는 받은 데이터를 그대로 다시 돌려주는 echo 서버이다.




4. Thread

위의 예제를 실행해 보면 클라이언트가 데이터를 한번만 보내고 종료가 된다. 서버 또한 한 개의 클라이언트와의 접속 이후 종료 된다. 이번 장에서는 실질적인 게임 서버 구현을 위해 서버와 클라이언트를 수정하기로 한다. 클라이언트는 데이터를 계속 보낼 수 있고, 서버는 여러 개의 클라이언트를 관리 하고, 계속 다음 클라이언트의 접속을 받을 수 있게 수정한다.

앞서 만들어 본 서버의 흐름을 살펴보자. 그리고 어떤 부분이 수정이 되어야 하는지 알아본다.



(그림 3 : 게임 서버의 흐름도)


위 흐름도에서 수정 하여야 할 사항은 연결 요청을 수락하는 부분과 데이터를 송수신하는 부분이다. 이 두 부분은 서로 분리되어 있어야 한다. 각각의 부분은 서로 독립된 프로세스로 진행이 되어야 한다는 의미이다. accept() 함수는 계속해서 클라이언트가 접속할 때를 기다리고 있어야 한다. 게임 서버가 이미 접속해 있는 클라이언트에게 데이터를 보내거나 받는 작업과 상관없이 계속해서 요청을 수락하고 있어야 하는 것이다. 또한 accept() 함수가 실행되고 있음과 동시에 send(), recv() 함수는 계속해서 실행이 되고 있어야 한다.

위의 두 작업을 분리하는 것은 Thread를 사용하면 가능하다. Thread는 프로그램에 독립적입 프로세스를 생성할 수 있다. Thread로 생성된 프로세스는 백그라운드로 계속 돌아가게 된다. Thread는 CreateThread() 함수로 생성할 수 있다. CreateThread() 함수는 Thread를 생성함과 동시에 return 한다. 이 의미는 Thread로 생성된 프로세스가 아무리 시간이 많이 걸리는 작업이라해도 바로 다음 줄의 코드로 넘어 갈 수 있음을 의미한다.

Thread 예제



#include <stdlib.h>
#include <stdio.h>
#include <Windows.h>

DWORD WINAPI ThreadFunction(LPVOID pvoid) ;

/*
1 부터 상당히 큰수까지 더하는 예제
*/

void main()
{
       char  buf[100] ;

       // 1 부터 상당히 큰수까지 더하는 쓰레드 생성
       CreateThread( NULL , 0 , ThreadFunction , NULL , 0 , NULL ) ;
       printf( "process Started\n" ) ;

       // 프로그램이 종료해버리면 쓰레드도 강제 종료 된다. 
       // 따라서 쓰레드가 완료 할때까지 기다리게 한다.
       gets(buf) ;
}

// 쓰레드 함수
DWORD WINAPI ThreadFunction(LPVOID pvoid)
{
       int sum = 0 ;
       for ( int i = 0 ; i < 100000000 ; i++ )
       sum += i ;
       printf( "Result : %d\n" , sum ) ;
       return 0 ;
}



위 프로그램을 실행 해보면 CreateThread() 함수가 호출된 뒤 바로 다음줄의 코드가 실행이 되는 것을 볼 수가 있다. (process Started라는 문자열이 출력이 된다.) 그 후 얼마정도의 시간이 지나면 더한 결과를 볼 수 있다. 이 처럼 시간이 많이 소요되는 작업이라해도 쓰레드로 생성하면 해당 작업이 끝나지 않았더라도 다음 작업을 계속 진행 할 수 있다. 쓰레드 함수는 DWORD WINAPI 로 선언이 되어야 하며, LPVOID 형의 인자를 받는 형태로만 작성해야 한다. 쓰레드 함수는 정해진 형태로만 작성해야 된다는 점을 꼭 명심하고 있어야 한다.

Accept를 호출하면 클라이어트와의 연결을 위한 소켓을 리턴하는데 이 소켓을 적절한 작업 쓰레드에 넘겨주고, 다시 accept() 함수를 호출 할 수 있는 구조를 만들어 보자. 이 구조를 Thread를 사용하여 구현한다.

Thread를 이용한 서버



#include <winsock.h>
#include <stdio.h>
#include <stdlib.h>

DWORD WINAPI ThreadFunction(LPVOID pvoid) ;

void main()
{
       WSADATA        wsaData ;      // 윈속 초기화를 위한 구조체
       SOCKET         ListenSocket ; // 리슨 소켓
       SOCKET         AcceptSocket ; // 클라이언트 소켓
       SOCKADDR_IN    ServerAddr ;   // 소켓 초기화를 위한 구조체
       int            port = 8034 ;  // 포트
       char           buf[500] ;     // 버퍼

       // 윈속 초기화 
       WSAStartup( MAKEWORD(2,2) , &wsaData ) ;

       // 서버 소켓 생성
       ListenSocket = socket( AF_INET , SOCK_STREAM , IPPROTO_TCP ) ;

       // 소켓 주소, 타입 설정
       ServerAddr.sin_family = AF_INET ;
       ServerAddr.sin_port = htons(port) ;
       ServerAddr.sin_addr.s_addr = htonl(INADDR_ANY) ;

       // 바인드
       bind( ListenSocket , (SOCKADDR *)&ServerAddr , sizeof(SOCKADDR_IN) ) ;

       // 리슨
       listen( ListenSocket , 5 ) ;

       // 접속 요청을 루프를 돌면서 계속 수행한다.
       // 접속한 클라이언트를 관리하는 부분을 Thread로 넘겨서
       // 계속해서 접속 요청을 수락할 수 있는 구조를 구현한다.
       while( true )
       {
              // 접속 요청 수락
              AcceptSocket = accept( ListenSocket , NULL , NULL ) ;
              printf( "Accept Granted\n" ) ;

              // 클라이언트 관리 작업 쓰레드
              // 글라이언트 소켓을 인자로 넘긴다.
              CreateThread( NULL , 0 , ThreadFunction , (LPVOID)&AcceptSocket , 0 , NULL ) ;
              printf( "Creating a Worker Process\n" ) ;
       }
}

DWORD WINAPI ThreadFunction(LPVOID pvoid)
{
       char     buffer[500] ;

       // 클라이언트 소켓을 넘겨 받는다.
       SOCKET   AcceptSocket = *((SOCKET *)pvoid) ;

       // 데이터를 받으면 다시 그대로 넘겨주는 echo 서버를 만든다.
       while( true )
       {
              // 해당 클라이언트에게서 데이터가 들어올때까지 기다린다.
              if ( recv( AcceptSocket , buffer , 500 , 0 ) == SOCKET_ERROR )
              break ;


              // 데이터를 받으면 바로 다시 보낸다.
              send( AcceptSocket , buffer , strlen(buffer) + 1 , 0 ) ;
              printf( "%s : Data Transferred\n" , buffer ) ;
       }
       printf( "Connection Closed\n" ) ;
       return 0 ;
}






(그림 4 : 쓰레드 서버를 실행한 모습)


위의 예제를 실행하면 서버가 실행이 된다. 앞서 작성했던 예제와 다른점은 서버가 계속해서 클라이언트의 접속을 받을 수 있다는 것이다. 현재의 서버를 실행 시켜놓고, 계속해서 앞서 작성했던 클라이언트 프로그램을 실행 시키면, 서버는 계속해서 쓰레드 프로세스를 생성하며, 클라이언트를 독립적으로 관리 하고 있음을 알 수 있다. 각각의 프로세스는 클라이언트의 소켓을 가지고 있으며, 서로 독립적으로 데이터의 송수신을 관리하고 있는 것이다.


5. Overlapped I/O

쓰레드를 이용해서 구현한 서버는 여러 개의 클라이언트를 관리 할 수 있지만, 데이터의 전송을 처리하고 있을 때는 CPU 점유율이 90~100%로 올라감을 알 수 있다. CPU 자원이 100%까지 올라가게 되면 네트워크 선을 타고 오는 데이터가 시스템에서 처리되지 못할 수도 있음을 의미한다. CPU가 다른 작업을 처리할 여유가 없어지기 때문이다. 이는 전송되고 있는 데이터가 유실될 수 있는 확률이 높아지는 결과를 초래하게 된다. 이러한 결함은 높은 안정성을 필요로 하는 게임 서버에서 치명적이다.

점유율이 100%까지 올라가는 이유는 recv(), send() 함수가 블록킹 방식으로 동작하고 있기 때문이다. recv()는 데이터 수신이 완료하기 전까지는 return 하지 않는다. 이는 send() 함수의 경우에도 마찬가지이다. 이러한 특성 때문에 데이터가 전송이 되고 있는 동안에는 시스템의 자원을 100% 사용하게 되는 것이다. While() 문이 계속해서 돌고 있는 경우와 마찬가지라고 생각하면 된다.

Overlapped I/O 모델은 위와 같은 문제점을 해결할 수 있다. Overlapped I/O는 데이터의 송수신 과정을 커널에게 넘긴다. 그러면 커널은 송수신이 완료될 때까지 작업을 맡아서 하다가, 송수신이 완료되는 시점에 프로세스에게 통지한다. 그럼 프로세스(쓰레드)에서는 통지를 기다리고 있다가, 통지가 오면 다음 작업을 수행하면 된다. 이는 커널과 최소한의 통신(Context Exchange)으로 작업을 수행하기 때문에 시스템의 부하를 현저하게 줄일 수 있다.

Overlapped I/O 모델로 구현한 서버에서는 WSARecv() 함수를 사용하게 된다. 이는 윈속 확장 함수이다. WSARecv()는 호출이 되는 순간 return을 하는 특징을 가지고 있다. 따라서 데이터가 수신이 되고 있는 동안 다른 작업을 진행 할 수 있게 된다. 입출력이 완료되는 시점은 WSAWaitForMultipleEvents() 함수를 호출함으로써 알 수 있다. 이 함수는 해당 소켓에서 입출력 완료 이벤트가 통지되는 순간 알려준다. 입출력 완료 이벤트가 통지가 되는 순간, 어떠한 소켓에서 이벤트가 발생되었는지에 대한 정보를 return 한다. 정리하면, 데이터를 수신할 때, WSARecv() 함수를 호출하고 나서 WSAWaitForMultipleEvents() 함수를 호출하여, 입출력이 완료되는 시점을 알아내고 받은 데이터를 처리한 후, 다시 WSARecv() 함수를 호출하여 다음 데이터를 수신하면 된다.

Overlapped는 중첩이라는 뜻이다. 이는 여러 개의 입출력이 한 개의 프로세스에서 동시에 관리 될 수 있음을 의미한다. 따라서 Overlapped I/O 모델로 구현하면, 한 개의 작업 쓰레드에서 여러 개의 클라이언트 소켓을 관리 할 수 있음을 의미한다. 몇 개의 클라이언트 소켓을 관리 할 것인가는 시스템이 정한 최대치 내에서 개발자가 임의로 조절 할 수 있을 것이다. 하지만 이 MSDN에서는 한 프로세스에서 한 개의 입출력만을 관리 할 것을 권장하고 있다.


6. 게임 서버 구현

앞서 배운 Overlapped I/O 모델을 사용한 게임 서버를 구현해 보자. Overlapped I/O 입출력 모델을 사용할 지에 대한 여부는 소켓을 생성하는 시점에서 결정된다. 소켓을 생성할 때 WSASocket() 함수를 사용하며, 마지막 인자에 WSA_FLAG_OVERLAPPED 옵션을 설정 하면 된다. 이렇게 생성된 소켓은 WSARecv(), WSASend() 함수에 의해서만 데이터 송수신을 할 수 있다. 또한 추가적으로 WSAWaitForMultipleEvents() 라는 함수에 의해서 입출력이 관리가 되며, 이벤트에 발생 여부를 WSAEVENT 구조체 변수에 받으며, WSAOVERLAPPED 구조체에 해당 이벤트의 구체적인 내용이 저장되어 있다.

예제를 살펴보면서 위의 내용들을 확인 해보자. 아래 예제는 쓰레드 서버 예제에서 소켓을 생성하는 부분과 송수신 작업 쓰레드 부분이 변경이 되었다. 또한 winsock2.h 파일을 인클루드 하여야 한다.


Overlapped I/O 모델 게임 서버



#include <winsock2.h>
#include <stdio.h>
#include <stdlib.h>

DWORD WINAPI ThreadFunction(LPVOID pvoid) ;

void main()
{
            WSADATA        wsaData ;        // 윈속 초기화를 위한 구조체
            SOCKET         ListenSocket ;   // 리슨 소켓
            SOCKET         AcceptSocket ;   // 클라이언트 소켓
            SOCKADDR_IN    ServerAddr ;     // 소켓 초기화를 위한 구조체
            int            port = 8034 ;    // 포트

            // 윈속 초기화 
            WSAStartup( MAKEWORD(2,2) , &wsaData ) ;

            // 서버 소켓 생성
            // Overlapped I/O 모델을 사용하는 소켓으로 생성한다.
            ListenSocket = WSASocket( AF_INET , SOCK_STREAM , IPPROTO_TCP , NULL , NULL , WSA_FLAG_OVERLAPPED ) ;

            // 소켓 주소, 타입 설정
            ServerAddr.sin_family = AF_INET ;
            ServerAddr.sin_port = htons(port) ;
            ServerAddr.sin_addr.s_addr = htonl(INADDR_ANY) ;

            // 바인드
            bind( ListenSocket , (SOCKADDR *)&ServerAddr , sizeof(SOCKADDR_IN) ) ;

            // 리슨
            listen( ListenSocket , 5 ) ;

            // 접속 요청을 루프를 돌면서 계속 수행한다.
            // 접속한 클라이언트를 관리하는 부분을 Thread로 넘겨서
            // 계속해서 접속 요청을 수락할 수 있는 구조를 구현한다.
            while( true )
            {
                          // 접속 요청 수락
                          AcceptSocket = accept( ListenSocket , NULL , NULL ) ;
                          printf( "Accept Granted\n" ) ;

                          // 클라이언트 관리 작업 쓰레드
                          // 글라이언트 소켓을 인자로 넘긴다.
                          CreateThread( NULL , 0 , ThreadFunction , (LPVOID)&AcceptSocket , 0 , NULL ) ;
                          printf( "Creating a Worker Process\n" ) ;
            }
}

DWORD WINAPI ThreadFunction(LPVOID pvoid)
{
            char     buffer[500] ;

            // Overlapped I/O 를 사용하면서 필요한 변수들
            WSABUF          DataBuf ;
            WSAOVERLAPPED   Overlapped ;
            WSAEVENT        EventArray[1] ;
            DWORD           Flags = 0 ;
            DWORD           RecvBytes = 0 ;
            DWORD           BytesTransferred ;

            // 클라이언트 소켓을 넘겨 받는다.
            SOCKET         AcceptSocket = *((SOCKET *)pvoid) ;

            // Overlapped I/O 변수들을 초기화 한다.
            EventArray[0] = WSACreateEvent() ;  
            ZeroMemory( &Overlapped , sizeof(WSAOVERLAPPED) ) ;
            Overlapped.hEvent = EventArray[0] ;
            DataBuf.len = 500 ;
            DataBuf.buf = buffer ;        

            // WSARecv() 함수를 실행한다. 실행하자 마자 return 된다.
            // return 값은 언제나 SOCKET_ERROR 이며, 
            // WSAGetLastError() 에서 WSA_IO_PENDING 이라는 에러 메시지인 경우에 에러로 처리한다.
            if ( WSARecv(AcceptSocket , &DataBuf , 1 , &RecvBytes , &Flags , &Overlapped , NULL )
                          == SOCKET_ERROR )
            {
                          if ( WSAGetLastError() != WSA_IO_PENDING )
                          {
                                       fprintf( stderr , "WSARecv failed with Error %d\n" , WSAGetLastError() ) ;
                                       fprintf( stderr , "Failed On AcceptSocket : %d" , AcceptSocket ) ;
                                       return -1 ;
                          }
            }

            // 데이터를 받으면 다시 그대로 넘겨주는 echo 서버를 만든다.

            // WSAWaitForMultipleEvents() 이벤트가 생성 될때를 포작한 후 

            // 받은 데이터를 처리한다.
            while( true )
            {
                          // 이벤트 요청을 기다린다.
                          DWORD Index ;
                          Index = WSAWaitForMultipleEvents( 1 , EventArray , FALSE , WSA_INFINITE , FALSE ) ;

                          // 이벤트 리셋
                          WSAResetEvent( EventArray[Index - WSA_WAIT_EVENT_0 ] ) ;

                          // I/O 상태 확인 
                          WSAGetOverlappedResult(AcceptSocket, &Overlapped , &BytesTransferred , FALSE , &Flags ) ;

                          // 연결이 끊어 졌음
                          if ( BytesTransferred == 0 )
                          {
                                       fprintf( stderr , "Closing Socket : %d\n" , AcceptSocket ) ;
                                       closesocket(AcceptSocket) ;
                                       WSACloseEvent(EventArray[Index - WSA_WAIT_EVENT_0 ]) ;
                                       return -1 ;
                          }

//--------------------------------------------------------------------------------
// 수신 데이터 처리부 
//--------------------------------------------------------------------------------
         // 에코 서버이기 때문에 받은 데이터를 다시 보낸다. 
         send( AcceptSocket , DataBuf.buf , strlen( DataBuf.buf ) + 1 , 0 ) ;
         printf( "Data Transferred : %s\n" , DataBuf.buf ) ;

//--------------------------------------------------------------------------------

                          // Recv 함수 다시 호출
                          Flags = 0 ;
                          ZeroMemory(&Overlapped , sizeof(WSAOVERLAPPED) ) ;
                          Overlapped.hEvent = EventArray[ Index - WSA_WAIT_EVENT_0 ] ;
                          DataBuf.len = 500 ;
                          DataBuf.buf = buffer ;
                          if ( WSARecv(AcceptSocket, &DataBuf , 1 , &RecvBytes , &Flags , &Overlapped , NULL ) == SOCKET_ERROR )
                          {
                                       if ( WSAGetLastError() != WSA_IO_PENDING )
                                       {
                                                    fprintf( stderr , "Recv Error! While Calling Next WSARecv!\n" ) ;
                                                    closesocket(AcceptSocket) ;
                                                    return -1 ;
                                       }
                          }
            }
            printf( "Connection Closed\n" ) ;
            return 0 ;
}





(그림 5 : Overlapped I/O 모델 게임 서버 실행 모습)


7. 결론

게임 서버를 만든다는 목적아래 쓰레드를 사용한 서버, Overlapped I/O 모델을 적용시킨 서버를 예제로 살펴보았다. 사실 실전에서는 IOCP 라는 입출력 모델을 게임 서버로 가장 많이 사용한다. 이 모델은 Overlapped I/O Completion Port의 약자이다. IOCP 모델은 현재 가장 높은 성능을 보여주는 서버 모델이다. 본 강좌에서는 Overlapped I/O Event 모델까지만 예제로 하였는데, 예제를 나름대로 확장하고 수정하면 충분히 IOCP 모델의 게임 서버를 개발 할 수 있을 것이다.

=======================================
출처 : unkyulee.net
=======================================

댓글

Designed by JB FACTORY