| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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
- ccw 알고리즘
- 그래프 탐색
- 강한 연결 요소
- Overlapped Model
- HTTP
- DP
- Lock-free Stack
- reference counting
- 비트마스킹
- 트라이
- Github
- Spin Lock
- 자바스크립트
- Delete
- Binary Lifting
- 게임 서버 아키텍처
- PROJECT
- Behavior Design Pattern
- JavaScript
- trie
- select 모델
- 최소 공통 조상
- 이분 탐색
- map
- Prisma
- SCC
- 벨만-포드
- Strongly Connected Component
- 비트필드를 이용한 dp
- 2-SAT
Archives
- Today
- Total
dh_0e
[C++/Game Server] Completion Port 모델 (IOCP) 본문
IOCP (Completion Port) 모델
- APC queue 대신 Completion Port를 사용하는 모델
- Completion Port: 스레드마다 있는 것이 아닌 유일한 포트 / 중앙에서 관리하는 APC queue 느낌
- 멀티 스레드 환경과 궁합이 굉장히 좋음
- Alertable Wait 대신 Completion Port 결과 처리를 위한 함수 호출
- CreateIoCompletionPort(): Completion Port를 생성 / 소켓을 Completion Port에 등록
- GetQueuedCompletionStatus(): OS에게 일감을 기다림
CICP (CreateIoCompletionPort)
HANDLE hResult = CreateIoCompletionPort(
HANDLE FileHandle, // [입력] 등록할 소켓 핸들 (생성 시에는 INVALID_HANDLE_VALUE)
HANDLE ExistingCompletionPort, // [입력] 기존 CP 핸들 (생성 시에는 NULL)
ULONG_PTR CompletionKey, // [입력] 완료 시 돌려받을 식별 Key (보통 Session* 주소)
DWORD NumberOfConcurrentThreads // [입력] 동시 실행 허용 스레드 수 (0이면 코어 개수와 동일)
);
- 2가지 기능이 있음
- Completion Port 생성
- FileHandle을 INVALID_HANDLE_VALUE으로 설정하면 Completion Port를 생성해서 Handle을 반환해 줌
- 나머지 인자들은 NULL, 0으로 채워주면 됨
- 소켓을 Completion Port에 등록
- FileHandle: 소켓 핸들을 넣어줌
- ExistingCompletionPort: 생성한 IOCP Handle을 넣어줌
- CompletionKey: 식별용 key를 넣어줌 (아무거나 넣으면 됨)
- Key 값을 Session으로 넣어서 GetQueuedCompletionStatus() 이후 세션을 사용할 수 있게 할 수 있음
- NumberOfConcurrentThreads: iocp에서 활용할 코어 개수 (0으로 입력하면 최대로 잡아줌)
- Completion Port 생성
GQCS (GetQueuedCompletionStatus)
BOOL bRet = GetQueuedCompletionStatus(
HANDLE CompletionPort, // [입력] 지켜볼 IOCP 전광판 핸들
LPDWORD lpNumberOfBytesTransferred, // [출력] 실제 송수신된 데이터 크기(Byte)를 받을 주소
PULONG_PTR lpCompletionKey, // [출력] 등록 시 넣었던 식별 Key를 받을 주소
LPOVERLAPPED* lpOverlapped, // [출력] 요청 시 던졌던 Overlapped 구조체 주소를 받을 주소
DWORD dwMilliseconds // [입력] 대기 시간 (INFINITE: 무한 대기)
);
- Completion Port에서 IO 작업이 끝날 때까지 대기하여 결과를 받아옴
- Blocking 함수임
- Completion Port에 등록된 소켓 IO 작업 중 완료가 되면 OS가 대기 중인 스레드 중 하나를 깨움
- 함수 인자
- CompletionPort: IOCP Handle을 넣어줌
- lpNumberOfBytesTransferred: 전송/수신된 데이터의 크기를 받아올 객체를 넣어줌
- lpCompletionKey: 식별용 key를 받아옴
- 이를 통해 Session 정보를 받아올 수 있음
- lpOverlapped: overlapped 정보를 가져올 객체를 넣어줌
- 마찬가지로 overlapped 정보를 가져올 수 있음
- OverlappedEx 구조체를 만들어, type을 추가하여 어떤 IO 작업이었는지 확인 가능하게 할 수 있음
- dwMilliseconds: IO 작업이 끝나기까지 기다릴 대기 시간을 넣어줌
- INFINITE로 설정하면 일감이 올 때까지 무한정 대기함
GameServer.cpp
#include "CoreGlobal.h"
#include "pch.h"
#include <iostream>
#include "CorePch.h"
#include <atomic>
#include <mutex>
#include <Windows.h>
#include <future>
#include "ThreadManager.h"
#include <handleapi.h>
#include <ioapiset.h>
#include <urlmon.h>
#include <winSock2.h>
#include <mswsock.h>
#include <WS2tcpip.h>
#include <winnt.h>
#include "Memory.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;
};
enum IO_TYPE {
READ,
WRITE,
ACCEPT,
CONNECT,
};
struct OverlappedEx {
WSAOVERLAPPED overlapped = {};
IO_TYPE type = IO_TYPE::READ; // 0
};
void WorkerThreadMain(HANDLE iocpHandle) {
while (true) {
DWORD bytesTransferred = 0;
Session* session = nullptr;
OverlappedEx* overlappedEx = nullptr;
bool ret = ::GetQueuedCompletionStatus(iocpHandle, &bytesTransferred, (ULONG_PTR*)&session,
(LPOVERLAPPED*)&overlappedEx, INFINITE);
if (ret == false || bytesTransferred == 0) {
// TODO: 연결 끊김
continue;
}
ASSERT_CRASH(overlappedEx->type == IO_TYPE::READ); // 현재 recv만 작업중
cout << "Recv Data! Len =" << bytesTransferred << endl;
WSABUF wsaBuf;
wsaBuf.buf = session->recvBuffer;
wsaBuf.len = BUFFER_SIZE;
DWORD recvBytes = 0;
DWORD flags = 0;
// Recv가 아니라면 overlappedEx->type을 변경해서 넣어줘야 함
::WSARecv(session->socket, &wsaBuf, 1, &recvBytes, &flags, &overlappedEx->overlapped, NULL);
}
}
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;
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;
// 모든 세션(Client)을 관리해주는 session 관리자가 있다고 가정
vector<Session*> sessionManager;
HANDLE iocpHandle = ::CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
// Worker Thread 생성
for (int32 i = 0; i < 5; i++) {
GThreadManager->Launch([=]() { WorkerThreadMain(iocpHandle); });
}
while (true) {
SOCKADDR_IN clientAddr;
int clientAddrLen = sizeof(clientAddr);
// 추후에 accept도 비동기로 바꾸면 코드가 깔끔해질 예정
SOCKET clientSocket = ::accept(listenSocket, (SOCKADDR*)&clientAddr, &clientAddrLen);
if (clientSocket == INVALID_SOCKET)
return 0;
Session* session = new Session();
session->socket = clientSocket;
sessionManager.push_back(session);
cout << "Connected to Client!" << endl;
::CreateIoCompletionPort((HANDLE)clientSocket, iocpHandle, (ULONG_PTR)session, 0);
WSABUF wsaBuf;
wsaBuf.buf = session->recvBuffer;
wsaBuf.len = BUFFER_SIZE;
OverlappedEx* overlappedEx = new OverlappedEx();
overlappedEx->type = IO_TYPE::READ;
DWORD recvBytes = 0;
DWORD flags = 0;
::WSARecv(clientSocket, &wsaBuf, 1, &recvBytes, &flags, &overlappedEx->overlapped, NULL);
// ::closesocket(session.socket);
// ::WSACloseEvent(wsaEvent);
// ---------------------------------------
}
GThreadManager->Join();
// 윈속 종료
::WSACleanup();
return 0;
}
- Main Thread
- 각 클라이언트에 대한 역할: Accept() 후 WSARecv()을 한 번 호출하고 끝
- Accept & WSARecv만 담당하므로 Non-Blocking이 필요 없음

