본문으로 건너뛰기

PrivateServerToolKit

2026.08 – 2026.09 · 개인

C++ · C# · Code Generation

C++/C# 패킷 코드 생성과 서비스 연결을 자동화한 서버 개발 도구

JSON 패킷 스키마에서 C++과 C# 코드를 생성하는 흐름

주요 내용

구조

공통 스키마에서 생성된 코드는 Service Host까지 전달되고, 모든 모듈이 공통 결과 계약인 TkResult, TkDiagnostic을 사용합니다.

설계 결정

  1. ① 패킷 정의는 스키마 하나로

    문제
    패킷마다 C++ struct와 C# record, 필드 배치, Encode/Decode 코드를 추가해야 함
    선택
    JSON 스키마에서 공통 Descriptor를 만들고 언어별 코드를 생성, 구조는 Protobuf를 참고하되 기존 바이트 규칙은 유지
    트레이드오프
    생성기는
    자세히
  2. ② 생성 이후의 연결도 공통으로

    문제
    코드를 생성한 뒤에도 패킷마다 검증과 handler 연결 작업이 반복됨
    선택
    Decode·입력 검증 → middleware → handler → 응답 Encode를 하나의 파이프라인으로 두고, 패킷별 연결은 binding으로 선언
    트레이드오프
    요청마다 Service Job을 할당하고, 제출에 성공한 Job은 쓰는 쪽이 실행하고 정리해야 함
    자세히
  3. ③ 결과는 공통 계약으로

    문제
    계층을 새로 만들 때마다 전용 결과 타입을 다시 정의함
    선택
    제어 흐름용 TkResult와 상세 원인을 담는 TkDiagnostic(callback)을 분리하고, 공통 계층은 header-only의 작은 계약만 소유
    트레이드오프
    상세 원인을 받으려면 쓰는 쪽이 callback과 diagnostic 수명을 관리해야 함
    자세히
  4. ④ I/O 계층은 I/O 작업에만 집중한다

    문제
    PrivateServer에서는 IOCP Worker가 패킷 헤더만 검증해 세션 Mailbox에 넣을 때, 세부 처리를 맡을 Actor Worker의 실행 예약까지 함께 처리해야 했음
    선택
    Akka 메일박스를 참고해 실행 예약을 Mailbox(Work Lane)가 관리하도록 Execution 레이어로 분리, IOCP Worker는 게시만 하고 실행은 Worker Pool이 lane 단위로 처리(같은 lane은 직렬, 다른 lane은 병렬)
    트레이드오프
    Mailbox는 작업이 가리키는 대상을 소유하지 않기 때문에, 그 대상의 라이프사이클 관리는 사용자가 담당해야 함
    자세히

사용 예시

패킷 정의 — MovementInput

C++과 C#에서 따로 작성하던 바이트 배치를 스키마 한 곳으로 옮기고, 스키마를 바탕으로 패킷 코드를 생성합니다.

이전 · C#에서 직접 작성하던 바이트 배치 (C++도 같은 작업을 반복)
V1WireCodec.WriteVersion(output);
V1WireCodec.WriteU32(ControlledEntityGeneration, 2, output);
V1WireCodec.WriteU32(TargetServerTick, 6, output);
V1WireCodec.WriteI16(MoveX, 10, output);
V1WireCodec.WriteI16(MoveY, 12, output);
이후 · 공통 스키마
{
  "packet": {
    "name": "MovementInput",
    "id": 257,
    "fields": [
      { "name": "controlledEntityGeneration", "type": "uint32" },
      { "name": "targetServerTick", "type": "uint32" },
      { "name": "moveX", "type": "int16" },
      { "name": "moveY", "type": "int16" }
    ]
  }
}

서비스 연결 — TimeSync

handler에는 요청을 받아 응답을 채우는 코드만 남고, 패킷과 handler의 연결은 binding으로 선언합니다.

handler
TkResult OnTimeSync(
    const TkServiceContext&,
    const WorldTimeSyncRequest& request,
    WorldTimeSyncResponse* outResponse) noexcept
{
    outResponse->probeSequence = request.probeSequence;
    outResponse->serverTick = currentServerTick;
    return TK_SUCCESS;
}
binding 등록
constexpr auto timeServiceBindings = pstk::service::MakeBindings(
    pstk::service::BindRequestResponse<
        TimeService, WorldTimeSyncRequest,
        WorldTimeSyncResponse, &TimeService::OnTimeSync>());

pstk::service::RegisterService(
    host, timeService, executor, middlewares, timeServiceBindings);

관련 글

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