← 문제로

25. 수신 Stream.Read 반환값 오처리(정상 종료·부분 수신)

난이도 하
내 리뷰 · C#
해설 · C#

해설 — 수신 Stream.Read 반환값 오처리(정상 종료·부분 수신)

난이도: 하

요약

Stream.Read 의 반환값을 잘못 해석한다. 반환 0 은 "데이터 없음"이 아니라 상대의 정상 종료(EOF) 인데 continue 로 재시도해 죽은 연결에서 바쁘게 무한 루프(CPU 100%·좀비 세션)를 돈다(A). 또 한 번의 Read 가 요청보다 적게 줄 수 있는데 헤더·본문 모두 부분 수신을 무시해, 헤더가 덜 차면 길이를 오해석하고(A), 본문이 덜 차면 반쯤 빈 버퍼를 그대로 디스패치한다(B)(C).

문제점

(1) [반환값 의미 오해] 0 = 정상 종료를 재시도로 처리 — (A)

  • 증상: 상대가 연결을 닫으면 Read 가 계속 0 을 반환. if (n <= 0) continue; 가 이를 무한 반복 → 한 코어를 태우고 세션이 정리되지 않음(좀비).
  • 재현: 클라가 정상적으로 소켓을 close.
  • 근본원인: 스트림 API 에서 0 은 EOF(더 이상 데이터 없음, 연결 종료) 라는 계약을 무시.

(2) [부분 수신 미처리] 헤더 — (A)

  • 증상: Read(header,0,4) 가 1~3 만 반환하면 나머지 바이트는 이전/0 값. BitConverter.ToInt32 가 엉뚱한 bodyLen 을 만든다(거대 할당/오파싱).
  • 근본원인: 4바이트가 다 모일 때까지 반복해서 읽지 않음.

(3) [부분 수신 미처리] 본문 — (B)(C)

  • 증상: got 을 쓰지 않고 body 전체를 디스패치. got < bodyLen 이면 뒷부분이 비어(0) 있는 채로 핸들러가 처리 → 프레임 경계 붕괴·오파싱.
  • 근본원인: 반환값(got)을 무시하고 한 번의 Read 로 다 받았다고 가정.

수정안

"필요한 만큼 다 채울 때까지 읽는" 헬퍼를 만들고, 0 은 종료로 다룬다. bodyLen 검증도 추가한다.

// false = 상대가 종료(EOF). 그 외엔 count 바이트를 모두 채움.
private bool ReadFull(byte[] buf, int count)
{
    int off = 0;
    while (off < count)
    {
        int n = _stream.Read(buf, off, count - off);
        if (n == 0) return false;          // EOF: 연결 종료
        off += n;                          // 부분 수신 누적
    }
    return true;
}

public void ReceiveLoop()
{
    var header = new byte[4];
    while (_running)
    {
        if (!ReadFull(header, 4)) break;               // (A') 0 → 루프 종료·정리
        int bodyLen = BitConverter.ToInt32(header, 0);
        if (bodyLen < 0 || bodyLen > MaxBody) break;   // 길이 검증

        var body = new byte[bodyLen];
        if (!ReadFull(body, bodyLen)) break;           // (B')(C') 본문 완전 수신
        Dispatch(body);
    }
    Cleanup();   // 세션 해제·맵 제거·자원 반납
}
  • .NET 7+ 이면 stream.ReadExactly(buf, off, count) 로 대체 가능(EOF 시 예외).
  • SocketException(연결 리셋 등)은 루프 밖에서 잡아 동일하게 정리한다.

더 나은 설계 (트레이드오프)

  • 비동기 I/O: ReadAsync + 파이프라인(System.IO.Pipelines)으로 부분 수신· 버퍼 관리를 프레임워크에 위임하면 휴먼에러가 준다(러닝커브).
  • 수신 버퍼 누적기: 세션별 링버퍼에 도착분을 쌓고 "완전한 프레임이 모였을 때만" 디스패치하는 상태머신 — 여러 프레임이 한 번에 와도 안전.

면접 포인트

  • 스트림 Read0 = EOF/연결 종료, 양수 = 그만큼 읽음, 예외 = 오류. 이 세 가지를 구분 못 하면 좀비 연결·바쁜 대기·오파싱이 모두 난다.
  • TCP 는 바이트 스트림이라 메시지 경계가 없다 → 항상 "필요한 만큼 다 읽기" 루프가 필요. 한 번의 Read 가 한 메시지라는 가정은 틀렸다.