PacketHandler의 책임
한 줄 정의
PacketHandler는 PacketSession이 넘긴 완전체 패킷을 packetId 기준으로 비즈니스 처리 함수에 라우팅하는 계층입니다.
책임 분리
| 계층 | 역할 |
|---|
PacketSession | 경계 복구, 헤더 검증, 완전체 전달 |
PacketHandler | ID 분기, 역직렬화, 핸들러 실행 |
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_* 패턴(전송용 패킷 생성)
표준 순서
SendBuffer 확보
PacketHeader 자리 예약
- payload 직렬화
- 최종
size/id 채움
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
가변 데이터 규칙
- 길이(또는 개수) 먼저 기록
- 그 다음 실제 데이터 기록
예시
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에서 테이블 방식으로 바꾸면 유지보수성이 왜 좋아지는가?
- 가변 길이 리스트를 직렬화할 때 "개수 먼저" 규칙이 왜 필요한가?
- 역직렬화 실패 시 세션 종료와 패킷 드롭 중 어떤 정책을 선택할지 기준은 무엇인가?