KoreanFoodie's Study

[C++ 게임 서버] 3-9. WSAEventSelect 모델 본문

Game Dev/Game Server

[C++ 게임 서버] 3-9. WSAEventSelect 모델

GoldGiver 2023. 10. 16. 21:51

Rookiss 님의 '[C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버' 를 들으며 배운 내용을 정리하고 있습니다. 관심이 있으신 분은 꼭 한 번 들어보시기를 추천합니다!

[C++ 게임 서버] 3-9. WSAEventSelect 모델

핵심 :

1. WSAEventSelect 모델을 사용하면, 특정 소켓과 특정 네트워크 이벤트를 연결해, 실제 이벤트가 발동했을 때 바인딩된 소켓으로부터 원하는 정보를 얻을 수 있다.

2. Select 모델과 달리, 매번 Set 을 만들지 않고 사용한 소켓에 해당한 이벤트만 Reset 해 주면 된다.

3. 이벤트 발동 시, 적절한 함수를 호출해 주어야 해당 이벤트를 다시 사용할 수 있다.

우리는 이전 글에서 Select 모델에 대해 배웠다. 간단히 얘기해서, Select 모델을 활용하면 특정 역할을 하는 SET(READ_SET/WRITE_SET)에 특정 소켓을 넣어 '관찰' 할 수 있게 만듦으로써 우리가 원하는 특정 동작을 특정 소켓이 수행하도록 만들 수 있다.

 

이번 글에서는 기존 Select 모델에서 조금 더 나아가서, WSAEventSelect 모델에 대해 알아볼 것이다.

WSAEventSelect 모델은, 요약하자면... 소켓과 관련된 네트워크 이벤트를 [이벤트 객체]를 통해 감지하는 방법이다.

즉, 소켓에 이벤트 객체를 연동하고, 특정 이벤트가 불렸을 때 해당하는 소켓을 꺼내 우리가 원하는 동작(e.g. ::accept, ::receive)을 수행하면 된다.

먼저 이벤트 객체 관련 함수들을 살펴보자.

 

 이벤트 객체 관련 함수들

  • 생성 : WSACreateEvent (수동 리셋 Manual-Reset + Non-Signaled 상태 시작)
  • 삭제 : WSACloseEvent
  • 신호 상태 감지 : WSAWaitForMultipleEvents
  • 구체적인 네트워크 이벤트 알아내기 : WSAEnumNetworkEvents

먼저, 이벤트 생성을 위해 WSACreateEvent 를 사용할 것이다. 참고로, 이벤트는 사용후 수동으로 Reset 을 해 주어야 하며, Non-Signaled 상태로 시작한다. Select 모델과는 다르게, WSACreateEvent 모델은 비동기 방식이다.

이벤트 삭제에는 WSACloseEvent 를, 신호 상태 감지에는 WSAWaitForMultipleEvents 를 사용한다. WSAWaitForMultipleEvents 에 인자를 잘 설정해 주면 '특정 이벤트를 감지할 것인지/모든 이벤트를 기다릴 것인지', '얼마나 기다릴 것인지' 등을 설정할 수 있다.

그리고, 이벤트가 발동했을 때, WSAEnumNetworkEvents 함수를 이용해서 실제 발동한 특정 이벤트를 골라낼 수 있다(소켓이 연결되어 있다면 소켓까지).

 

그럼 소켓에 연동할 수 있는 실제 이벤트 객체에 대해 알아보자. 소켓과 이벤트를 연동할 때는 WSAEventSelect 함수를 사용한다. 인자는 다음과 같다 :

WSAEventSelect(socket, event, networkEvents)

네트워크 이벤트(NetworkEvent) 객체

  • FD_ACCEPT : 접속한 클라가 있음 accept
  • FD_READ : 데이터 수신 가능 recv, recvfrom
  • FD_WRITE : 데이터 송신 가능 send, sendto
  • FD_CLOSE : 상대가 접속 종료
  • FD_CONNECT : 통신을 위한 연결 절차 완료
  • FD_OOB : MSG_OOB로 보내는 특별한 데이터

참고로 WSAEventSelect 함수를 호출하면, 해당 소켓은 자동으로 논블로킹 모드로 전환된다.

또한 ::accept 함수가 리턴하는 소켓은 listenSocket 과 동일한 속성을 갖게 되므로, clientSocket 은 FD_READ, FD_WRITE 등을 다시 등록해야 한다.

이벤트 발생 시, 적절한 소켓 함수를 호출해야 하는데, 그렇지 않을 경우 다음 번에는 네트워크 이벤트가 발생하지 않는다. 즉, FD_READ 이벤트가 뜨면 ::recv 를 호출해 주어야 한다. 만약 빼먹는다면 FD_READ 가 두 번 다시 불리지 않을 것이다 😬

