IOCP를 이용한 서버 만들기

이미 IOCP가 많이 알려진 상태이지만, 확장하기 쉽고 범용성있는 서버를 만들어 보고자, 정리차원에서 글을 써본다. 그리고 밑에 참고사이트와 자료를 링크해 놓고 각각의 소스를 비교해 보았으니 참조하기 바란다.
(본문의 내용과 관련 소스는 다른 곳에 링크 및 업로드를 삼가해 주시기 바랍니다)

I/O Completion Port ?

IOCP가 무엇인지 더욱 완벽한 설명을 원한다면,  Programming Server-Side Applications for Microsoft Windows 2000이란 책을 추천한다. 그 책에서 자세히 설명되어 있다. 

IOCP는 멀티프로세서 환경을 위해 특별히 설계된 윈도우 파일 입출력 모델중의 하나이다. 그래서 IOCP는 윈도우즈 NT계열(NT, 2000이상)에서만 유용하다는 점을 인식하고 있기 바란다.

##########0*

 개념적으로 일단 IOCP 프로그래밍을 설명한다면 그림에서와 같이 APP(프로그램)가 IOCP로 행할 포트 를 정의한 뒤,(CreateIoCompletionPort함수이용) 그 포트로 I/O (뭔가를 쓴다던지 읽는다던지)하면 그 I/O가 끝날 때까지 프로그램은 기다릴 필요가 없이 리턴되고 OS가 직접 그 I/O를 행하여서 그것이 완료 되면 그 결과값을 가지고 있다가 프로그램이 그 결과값을 물어 볼 때(GetQueuedCompletionStatus or Callback 함수이용) 알려주고 프로그램은 그에 맞은 액션을 취하면 되는 것이다. 왜 이것을 IOCP(I/O 완료 포트 : Input/Output Completion Port)라고 부르는지 이해가 됐는가?

 IOCP 소켓 프로그래밍

 위에서 간단하게 개념적인 것을 설명했지만, 본격적으로 이것을 응용한 윈도우즈 소켓 프로그래밍에 대해서 알아보자. 윈도우에서는 소켓을 화일과 마찬가지로 보기 때문에 소켓 프로그래밍에 IOCP를 사용할 수 있다. 물론 윈도우즈 소켓 프로그래밍에는 IOCP방식이외에 여러가지 방식이 있지만, 서버와 같은 대규모 유저가 접속할 가능성이 있는 프로그램을 작성할 때에는 IOCP방식을 사용해서 프로그래밍하는 것이 좋다고 한다. MS에서...

 먼저 간단하게 IOCP 소켓 프로그래밍 구조를 디자인해 보자.

1. 새로운 IOCP용 포트를 생성한다. (CreateIoCompletionPort함수이용)
2. listen용 소켓을 하나 만들고 만들어 놓은 IOCP용 포트에 추가한다. (CreateIoCompletionPort함수이용)
3
. 적당한 수의 클라이언트용 소켓을 생성하고 accept시켜 놓는다. 접속할 때마다 만들어도 된다. (accept나 AcceptEx이용)
4. 콜백함수나 쓰레드 등을 이용하여 OS의 IOCP결과값을 기다린다 (GetQueuedCompletionStatus or Callback 함수이용)
5. 결과값을 이용하여 행동한다. (ReadFile, WriteFile, WSARecv, WSASend, ...)

 아래의 참고 소스들을 보면 알겠지만은 구조는 사용 용도에 따라 세부적인 부분이라든지, 순서가 조금씩 달라지게 된다. 하지만 전체적인 관점에서 보게 된다면 서로가 위와 같은 비슷한 구조를 취하고 있다는 것을 알게 될 것이다.

 이제 위와 같은 구조를 바탕으로 확장하기 쉬운 범용성있는 서버를 한번 디자인해보자.

서버 제작

 여기서 제작하고자 하는 서버 프로그램은 아래의 참고 소스들에서 조금씩 참조되였다. 그리고 서버를  ZenServer 라고 명명지였다. (참고로 Zen은 도사라는 의미로 사용되기도 한다고 한다. ^^;)

 ZenServer (ver 0.2)의 특징은 다음과 같다.

