dh_0e

[C++/Game Server] Overlapped Model (Event 기반) 본문

C++/Game Server

[C++/Game Server] Overlapped Model (Event 기반)

dh_0e 2026. 3. 24. 18:08

Overlapped Model (이벤트 기반)

  • Overlapped IO (비동기 + 논블로킹) like node.js
    • Overlapped는 중첩이란 의미로 입출력(IO)을 Non-blocking 모드를 사용해 중첩해서 처리한다는 뜻
  • 동작 과정
    • Overlapped 함수를 건다 (ex. WSARecv, WSASend)
    • Overlapped 함수가 성공했는지 확인 후
      • 성공: 결과 얻어서 처리
      • 실패:사유 확인
        • Pending >> EWOULDBLOCK 비슷한 느낌
        • 추후에 완료가 되었을 때 요청할 수 있는 방법
          • 이벤트 방식
          • 콜백 방식
  • 비동기 입출력을 지원하는 소켓 생성 + 통지받기 위한 이벤트 객체 생성
  • 비동기 입출력 함수(WSASend, WSARecv) 호출
    • WSAOVERLAPPED 구조체를 통해 이전에 만든 이벤트를 넘겨줌
  • 비동기 작업이 바로 완료되지 않으면, WSA_IO_PENDING 오류 코드 발생
    • 바로 될 수도 있고, 안 될 수도 있음
      • 바로 되면 문제없음
    • 바로 완료되지 않은 경우, OS는 작업이 완료될 시 이벤트 객체를 signaled 상태로 만들어서 완료 상태 알려줌
    • WSAWaitForMultipleEvents() 함수를 호출해서 이벤트 객체의 signal 판별
      • 블로킹 함수인 WSAWait...()을 사용하여 데이터를 기다리게 하기 위함
    • WSAGetOverlappedResult() 함수 호출해서 비동기 입출력 결과 확인 및 데이터 처리


WSASend/WSARecv(socket, &wsaBuf, dwBufferCount, &recvBytes, &flags, &session.overlapped, nullptr)

  • AcceptEx, ConnectEx도 있음
    • 포인터를 얻어오는 등 복잡한 준비 과정이 필요함
  • 함수 인자
    • 1. socket: 비동기 입출력 소켓 (전송/송신 대상)
    • 2. &wsaBuf: WSABUF 배열의 시작 주소
      • 배열의 시작 주소인 이유?
        >> Scatter-Gather: 여러 개로 나뉜 패킷을 모아서 보낼 수 있는 우아한 기법
char sendBuffer[100];
WSABUF wsaBuf[2];
wsaBuf[0].buf = sendBuffer;
wsaBuf[0].len = 100;

char sendBuffer2[100];
wsaBuf[1].buf = sendBuffer2;
wsaBuf[1].len = 100;
  • 3. dwBufferCount: WSABUF 배열의 시작 주소
  • 4. &recyBytes: 보내고/받은 바이트 수를 받을 변수
  • 5. &flags: 상세 옵션, 0으로 설정
  • 6. &session.overlapped: WSAOVERLAPPED 구조체 주소값;
    • WSAOVERLAPPED overlapped;
    • overlapped.hEvent = wsaEvent;
  • 7. nullptr: 입출력이 완료되면 OS가 호출할 콜백 함수 (콜백 방식에서 사용)

 

WSAWaitForMultipleEvents

 

[C++/Game Server] WSAEventSelect Model

WSAEventSelect Model (Windows Socket API Event Select Model)WSAEventSelect() 함수가 핵심이 되는 모델소켓과 관련된 네트워크 이벤트(send, recv) 사용 가능 시기를 이벤트 객체를 통해 감지함소켓과 이벤트 객체를 1

dh-0e.tistory.com

 

 

WSAGetOverlappedResult(session.socket, &session.overlapped, &recvBytes, false, &flags)

  • 함수 인자
    1. session.socket: 비동기 소켓
    2. &session.overlapped: 이벤트를 넘겨준 overlapped 구조체의 ptr
    3. &recvBytes: 전송된/받은 바이트 수를 받을 변수의 ptr
    4. &false: 비동기 입출력 작업이 끝날 때까지 대기할지
      • 어차피 이전에 블로킹 함수 WaitFor~()에서 기다렸으니 fWait에 무슨 값을 넣든 상관없음
    5. &flags: 비동기 입출력 작업 관련 부가 정보 (거의 사용 안 함)

 

GameServer.cpp

#include "pch.h"
#include <iostream>
#include "CorePch.h"
#include <atomic>
#include <mutex>
#include <Windows.h>
#include <future>
#include "ThreadManager.h"

#include <urlmon.h>
#include <winSock2.h>
#include <mswsock.h>
#include <WS2tcpip.h>
#pragma comment(lib, "ws2_32.lib")

void HandleError(const char* funcName) {
	int32 errCode = ::WSAGetLastError();
	cout << "Causon Func: " << funcName << endl << "socket error : " << errCode << endl;
}

const int32 BUFFER_SIZE = 1000;

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

