패킷 핸들러

Jaemyeong Lee·2025년 3월 23일

게임 서버1

목록 보기
152/220

PacketHandler의 책임

한 줄 정의

PacketHandlerPacketSession이 넘긴 완전체 패킷을 packetId 기준으로 비즈니스 처리 함수에 라우팅하는 계층입니다.

책임 분리

계층역할
PacketSession경계 복구, 헤더 검증, 완전체 전달
PacketHandlerID 분기, 역직렬화, 핸들러 실행
GameSession/로직실제 게임 규칙 처리

핵심 효과

  • 파싱/검증 로직과 비즈니스 로직을 분리해 유지보수성을 높입니다.

분기 방식: switch vs 핸들러 테이블

기본 방식(switch)

bool HandlePacket(SessionRef s, const uint8* buf, int32 len)
{
    auto* h = reinterpret_cast<const PacketHeader*>(buf);
    switch (h->id)
    {
    case PKT_C_LOGIN: return Handle_C_LOGIN(s, buf, len);
    case PKT_C_MOVE:  return Handle_C_MOVE(s, buf, len);
    default:          return false;
    }
}

확장 방식(테이블)

using HandlerFunc = bool(*)(SessionRef, const uint8*, int32);
std::array<HandlerFunc, UINT16_MAX + 1> GHandlers{};

bool HandlePacket(SessionRef s, const uint8* buf, int32 len)
{
    const auto* h = reinterpret_cast<const PacketHeader*>(buf);
    HandlerFunc f = GHandlers[h->id];
    return (f != nullptr) ? f(s, buf, len) : false;
}

선택 기준

  • 패킷 수가 적으면 switch도 충분합니다.
  • 패킷 수가 늘고 자동 생성기를 붙일 계획이면 테이블 방식이 관리에 유리합니다.

Server/Client 핸들러 대칭

방향 규칙

접두사 예시의미
C_클라이언트 -> 서버
S_서버 -> 클라이언트

대칭 구조

  • 서버: Handle_C_*를 주로 처리
  • 클라이언트: Handle_S_*를 주로 처리
  • 양쪽 모두 같은 헤더 규약과 직렬화 순서를 공유해야 합니다.

운영 팁

  • ID/이름 매핑을 공용 헤더로 유지하면 양쪽 불일치 버그를 크게 줄일 수 있습니다.

Make_* 패턴(전송용 패킷 생성)

표준 순서

  1. SendBuffer 확보
  2. PacketHeader 자리 예약
  3. payload 직렬화
  4. 최종 size/id 채움
  5. Close(writeSize) 호출

예시

SendBufferRef Make_S_TEST(uint64 id, uint32 hp, uint16 attack)
{
    SendBufferRef sb = std::make_shared<SendBuffer>(4096);
    BufferWriter bw(sb->Buffer(), sb->Capacity());

    PacketHeader* header = bw.Reserve<PacketHeader>();
    bw << id << hp << attack;

    header->size = static_cast<uint16>(bw.WriteSize());
    header->id   = S_TEST;

    sb->Close(bw.WriteSize());
    return sb;
}

주의점

  • Close 누락 시 전송 길이가 0으로 남는 버그가 자주 발생합니다.
  • WriteSize가 최대 크기를 넘는지 항상 검증하세요.

BufferReader / BufferWriter 안전 계약

Writer 계약

  • Reserve<T>()는 남은 공간이 충분해야 합니다.
  • << 연산은 실패(공간 부족) 가능성을 반환값 또는 assert로 표준화하세요.

Reader 계약

  • >>는 읽을 수 있는 바이트가 충분한지 확인해야 합니다.
  • Peek는 커서를 이동하지 않고 읽기 검증에 사용합니다.

권장 규칙

규칙이유
읽기/쓰기 전 길이 검사OOB 접근 방지
실패 시 즉시 false 반환오염된 상태 전파 차단
커서 이동은 성공 후에만상태 일관성 유지

직렬화 순서와 가변 길이 데이터

순서 일치 원칙

  • 쓰는 순서와 읽는 순서가 1바이트라도 다르면 데이터가 즉시 깨집니다.
  • 예: bw << id << hp << attack <-> br >> id >> hp >> attack

가변 데이터 규칙

  1. 길이(또는 개수) 먼저 기록
  2. 그 다음 실제 데이터 기록

예시

uint16 count = static_cast<uint16>(items.size());
bw << count;
for (const auto& v : items)
    bw << v;

실패 처리와 관측 포인트

실패 처리 정책

상황대응
unknown packet id로그 후 드롭 또는 종료 정책 적용
역직렬화 실패세션 종료(프로토콜 위반) 권장
핸들러 예외/실패실패 코드 기록 후 보호 경로 실행

로그 필수 항목

  • sessionId, packetId, packetSize, handlerResult
  • 역직렬화 실패 위치(필드명/오프셋)

디버깅 팁

  • 문제 패킷은 헤더+앞부분 payload hex dump를 함께 남기면 재현 속도가 빨라집니다.

강의 시 유의사항

강조 포인트

  • PacketHandler의 본질은 "분기"가 아니라 "계약 지키기(순서/길이/검증)"입니다.
  • Make_*Handle_*를 한 쌍으로 설명하면 이해가 빠릅니다.
  • Part 11(포맷)과 Part 12(PacketSession)를 연결해 전체 흐름으로 가르치세요.

자주 하는 오해

오해바로잡기
ID 분기만 맞으면 된다역직렬화 순서/길이 검증이 더 중요하다
unknown id는 무시해도 된다반복되면 공격/버전 불일치 신호일 수 있다
Reader/Writer는 단순 편의 클래스다안전 계약을 강제하는 핵심 도구다

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

  • switch에서 테이블 방식으로 바꾸면 유지보수성이 왜 좋아지는가?
  • 가변 길이 리스트를 직렬화할 때 "개수 먼저" 규칙이 왜 필요한가?
  • 역직렬화 실패 시 세션 종료와 패킷 드롭 중 어떤 정책을 선택할지 기준은 무엇인가?

profile
李家네_공부방

0개의 댓글