KoreanFoodie's Study

[C++ 게임 서버] 3-10. Overlapped 모델 (이벤트 기반) 본문

Game Dev/Game Server

[C++ 게임 서버] 3-10. Overlapped 모델 (이벤트 기반)

GoldGiver 2023. 10. 17. 22:19

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

[C++ 게임 서버] 3-10. Overlapped 모델 (이벤트 기반)

핵심 :

1. 블로킹 방식을 사용하면 함수 호출 후, 작업 완료시까지 대기를 하며 논블로킹 방식을 사용하면 다른 작업을 한다.

2. 논블로킹/비동기 방식의 모델이 AIO(Overlapped IO) 모델로, 운영체제는 이벤트 객체를 Signaled 상태로 만들어 완료 상태를 알려준다.

3. WSAWaitForMultipleEvents 함수를 호출해서 이벤트 객체의 Signal 을 판별 후, WSAGetOverlappedResult 를 호출해 비동기 입출력 결과를 확인한다. 

이전 글에서 정리했던 Select 모델 말고, 실제로 사용할 IOCP 모델을 위한 기초 작업을 해 보자.

이번 글에서는 Overlapped 모델에 대해 다룰 것인데, 일단 그 전에 기초적인 개념을 다시 한 번 짚고 넘어가자.

이전에 사용했던 위 개념들에 대해 다시 짚고 넘어갈 필요가 있다.

먼저 출처를 알 수 없는(?) 위 사진을 보면, 두 가지 개념이 짬뽕될 때 어떻게 되는지가 잘 나와 있다.

사실 Blocking/NonBlocking 의 차이는 간단하다. Blocking 방식일 때는, 특정 호출을 시도를 한 후, 끝나기 전까지 다른 일을 하지 못하고 대기를 한다. 반면 NonBlocking 방식일 때는 특정 호출을 시도하고 그 동안(완료 Callback 이 올 때 까지) 다른 일을 한다.

이렇게 풀어 써 보니, 당연히 NonBlocking 을 써야 할 것만 같은 기분이 든다 😅

 

동기와 비동기는, 이미 잘 알듯이 해당 동작이 동기적으로 이루어지는지, 비동기적으로 이루어지는지의 차이다.

위의 표에 따른 모델의 분류를 그려보면...

위처럼 나오는데, 사실 Blocking/Asynchronous 조합은... 거의 안쓴다고 생각하면 된다.

대체로 고민해 봐야 할 것은 Blocking/Synchronous 와 Non-Blocking/Asynchronous 조합일 것이다 🙃

실제로, System Call 을 AIO 방식으로 하면 위처럼 UML 을 간단히 그려볼 수 있을 것이다 😊

 

그럼 이제 본격적으로 Overlapped IO (비동기 + 논블로킹) 모델을 알아보자. 사실 요약하면 다음과 같이 정리된다 :

  1. 비동기 입출력 지원하는 소켓 생성 + 통지 받기 위한 이벤트 객체 생성
  2. 비동기 입출력 함수 호출 (1에서 만든 이벤트 객체를 같이 넘겨줌)
  3. 비동기 작업이 바로 완료되지 않으면, WSA_IO_PENDING 오류 코드
  4. 운영체제는 이벤트 객체를 signaled 상태로 만들어서 완료 상태 알려줌
  5. WSAWaitForMultipleEvents 함수 호출해서 이벤트 객체의 signal 판별
  6. WSAGetOverlappedResult 호출해서 비동기 입출력 결과 확인 및 데이터 처리

 

조금 더 구체적으로 파 보자.

2번을 보면, 비동기 입출력 함수를 호출하는데, Overlapped 함수로는 WSASend 와 WSARecv 가 있다.

인자로는 다음과 같은 것들이 필요하다 :

한국말로 잘 풀어 쓰면... 다음과 같다 :

 1) 비동기 입출력 소켓
 2) WSABUF 배열의 시작 주소 + 개수 // Scatter-Gather
 3) 보내고/받은 바이트 수
 4) 상세 옵션인데 0
 5) WSAOVERLAPPED 구조체 주소값
 6) 입출력이 완료되면 OS가 호출할 콜백 함수

 

5번에서... 아직 일이 끝나지 않은(Pending) 상태이면, WSAWaitForMultipleEvents 함수를 호출해서 Signal 을 판별하고, 6번에서 결과를 확인한다.

