Huny's Dev Blog
huny.dev

TCP recv 한 번으로 메시지를 읽을 수 없는 이유: 길이 프리픽스 프레이밍

TCP의 메시지 경계와 recv의 짧은 읽기를 구분한다. 4바이트 길이 프리픽스 Python 예제로 연속 프레임·중도 종료·최대 길이 제한을 처리하고 로컬 TCP 검증 결과를 정리한다.

Hun Jang
Hun Jang Oct 28, 2026

TCP 서버에서 recv()를 한 번 호출한 결과를 메시지 하나로 처리하면, 정상적인 통신에서도 파싱 오류가 날 수 있다. TCP는 메시지 경계가 없는 바이트 스트림이므로, 애플리케이션이 프레임 규칙을 정하고 헤더와 본문을 필요한 길이만큼 반복해서 읽어야 한다. 이 글에서는 4바이트 길이 프리픽스와 Python 예제로 짧은 읽기, 연속 메시지, 연결 종료를 처리하는 방법을 확인한다.

연속된 바이트 흐름을 별도 메시지로 구분하는 프레이밍의 개념 일러스트. 실제 패킷이나 프로토콜 구조를 나타내는 도식은 아니다.

send 호출 횟수와 recv 호출 횟수는 일치하지 않는다

송신 측에서 send(A)와 send(B)를 각각 호출해도 수신 측이 정확히 A, B를 한 번씩 읽는다는 보장은 없다. A의 일부만 먼저 읽거나, 여러 메시지에 해당하는 바이트가 한 번에 읽힐 수 있다. Linux tcp(7)은 TCP가 레코드 경계를 보존하지 않는다고 설명한다.

recv(4096)의 4096은 이번 호출에서 읽을 최대 크기다. 4096바이트를 모을 때까지 기다리거나 애플리케이션 메시지 하나를 반환하라는 뜻이 아니다. recv(2)에서 설명하듯 일반적인 수신 호출은 요청 크기 이하의 현재 사용 가능한 데이터를 반환할 수 있다.

따라서 네트워크 패킷 개수나 호출 사이의 지연 시간을 메시지 경계로 사용하지 않는다. TTS 서버에서 JSON 요청과 PCM 데이터를 주고받는 자체 TCP 프로토콜도 같은 원칙을 따른다.

예제 프레임: 4바이트 길이와 본문

이 글의 예제는 먼저 본문 바이트 수를 나타내는 unsigned 32-bit big-endian 정수를 전송하고 그 뒤에 본문을 붙인다. 길이는 헤더를 제외한 바이트 수다. UTF-8 문자열의 문자 수와 혼동하지 않는다.

Python의 struct에서 !I는 network byte order의 4바이트 unsigned integer를 지정한다. 플랫폼의 native byte order와 패딩을 통신 규약으로 암묵적으로 사용하지 않도록 한다. struct 공식 문서

본문 크기는 예제 정책상 1 MiB로 제한한다. 헤더를 읽은 직후 검사해 과도한 본문을 계속 받지 않도록 한다. 이 제한은 동시 연결 전체의 메모리나 처리 시간을 제한하는 기능은 아니다.

실행 가능한 송수신 예제

아래 코드를 framing_demo.py로 저장한다. 외부 패키지 없이 IPv4 loopback TCP 연결을 만들고, 두 프레임을 연속 전송한다. OneByteReads는 수신 호출이 최대 1바이트만 반환하도록 제한하는 검증용 어댑터다. TCP가 실제로 1바이트 패킷을 만든다는 뜻은 아니다.

정상 실행 결과는 다음과 같다.

송신은 Python의 sendall()을 사용해 주어진 데이터를 모두 보내거나 오류가 발생할 때까지 처리한다. 이것은 수신 애플리케이션이 메시지를 처리했다는 확인 응답은 아니다. 필요하다면 별도의 애플리케이션 응답을 설계한다. socket 공식 문서

recv_frame()은 4바이트 헤더를 모은 뒤 길이를 해석하고, 그 길이만큼 본문을 모은다. 한 번에 필요한 나머지 크기까지만 요청하므로 다음 프레임의 바이트를 이번 본문으로 소비하지 않는다.

빈 본문과 연결 종료를 구분한다

  • b"": 길이 0인 정상 프레임의 본문이다.
  • None: 새 헤더를 읽기 전에 연결이 정상 종료돼 더 이상 프레임이 없다는 예제의 반환 규약이다.
  • EOFError: 헤더 일부나 본문 일부를 받은 뒤 연결이 종료된 경우다.
  • ValueError: 선언된 길이가 예제의 허용 크기를 초과한 경우다.

길이 오류나 중간 종료 뒤에는 이 예제에서 해당 연결을 닫는 것이 안전한 기본 처리다. 임의의 바이트를 건너뛰며 다음 메시지를 찾는 복구는 별도의 재동기화 규약 없이 시도하지 않는다. Linux의 MSG_WAITALL을 쓰더라도 연결 종료나 오류로 짧은 결과가 나올 수 있어 반환값 검사가 필요하다. recv(2)

검증 범위와 실제 서버에 추가할 것

Python 3.12.14 / Linux x86_64의 로컬 TCP 연결에서 7개 검증을 통과했다: 1바이트씩 읽기, 연속 프레임 2개, 빈 본문, 프레임 사이의 정상 EOF, 헤더 중도 종료, 본문 중도 종료, 최대 길이를 초과한 헤더다. 위 실행 예제의 두 줄 출력도 확인했다. 실제 인터넷 구간의 패킷 분할 패턴이나 처리량을 측정한 결과는 아니다.

프로덕션에서는 프레임 전체의 완료 기한, 동시 연결·누적 버퍼 제한, 인증과 TLS, 프로토콜 버전, 오류 처리도 설계한다. 예제의 소켓 2초 타임아웃만으로 프레임 전체를 2초 안에 완료한다고 보장할 수는 없다. 한 연결에 여러 송신 작업이 있다면 프레임 전체의 쓰기를 직렬화해야 한다.

TCP_NODELAY는 메시지 경계를 복원하는 기능이 아니다. Nagle 설정과 프레이밍을 별도 문제로 다루는 것이 좋다. 관련 기존 글:

TCP_NODELAY는 항상 켜는 게 좋을까?

참고 문서

English version

TCP Message Framing: Why One recv Call Is Not One MessageTCP Message Framing: Why One recv Call Is Not One Message

추천 글