Networking in C · intermediate · ~10 min
- By the end you can call `send()` and `recv()` on a connected TCP socket and interpret every possible return value. - By the end you can write robust `send_all` / `recv_all` helpers that loop over short writes and short reads. - By the end you can tell the difference between a clean end-of-stream (`recv` returns 0), a real error (`recv` returns -1), and "got some but not all" (a short read). - By the end you can design a simple length-prefixed message frame so a byte stream can carry discrete messages. - By the end you can pick the right `flags` (`MSG_NOSIGNAL`, `MSG_DONTWAIT`, `MSG_WAITALL`) and know when *not* to bother.
In the previous lesson you used connect() to establish a TCP connection and got back a connected file descriptor. Now that the pipe exists, this lesson is about pushing bytes through it. send() and recv() are the socket-specific siblings of write() and read(): same idea, plus a flags argument for socket-only behavior.
The API itself is tiny — you will learn it in two minutes. The real subject of this lesson is a property of TCP that trips up almost every beginner: TCP is a byte stream, not a message service. The kernel is free to hand you fewer bytes than you asked for, split your message across calls, or glue two messages together. Everything here builds on the connected socket from connect-syscall; we take that fd and learn to move data over it correctly.
Every networked program a defender touches — a web server, a logging agent, a reverse proxy, a database client — is built on loops around send/recv. The single most common networking bug in production C is assuming one recv() returns a whole message; under load or across the internet it frequently does not, causing truncated parses, hangs, and desyncs. From a security angle, trusting an attacker-supplied length prefix without bounds-checking it is a classic remote buffer overflow, and mishandling recv == 0 versus recv == -1 leads to busy-loops and denial of service. Getting these fundamentals right is the difference between a robust service and one an attacker can wedge with a slow or malformed stream.
ssize_t send(int fd, const void *buf, size_t len, int flags);
ssize_t recv(int fd, void *buf, size_t cap, int flags);
With flags == 0 these behave exactly like write() and read() on the socket. Both return the number of bytes actually transferred, which can be less than you asked for. On a connected TCP socket you can freely mix send/recv with write/read; the flags argument is the only reason to prefer the socket versions.
This is the idea the whole lesson rests on. When you send(fd, buf, 1000, 0), TCP does not promise the peer will recv 1000 bytes in one call. It promises only that the bytes arrive in order, exactly once. The boundaries you sent with are erased.
You send: [ "HELLO" ][ "WORLD" ] (two send() calls)
Wire (TCP): H E L L O W O R L D (one ordered stream)
Peer may recv: [ "HELL" ] <- short read (4 bytes)
[ "OWORL" ] <- boundary crossed!
[ "D" ]
So a single recv() may return part of one message, or several messages stuck together. Your code must impose its own structure on the stream.
| Call | Return | Meaning | What to do |
|---|---|---|---|
recv |
> 0 |
got that many bytes (maybe fewer than cap) |
process them, keep looping if you need more |
recv |
0 |
peer performed an orderly shutdown (sent FIN) | this is normal EOF — stop reading, close |
recv |
-1 |
error; inspect errno |
EINTR/EAGAIN → retry; else real failure |
send |
> 0 |
queued that many bytes (maybe fewer than len) |
advance pointer, loop for the rest |
send |
-1 |
error; inspect errno |
EPIPE = peer gone; EINTR → retry |
The two classic mistakes both live in this table: treating recv == 0 as an error (it is normal end-of-stream), and assuming send/recv transfer everything in one shot (they may not).
Because any single call can be short, correct code loops until the byte budget is exhausted:
ssize_t send_all(int fd, const void *buf, size_t n) {
const char *p = buf;
while (n > 0) {
ssize_t k = send(fd, p, n, 0);
if (k < 0) { if (errno == EINTR) continue; return -1; }
p += k; n -= (size_t)k;
}
return 0;
}
The read side is the same shape, except it must also stop on recv == 0 (EOF).
Knowledge check: your peer sends the 8-byte word "DEFENDER" with one send. You call recv(fd, buf, 8, 0) and it returns 5. Is that an error, and what do you do?
Not an error. It is a short read — completely normal for TCP. You have 5 valid bytes ("DEFEN"). Call
recvagain withbuf + 5and remaining capacity3; it will hand you the last 3 bytes ("DER"). Only-1is an error and only0is end-of-stream.
Since TCP erases message boundaries, you must add them yourself. The two standard techniques are length-prefixing (send a fixed-size count, then that many payload bytes) and delimiters (e.g. HTTP ends headers with \r\n\r\n). Length-prefixing is simpler and used in the example below:
Frame on the wire:
+---------------------+-----------------------------+
| 4-byte length (N) | N bytes of payload |
| big-endian uint32 | |
+---------------------+-----------------------------+
The receiver recv_alls exactly 4 bytes to learn N, then recv_alls exactly N payload bytes. Defensive rule: before trusting N, check it against your buffer capacity — an attacker who controls the length prefix will happily send 0xFFFFFFFF to make you over-read or over-allocate.
| Flag | Side | Effect |
|---|---|---|
0 |
both | default; behaves like write/read |
MSG_NOSIGNAL |
send | on a broken pipe, return EPIPE instead of raising SIGPIPE |
MSG_DONTWAIT |
both | this one call is non-blocking (returns EAGAIN if it would block) |
MSG_WAITALL |
recv | try to fill the whole buffer before returning (still not guaranteed) |
MSG_PEEK |
recv | copy bytes but leave them in the queue for the next recv |
MSG_NOSIGNAL is the one you almost always want on send for a server: otherwise writing to a socket whose peer has vanished delivers SIGPIPE, which by default kills your process.
#include <sys/socket.h>
ssize_t send(int fd, const void *buf, size_t len, int flags);
fd: a connected socket descriptor (from connect or accept).buf / len: the bytes to send. len is a maximum; the return value is how many were actually accepted into the kernel send buffer.flags: bitwise-OR of MSG_*, or 0. Common: MSG_NOSIGNAL.>= 0 bytes queued, or -1 with errno (EPIPE, ECONNRESET, EINTR, EAGAIN).ssize_t recv(int fd, void *buf, size_t cap, int flags);
cap: capacity of buf; the kernel writes at most this many bytes. It does not null-terminate — you do that yourself if treating data as a C string.> 0 bytes read, 0 for orderly shutdown (EOF), or -1 with errno.uint32_t htonl(uint32_t hostlong); /* host -> network (big-endian) byte order */
uint32_t ntohl(uint32_t netlong); /* network -> host byte order */
<arpa/inet.h>.No heap is allocated by these calls, so there is nothing to free; you still own and must close(fd) when done.
send() and recv(): the socket versions of write/readsend(fd, buf, n, flags) and recv(fd, buf, cap, flags) are the socket-specific siblings of write and read.
0 for flags.MSG_NOSIGNAL, MSG_DONTWAIT, ...) are situational and rarely needed at first.The hard part of TCP is not the API. It is that a single call can move fewer bytes than you asked for. This is called a short send or read.
For example, send(fd, buf, 8, 0) might return 5. You then have to call again with the remaining buf + 5, 3.
The fix is to always loop until every byte is handled:
ssize_t send_all(int fd, const void *buf, size_t n) {
const char *p = buf;
while (n > 0) {
ssize_t k = send(fd, p, n, 0);
if (k < 0) { if (errno == EINTR) continue; return -1; }
if (k == 0) return -1;
p += k; n -= (size_t)k;
}
return 0;
}
recv() returning 0 means the peer cleanly closed the connection (it sent a FIN). This is normal end-of-stream, not an error.recv() returning -1 (with errno set) is a real error.#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <arpa/inet.h> /* htonl / ntohl */
/* Write ALL n bytes, looping over short sends. Returns 0 on success, -1 on error. */
static int send_all(int fd, const void *buf, size_t n) {
const char *p = buf;
while (n > 0) {
ssize_t k = send(fd, p, n, 0);
if (k < 0) {
if (errno == EINTR) continue; /* interrupted: retry */
return -1; /* real error */
}
if (k == 0) return -1; /* shouldn't happen for send */
p += k;
n -= (size_t)k;
}
return 0;
}
/* Read EXACTLY n bytes, looping over short reads.
Returns 1 = got all n, 0 = clean EOF before n, -1 = error. */
static int recv_all(int fd, void *buf, size_t n) {
char *p = buf;
while (n > 0) {
ssize_t k = recv(fd, p, n, 0);
if (k < 0) {
if (errno == EINTR) continue;
return -1;
}
if (k == 0) return 0; /* peer closed early */
p += k;
n -= (size_t)k;
}
return 1;
}
/* Frame = 4-byte big-endian length prefix, then that many payload bytes. */
static int send_msg(int fd, const char *msg) {
uint32_t len = (uint32_t)strlen(msg);
uint32_t netlen = htonl(len);
if (send_all(fd, &netlen, sizeof netlen) < 0) return -1;
return send_all(fd, msg, len);
}
static int recv_msg(int fd, char *out, size_t cap) {
uint32_t netlen;
int r = recv_all(fd, &netlen, sizeof netlen);
if (r <= 0) return r; /* 0 = EOF, -1 = error */
uint32_t len = ntohl(netlen);
if (len >= cap) return -1; /* would overflow buffer */
r = recv_all(fd, out, len);
if (r != 1) return -1; /* short frame = protocol error */
out[len] = '\0';
return 1;
}
int main(void) {
int sv[2];
if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) < 0) { perror("socketpair"); return 1; }
pid_t pid = fork();
if (pid < 0) { perror("fork"); return 1; }
if (pid == 0) {
/* ---- child: the "server" that echoes framed messages ---- */
close(sv[0]);
char buf[256];
for (;;) {
int r = recv_msg(sv[1], buf, sizeof buf);
if (r == 0) { printf("[server] peer closed (recv returned 0)\n"); break; }
if (r < 0) { printf("[server] error\n"); break; }
printf("[server] got %zu bytes: \"%s\"\n", strlen(buf), buf);
send_msg(sv[1], buf); /* echo it back */
}
close(sv[1]);
_exit(0);
}
/* ---- parent: the "client" ---- */
close(sv[1]);
const char *msgs[] = { "hello", "TCP is a byte stream", "goodbye" };
char reply[256];
for (int i = 0; i < 3; i++) {
if (send_msg(sv[0], msgs[i]) < 0) { perror("send_msg"); break; }
int r = recv_msg(sv[0], reply, sizeof reply);
if (r == 1) printf("[client] echo: \"%s\"\n", reply);
else printf("[client] no reply (r=%d)\n", r);
}
close(sv[0]); /* sends FIN: server's recv now returns 0 */
wait(NULL);
printf("[client] done\n");
return 0;
}
send_all — the core short-write loop. p walks forward and n counts down. If send returns -1 with errno == EINTR (a signal interrupted it) we simply retry; any other -1 is a genuine error. On each success we advance p += k and shrink n -= k. The loop ends only when every byte is queued.recv_all — the mirror image, with one extra case: recv == 0 means the peer closed the connection, so we return 0 to signal EOF rather than looping forever. Return 1 means we filled the whole request; -1 is an error.send_msg — implements framing. htonl converts the length to big-endian (network) byte order so the two ends agree regardless of CPU. We send_all the 4-byte prefix, then send_all the payload. Two send_alls, one logical message.recv_msg — reads the 4-byte prefix with recv_all; if that returns <= 0 we propagate EOF/error. ntohl converts back to host order. The bounds check if (len >= cap) return -1; is the security-critical line: it refuses a length that would overrun out before reading a single payload byte. Then recv_all(out, len) reads exactly the payload and we null-terminate so it prints as a string.socketpair — gives us two already-connected stream sockets without any real network, listener, or root. Perfect hermetic stand-in for a TCP connection.fork — the child plays server (echo loop), the parent plays client. Each closes the descriptor it does not use.closes its socket, which sends a FIN; the server's next recv_all returns 0, the server prints "peer closed" and exits. wait(NULL) reaps the child.1. Assuming one recv = one message
char buf[1024];
ssize_t n = recv(fd, buf, sizeof buf, 0); // WRONG: assumes whole message arrives
parse(buf, n);
Why it breaks: TCP is a byte stream; recv may return a partial message or two messages glued together, so parse sees garbage or truncated data.
uint32_t len; recv_all(fd, &len, 4); len = ntohl(len); // FIXED: frame + loop
recv_all(fd, buf, len);
2. Treating recv == 0 as an error
ssize_t n = recv(fd, buf, cap, 0);
if (n <= 0) { perror("recv"); exit(1); } // WRONG: 0 is not an error
Why it breaks: 0 is the normal orderly-shutdown signal; this reports a spurious error (and perror prints a stale/meaningless errno) on every clean disconnect.
ssize_t n = recv(fd, buf, cap, 0);
if (n == 0) { /* peer closed: clean up */ }
else if (n < 0) { if (errno==EINTR) continue; perror("recv"); }
else { /* got n bytes */ }
3. Trusting the length prefix from the wire
uint32_t len; recv_all(fd, &len, 4); len = ntohl(len);
char *p = malloc(len); // WRONG: attacker sends len = 0xFFFFFFFF
recv_all(fd, p, len);
Why it breaks: an untrusted peer controls len; a huge value causes a giant allocation (DoS) or, with a fixed buffer, a heap/stack overflow.
if (len > MAX_MSG) return -1; // FIXED: bound it before allocating/reading
char *p = malloc(len);
4. Ignoring SIGPIPE on send
send(fd, buf, n, 0); // WRONG: peer gone -> SIGPIPE kills the process
Why it breaks: writing to a socket whose peer closed raises SIGPIPE, whose default action terminates the program — an easy remote crash.
send(fd, buf, n, MSG_NOSIGNAL); // FIXED: get EPIPE in errno instead of a signal
fprintf(stderr, "recv=%zd errno=%d (%s)\n", k, errno, strerror(errno));. Seeing a recv of 5 when you expected 8 instantly reveals a short read.strace -e trace=network ./prog (Linux) or dtruss (macOS) shows the exact send/recv syscalls and their byte counts — the ground truth for how the kernel split your stream.valgrind ./prog catches the classic framing bug where you read len bytes into a buffer smaller than len, or forget to null-terminate before printf("%s") (reads past the buffer).recv_all that never returns usually means the peer sent fewer bytes than the length prefix promised (or you double-counted the prefix). Reproduce with a tiny sender that stops early.nc/ncat as a manual peer: nc -l 8080 on one side lets you type bytes and watch how your program frames and drains them.recv_all loop and print n and k each iteration to confirm the pointer advances and the count decreases.recv never null-terminates. If you printf("%s", buf) a buffer that recv filled to capacity, you read past the end. Always reserve one byte and set buf[n] = '\0' yourself (as recv_msg does with out[len] = '\0').if (len >= cap) return -1; must run before recv_all(out, len). This single check is the boundary between a robust parser and a remote buffer overflow.len near SIZE_MAX, or arithmetic like malloc(len + 1) that wraps to 0, produces an undersized buffer. Compare against a fixed MAX_MSG and reject anything larger.send/recv return ssize_t (signed). Assigning to an int can truncate on huge buffers, and comparing an unsigned count against a signed return mixes signedness — keep the return in ssize_t and cast to size_t only after you have confirmed it is > 0.send_all.close(fd) when done; a leaked descriptor is both a resource leak and, for a server, a slow path to EMFILE denial of service.send/recv loops with framing on top.recv_all does.send_all/recv_all helpers (never a bare call), always frame your messages, always bound untrusted lengths, set MSG_NOSIGNAL on server sends, and treat recv == 0 as a normal lifecycle event. For high-concurrency servers, combine these with non-blocking sockets and an event loop (a later topic).len >= cap rejection.sends a 10-byte message in two send calls of 5 bytes each with a tiny usleep between them, and a receiver that logs the size of every recv. Show that a single recv sometimes returns fewer than 10 bytes.recv_line that reads a byte at a time (or buffers) until it sees \n, and handle a line split across two recv calls.MAX_MSG cap and a check that rejects a length prefix larger than it before reading the payload. Feed it a crafted 4-byte prefix of 0xFFFFFFFF and verify your program refuses it instead of crashing or hanging.echo-server exercise theme, build a program that loops recv-ing framed messages and send_alls them back, correctly terminating when recv returns 0, and never assuming a full message arrived in one call.send/recv are write/read plus a flags argument; pass 0 unless you need MSG_NOSIGNAL (server sends) or non-blocking behavior.send_all/recv_all.recv returning 0 = orderly shutdown (normal EOF); -1 = error (check errno, retry on EINTR); >0 = that many bytes, possibly a short read.close the fd when done.