패킷 규약(Contract) 먼저 고정
기본 형식
┌──────────┬──────────┬─────────────────────┐
│ Size(2B) │ ID(2B) │ Payload (가변) │
└──────────┴──────────┴─────────────────────┘
Size: 패킷 전체 길이(헤더 포함)로 고정하는 것이 가장 직관적입니다.
ID: 패킷 종류 식별자(로그인/이동/공격 등).
구조체와 검증
struct PacketHeader
{
uint16 size;
uint16 id;
};
static_assert(sizeof(PacketHeader) == 4);
규약 문서화 항목
size가 헤더 포함인지 여부
- 최대 패킷 크기(
kMaxPacketSize)
- 허용 ID 범위/예약 ID
- 바이트 오더(서버-클라 이기종 가능성 있으면 네트워크 바이트 오더 명시)
송신 측 직렬화 원칙
순서
- Payload 작성
- Header(
size, id) 채움
- Header + Payload를 하나의 전송 단위로 enqueue
예시
PacketHeader h{};
h.size = static_cast<uint16>(sizeof(PacketHeader) + payloadSize);
h.id = PKT_C_LOGIN;
실수 방지
size 계산 시 오버플로우/캐스팅 범위 확인
- payload 없는 패킷도
size >= sizeof(PacketHeader) 조건 유지
수신 측 파싱 상태 머신
표준 루프
DataSize >= sizeof(PacketHeader)인지 확인
- 헤더를 임시 변수로 복사해 읽기
size 검증(최소/최대)
DataSize >= header.size면 완전체 처리
OnRead(header.size) 후 다음 패킷 반복
예시 코드
while (recvBuffer.DataSize() >= sizeof(PacketHeader))
{
PacketHeader h;
memcpy(&h, recvBuffer.ReadPtr(), sizeof(h));
if (h.size < sizeof(PacketHeader) || h.size > kMaxPacketSize)
{
Disconnect(L"invalid packet size");
return;
}
if (recvBuffer.DataSize() < h.size)
break;
OnRecvPacket(recvBuffer.ReadPtr(), h.size);
recvBuffer.OnRead(h.size);
}
recvBuffer.Clean();
핵심
- "헤더 가능"과 "완전체 가능"은 다른 조건입니다.
- 이 두 조건을 분리해야 경계 없는 TCP에서 안전하게 동작합니다.
검증과 보안(필수)
기본 검증 체크리스트
size < sizeof(PacketHeader) 차단
size > kMaxPacketSize 차단
- 정의되지 않은
id 차단
- payload 길이와 타입 규약 불일치 차단
왜 필요한가
- 클라이언트 입력은 신뢰할 수 없습니다.
- 헤더 조작만으로도 OOB 파싱, 메모리 압박, 서비스 지연을 유발할 수 있습니다.
대응 정책
| 상황 | 권장 대응 |
|---|
| 단순 포맷 오류 1회 | 경고 로그 후 세션 종료 |
| 반복 비정상 패킷 | 즉시 차단 + 모니터링 알림 |
| 대량 유사 패턴 | IP/계정 단위 rate limit 고려 |
길이 기반 프레이밍 vs 구분자 기반
| 방식 | 장점 | 단점 |
|---|
길이 기반(size header) | 바이너리 안전, 파싱 고속, 명확한 완전체 판별 | 헤더 검증 필수 |
구분자 기반(\n, \0) | 텍스트 프로토콜 구현 단순 | 바이너리 payload에서 충돌 위험 |
게임 서버의 바이너리 패킷에는 길이 기반 프레이밍이 일반적으로 더 안전합니다.
운영 관측 포인트
로그 필수 항목
sessionId, packetId, packetSize, dataSizeAtParse
- 파싱 실패 사유 코드(
sizeTooSmall, sizeTooLarge, unknownId)
지표
- 초당 파싱 성공/실패 횟수
- 평균/최대 패킷 크기
- 비정상 ID 비율
디버깅 팁
- 의심 세션은 헤더 16~32바이트 hex dump를 남기면 원인 파악 속도가 빨라집니다.
강의 시 유의사항
강조 포인트
- TCP는 메시지 경계가 없고, 패킷 포맷이 그 경계를 복구합니다.
- 패킷 포맷의 핵심은 "읽는 방법"보다 "검증 규칙"입니다.
- Part 9(ReceiveBuffer)와 Part 12(PacketSession)을 연결해 설명해야 이해가 완성됩니다.
자주 하는 오해
| 오해 | 바로잡기 |
|---|
| 헤더만 읽히면 바로 처리 가능 | 완전체(DataSize >= size) 확인이 먼저다 |
| 정상 클라만 오니 검증은 나중에 | 입력 검증 누락은 즉시 취약점이 된다 |
| 구분자 방식이 더 쉽고 안전 | 바이너리 프로토콜에서는 충돌/오검출 위험이 크다 |
체크 질문 (스스로 답해보기)
size가 헤더 크기보다 작을 때 즉시 종료해야 하는 이유는?
- 파싱 루프에서
break와 Disconnect를 구분하는 기준은?
unknown packet id를 발견했을 때 서버 정책을 어떻게 둘 것인가?