CompletionKey vs Overlapped vs iocpHandle
CompletionKey: 손님의 이름표 (혹은 테이블 번호)
- 손님(clientSocket)이 카페(Server)에 들어와서 자리를 잡을 때 딱 한 번 부여받는 고유 ID
- "이 손님은 7번 테이블 손님이다"라는 정보는 그 손님이 나갈 때까지 변하지 않음
OVERLAPPED 구조체: 개별 주문서 (진동벨 그 자체)
- OVERLAPPED는 하나의 I/O 요청이 누구의 것인지, 어떤 상태인지를 담고 있는 개별 진동벨
- 역할: "이 데이터는 A번 세션의 것이고, 현재 읽기 작업 중이다"라는 정보를 담음
- 특징: WSARecv를 호출할 때마다 매번 던져줘야 함. 주문을 10번 하면 진동벨도 10개가 필요하듯, I/O 요청마다 별도의 OVERLAPPED 객체(혹은 이를 확장한 OverlappedEx)가 필요
iocpHandle: 전광판 (진동벨이 울리는 수신 장치)
- iocpHandle은 수많은 진동벨(Overlapped) 중 어떤 것이 울렸는지 한데 모아서 알려주는 전광판
- 역할: 수천 개의 진동벨을 일일이 들여다볼 필요 없이, 전광판(GetQueuedCompletionStatus)만 보고 있으면 "방금 5번 벨 울렸다"라고 알려줌
- 특징: 여러 스레드가 이 전광판 하나를 같이 쳐다보고 있다가, 주문이 완료되는 대로 가장 먼저 손이 남는 스레드가 튀어나가서 업무를 처리
Problem) 유저가 Accept 이후 접속 종료
// Problem) 유저가 접속 종료함
Session* s = sessionManager.back();
sessionManager.pop_back();
delete s;
- Main Thread의 첫 번째 WSARecv() 다음 줄에 위 코드를 넣어서, "유저가 WSASend()를 한 이후에, 접속을 종료하여 session이 사라진 상황"을 가정
- GQCS() 이후 값을 확인하면 session에 쓰레기 값이 들어가 문제가 발생하지만 CRASH가 나진 않음

