ReceiveBuffer가 필요한 이유
TCP는 스트림이다
- 보낸 단위와 받은 단위가 일치하지 않습니다.
100B를 보내도 10 + 90, 40 + 30 + 30, 또는 여러 패킷이 합쳐져 올 수 있습니다.
- 즉, "수신 콜백 1회 = 패킷 1개"라는 가정은 틀립니다.
핵심 문제
- 반쪽 패킷을 파싱하면 헤더/길이/본문이 꼬입니다.
- 합쳐진 패킷을 하나로 처리하면 다음 패킷 경계가 무너집니다.
해결 방향
ReceiveBuffer는 "완전한 패킷 경계가 확인될 때까지 보관"하는 누적 버퍼입니다.
내부 구조와 불변식(invariant)
구성 요소
| 멤버 | 의미 |
|---|
Buffer | 실제 바이트 저장소 |
ReadPos | 아직 처리하지 않은 데이터 시작 위치 |
WritePos | 새 데이터가 써질 위치 |
파생 값
DataSize = WritePos - ReadPos
FreeSize = Capacity - WritePos
반드시 지킬 불변식
0 <= ReadPos <= WritePos <= Capacity
이 불변식이 깨지면 이후 파싱/복사 모든 로직이 오염됩니다.
OnWrite / OnRead / Clean 규칙
OnWrite
- 커널이 채운 바이트 수만큼
WritePos를 증가시킵니다.
OnWrite(numBytes) 전제: numBytes <= FreeSize.
OnRead
- 파서가 소비한 바이트 수만큼
ReadPos를 증가시킵니다.
OnRead(processedLen) 전제: processedLen <= DataSize.
Clean
| 상황 | 동작 |
|---|
ReadPos == WritePos | 데이터 없음 -> 둘 다 0으로 리셋 |
ReadPos > 0 && 데이터 잔존 | 남은 데이터를 앞으로 memmove 후 커서 재정렬 |
ReadPos == 0 | 이동 불필요, 그대로 유지 |
구현 주의
- 겹치는 메모리 이동은
memcpy가 아니라 memmove를 사용해야 안전합니다.
RegisterRecv에서의 버퍼 연결
작성 위치
WSABUF.buf는 항상 WritePos부터 시작해야 합니다.
- 길이는
FreeSize까지만 요청합니다.
WSABUF wsaBuf;
wsaBuf.buf = reinterpret_cast<char*>(recvBuffer.WritePtr());
wsaBuf.len = static_cast<ULONG>(recvBuffer.FreeSize());
FreeSize가 0인 경우
- 먼저
Clean() 시도
- 그래도
FreeSize == 0이면
- 패킷 과대/파싱 정체/공격 가능성으로 간주
- 로그 후 세션 종료 또는 버퍼 확장 정책 적용
핵심
- "버퍼 공간이 없는데도 Recv 등록"은 즉시 버그로 이어집니다.
패킷 파싱 루프 표준 패턴
헤더 기반 예시
while (recvBuffer.DataSize() >= sizeof(PacketHeader))
{
PacketHeader header;
memcpy(&header, recvBuffer.ReadPtr(), sizeof(header));
const uint32 packetSize = header.size;
if (packetSize < sizeof(PacketHeader))
{
Disconnect(L"invalid packet size");
return;
}
if (recvBuffer.DataSize() < packetSize)
break;
HandlePacket(recvBuffer.ReadPtr(), packetSize);
recvBuffer.OnRead(packetSize);
}
recvBuffer.Clean();
Session 구현과의 연결
OnRecv가 반환한 processedLen도 결국 이 루프의 소비량 개념과 동일합니다.
processedLen 검증(0 <= processedLen <= dataSize)을 반드시 유지하세요.
악성 입력 방어
packetSize 상한(예: max 64KB) 검사
- 비정상 헤더 값 즉시 차단
버퍼 크기 전략과 성능
크기 선택 기준
| 전략 | 장점 | 단점 |
|---|
| 작게(예: 4KB) | 메모리 절약 | Clean/재등록 빈도 증가 |
| 중간(예: 16~64KB) | 균형적 | 트래픽 편차에 민감 |
| 크게(예: 128KB+) | 복사 빈도 감소 | 동접이 많으면 메모리 부담 |
실무 권장
- 초기값은 트래픽 특성(평균 패킷, 최대 패킷)으로 정하고, 측정으로 조정합니다.
- 동접 * 버퍼 크기가 총 메모리 상한을 넘지 않도록 계산해야 합니다.
관측 지표
- 세션별
Clean 호출 빈도
FreeSize == 0 발생률
- 파싱 실패 횟수/비정상 패킷 비율
자주 나는 버그
| 버그 | 증상 | 원인 |
|---|
OnRead 과소/과대 이동 | 패킷 경계 붕괴 | processedLen 검증 누락 |
Clean 누락 | FreeSize 고갈, Recv 정지 | 커서 정리 타이밍 미흡 |
memcpy로 겹침 이동 | 간헐 데이터 깨짐 | memmove 미사용 |
| 헤더 크기 검증 없음 | OOB 파싱/크래시 | 비정상 입력 방어 부재 |
강의 시 유의사항
강조 포인트
- "TCP는 메시지 경계가 없다"를 이 파트의 1번 원칙으로 반복하세요.
- ReceiveBuffer는 단순 배열이 아니라 프로토콜 정합성 장치입니다.
- Part 8(
OnRecv 반환 계약)과 연결해서 설명해야 이해가 완성됩니다.
자주 하는 오해
| 오해 | 바로잡기 |
|---|
| recv 한 번이면 패킷 하나다 | 스트림이라 분할/병합이 항상 가능 |
Clean은 성능 최적화용 부가 기능이다 | 커서 일관성과 공간 회수를 위한 필수 로직 |
| 버퍼는 크게만 잡으면 해결된다 | 메모리/동접/파싱 정책을 함께 봐야 한다 |
체크 질문 (스스로 답해보기)
ReadPos, WritePos 불변식이 깨지면 어떤 장애가 즉시 발생하는가?
FreeSize == 0일 때 어떤 순서로 대응해야 안전한가?
- 헤더 기반 루프에서 "반쪽 패킷"을 구분하는 조건은 정확히 무엇인가?