WSAGetOverlappedResult 를 통해 결과를 호출할 때, 필요한 인자는 다음과 같다 :

한국말로 풀어쓰면 다음과 같다 🤣 :

1) 비동기 소켓
2) 넘겨준 overlapped 구조체
3) 전송된 바이트 수
4) 비동기 입출력 작업이 끝날때까지 대기할지?
5) 비동기 입출력 작업 관련 부가 정보. 거의 사용 안 함.

 

이제 코드를 보자.

일단, 기존에 사용했던 Session 에서 WSAOVERLAPPED 구조체를 추가해 줄 것이다.

struct Session
{
	SOCKET socket = INVALID_SOCKET;
	char recvBuffer[BUFSIZE] = {};
	int32 recvBytes = 0;
	WSAOVERLAPPED overlapped = {};
};

 

일단, 클라이언트 소켓을 만드는 부분과 이벤트 객체 생성은 사실 거의 비슷하다.

// 클라이언트 소켓 생성
SOCKADDR_IN clientAddr;
int32 addrLen = sizeof(clientAddr);

SOCKET clientSocket;
while (true)
{
	clientSocket = ::accept(listenSocket, (SOCKADDR*)&clientAddr, &addrLen);
	if (clientSocket != INVALID_SOCKET)
		break;

	if (::WSAGetLastError() == WSAEWOULDBLOCK)
		continue;

	// 문제 있는 상황
	return 0;
}

// 이벤트 객체 생성
Session session = Session{ clientSocket };
WSAEVENT wsaEvent = ::WSACreateEvent();
session.overlapped.hEvent = wsaEvent;

 

그럼 이제 비동기 통신을 아래와 같이 하면 된다 :

while (true)
{
    WSABUF wsaBuf;
    wsaBuf.buf = session.recvBuffer;
    wsaBuf.len = BUFSIZE;

    DWORD recvLen = 0;
    DWORD flags = 0;
    // 2번 : 비동기 입출력 함수 호출
    if (::WSARecv(clientSocket, &wsaBuf, 1, &recvLen, &flags, &session.overlapped, nullptr) == SOCKET_ERROR)
    {
        // 3번 : 비동기 작업이 바로 완료되지 않으면...
        if (::WSAGetLastError() == WSA_IO_PENDING)
        {
            // 4~6번 : 이벤트 객체가 Signaled 될 때까지 기다린 후, Signal 판별하고, 결과 확인
            ::WSAWaitForMultipleEvents(1, &wsaEvent, TRUE, WSA_INFINITE, FALSE);
            ::WSAGetOverlappedResult(session.socket, &session.overlapped, &recvLen, FALSE, &flags);
        }
        else
        {
            // TODO : 문제 있는 상황
            break;
        }
    }

    cout << "Data Recv Len = " << recvLen << endl;
}

// 소켓 사용이 완료되었으면 잘 닫아준다
::closesocket(session.socket);
::WSACloseEvent(wsaEvent);

이때, ::WSAGetOverlappedResult 함수에서 &session.overlapped 를 사용하는데, 이때 오염이 발생해서는 안된다.

 

위의 작업들은 전부 서버에서 진행한 것으로, 이제 클라이언트 쪽도 약간의 수정을 해 주어야 한다. 크게 다른 것은 없으니, 참고로 보고 넘어가도록 하자 😄

char sendBuffer[100] = "Hello World";
WSAEVENT wsaEvent = ::WSACreateEvent();
WSAOVERLAPPED overlapped = {};
overlapped.hEvent = wsaEvent;

// Send
while (true)
{
    WSABUF wsaBuf;
    wsaBuf.buf = sendBuffer;
    wsaBuf.len = 100;

    DWORD sendLen = 0;
    DWORD flags = 0;
    if (::WSASend(clientSocket, &wsaBuf, 1, &sendLen, flags, &overlapped, nullptr) == SOCKET_ERROR)
    {
        if (::WSAGetLastError() == WSA_IO_PENDING)
        {
            // Pending
            ::WSAWaitForMultipleEvents(1, &wsaEvent, TRUE, WSA_INFINITE, FALSE);
            ::WSAGetOverlappedResult(clientSocket, &overlapped, &sendLen, FALSE, &flags);
        }
        else
        {
            // 진짜 문제 있는 상황
            break;
        }
    }

    cout << "Send Data ! Len = " << sizeof(sendBuffer) << endl;

    this_thread::sleep_for(1s);
}
Comments