나만의 작은 도서관

[C++] 게임서버 탐구 일지 #7. 소켓 모델 - WSAEventSelect 모델 본문

C++/Windows 게임서버

[C++] 게임서버 탐구 일지 #7. 소켓 모델 - WSAEventSelect 모델

pledge24 2025. 1. 4. 18:02

목차

  • Select 모델의 문제점
  • WSAEventSelect 모델의 특징
  • WSAEventSelect 모델 이용한 echo 서버 만들기
  • WSAEventSelect 모델의 문제점
  • 깃헙 코드

Select 모델의 문제점

Select 모델은 기존의 블로킹/논블로킹 방식에서 소켓 작업이 진행될 수 없는 상황에서도 진행하려는 문제를 해결하기 위해 사용하였다. 하지만 1) 매 루프마다 fd_set에 소켓을 등록하고 select() 함수가 지우는 과정을 반복한다는 점, 2) select() 함수는 동기 방식을 따른다는 점과 같은 문제점이 있었다. 

 

Select모델의 장점을 가져가면서 위와 같은 문제점을 개선한 모델이 바로 WSAEventSelect 모델이다.


WSAEventSelect 모델의 특징

WSAEventSelect() 함수가 핵심

Select 모델에서 select() 함수가 핵심이었던 것처럼, WSAEventSelect 모델은 WSAEventSelect() 함수가 핵심이다. select() 함수와 다르게 비동기 방식(Asynchronous)으로 동작한다.

 

[이벤트 객체]를 사용

WSAEventSelect 모델(줄여서 이벤트 모델이라고 부르기도 한다)은 [이벤트 객체]를 통해 소켓의 네트워크 이벤트를 탐지한다. 예를 들어, 특정 소켓의 recvBuffer에 데이터가 들어오는 수신 이벤트 발생을 감지하고 싶다면, 이벤트 객체를 생성한 다음, 수신 이벤트 플래그를 세워서 해당 소켓과 함께 WSAEventSelect() 함수에 넣어주는 방식으로 이벤트를 탐지할 목록에 등록한다.

 

이벤트 객체와 관련된 함수들 중 대표적인 함수들의 사용 예제는 다음과 같다.

// 이벤트 생성(수동 리셋 Manual-Reset + Non-Signaled 상태 시작)
WSAEVENT event = ::WSACreateEvent();

// 이벤트 삭제
// 1) 이벤트 객체
::WSACloseEvent(event);

// 이벤트 발생 감지.(Signal의 상태가 Signaled인지 확인) 
// 1) 이벤트 배열 크기 
// 2) 이벤트 배열 주소 
// 3) timeout 
// 4) 관찰하는 모든 이벤트의 완료까지 대기 
// 5) 일단 false로
int32 index = ::WSAWaitForMultipleEvents(wsaEvents.size(), &wsaEvents[0], FALSE, WSA_INFINITE, FALSE);

// 구체적인 네트워크 이벤트 정보 알아내기
// 1) 소켓 
// 2) 이벤트 객체(옵션). socket과 연동된 이벤트 객체 핸들을 넘겨주면 해당 이벤트 객체를 non-signaled로 변경
// 3) 결과를 저장할 WSANETWORKEVENTS 타입 변수. 오류 정보가 저장되어있음
WSANETWORKEVENTS networkEvents;
::WSAEnumNetworkEvents(sessions[index].socket, wsaEvents[index], &networkEvents);

 

소켓과 이벤트 객체는 일대일 대응

감지하고 싶은 네트워크 이벤트가 여러 개라도 이벤트 객체에서 해당 이벤트 플레그를 세워 추가하는 방식이므로 하나의 소켓은 하나의 이벤트 객체에 대응한다. 물론 자동으로 대응하진 않고, 수동으로 소켓과 이벤트 객체를 붙여야 한다.

 

일부 주의사항

  • WSAEVENTSelect() 함수를 호출하면, 해당 소켓은 자동으로 논블로킹 모드로 전환된다
  • accept() 함수가 리턴하는 소켓은 listenSocket과 동일한 속성을 갖는다. 
  • 드물게 WSAEWOULDBLOCK 오류가 뜰 수 있으니 예외처리를 해야 한다.
  • 이벤트 발생 시 적절한 해당 소켓 함수 호출해야 한다. 그렇지 않으면 다음번에는 동일 네트워크 이벤트가 발생하지 않는다.

WSAEventSelect 모델을 이용한 echo 서버 만들기

socket() -> bind() -> listen()

TCP 서버를 만들 때는 항상 listen 소켓을 만들고 시작한다. socket() -> bind() -> listen()을 통해 listen 소켓을 하나 만들어준다. 

 