int main() {
	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) == SOCKET_ERROR)
		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;
	

	while (true) {
		SOCKADDR_IN clientAddr;
		int clientAddrLen = sizeof(clientAddr);

		SOCKET clientSocket;
		// 추후에 accept도 비동기로 바꾸면 코드가 깔끔해질 예정
		while (true) {
			clientSocket = ::accept(listenSocket, (SOCKADDR*)&clientAddr, &clientAddrLen);
			if (clientSocket != INVALID_SOCKET)
				break;

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

			// Error
			return 0;
		}

		Session session = Session{clientSocket};
		WSAEVENT wsaEvent = ::WSACreateEvent();
		session.overlapped.hEvent = wsaEvent;

		cout << "Connected to Client!" << endl;

		while (true) {
			::WSAResetEvent(wsaEvent);
			WSABUF wsaBuf;
			wsaBuf.buf = session.recvBuffer;
			wsaBuf.len = BUFFER_SIZE;

			DWORD recvBytes = 0;
			DWORD flags = 0;
			// 비동기 recv
			// 수동 초기화 안 해주면 WSARecv 호출전에 자동으로 신호 모두 꺼버림
			if (::WSARecv(clientSocket, &wsaBuf, 1, &recvBytes, &flags, &session.overlapped, nullptr) == SOCKET_ERROR) {
				if (::WSAGetLastError() == WSA_IO_PENDING) {
					// Pending (recv 지연중)
					// 완료 통지 방법 >> 이벤트! (이벤트 블로킹 함수로 데이터 기다림)
					::WSAWaitForMultipleEvents(1, &wsaEvent, true, WSA_INFINITE, FALSE);
					::WSAGetOverlappedResult(session.socket, &session.overlapped, &recvBytes, false, &flags);
				} else {
					// TODO: 문제 있는 상황
					break;
				}
			}

			cout << "Recv Data! Len =" << recvBytes << endl;
		}

		::closesocket(session.socket);
		::WSACloseEvent(wsaEvent);
	}

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

	// 윈속 종료
	::WSACleanup();
	return 0;
}
  • Accept는 복잡하기 때문에 동기 방식으로 구현하고, send, recv만 비동기 방식으로 구현함
  • 사실 WSARecv 하는 while문 루프를 시작할 때, ::WSAResetEvent(wsaEvent);를 추가해서 이벤트를 수동으로 초기화해줘야 함
    • CPU 사용량을 확인해 봤는데 비슷해서 이유를 찾아보니, WSARecv 같은 비동기 함수들이 이벤트들이 초기화 안 되어있으면 알아서 초기화한다고 함

 

DummyClient.cpp

#include "pch.h"
#include <iostream>

#include <winSock2.h>
#include <mswsock.h>
#include <WS2tcpip.h>
#pragma comment(lib, "ws2_32.lib")

void HandleError(const char* funcName) {
	int32 errCode = ::WSAGetLastError();
	cout << "Causon Func: " << funcName << endl << "socket error : " << errCode << endl;
}

int main() {
	// this_thread::sleep_for(1s);

	WSADATA wsaData;
	if (::WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
		return 0;

	SOCKET clientSocket = ::socket(AF_INET, SOCK_STREAM, 0);
	if (clientSocket == INVALID_SOCKET)
		return 0;

	u_long on = 1;
	if (::ioctlsocket(clientSocket, FIONBIO, &on) == SOCKET_ERROR)
		return 0;

	SOCKADDR_IN serverAddr;
	::memset(&serverAddr, 0, sizeof(serverAddr));
	serverAddr.sin_family = AF_INET;
	::inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr);
	serverAddr.sin_port = htons(7777);

	// Connect to Server
	while (true) {
		if (::connect(clientSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {
			// 원래 connect 될 때까지 블록인데, 논블록 설정 중이라 넘어옴
			int WSA = ::WSAGetLastError();
			if (WSA == WSAEWOULDBLOCK || WSA == WSAEALREADY)
				continue;

			// 이미 연결되어 있음
			if (::WSAGetLastError() == WSAEISCONN)
				break;

			// Error
			cout << WSA << endl;
			// return 0;
		}
	}

	cout << "Connected to Server!" << endl;
	WSAEVENT wsaEvent = ::WSACreateEvent();
	WSAOVERLAPPED overlapped = {};
	overlapped.hEvent = wsaEvent;

	char sendBuffer[100] = "Hello Server!";
	while (true) {
		::WSAResetEvent(wsaEvent);
		WSABUF wsaBuf;
		wsaBuf.buf = sendBuffer;
		wsaBuf.len = 100;

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

		cout << "Send Data! Len =" << sendBytes << endl;

		this_thread::sleep_for(1s);
	}

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

	// 소켓 리소스 반환
	::closesocket(clientSocket);

	// 윈속 종료
	::WSACleanup();
	return 0;
}
  • Client도 마찬가지로 비동기 send인 WSASend를 사용해서 비동기 통신을 하는 것을 확인할 수 있음
    • 사실 방식은 비동기 함수(WSARecv)를 쓰지만 결국 코드 흐름은 동기(Blocking)처럼 작동 중임

 

장단점

  • 장점: 성능
  • 단점
    • Event 64개 제한