IOCP 엔진화 개요

Jaemyeong Lee·2024년 8월 14일

게임 서버1

목록 보기
140/220

배경 및 목표

지금까지 만든 것과 한계

  • IOCP 기반 송수신(WSARecv, WSASend) 자체는 이미 동작합니다.
  • 하지만 샘플 코드 수준에서는 네트워크 처리, 게임 로직, 수명 관리가 한곳에 섞이기 쉽습니다.
  • 이 상태로는 다른 프로젝트에서 재사용하거나 팀 단위로 유지보수하기 어렵습니다.

엔진화의 의미

항목목표
Core(엔진)IOCP, 세션, 버퍼, 이벤트 디스패치 같은 공통 인프라 담당
Game(컨텐츠)패킷 해석 이후 게임 규칙/월드 상태 처리 담당
경계Core는 게임 규칙을 모름, Game은 Winsock 저수준 API를 모름

이 파트의 핵심 질문

"무엇을 라이브러리로 빼야 오래 버티는가?"

  • 답은 IOCP 3단계 흐름 + Register/Dispatch/Process 분리 + 수명/재등록 규칙 고정입니다.

IOCP 핵심 3단계

단계별 의미

단계API시점의미
1CreateIoCompletionPort(INVALID_HANDLE_VALUE, ...)서버 시작Completion Port(완료 큐) 생성
2CreateIoCompletionPort((HANDLE)socket, iocp, key, ...)소켓 준비 후소켓을 IOCP에 연결(Association)
3GetQueuedCompletionStatus워커 루프완료 이벤트 dequeue + 처리

최소 코드 패턴

HANDLE iocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, nullptr, 0, 0);
if (iocp == nullptr) {
    // 생성 실패
}

// key에는 보통 Session* 또는 IocpObject*를 넣는다.
if (CreateIoCompletionPort(reinterpret_cast<HANDLE>(sock), iocp,
                           reinterpret_cast<ULONG_PTR>(object), 0) == nullptr) {
    // 등록 실패
}

DWORD bytes = 0;
ULONG_PTR key = 0;
OVERLAPPED* ov = nullptr;
BOOL ok = GetQueuedCompletionStatus(iocp, &bytes, &key, &ov, INFINITE);

GQCS 결과 해석 기본

조건해석대응
ok == TRUE정상 완료ioType별 처리
ok == FALSE && ov != nullptr완료는 왔지만 작업 실패오류 코드 기록 후 정리
ok == FALSE && ov == nullptr종료/제어 경로 가능성워커 종료 조건 확인

Register / Dispatch / Process 분리

왜 분리하는가

  • 등록(Register), 전달(Dispatch), 처리(Process)를 섞으면 버그가 생겼을 때 원인 추적이 어렵습니다.
  • 각 단계의 책임을 분리하면 테스트/로깅/확장이 쉬워집니다.

역할 정의

단계책임대표 함수
RegisterX비동기 요청 등록(낚싯대 던짐)RegisterRecv, RegisterSend, RegisterAccept
Dispatch완료 이벤트를 객체/타입으로 라우팅object->Dispatch(ctx, bytes)
ProcessX완료 결과 실제 처리 + 다음 요청 재등록ProcessRecv, ProcessSend, ProcessAccept

흐름 다이어그램

flowchart TD
    A[RegisterX<br/>WSARecv, WSASend, AcceptEx] --> B[커널에서 비동기 I/O 진행]
    B --> C[IOCP 큐 완료 도착]
    C --> D[GQCS로 key/ov 수신]
    D --> E[object->Dispatch ctx]
    E --> F[ProcessX]
    F --> G[필요 시 재등록 RegisterX]
    G --> A

재등록 규칙

  • ProcessRecv 끝에서 다음 RegisterRecv를 누락하면 수신이 멈춥니다.
  • ProcessAccept 끝에서도 다음 RegisterAccept를 넣어 accept 파이프라인을 유지해야 합니다.

확장 함수 (AcceptEx, ConnectEx, DisconnectEx)

왜 동적 로드가 필요한가

  • 이 함수들은 일반 Winsock import 함수처럼 정적으로 바로 호출되지 않습니다.
  • WSAIoctl(SIO_GET_EXTENSION_FUNCTION_POINTER)로 런타임에 함수 주소를 얻어야 합니다.

로드 예시

LPFN_ACCEPTEX acceptEx = nullptr;
LPFN_CONNECTEX connectEx = nullptr;
LPFN_DISCONNECTEX disconnectEx = nullptr;

DWORD bytes = 0;
GUID guidAcceptEx = WSAID_ACCEPTEX;
WSAIoctl(dummySock, SIO_GET_EXTENSION_FUNCTION_POINTER,
         &guidAcceptEx, sizeof(guidAcceptEx),
         &acceptEx, sizeof(acceptEx),
         &bytes, nullptr, nullptr);

실무 주의사항

  • WSAStartup 이후에 로드해야 합니다.
  • 같은 provider 계열 소켓에서 얻은 함수 포인터를 사용해야 안전합니다.
  • ConnectEx는 호출 전에 소켓 bind가 필요합니다.

엔진 부트스트랩 전체 흐름

초기화 시퀀스

[Server Start]
  -> WSAStartup
  -> 확장 함수 포인터 로드(AcceptEx/ConnectEx/DisconnectEx)
  -> IOCP 생성
  -> Listener 생성 + IOCP 등록
  -> RegisterAccept N개 선등록
  -> Worker Thread 시작(Dispatch Loop)

런타임 시퀀스

  1. Accept 완료 -> Session 할당 -> IOCP 등록
  2. Session에 RegisterRecv 선등록
  3. Worker가 완료를 받아 Dispatch -> Process
  4. 처리 후 필요한 I/O를 재등록

설계 핵심 문장

엔진은 "등록과 완료 전달"을 책임지고, 컨텐츠는 "패킷 의미와 게임 규칙"을 책임진다.


강의 시 유의사항

강조 포인트

  • 이 파트의 본질은 함수 암기가 아니라 역할 분리입니다.
  • "등록 -> 완료 통지 -> 처리 -> 재등록" 루프를 몸에 익히면 Part 2 이후가 쉬워집니다.
  • 특히 재등록 누락과 수명 관리 실패가 실전 버그의 대부분입니다.

자주 하는 오해

오해바로잡기
IOCP는 API 몇 개만 알면 끝난다객체 경계/수명/종료 절차가 핵심이다
GQCS에서 FALSE면 모두 치명 오류ov 유무로 실패 완료/제어 경로를 구분해야 한다
Accept 완료만 처리하면 된다다음 RegisterAccept를 넣어 파이프라인을 유지해야 한다

체크 질문 (스스로 답해보기)

  • Core와 Game 사이 경계를 코드 레벨로 어디에 둘 것인가?
  • RegisterRecv를 언제, 어떤 조건에서 다시 호출해야 하는가?
  • 서버 종료 시 워커를 안전하게 종료하려면 어떤 제어 이벤트가 필요한가?

profile
李家네_공부방

0개의 댓글