1. IOCP, 쓰레드, 이벤트를 사용한다.
2. 사용자를 위한 클라이언트용 클래스가 존재하여 객체지향적으로 프로그램을 작성할 수 있다.
3. 확장성이 강하다.
4. 프로그램으로 가동된다.

 우선 초기버전이므로 서버의 골격만 잡아 놓았다고 해도 과언이 아니다. 하지만 확장성이 준비되어 있는 만큼 다음 버전에서는

1. 로비(= 채널 or 방) 이동, 로비 생성
2. 사용자 로긴 가능
3. 서비스로 구현

등이 가능할 것이다. (기능들은 다 준비되었지만, 코딩할 시간이 없어서.. 이번 버전에서는 제외된다.)

 ZenServer의 구조 디자인은 다음과 같다.

  • 이벤트를 하나 생성한다 . (CreateEvent)
     : ZenServer는 쓰레드로 구동되어 질 것이기 때문에 내부적으로 프로그램을 마칠 수 있는 이벤트를 하나 작성하였다.
  • 시스템 컨트롤 핸들러를 재정의한다 . (SetConsoleCtrlHandler)
     : OS에서 유저가 로그오프를 한다던지 Ctrl + C키를 눌렸을 때 ZenServer를 마치기 위해서 재정의한다. 위에서 생성한 이벤트에 메세지를 보내 프로그램을 마치게 한다. (SetEvent)
  • 새로운 IOCP용 포트를 생성한다 . (CreateIoCompletionPort)
     : CreateIoCompletionPort(
        HANDLE hFile,
        HANDLE hExistingPort,
        DWORD dwCompletionKey,
        DWORD dwConcurrentThreads);
     함수에서 hFile이 소켓(화일)을 나타내는데 OS에 새로운 입출력 완료 포트를 생성하려면 hExistingPort에 NULL값을 전달해야 한다. hFile에 INVALID_HANDLE_VALUE를 전달하면 파일에 연결되지 않은 IOCP를 생성할 수 있다. 이미 존재하는 포트에 추가로 소켓을 연결시키려면 이전에 생성한 IOCP값을  전달해야 한다.
  • 작업 쓰레드를 생성한다 (_beginthreadex)
     : 서버에서 OS가 I/O 액션이 완료된 후 결과를 알려주는 부분을 쓰레드로 작성하여 처리한다. ZenServer에서는 쓰레드를 서버가 작동되는 시스템에 따라 적당하게 생성하도록 하고 있다.

    SYSTEM_INFO si;
    GetSystemInfo( &si );
    m_MaxThread = ( int )si.dwNumberOfProcessors * 2 + 2;

     쓰레드를 많이 만든다고 해서 성능이 좋아지는 것이 아니므로 위의 코드를 잘 참조하기 바란다. 그리고 쓰레드를 생성하기 위해 CreateThread함수 대신 _beginthreadex함수를 사용한 이유는 CreateThread함수는 쓰레드에서 Win32함수만을 사용하도록 요구하고 있어서 C 런타임 라이브러리는 사용할 수 없기 때문이다. 이러한 차이점을 알고 있기 바란다.
  • 소켓을 초기화한다 (WSAStartup)
     : ZenServer는 소켓 2.2를 사용한다. 소켓 버전은 다음과 같은 OS에서 유효하다.

    1.1 : Win CE/95/98/NT/2000
    2.0 : Win 95(upgrade)/98/NT/2000
    2.2 : Win 95(upgrade)/98/NT/2000
       
  • listen용 소켓을 만든다
    : listen용 소켓을 하나 만들고 만들어 놓은 IOCP용 포트에 추가한다. (CreateIoCompletionPort)
  • 클라이언트 소켓을 만든다. (AcceptEx)
    : 소켓과 OVERLAPPED데이타를 지닌 클라이언트 클래스를 지정된 클라이언트 수만큼 만들고 OVERLAPPED데이타를 초기화하고, AcceptEx 시켜 놓는다. 여기서 accept함수를 쓰지 않고 AcceptEx를 쓰는 이유는 다음과 같다.
     accept함수는 블록(대기상태)되지만 AcceptEx는 블록되지 않고, 또한 클라이언트가 접속되지 마자 데이타를 보낸다면 그 데이타까지 처리할 수 있는 함수이기 때문이다. AcceptEx함수는 그 인자들에 대해서도 신경을 써야 한다.

    BOOL AcceptEx(
      SOCKET sListenSocket,     
      SOCKET sAcceptSocket,     
      PVOID lpOutputBuffer,     
      DWORD dwReceiveDataLength, 
      DWORD dwLocalAddressLength, 
      DWORD dwRemoteAddressLength, 
      LPDWORD lpdwBytesReceived, 
      LPOVERLAPPED lpOverlapped 
    );

     
    dwReceiveDataLength는 lpOutputBuffer의 크기를 지정하는데, 실질적으로는
    (lpOutputBuffer의 크기 - 2 * (sizeof(SOCKADDR_IN) + 16)) 이어야 한다. 사용자가 접속해서 데이타를 보낼 때 데이타와 함께 lpOutputBuffer의 뒷부분에 어드레스가 붙어서 넘어오기 때문이다. dwReceiveDataLength에 0을 지정하면 사용자는 접속했을 때 데이타를 바로 보낼 수 없고 서버가 먼저 데이타를 보내는 방식으로 처리를 해주어야 한다. 이 부분은 mvps의 소스부분에 자세히 언급되어 있다.
     OVERLAPPED(중첩)데이타는 시스템에서 선언되어 있는 스트럭처를 상속받아 사용자가 필요한 클래스나 구조체로 선언하면 된다.
  • 이벤트를 기다린다. (WaitForSingleObject)
    : 프로그램이 초반부에 선언했던 프로그램을 마칠수 있는 이벤트를 기다린다.
  • 작업 쓰레드에서 OS가 IOCP의 결과를 처리한다. (GetQueuedCompletionStatus)
    : 작업 쓰레드에서 결과를 처리하여 클라이언트 접속, 쓰기, 읽기등을 각각 처리한다.


 위와 같이 ZenServer의 구조는 간단하다(?). 이 부분은 앞으로 업그레이드가 되어도 크게 변경될 여지가 없는 곳이고 앞으로 점차 복잡해질 여지가 있는 부분은 로비부분과 클라이언트 클래스부분이다. 그렇다면 ZenServer에서 사용하고 있는 클라이언트 클래스는 어떻게 설계되었는지 설명하겠다.

