Networking · lesson 02

A socket is a file descriptor

send() returned 5, so the message was sent — except it wasn't. Nothing has left your machine yet. Understanding where those 5 bytes actually went explains almost every surprising thing sockets do.

in progress
12 min interactive

send() returned. Where are the bytes?

You call send(fd, "hello", 5, 0). It returns 5 immediately. Then you unplug the network cable. The call already returned success. Did the message arrive?

No. send did not send anything. It copied your five bytes into a buffer owned by the kernel and returned. The kernel will try to get them onto the wire later, without you. That buffer, and its twin on the receiving side, is the whole subject of this lesson. Once you can picture the two buffers, the rest of the socket API stops being mysterious.

socket() returns an integer

You saw in the file-descriptors lesson that every open file is an index into a per-process table, and that the table entry points at a kernel object. A socket is the same thing with a different kernel object behind it.

c
#include <sys/socket.h> #include <unistd.h> // AF_INET: IPv4. SOCK_STREAM: TCP (a byte stream). SOCK_DGRAM would be UDP. int fd = socket(AF_INET, SOCK_STREAM, 0); if (fd == -1) { perror("socket"); return 1; } printf("%d\n", fd); // prints 3 — the lowest free slot after 0, 1, 2 // … connect it to a peer (lesson 03 covers the address struct) … write(fd, "hello", 5); // yes, plain write() works on a socket close(fd);

The return value is 3 because 0, 1, and 2 are taken. read, write, close, dup2, and poll all accept it. The shell can even redirect it. There is nothing special about the integer. What is special is the object it points to.

The kernel object: two buffers

When socket runs, the kernel allocates a socket structure. Among the many fields it holds, two matter for everything you will do:

  • The send buffer. Bytes your program has handed over that have not yet been transmitted and acknowledged. Default size on Linux: about 200 KB, tunable with SO_SNDBUF.
  • The receive buffer. Bytes that have arrived from the network that your program has not yet read. Default about 200 KB, tunable with SO_RCVBUF.

Both live in kernel memory. You cannot point at them. You can only move bytes into one and out of the other through system calls. Every socket call you will ever make is a copy to or from these two buffers, or a change to the rules that govern them.

💡
send() is a memcpy into kernel memory. It copies from your buffer into the socket's send buffer and returns the number of bytes it copied. Nothing about the network has happened yet. The kernel transmits from its copy later, on its own schedule, wrapped in the headers from lesson 01. This is why you may free or overwrite your buffer the moment send returns.

Step through a round trip

The visualization below shows the three places bytes can be: your memory (blue), the kernel's two buffers (teal — memory you did not allocate and cannot touch), and the fd table entry that connects them. Watch which calls copy, which calls block, and when the wire is actually involved.

one socket, two kernel buffers
Click next to start the walkthrough.

recv() gives you what is there, not what you asked for

recv(fd, buf, 64, 0) does not mean "give me 64 bytes". It means "copy up to 64 bytes from the receive buffer into buf". If 3 bytes have arrived, you get 3 and recv returns 3. If 500 have arrived, you get 64 and the remaining 436 wait in the kernel for your next call. The return value is the only truth.

c
char buf[64]; ssize_t n = recv(fd, buf, sizeof buf, 0); if (n > 0) { // n bytes are valid: buf[0] .. buf[n-1]. buf is NOT null-terminated. fwrite(buf, 1, n, stdout); } else if (n == 0) { // the peer closed its side. There will never be more bytes. } else { perror("recv"); // -1: check errno (ECONNRESET, EINTR, …) }

Three outcomes, and you must handle all three. Positive: that many bytes. Zero: end of stream, the peer is gone. Negative: an error, with the reason in errno. A fourth outcome is not a return value at all: if the receive buffer is empty, recv does not return. Your thread sleeps inside the kernel until bytes arrive or the peer closes.

🚫
Treating the receive buffer as a C string. recv copies raw bytes. It does not write a '\0'. printf("%s", buf) after recv reads past the received bytes into whatever was on your stack, and past the end of buf if it never finds a zero. Either terminate it yourself (buf[n] = 0, leaving room) or use length-aware calls like fwrite.

When the buffers fill up

Buffers have limits, and the limits are what make sockets behave. Suppose your program sends 64 KB in a loop and the peer never reads. The peer's receive buffer fills. Its kernel tells your kernel to stop. Your send buffer stops draining and fills too. Your next send finds no room — and blocks. Your program slows down to exactly the speed the reader can handle. This is backpressure, and it is why a fast producer cannot flood a slow consumer and exhaust memory. The kernel enforces it for you.

✅
Blocking is the default and it is correct for most programs. recv on an empty buffer sleeps until data arrives; send on a full buffer sleeps until there is room. No busy loop, no polling, no wasted CPU. The time blocking becomes a problem is when one thread must watch several sockets at once — lesson 08 solves that without giving up this simplicity.

Look at the buffers

The kernel will show you the two buffers of every socket on the machine. ss prints them as Recv-Q and Send-Q, in bytes:

sh
$ ss -tn State Recv-Q Send-Q Local Address:Port Peer Address:Port ESTAB 0 0 192.168.1.7:51234 93.184.216.34:443 ESTAB 18340 0 192.168.1.7:51240 10.0.0.5:5432 ← 18 KB waiting; nobody has called recv ESTAB 0 204800 192.168.1.7:51252 10.0.0.9:9000 ← send buffer full; send() is blocking

A growing Recv-Q means the program that owns the socket is not reading. A full Send-Q means the peer is not reading. Both are visible from outside the program, with no debugger, because the buffers belong to the kernel, not to you.

one-line takeaway

A socket is an fd pointing at two kernel buffers: send copies in and returns before anything is transmitted, recv copies out whatever has arrived, and both block when their buffer is full or empty.