그리고 드물게 WSAEWOULDBLOCK 오류가 뜰 수 있으니, 해당 케이스에 대한 예외 처리가 필요하다. 이 에러는 송수신 버퍼의 상태가 비어 있거나 가득 차 있어 송수신이 불가능한 임시적인 상태를 의미한다.

 

그럼 이제 예제 코드를 보자. 우리는 GameServer.cpp 를 수정할 것인데, 이전에 작성하던 WSA 기본 세팅 관련 코드는 접은 글로 가볍게 넘기자 🤣

더보기
WSAData wsaData;
if (::WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
    return 0;

SOCKET listenSocket = ::socket(AF_INET, SOCK_STREAM, 0);
if (listenSocket == INVALID_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);

if (::bind(listenSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
    return 0;

if (::listen(listenSocket, SOMAXCONN) == SOCKET_ERROR)
    return 0;

cout << "Accept" << endl;

 

일단, 이벤트와 세션부터 만들어 줄 것이다.

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

WSAEVENT listenEvent = ::WSACreateEvent();
wsaEvents.push_back(listenEvent);
sessions.push_back(Session{ listenSocket });
if (::WSAEventSelect(listenSocket, listenEvent, FD_ACCEPT | FD_CLOSE) == SOCKET_ERROR)
    return 0;

보면 WSAEVENT 타입의 벡터와 세션 벡터를 만들고, listenEvent 를 만든 다음, 세션과 WSAEVENT 벡터의 짝을 맞춰주었다.

WSAEventSelect 함수를 사용해 listenSocket 의 listenEvent 객체에 FD_ACCEPT 와 FD_CLOSE 네트워크 이벤트를 바인딩해 주었다.

 

그리고 이제 무한 루프를 돌며 통신을 진행한다 :

while (true)
{
  /* 앞으로 작성할 모든 코드 */
}

 

이제 통신을 해 보자.

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

index -= WSA_WAIT_EVENT_0;

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

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

일단, WSAWaitForMultipleEvents 를 사용하여 이벤트를 기다릴 것이다. WSAWaitForMultipleEvents 인자를 잠깐 보면...

특정 이벤트를 바인딩할 수도 있고, 모든 이벤트를 기다릴 수도 있다. 일단 우리는 아까 생성한 listenSocket 에 붙은 Event 하나에 대해서만 기다릴 것이고, 올 때까지 무한정(WSA_INFINITE) 기다린다.

WSAWaitForMultipleEvents 는 해당 이벤트가 속한 배열의 인덱스에 WSA_WAIT_EVENT_0 을 더한 값을 반환하므로, 이 값을 다시 빼 주면 우리가 원하는 실제 이벤트의 배열 내 index 를 구할 수 있다. 😉

해당 인덱스 값을 이용해 다시 WSAEnumNetworkEvents 함수를 이용해 우리가 원하는 이벤트 객체로부터 네트워크 이벤트(WSANETWORKEVENTS)를 골라낼 수 있다.

아, 위에 ::WSAResetEvent 가 주석처리되어 있는데... 사실 Manual Reset 이 필요하지만, WSAEnumNetworkEvents 함수 자체에 특정 이벤트에 대한 리셋 기능이 있어 주석처리를 했다는 것을 알아두자 😊

 

사실 윗 부분이 핵심이고, 다음 부분부터는 Select 모델에서 사용했던 코드와 역할이 거의 비슷하다.

먼저 Listener 소켓을 체크하는 코드를 보면...

// 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" << endl;

        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;
    }
}

해당 네트워크 이벤트가 FD_ACCEPT(접속한 클라가 있음) 이면, 에러 체크 후 "Client Connected" 메시지를 출력한다.

 

그 후, 아까 만들었던 것과 같은 방식으로 이벤트를 만들고 세션과 이벤트 벡터에 객체를 넣어준다. 사실 아래 부분은 Select 모델 코드와 거의 같아, networkEvents 의 플래그를 어떻게 검사해 주는지 정도만 보면 될 것 같다! 😄

// 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 && ::WSAGetLastError() != WSAEWOULDBLOCK)
        {
            // TODO : Remove Session
            continue;
        }

        s.recvBytes = recvLen;
        cout << "Recv Data = " << recvLen << endl;
    }

    // Write
    if (s.recvBytes > s.sendBytes)
    {
        int32 sendLen = ::send(s.socket, &s.recvBuffer[s.sendBytes], s.recvBytes - s.sendBytes, 0);
        if (sendLen == SOCKET_ERROR && ::WSAGetLastError() != WSAEWOULDBLOCK)
        {
            // TODO : Remove Session
            continue;
        }

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

        cout << "Send Data = " << sendLen << endl;
    }
}

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