| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |
| 31 |
Tags
- Spin Lock
- 자바스크립트
- trie
- Behavior Design Pattern
- select 모델
- ccw 알고리즘
- 비트필드를 이용한 dp
- JavaScript
- 비트마스킹
- PROJECT
- reference counting
- map
- 2-SAT
- DP
- Strongly Connected Component
- 벨만-포드
- Binary Lifting
- HTTP
- 최소 공통 조상
- 그래프 탐색
- Github
- Delete
- 게임 서버 아키텍처
- Overlapped Model
- Prisma
- SCC
- 강한 연결 요소
- 이분 탐색
- 트라이
- Lock-free Stack
Archives
- Today
- Total
dh_0e
[C++/Game Server] Overlapped Model (Callback 함수 기반) 본문
Overlapped 모델 (Completion Routine: 콜백 함수 기반)
- 비동기 입출력을 지원하는 소켓 생성 + 비동기 입출력 함수 호출 (완료 루틴의 시작 주소를 넘겨줌)
- 바로 완료되지 않으면, WSA_IO_PENDING 반환
- 비동기 IO가 완료되면, OS는 완료 루틴(Callback 함수) 호출
- 하지만 스레드가 중요한 작업 중에 완료 루틴이 호출돼서 방해받으면 안 됨
- 비동기 입출력 함수를 호출하여 스레드를 Alertable Wait 상태로 만듦
- Alertable Wait 상태: 완료 루틴을 받을 수 있는 상태
- Alertable Wait 상태로 만들어주는 함수들
- WaitForSingleObjectEx(), WaitForMultipleObjectsEx(), SleepEx(), WSAWaitForMultipleEvents()
- 완료 루틴 호출이 "모두" 끝나면, 스레드는 Alertable Wait 상태에서 빠져나옴
- 비동기 입출력 함수를 호출하여 스레드를 Alertable Wait 상태로 만듦
Callback 함수

- 함수 인자
- dwError: 오류 발생 시 0이 아닌 값이 return 됨
- cbTransferred: 전송 바이트 수가 return 됨
- lpOverlapped: 비동기 입출력 함수 호출 시 넘겨준 WSAOVERLAPPED 구조체의 ptr
- dwFlags: 0 (사용 안 함)
- 다음 양식을 맞춰 Callback 함수를 만들어 비동기 IO 함수에 함수 포인터를 넣어줘야 함
void CALLBACK RecvCallback(DWORD dwError, DWORD recvLen, LPWSAOVERLAPPED lpOverlapped, DWORD dwFlags) {
cout << "Data Recv Len Callback = " << recvLen << endl;
}
- CALLBACK은 함수의 호출 규약(_sdcall)을 의미 (안 넣어도 상관 X)
GameServer.cpp (bind, listen 이후부터)
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
if (::WSARecv(clientSocket, &wsaBuf, 1, &recvBytes, &flags, &session.overlapped, RecvCallback) ==
SOCKET_ERROR) {
if (::WSAGetLastError() == WSA_IO_PENDING) {
// Pending (recv 지연중)
// 스레드를 Alertable Wait 상태로 바꿔줘야 함
::SleepEx(INFINITE, TRUE);
// ::WSAWaitForMultipleEvents(1, &wsaEvent, true, WSA_INFINITE, TRUE);
// WSAWait..() 함수도 Alertable Wait 상태로 바꿔줌 (== SleepEx)
} else {
// TODO: 문제 있는 상황
break;
}
} else {
cout << "Recv Data! Len =" << recvBytes << endl;
}
}
::closesocket(session.socket);
::WSACloseEvent(wsaEvent);
}
- SleepEx() 함수가 현재 스레드를 Alertable Wait 상태로 바꿔주고, 스레드는 APC queue에 들어온 Callback 함수를 기다렸다가 호출함
- APC queue: 각 스레드마다 독립적으로 존재하는 Callback 함수를 위한 queue
- 스레드는 모든 콜백 함수를 호출하여 APC queue를 비운 뒤, Alertable Wait 상태에서 빠져나옴