클라이언트 클래스 (CClient)

 CNetPrtc
     |
   CIOCPPrtc
       |
      CClientIOCP
         |
        CClientIOCPEx
           |
          CClient

 서버 프로그램에서 접속해오는 사용자를 관리하는 일을 하는 클라이언트 클래스 CClient는 위와 같이 상속되어진 클래스이다. 왜 이렇게 많이 상속받는냐고 물어본다면 확장성때문이라고 필자는 이야기하고 싶다. 그럼 차근 차근 각클래스들이 어떤 할 일(역할이라는 말이 일본식 한자말이라 하더군요. ㅡㅡ;)을 하는지 보도록 하자.

 CNetPrtc는 가장 기본이 되는 네트워크 클래스로 모든 네트워크 통신 클래스의 공통사항이 담겨져 있다. 그래서 멤버에는 기본적인 소켓데이타밖에 존재하지 않는다. 이것을 상속받아 IPX나 TCP/IP, IOCP클래스들이 만들어 진다. (먼 미래를 바라보는 포석이라고 할까? ^^;)
 CIOCPPrtc는 IOCP와 관련된 클래스의 기본이 된다. IOCP관련된 서버 소켓과 IOCP관련된 클라이언트 소켓이 하는 일이 다르므로 우선 기본이 되는 클래스가 필요하게 되어 생성하였다. 
 CClientIOCP는 CIOCPPrtc를 상속받아 클라이언트에서만 사용되는 IOCP관련 클래스이다. 이 클래스에서는 읽기용 OVERLAPPED 데이타와 쓰기용 OVERLAPPED 데이타, 큐가 각각 존재하게 된다. (큐에는 읽어온 데이타나 보내고자하는 데이타를 저장하는 장소로 사용된다. 여기서 데이타를 구분하기 위해 데이타끝에 구분자를 넣는 방식을 사용하고 있는데, 이 방식은 나중에 TCPIP로 클라이언트 프로그램을 만들 때도 역시 유용하다) CClientIOCP는 일반적인 용도에 쓰기에 가장 유용한 클래스이므로 이 클래스를 상속받아 사용자가 자유스럽게 사용해도 된다.
 하지만 필자는 여기서 이것을 상속받은 CClientIOCPEx라는 클래스를 하나 더 만들었는데, 이것은 보다 더 쉽게 클라이언트를 제어할 클래스를 만들고자 생성한 것이다.
 여기서 필자 나름대로 서버와 클라이언트가 통신할 때 사용할 통신 데이타 프로토콜을 정의하였는데(채팅만을 사용할 서버가 아니므로), 그 형식은 다음과 같다.

