본문으로 건너뛰기

PrivateServer

2026.06 – 2026.08 · 개인

C++ · Windows IOCP · Godot

C++, Windows IOCP로 만든 실시간 서버 프로젝트

주요 내용

구조

입력은 왼쪽에서 오른쪽으로, 판정 결과는 오른쪽에서 왼쪽으로 흐릅니다. 번호는 아래 설계 결정 번호와 같습니다.

설계 결정

  1. ① 세션 상태는 한 번에 한 Worker만 바꾼다

    문제
    여러 IOCP 작업 스레드가 같은 세션 상태를 동시에 바꿀 수 있음
    선택
    완료 이벤트를 세션별 Mailbox에 넣고 Actor Worker 하나만 처리, 실행 예약 누락은 Admission Gate(atomic + CAS)로 방지
    트레이드오프
    이벤트마다 Mailbox를 거치기 때문에 발생하는 Admission Gate 비용
    자세히
  2. ② I/O 메모리는 용량을 정해 둔다

    문제
    I/O context와 payload 메모리를 역할별로 제한하고 관측해야 함
    선택
    역할별 고정 블록 풀, 고갈은 PoolExhausted 예외로 명시적으로 처리하고 수명은 RAII로 관리
    트레이드오프
    블록 내부 단편화, 공유 free-list 경합으로 스레드 4개부터 new/delete보다 느림
    자세히
  3. ③ 입력은 tick 경계에서 확정한다

    문제
    비동기로 도착하는 네트워크 이벤트를 바로 반영하면, 같은 tick 안에서도 도착 순서에 따라 판정 결과가 달라질 수 있음
    선택
    이벤트를 모았다가 tick 시작에 읽기 전용 입력으로 확정, 계산 결과는 Coordinator가 정한 순서로 반영
    트레이드오프
    입력이 한 tick 늦게 반영되고, tick 경계 안의 입력 순서까지는 보장하지 않음
    자세히
  4. ④ payload 송신은 tick 계산을 블로킹하지 않아야 한다

    문제
    더블 버퍼를 사용해도 이전 송신이 끝날 때까지 다음 tick 계산이 블로킹됨
    선택
    트리플 버퍼를 사용해 송신 중, 대기, 계산용 버퍼 세 개를 동시에 사용
    트레이드오프
    고부하인 Peak 구간 p99는 5.78% 줄었지만, 부하가 낮은 Early·Late 구간 p99는 늘고(+0.22 ms, +1.01 ms) 메모리 +4.86 MiB
    자세히
  5. ⑤ 네이티브 수명은 SafeHandle이 관리한다

    문제
    C++ NetworkRuntime을 Godot C#에서 써야 하고, 네이티브 객체 수명이 C# 가비지 컬렉터와 어긋날 수 있음
    선택
    opaque handle과 C 함수로 만든 C ABI 브리지, 수명은 SafeHandle이 소유하고 이벤트는 메인 스레드에서 가져옴
    트레이드오프
    경계를 넘을 때 payload를 복사해 소유권을 바꾸는 비용
    자세히

검증

로컬 · 독립 World Host 2개 × 네이티브 클라이언트 100개 · 60Hz · 180초 · x64 Release · 3회 측정 중앙값

라운드 완료
Host 2개 × 클라이언트 100개
Peak(고부하) 구간 tick p99 (더블 → 트리플 버퍼)
16.607 → 15.648 ms
tick 예산(16.67 ms) 초과 비율
0.954% → 0.629%
메모리(Working set)
58.25 → 63.11 MiB

플레이 흐름

  1. 1. 접속할 World Host 채널 선택

  2. 2. 닉네임 입력 후 접속, 서버가 플레이어 ID 발급

  3. 3. 이동은 클라이언트가 먼저 예측해 보여 주고, 판정은 서버가 맡음

  4. 4. 시뮬레이션에 참여하지 않는 Observer의 라운드 관측

  5. 5. 라운드 종료 후 순위와 승자 표시

관련 글

서버·시스템 (C++) 프로젝트 보기