이때, listen소켓은 논블로킹 방식을 사용한다. 후에 WSAEventSelect() 함수를 호출할 경우 넣어준 소켓은 자동으로 논블로킹 모드로 전환되기 때문이다. 괜히 블로킹 방식으로 설정하여 혼동을 주지 않는 것이 좋다. (논블로킹, Select 모델의 echo 서버 코드와 동일)

int main()
{
    WSAData wsaData;
    if (::WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
        return 0;

    // socket(): 소켓 하나를 생성해 리턴한다.
    SOCKET listenSocket = ::socket(AF_INET, SOCK_STREAM, 0);
    if (listenSocket == INVALID_SOCKET)
    {
        HandleError("Socket");
        return 0;
    }

    // 논블로킹 방식으로 소켓 모드 설정
    u_long on = 1;
    if (::ioctlsocket(listenSocket, FIONBIO, &on) == INVALID_SOCKET)
        return 0;

    SOCKADDR_IN serverAddr;
    ::memset(&serverAddr, 0, sizeof(serverAddr));
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_addr.s_addr = ::htonl(INADDR_ANY);
    serverAddr.sin_port = ::htons(7777);
    
    // bind()
    if (::bind(listenSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
        return 0;
    
    // listen()
    if(::listen(listenSocket, SOMAXCONN) == SOCKET_ERROR)
        return 0;

    cout << "Accept" << '\n';
...

 

 

세션 준비

Select 모델은 여러 소켓을 동시에 다루기 때문에 각 소켓 데이터를 관리하는 세션 자료구조가 필요하다. 따라서 구조체 방식으로 세션을 준비해 둔다. (이 또한 Select 모델과 동일하다)

const int32 BUFSIZE = 1000;

struct Session
{
    SOCKET socket;
    char recvBuffer[BUFSIZE] = {};
    int32 recvBytes = 0;	// 수신한 바이트 수
    int32 sendBytes = 0;	// 송신한 바이트 수
};

 

 

listen 소켓 이벤트 객체 붙여주기

Select 모델 때와 달리 각 소켓에서 발생하는 네트워크 이벤트를 감지하기 위해선 이벤트 객체를 붙여줘야 한다. listen 소켓에서 감지해야 하는 네트워크 이벤트는 Accept와 Close로, 각각의 비트 플래그를 세워서 WSAEventSelect() 함수에 감지할 이벤트 목록에 등록한다.

vector<WSAEVENT> wsaEvents;
vector<Session> sessions;
sessions.reserve(100);

WSAEVENT listenEvent = ::WSACreateEvent();
wsaEvents.push_back(listenEvent);
// listen 소켓은 세션이 필요없지만 이벤트 인덱스와 일치시키기 위해 넣는다
sessions.push_back(Session{ listenSocket }); 
if (::WSAEventSelect(listenSocket, listenEvent, FD_ACCEPT | FD_CLOSE) == SOCKET_ERROR)
    return 0;

 

 

이벤트가 감지된 소켓 반환

WSAEventSelect 모델은 WSAWaitForMultipleEvents() 함수의 반환 값을 통해 네트워크 이벤트가 발생한 소켓 인덱스를 알 수 있다. 예를 들어, WSAWaitForMultipleEvents() 함수의 반환 값이 3이라면, 인자로 넣어준 이벤트 배열의 3번 인덱스에 위치한 소켓에서 이벤트가 발생했음을 의미한다. 

 

서버를 막 열었을 때 listen 소켓의 이벤트 발생을 감지하기 위해 while문 맨 앞에 넣어준다.

 

+) 주의할 점은 주석 처리된 WSAResetEvent() 함수를 실행해서 Signaled 상태가 되어 감지된 이벤트를 수동으로 Non-Signaled 상태로 변경하여 리셋해야 한다. 만약 리셋을 해주지 않는다면, 해당 이벤트는 두 번 다시 감지되지 않는다. 그럼에도 아래 예제에선 따로 리셋해주지 않았는데, 이는 WSAWaitForMultipleEvents() 함수의 인자로 이벤트 객체를 넣어주면 이벤트 리셋을 해주기 때문이다. (리셋을 안 한 게 아니라는 것이다!)

    while (true)
    {
        int32 index = ::WSAWaitForMultipleEvents(wsaEvents.size(), &wsaEvents[0], FALSE, WSA_INFINITE, FALSE);
        if (index == WSA_WAIT_FAILED)
            continue;

        index -= WSA_WAIT_EVENT_0; // WSA_WAIT_EVENT_0를 빼줘야 배열의 index가 나온다.

        //::WSAResetEvent(wsaEvents[index]);

        WSANETWORKEVENTS networkEvents;
        if(::WSAEnumNetworkEvents(sessions[index].socket, wsaEvents[index], &networkEvents) == SOCKET_ERROR)
            continue;
            
        ...

 

 

감지된 이벤트 종류 확인 및 소켓 함수 실행

Select 모델에서도 그랬던 것처럼 해당 소켓에서 어느 이벤트가 발생했는지 확인하려면 추가적인 코드를 짜야한다. 

위에서 WSAWaitForMultipleEvents() 함수 실행으로 networkEvents에 저장된 네트워크 이벤트 데이터를 이용하여 어느 이벤트가 감지되었는지 확인하고 해당 이벤트에 대응하는 소켓 함수를 실행한다. 예제 코드는 아래와 같다.

// Listener 소켓 체크
if (networkEvents.lNetworkEvents & FD_ACCEPT)
{
    // Error-Check용. 그냥 하나의 패턴으로만 알고 있자
    if (networkEvents.iErrorCode[FD_ACCEPT_BIT] != 0)
        continue;

    SOCKADDR_IN clientAddr;
    int32 addrLen = sizeof(clientAddr);

    SOCKET clientSocket = ::accept(listenSocket, (SOCKADDR*)&clientAddr, &addrLen);
    if (clientSocket != INVALID_SOCKET)
    {
        cout << "Client Connected" << '\n';

        WSAEVENT clientEvent = ::WSACreateEvent();
        wsaEvents.push_back(clientEvent);
        sessions.push_back(Session{ clientSocket });
        if (::WSAEventSelect(clientSocket, clientEvent, FD_READ | FD_WRITE | FD_CLOSE) == SOCKET_ERROR)
            return 0;
    }
}

// Client Session 소켓 체크
if (networkEvents.lNetworkEvents & FD_READ || networkEvents.lNetworkEvents & FD_WRITE)
{
    // Error-Check
    if ((networkEvents.lNetworkEvents & FD_READ) && (networkEvents.iErrorCode[FD_READ_BIT] != 0))
        continue;

    // Error-Check
    if ((networkEvents.lNetworkEvents & FD_WRITE) && (networkEvents.iErrorCode[FD_WRITE_BIT] != 0))
        continue;

    Session& s = sessions[index];

    // Read
    if (s.recvBytes == 0)
    {
        int32 recvLen = ::recv(s.socket, s.recvBuffer, BUFSIZE, 0);

        if (recvLen == SOCKET_ERROR)
        {
            // 논블로킹 에러가 아닌 경우 에러 처리
            if (::WSAGetLastError() != WSAEWOULDBLOCK)
            {
                // TODO : Remove Session
            }
            continue;
        }

        s.recvBytes = recvLen;
        cout << "Recv Data = " << recvLen << '\n';
    }

    // Write
    if (s.recvBytes > s.sendBytes)
    {
        int32 sendLen = ::send(s.socket, &s.recvBuffer[s.sendBytes], s.recvBytes - s.sendBytes, 0);

        if (sendLen == SOCKET_ERROR)
        {
            // 논블로킹 에러가 아닌 경우 에러 처리
            if (::WSAGetLastError() != WSAEWOULDBLOCK)
            {
                // TODO : Remove Session
            }           
            continue;
        }

        s.sendBytes += sendLen;
        if (s.recvBytes == s.sendBytes)
        {
            s.recvBytes = 0;
            s.sendBytes = 0;
        }

        cout << "Send Data = " << sendLen << '\n';
    }
}

// FD_CLOSE 처리
if (networkEvents.lNetworkEvents & FD_CLOSE)
{
    // TODO : Remove Socket
}

 

 

실행 결과

문제없이 잘 작동하는 것을 확인할 수 있다.


WSAEventSelect 모델의 문제점

해결되지 않은 최대 이벤트 감지 개수

Select 모델에서도 fd_set 하나의 최대 크기가 64밖에 되지 않아 불편했었는데, 이 불편함은 WSAEventSelect에서도 동일하게 존재한다.

 

좋지 못한 성능

Select 모델보다는 성능이 좋아졌지만, MMORPG 게임 서버에서 사용하기에는 성능이 많이 부족하기 때문에 서버용으로 사용하기 힘들다. 그렇지만 서버가 아닌 클라이언트 측 소켓 모델로 사용하는 건 괜찮은 선택이다.

 

세션마다 이벤트를 연결해야 하는 번거로움

세션마다 이벤트를 만들고 연결하는 작업은 귀찮은 작업이다. 약간의 불평이지만 어찌 되었든 문제라면 문제라 볼 수 있다.


깃헙 코드

https://github.com/pledge24/WinGameServerPractice/tree/b7ea3b74731b23f493492fe14a02f35ca643afb6

 

GitHub - pledge24/WinGameServerPractice

Contribute to pledge24/WinGameServerPractice development by creating an account on GitHub.

github.com