Socket · Linux userspace / kernel ABI

TCP framing and backpressure

Explains how to build a length-prefix parser and partial-send queue over a byte stream without mistaking TCP for a message queue.

Series
33 / 38
Build
cc -std=c17 -Wall -Wextra -O2 frame_codec.c -o frame_codec
Run
./frame_codec
Kernel
Linux 6.18.37 LTS

Do one send and one recv preserve the same message boundary?

TCP is an ordered byte stream. Two sends from the sender can merge into one recv, or one send can be split across several recvs. The application must define framing such as a fixed length, delimiter, or length prefix.

When outbound data accumulates faster than the send buffer accepts it, an unbounded queue exhausts memory. At a per-connection high-water mark, stop producers or reject requests, then continue sending the remaining bytes on EPOLLOUT.

Structure diagram

Figure 1. Frame boundaries retained across two sends and three recvs
sender calls
len=5alphalen=4beta
TCP byte stream
00000005alpha00000004beta
recv chunks
recv #1: 3Brecv #2: 9Brecv #3: 5B
parser output
frame: alphaframe: beta

TCP preserves only byte order. The receiver parser reconstructs application frames by accumulating a length header and payload bytes.

Call path

Figure 2. From userspace code to observable results
message length + payload encode
send queue preserve offset
TCP stream arbitrary segments
receive buffer accumulate and parse
complete frame deliver when length is satisfied

Message state lives in application buffers; TCP kernel queues contain only bytes. Retain in the connection object how much of the header and payload the parser has acquired.

Figure 3. Major points along the kernel-internal path
tcp_sendmsg sk_write_queue
segmentation MSS/TSO
ACK/window update transferable amount
tcp_recvmsg receive queue copy
poll wake space/data readiness

This diagram is not for memorizing function names. Follow which return value, file descriptor, memory mapping, or wait queue is passed to the next stage.

Linux 6.18.37 LTS source locations

Go beyond the glibc function to the files where the syscall implementation meets kernel objects. Each link points to the original file at the same tag.

FileFunction / structureWhat to inspect
net/ipv4/tcp.c tcp_sendmsg(), tcp_recvmsg() Stream send/receive queues and partial progress
net/ipv4/tcp_output.c tcp_write_xmit() Send segments according to congestion/window conditions
net/ipv4/tcp_input.c tcp_data_queue() Attach received bytes to the socket receive queue

Complete runnable example

The code below is not pseudocode with explanatory lines omitted. It is a minimal example that can be built and run as a file.

Buildcc -std=c17 -Wall -Wextra -O2 frame_codec.c -o frame_codec
01#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

Source line 11uint32_t header = htonl

Makes the wire-format length a fixed 32-bit value in network byte order. Do not send a host-dependent type such as size_t directly.

Source line 13if (length > 256)

Applies a protocol maximum so a length field cannot grow frame allocations and parser resources without bound.

Source line 18while (total < size)

Even when send accepts only part of a frame, preserves the offset and continues with the remainder. On a nonblocking fd, retain queue state and return on EAGAIN.

Source line 19MSG_NOSIGNAL

Prevents a process-wide SIGPIPE termination on a socket closed by the peer and handles EPIPE as a return value.

Source line 35recv(pair[1]

Although two frames were sent, recv may return one frame, two frames, part of a header, or any other byte count. The parser must accumulate the buffer.

Detailed behavior

01

The parser is an incremental state machine

First wait for the full 4-byte header and validate the length, then emit a frame when that many payload bytes have accumulated. If bytes for the next frame remain, continue parsing within the same read event.

Define an explicit connection-close policy for a malformed length, integer overflow, or allocation failure.

02

Propagate backpressure to upstream producers

When the send queue exceeds its high-water mark, waiting for socket EPOLLOUT is not sufficient. Stop file reads, upstream RPC, and message generation too so memory remains bounded.

Reenable producer interest after the queue falls below the low-water mark.

03

A TCP ACK is not application receipt

Successful send and a TCP ACK mean only that bytes were accepted as far as the peer kernel. If the peer application must parse the frame and place it in durable storage, define an application acknowledgement.

Retries require request IDs and idempotency rules.

Objects and lifetimes

ObjectCreation and releaseValues to inspect
outbound frameCreated during encoding and retained until every byte is sent or the connection abortsbuffer, total, offset
receive parserRetains a partial header/payload for the connection lifetimebuffered bytes, expected length
TCP send queueAccepted by the send syscall and retained in the kernel until ACK/abortwmem, unacked, window

Failure conditions and common misconceptions

Observed symptomLikely causesHow to verify
Messages merge or splitTCP message boundaries were assumedInspect incremental parser state
Memory keeps growingThe outbound queue is unboundedTrack queued bytes/high-water per connection
The process terminates with SIGPIPEsend was called on a closed peerUse MSG_NOSIGNAL or define signal policy

Verify it yourself

  1. Vary the recv buffer from 1 to 7 bytes and write a parser that always reconstructs both frames exactly.
  2. Make the socket send buffer small and delay the receiver to observe partial send/EAGAIN and the queue high-water mark.
  3. Test a malicious peer that sends a frame length above the maximum and verify rejection before allocation.
Run./frame_codec
Tracestrace -e trace=socketpair,sendto,recvfrom,close ./frame_codec

Primary sources