ID (데이타를 정의하는 ID로 16진수형 두바이트) + 데이타

 지금 현재 정의된 것은  A0 (채팅데이타 명령) 밖에는 없지만, 나중에 추가될 예정이다.(사용할 수 있는 명령은 현재 00 ~ FF까지이다) 어쨌든 CClientIOCPEx에서는 전송받은 데이타를 분석하여 명령을 추출한 뒤 해당 함수, 즉 명령이 A0이라면 CClientIOCPEx에 있는 On0xA0함수를 직접 호출하여 준다. 또한 이러한 명령관련 함수가 모두 가상함수로 선언되어 있기 때문에 CClientIOCPEx를 상속하면 해당명령관련 함수만 재정의하여 사용하면 되므로 필자는 이를 상속한 CClient라는 클래스를 만들게 되었다.

 이제까지 장황하게 설명을 했는데, 잘 이해가 되었는지 모르겠다. 여러분이 소스를 보면 더 이해가 잘 갈지는 모르겠지만, 여기서 설명한 내용이 거의 주가 되므로 잘 이해하기 바란다.

 테스트용 클라이언트 프로그램은 전에 필요에 따라 만들었던 것을 조금 개조하여 사용했다. 클라이언트 프로그램소스에서는 위의 CNetPrtc를 계승한 CTcpPrtc이 있으므로 참조해 보기 바란다. 그럼 다음 버전의 ZenServer를 기약하면서...

ZenServer 0.2 다운로드  

 

참고 자료 :

1. 프로그램세계 (2002/03 노규남 - 윈도우용 IOCP 채팅서버 만들기)

    소스 다운로드

: 가장 간단한 구조의 서버 소스이다. 그래서 IOCP의 기초를 파악하는데 가장 쉬운 편이지만, 소스를 활용할 수는 없는 편이다.  

2. Multithreading Applications in Win32 (서적)

    소스다운로드

: 그 다음으로 간단한 구조의 에코서버 소스이다. 위보다는 조금 더 실용적이라고 할 수 있다.

2.  CodeProject (Writing scalable server applications using IOCP)

    소스 다운로드

: 실용적으로 IOCP 서버를 구성하기할 때 필요한 요소를 작성하였다. 다만, 소스만 나열되어 있어 사용자가 프로젝트를 생성해서 야 된다.

3. MVPS (Sockets, IOCPs, AcceptEx)

    소스 다운로드

: 2번 서버소스와 구조와 비슷하다. 소스를 조금만 다듬는 다면 활용할 수 있는 수준이다.

4. MSDN (Writing Scalable Application for Windows NT)

    소스 다운로드

: 조금 다른 구조로 서버를 구성하고 있는 소스이다. MSDN에서 나온 소스인 만큼 참조해야 할 소스이다.

5. 임창하 (IOCP - W2K용 서버와 NT용 서버)

    소스 다운로드

: 구조를 객체 지향적으로 만들려고 노력한 소스이다. 그래서 IOCP를 이해하는데에는 다소 좋지 않지만(?) 소스의 활용도는 높은 편이다. 그리고 W2K전용과 NT이상용을 구분하여 작성한 점이 눈에 띈다.

안정적인 DNS서비스 DNSEver DNS server, DNS service
Posted by 키르히아이스
,