- Completion Port에 넣은 session, overlapped들은 IO 작업이 걸려있는 상황에선 삭제가 불가능하게 만들어줘야 함
- Reference count 같은 방법을 사용해야 함
- 이전에 만든 Stomp Allocator를 사용하여 Use-After-Free 오류를 찾아 CRASH를 유도
[C++/Game Server] Stomp Allocator (Use-after-free & Overflow 오류 검증)
Use-After-Free 오류용 Stomp Allocatornew & deleteint main() { SYSTEM_INFO info; ::GetSystemInfo(&info); cout _hp = 100; delete test; test->_hp = 200; return 0;}test->_hp=200; 에서 CRASH가 나지 않음new, delete 같은 힙 할당이 유동적으로
dh-0e.tistory.com


- Stomp Allocator: Session 메모리를 해제(xdelete)하는 순간 커널 함수(VirtualFree)를 이용해 해당 메모리가 위치한 페이지 자체의 Read/Write 권한을 박탈

- 이후 권한이 박탈당한 페이지(xdelete 된 session이 저장되었던 페이지)에 접근 시도를 감지하여 CRASH를 내줌
PQCS (PostQueuedCompletionStatus)
BOOL bRet = PostQueuedCompletionStatus(
HANDLE CompletionPort, // [입력] 신호를 보낼 대상 IOCP 핸들
DWORD dwNumberOfBytesTransferred, // [입력] GQCS가 받을 바이트 수 (임의 지정)
ULONG_PTR dwCompletionKey, // [입력] GQCS가 받을 Key (임의 지정)
LPOVERLAPPED lpOverlapped // [입력] GQCS가 받을 Overlapped 주소 (임의 지정)
);
- 일감을 Completion Port에 억지로 집어넣는 함수
- GQCS (Get...): 바구니에 일감이 들어올 때까지 기다렸다가 하나 꺼내옴 (소비자)
- PQCS (Post...): 바구니에 내가 직접 일감을 하나 던져 넣음 (생성자)
- 사용 사례
- 워커 스레드 종료 (퇴근 신호)
- bytesTransferred에 0을 넣거나, 특정 예약된 CompletionKey를 PQCS로 던지면, GQCS에서 대기하던 스레드가 깨어나서 종료 신호임을 인지하고 루프를 빠져나가게 설계되어 있음
- 사용자 정의 이벤트 처리
- 네트워크 I/O는 아니지만, 워커 스레드들이 처리해줬으면 하는 로직(예: 로직 엔진의 특정 이벤트, 타이머 종료 등)이 있을 때 IOCP 큐를 이용해 일을 시킬 수 있음
- while(true) 루프를 돌며 GQCS에서 무한 대기 중인 워커 스레드들은 프로그램이 끝날 때까지 깨어나지 않음
- 이때 메인 스레드에서 프로그램을 종료하기 위해 스레드들을 모두 종료하라는 신호를 보내야 하는데, 이때 PQCS를 사용
- 보통 이 경우로 많이 사용함
- 워커 스레드 종료 (퇴근 신호)
'C++ > Game Server' 카테고리의 다른 글
| [C++/Game Server] IocpCore (0) | 2026.03.31 |
|---|---|
| [C++/Game Server] Socket Utils (WinSock Wrapper) (0) | 2026.03.29 |
| [C++/Game Server] Overlapped Model (Callback 함수 기반) (0) | 2026.03.25 |
| [C++/Game Server] Overlapped Model (Event 기반) (0) | 2026.03.24 |
| [C++/Game Server] WSAEventSelect Model (0) | 2026.03.23 |