Callback 기반의 장점 (vs Event 기반)
- Event 기반에선 다수의 event를 일일이 체크하는 식으로 만들었었는데, 애초에 WSAWaitForMultipleEvents()의 크기 한도가 있기 때문에, 이를 맞춰서 사용하기 번거로움
- Callback 기반에선 Alertable Wait 상태에 들어간 스레드가 모든 Callback 함수를 처리해 주기 때문에 아주 편함
RecvCallback 인자 사용법
- RecvCallback()의 인자들은 사실상 가치 있는 정보가 없음
- overlapped를 Session 구조체의 맨 처음으로 위치시켜 Callback 함수에서 Session의 ptr을 구해서 사용할 수 있음
- ex) clientSocket이 여러 개 있을 때, Callback 함수가 어떤 client를 대상으로 호출되었는지 알고 싶을 때
struct Session {
WSAOVERLAPPED overlapped = {};
SOCKET socket;
char recvBuffer[BUFFER_SIZE] = {};
int32 recvBytes = 0;
};
void CALLBACK RecvCallback(DWORD dwError, DWORD recvLen, LPWSAOVERLAPPED lpOverlapped, DWORD dwFlags) {
cout << "Data Recv Len Callback = " << recvLen << endl;
Session* session = (Session*)lpOverlapped;
}

- 나중에 IOCP에서도 이런 방식으로 정보를 넘겨줌
장단점
- 장점: 성능
- 단점
- 모든 비동기 소켓 함수에서 사용 가능하진 않음 (AcceptEx 등에는 콜백 루틴이 없음)
- 빈번한 Alertable Wait 전환으로 인한 성능 저하 (IOCP에 비해)
- APC Queue가 각 스레드마다 존재함
- 스레드 종속성 (작업을 요청한 스레드와 처리하는 스레드가 일치해야 함)
- 부하 분산 불가능 (특정 스레드에 작업이 몰릴 수 있음)
- Alertable Wait 상태로 가는 Context Switching도 부담이 될 수 있음
Reactor / Proactor Pattern
- Reactor Pattern: 작업 가능 상태 요청 후 대기하다가 작업 준비 완료 신호를 받고 IO 작업을 실행 / 동기식 Non-Blocking IO
- 이벤트를 감시(Select)
- 이벤트 발생 시 핸들러 호출
- 핸들러 내부에서 IO 함수(recv, send) 실행 (데이터 복사가 유저 타임에 발생)
- Proactor Pattern: 작업 요청 후 다른 작업 하다가 작업이 완료되면 커널이 통보 / 비동기식 IO
- 비동기 I/O 요청 (Overlapped I/O 등)
- 커널이 백그라운드에서 I/O 수행 및 완료
- 완료 알림(Completion)을 받고 결과만 처리 (데이터 복사가 커널 타임에 완료)
| 분류 | 관련 모델 및 기술 | 특징 |
| Reactor | Select, WSAEventSelect | 커널은 "이 소켓에 데이터가 왔다"는 상태만 알려줌. 유저가 직접 recv 해야 함. |
| Proactor | Overlapped I/O, IOCP | 커널이 직접 데이터를 버퍼에 복사까지 완료한 후 통보함. (Windows의 핵심 모델) |
'C++ > Game Server' 카테고리의 다른 글
| [C++/Game Server] Socket Utils (WinSock Wrapper) (0) | 2026.03.29 |
|---|---|
| [C++/Game Server] Completion Port 모델 (IOCP) (0) | 2026.03.27 |
| [C++/Game Server] Overlapped Model (Event 기반) (0) | 2026.03.24 |
| [C++/Game Server] WSAEventSelect Model (0) | 2026.03.23 |
| [C++/Game Server] Select Model (0) | 2026.03.22 |
