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인 경우

  1. 먼저 Clean() 시도
  2. 그래도 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일 때 어떤 순서로 대응해야 안전한가?
  • 헤더 기반 루프에서 "반쪽 패킷"을 구분하는 조건은 정확히 무엇인가?

profile
李家네_공부방

0개의 댓글