My TCP Parcel Arrived in Pieces
Rebuild the Boundaries of a Broken Parcel
한국어 원문으로 표시합니다.
목표
프레이밍·기한·반닫힘을 구현하고 실제 TCP에서 확인합니다.
왜 중요한가
받은 조각을 메시지 하나라고 믿으면 로컬에서는 성공해도 실제 입력에서 깨집니다. 길이와 전체 시간 예산, 소켓 소유권을 코드로 표현해 데이터 유실과 무한 대기를 구분합니다. 표준 라이브러리만 사용하고 인터넷 접속이나 설치는 필요 없습니다.
단계
- 봉투에는 글자 수가 아니라 바이트 수 — codec.py에 MAX_PAYLOAD=4096과 encode(payload)를 만드세요. bytes 본문 앞에 unsigned 4바이트 big-endian 길이를 붙여 bytes를 반환합니다. 빈 본문은 허용하고 bytes가 아니거나 4096바이트를 넘으면 ValueError입니다.
- 헤더도 반으로 잘릴 수 있다 — codec.py에 Decoder를 추가하세요. 새 인스턴스마다 수신 상태가 분리되고 feed(chunk: bytes)는 완성된 본문의 list[bytes]를 반환합니다. 한 프레임을 어느 바이트에서 잘라 두 번 공급해도 복원하고, 미완성 데이터는 다음 호출까지 보관합니다.
- 택배 셋이 한 봉지로 왔다 — Decoder.feed를 개선해 한 chunk에 여러 프레임이 있으면 순서대로 모두 반환하세요. 다음 프레임의 일부가 남으면 보관합니다. 빈 본문도 메시지 하나이며, 빈 chunk는 EOF가 아닙니다.
- 도착 완료와 배송 중 파손 — Decoder.finish()를 만드세요. 미완성 헤더나 본문이 있으면 EOFError, 잔여가 없으면 None을 반환합니다. 정상적으로 해석한 길이 0 본문을 중간 종료로 오해하지 마세요.
- 4 GiB 택배 예고장을 거절한다 — Decoder는 4096보다 큰 길이 헤더를 받는 즉시 ValueError를 내야 합니다. 실제 본문이 도착할 때까지 기다리거나 그 길이의 버퍼를 먼저 할당하지 마세요. 4096바이트 본문은 여전히 허용합니다. 오류 후 해당 연결은 폐기합니다.
- 한 글자씩 보내면 영원히 기다릴까 — transport.py에 recv_frame(sock, timeout, clock=None)을 구현하세요. 한 프레임의 헤더와 본문 전체가 같은 시간 예산을 공유합니다. clock은 인자 없는 초 단위 시계 함수이며 기본은 time.monotonic입니다. sock의 gettimeout/settimeout/recv만 사용합니다. 정상 프레임은 bytes, 새 헤더를 한 바이트도 받기 전 EOF는 None, 중간 EOF는 EOFError, 초과 길이는 ValueError, 기한 초과는 TimeoutError입니다. 양의 유한 timeout만 허용하고 성공·실패 모두 이전 소켓 timeout을 복원하세요.
- 보냈다는 말의 절반만 사실 — transport.py에 send_frame(sock, payload)를 추가하세요. encode한 전체 프레임을 보내고, 전송 오류를 숨기지 않습니다. send가 일부 바이트만 처리하는 경우에도 프레임을 빠뜨리지 않아야 합니다. 호출자가 소켓의 송신 timeout을 정합니다.
- 실제 연결로 반송장을 받는다 — server.py에 handle_connection(sock, timeout)을 만드세요. 받은 소켓의 소유권을 인수하고 프레임마다 같은 bytes를 에코합니다. recv_frame의 정상 EOF(None)에서 종료하되 b"" 본문은 에코합니다. 모든 종료 경로에서 소켓을 닫고 프로토콜 오류는 호출자에게 전달하세요. 검사기가 127.0.0.1의 임시 포트에서 실제 TCP 연결을 만들며, 클라이언트가 송신만 반닫은 뒤에도 남은 응답을 받아야 합니다.
참고
모든 파일은 /root/tcp-parcel 아래에 둡니다. 예시 파일에는 정답이 아닌 함수 틀이 있습니다. 이전 단계 함수도 계속 유지하세요. 채점은 제출 코드를 실행하며 12초가 넘으면 중단합니다. send_frame의 송신 timeout은 호출자가 정합니다. 실습 세션 종료 후 파일은 유지되지 않으므로 별도로 보관하세요.
봉투에는 글자 수가 아니라 바이트 수
codec.py에 MAX_PAYLOAD=4096과 encode(payload)를 만드세요. bytes 본문 앞에 unsigned 4바이트 big-endian 길이를 붙여 bytes를 반환합니다. 빈 본문은 허용하고 bytes가 아니거나 4096바이트를 넘으면 ValueError입니다.
struct.pack의 !I는 네트워크 순서의 32비트 부호 없는 정수입니다. UTF-8 변환은 호출자가 먼저 합니다.
헤더도 반으로 잘릴 수 있다
codec.py에 Decoder를 추가하세요. 새 인스턴스마다 수신 상태가 분리되고 feed(chunk: bytes)는 완성된 본문의 list[bytes]를 반환합니다. 한 프레임을 어느 바이트에서 잘라 두 번 공급해도 복원하고, 미완성 데이터는 다음 호출까지 보관합니다.
최소 4바이트를 모으기 전에 헤더를 해석하지 마세요. 길이를 알아도 본문 전체가 아직 없을 수 있습니다.
택배 셋이 한 봉지로 왔다
Decoder.feed를 개선해 한 chunk에 여러 프레임이 있으면 순서대로 모두 반환하세요. 다음 프레임의 일부가 남으면 보관합니다. 빈 본문도 메시지 하나이며, 빈 chunk는 EOF가 아닙니다.
완성된 프레임을 제거한 뒤에도 while로 다음 헤더를 검사하세요. 매 feed마다 Decoder를 새로 만들면 잔여가 사라집니다.
도착 완료와 배송 중 파손
Decoder.finish()를 만드세요. 미완성 헤더나 본문이 있으면 EOFError, 잔여가 없으면 None을 반환합니다. 정상적으로 해석한 길이 0 본문을 중간 종료로 오해하지 마세요.
연결 종료라는 외부 사건과 단순히 이번에 받은 바이트가 없다는 입력은 별도입니다. finish는 실제 EOF를 알았을 때 호출합니다.
4 GiB 택배 예고장을 거절한다
Decoder는 4096보다 큰 길이 헤더를 받는 즉시 ValueError를 내야 합니다. 실제 본문이 도착할 때까지 기다리거나 그 길이의 버퍼를 먼저 할당하지 마세요. 4096바이트 본문은 여전히 허용합니다. 오류 후 해당 연결은 폐기합니다.
헤더를 읽은 직후 길이를 검사하세요. 32비트 정수로 표현 가능하다는 사실과 서비스가 허용한다는 것은 다릅니다.
한 글자씩 보내면 영원히 기다릴까
transport.py에 recv_frame(sock, timeout, clock=None)을 구현하세요. 한 프레임의 헤더와 본문 전체가 같은 시간 예산을 공유합니다. clock은 인자 없는 초 단위 시계 함수이며 기본은 time.monotonic입니다. sock의 gettimeout/settimeout/recv만 사용합니다. 정상 프레임은 bytes, 새 헤더를 한 바이트도 받기 전 EOF는 None, 중간 EOF는 EOFError, 초과 길이는 ValueError, 기한 초과는 TimeoutError입니다. 양의 유한 timeout만 허용하고 성공·실패 모두 이전 소켓 timeout을 복원하세요.
시작 때 deadline=clock()+timeout을 한 번 정하고 매 recv 전에 남은 시간을 settimeout에 줍니다. recv(n)은 n바이트를 모두 준다는 약속이 아닙니다.
보냈다는 말의 절반만 사실
transport.py에 send_frame(sock, payload)를 추가하세요. encode한 전체 프레임을 보내고, 전송 오류를 숨기지 않습니다. send가 일부 바이트만 처리하는 경우에도 프레임을 빠뜨리지 않아야 합니다. 호출자가 소켓의 송신 timeout을 정합니다.
sendall은 전부 전송하거나 예외를 냅니다. 직접 반복한다면 send의 반환 길이만큼만 앞으로 이동하고 0 반환을 종료 오류로 처리하세요.
실제 연결로 반송장을 받는다
server.py에 handle_connection(sock, timeout)을 만드세요. 받은 소켓의 소유권을 인수하고 프레임마다 같은 bytes를 에코합니다. recv_frame의 정상 EOF(None)에서 종료하되 b"" 본문은 에코합니다. 모든 종료 경로에서 소켓을 닫고 프로토콜 오류는 호출자에게 전달하세요. 검사기가 127.0.0.1의 임시 포트에서 실제 TCP 연결을 만들며, 클라이언트가 송신만 반닫은 뒤에도 남은 응답을 받아야 합니다.
with sock으로 수명을 묶으세요. if not payload는 None과 빈 본문을 혼동합니다. 송신 방향만 닫은 상대에게 아직 응답할 수 있습니다.