TCP Is a Byte Stream: Message Framing in C
A successful socket example can make TCP appear to deliver one message for every call to send(). It does not. TCP carries an ordered stream of bytes: one send may require several reads, while several sends may arrive together. An application protocol must define where each message ends.
This article continues the simple socket server and client. After framing is clear, the SOCKS5 greeting and CONNECT article shows how a real application protocol uses the stream.
Why One recv() Is Not Enough
These calls do not form a guaranteed pair:
send(fd, "HELLO", 5, 0);
recv(fd, buffer, 5, 0);
The receive call may return anywhere from one to five bytes. It may also be interrupted, reach end-of-stream, or fail. Network congestion, kernel buffering, and scheduling all affect how the stream is divided between calls.
Send an Entire Buffer
#include <errno.h>
#include <stddef.h>
#include <sys/socket.h>
int send_all(int fd, const void *buffer, size_t length)
{
const unsigned char *bytes = buffer;
size_t sent = 0;
while (sent < length) {
ssize_t n = send(fd, bytes + sent, length - sent, 0);
if (n < 0) {
if (errno == EINTR) continue;
return -1;
}
if (n == 0) return -1;
sent += (size_t)n;
}
return 0;
}
Receive an Exact Number of Bytes
#include <errno.h>
#include <stddef.h>
#include <sys/socket.h>
int recv_exact(int fd, void *buffer, size_t length)
{
unsigned char *bytes = buffer;
size_t received = 0;
while (received < length) {
ssize_t n = recv(fd, bytes + received, length - received, 0);
if (n < 0) {
if (errno == EINTR) continue;
return -1;
}
if (n == 0) return 0;
received += (size_t)n;
}
return 1;
}
A return value of 1 means the buffer is complete, 0 means the peer closed the connection early, and -1 reports an error.
Length-Prefixed Messages
One common framing method sends a fixed-width length followed by that many payload bytes. Network protocols use network byte order so different machines agree on the integer representation.
#include <arpa/inet.h>
#include <stdint.h>
int send_message(int fd, const void *message, uint32_t length)
{
uint32_t network_length = htonl(length);
if (send_all(fd, &network_length, sizeof(network_length)) < 0)
return -1;
return send_all(fd, message, length);
}
The receiver reads the four-byte header first, converts it with ntohl(), validates it against a strict maximum, allocates or selects a suitable buffer, and then calls recv_exact() for the payload.
Other Framing Strategies
- Delimiter: End each record with a byte such as a newline. The receiver must retain incomplete data between reads.
- Fixed size: Every record has the same known length. This is simple but may waste space.
- Connection close: End-of-stream marks the end of one response. This prevents reuse of the connection.
- Structured protocol: A header describes type, flags, and payload length.
What TCP Does Guarantee
- Bytes arrive in order unless the connection fails.
- Duplicate bytes are removed by TCP.
- Checksums detect many transmission errors.
- Flow and congestion control regulate transmission.
TCP does not provide application message boundaries, authentication, encryption, or a guarantee that a disconnected peer processed the last bytes it received.
dispelled