QUESTION
send 한 번과 recv 한 번은 같은 message 경계를 유지하는가?
TCP는 순서가 보장된 byte stream이다. sender의 두 send가 receiver의 한 recv로 합쳐지거나 한 send가 여러 recv로 나뉠 수 있다. application이 fixed length, delimiter, length prefix 같은 framing을 직접 정의해야 한다.
outbound data가 send buffer보다 빨리 쌓이면 무제한 queue는 memory exhaustion을 만든다. connection별 high-water mark에서 producer를 멈추거나 요청을 거부하고, EPOLLOUT 때 남은 byte를 이어 보낸다.
STRUCTURE
구조 그림
TCP는 byte 순서만 보존한다. receiver parser가 length header와 payload byte 수를 누적해 application frame을 복원한다.
CALL PATH
호출 흐름
message 상태는 application buffer에 있고 TCP kernel queue에는 byte만 있다. parser가 header와 payload를 얼마나 확보했는지 connection object에 남긴다.
함수 이름을 외우기 위한 그림이 아니다. 반환값, 파일 디스크립터, 메모리 매핑, 대기 큐 가운데 무엇이 다음 단계로 전달되는지 확인한다.
SOURCE COORDINATES
Linux 6.18.37 LTS 소스 위치
glibc 함수에서 멈추지 않고 syscall 구현과 커널 객체가 만나는 파일까지 내려간다. 링크는 동일한 태그의 원본 파일을 가리킨다.
| 파일 | 함수·구조체 | 여기서 볼 것 |
|---|---|---|
| net/ipv4/tcp.c | tcp_sendmsg(), tcp_recvmsg() | stream send/receive queue와 partial progress |
| net/ipv4/tcp_output.c | tcp_write_xmit() | congestion/window 조건에 따른 segment 전송 |
| net/ipv4/tcp_input.c | tcp_data_queue() | 수신 byte를 socket receive queue에 연결 |
COMPLETE PROGRAM
실행 예제 원본
아래 코드는 설명을 위해 중간 줄을 생략한 의사 코드가 아니다. 파일로 빌드해 실행할 수 있는 최소 예제다.
cc -std=c17 -Wall -Wextra -O2 frame_codec.c -o frame_codec01#define _GNU_SOURCE
02#include <arpa/inet.h>
03#include <stdint.h>
04#include <stdio.h>
05#include <string.h>
06#include <sys/socket.h>
07#include <unistd.h>
08
09static int send_frame(int fd, const void *data, uint32_t length)
10{
11 uint32_t header = htonl(length);
12 unsigned char frame[4 + 256];
13 if (length > 256)
14 return -1;
15 memcpy(frame, &header, sizeof(header));
16 memcpy(frame + sizeof(header), data, length);
17 size_t total = 0, size = sizeof(header) + length;
18 while (total < size) {
19 ssize_t n = send(fd, frame + total, size - total, MSG_NOSIGNAL);
20 if (n <= 0)
21 return -1;
22 total += (size_t)n;
23 }
24 return 0;
25}
26
27int main(void)
28{
29 int pair[2];
30 if (socketpair(AF_UNIX, SOCK_STREAM, 0, pair) < 0)
31 return 1;
32 send_frame(pair[0], "alpha", 5);
33 send_frame(pair[0], "beta", 4);
34 unsigned char bytes[64];
35 ssize_t n = recv(pair[1], bytes, sizeof(bytes), 0);
36 printf("one recv returned %zd bytes\n", n);
37 close(pair[0]); close(pair[1]);
38 return n < 0;
39}
CODE NOTES
코드 조각별 설명
uint32_t header = htonlwire format length를 고정 32-bit network byte order로 만든다. size_t 같은 host-dependent type을 그대로 보내지 않는다.
if (length > 256)frame allocation과 parser resource를 length field만 믿고 무제한 확장하지 않도록 protocol 상한을 적용한다.
while (total < size)send가 frame 일부만 받아도 offset을 보존해 나머지를 이어 보낸다. nonblocking fd에서는 EAGAIN 때 queue state를 남기고 반환한다.
MSG_NOSIGNALpeer가 닫힌 socket에서 process 전체 SIGPIPE 종료를 피하고 EPIPE를 반환값으로 처리한다.
recv(pair[1]두 frame을 보냈지만 recv가 한 frame, 두 frame, 일부 header 등 어떤 byte 수로도 반환할 수 있다. parser가 buffer를 누적해야 한다.
DETAILS
세부 동작
parser는 incremental state machine이다
먼저 4-byte header가 모일 때까지 기다리고 length를 검증한 뒤 payload length만큼 모이면 frame 하나를 내보낸다. buffer에 다음 frame byte가 남으면 같은 read event에서 계속 parsing한다.
malformed length, integer overflow, allocation 실패에서 connection을 닫는 정책을 명확히 한다.
backpressure를 상위 producer까지 전달한다
send queue가 high-water mark를 넘으면 socket EPOLLOUT만 기다리는 것으로 끝나지 않는다. 파일 읽기, upstream RPC, message 생성도 중단해야 memory가 bounded된다.
low-water mark 아래로 줄면 producer interest를 다시 켠다.
TCP ACK는 application receipt가 아니다
send 성공과 TCP ACK는 peer kernel이 byte를 받았다는 범위의 의미다. peer application이 frame을 parse하고 durable store에 기록했다는 보장이 필요하면 application acknowledgement를 정의한다.
retry 시 request id와 idempotency 규칙이 필요하다.
OBJECTS
객체와 수명
| 대상 | 언제 생기고 없어지는가 | 확인할 값 |
|---|---|---|
outbound frame | encode에서 생기고 모든 byte send 또는 connection abort까지 유지된다 | buffer, total, offset |
receive parser | connection 수명 동안 partial header/payload를 보관한다 | buffered bytes, expected length |
TCP send queue | send syscall이 byte를 받아 ACK/abort까지 kernel에 유지한다 | wmem, unacked, window |
FAILURE PATH
실패 조건과 오해하기 쉬운 부분
| 겉으로 보이는 현상 | 실제 원인 후보 | 확인 방법 |
|---|---|---|
| message가 합쳐짐/잘림 | TCP에 message boundary 기대 | incremental parser 상태 |
| memory 계속 증가 | outbound queue 무제한 | connection별 queued bytes/high-water |
| SIGPIPE 종료 | closed peer에 send | MSG_NOSIGNAL 또는 signal policy |
LAB
직접 확인
- recv buffer를 1~7 byte로 바꿔 두 frame을 항상 정확히 복원하는 parser를 작성한다.
- socket send buffer를 작게 하고 receiver를 지연시켜 partial send/EAGAIN과 queue high-water를 관찰한다.
- frame length를 최대값보다 크게 보내는 악성 peer test로 allocation 전 거부를 검증한다.
./frame_codecstrace -e trace=socketpair,sendto,recvfrom,close ./frame_codecPRIMARY REFERENCES