| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 |
Tags
- Spin Lock
- Delete
- trie
- reference counting
- 그래프 탐색
- DP
- 비트마스킹
- 비트필드를 이용한 dp
- PROJECT
- Behavior Design Pattern
- 트라이
- 이분 탐색
- select 모델
- 2-SAT
- map
- Strongly Connected Component
- 벨만-포드
- 최소 공통 조상
- JavaScript
- 자바스크립트
- Overlapped Model
- HTTP
- Prisma
- 게임 서버 아키텍처
- SCC
- ccw 알고리즘
- Github
- 강한 연결 요소
- Binary Lifting
- Lock-free Stack
Archives
- Today
- Total
dh_0e
[C++/Game Server] Overlapped Model (Event 기반) 본문
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)
- 함수 인자
- session.socket: 비동기 소켓
- &session.overlapped: 이벤트를 넘겨준 overlapped 구조체의 ptr
- &recvBytes: 전송된/받은 바이트 수를 받을 변수의 ptr
- &false: 비동기 입출력 작업이 끝날 때까지 대기할지
- 어차피 이전에 블로킹 함수 WaitFor~()에서 기다렸으니 fWait에 무슨 값을 넣든 상관없음
- &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개 제한
'C++ > Game Server' 카테고리의 다른 글
| [C++/Game Server] Completion Port 모델 (IOCP) (0) | 2026.03.27 |
|---|---|
| [C++/Game Server] Overlapped Model (Callback 함수 기반) (0) | 2026.03.25 |
| [C++/Game Server] WSAEventSelect Model (0) | 2026.03.23 |
| [C++/Game Server] Select Model (0) | 2026.03.22 |
| [C++/Game Server] Non-Blocking Socket (0) | 2026.03.22 |