UDP: the postcard, not the phone call
Every socket you have written so far has been a TCP socket — a reliable, ordered, connection-based stream. Before any data moved, the three-way handshake set up a connection, and from then on TCP guaranteed that whatever you write() comes out the other side in the same order, with nothing lost, duplicated, or scrambled. It feels like a private telephone line between two programs. UDP is the other half of the TCP-versus-UDP choice, and it throws almost all of that away on purpose.
A UDP socket sends datagrams: self-contained little packets, each addressed and dropped into the network like a postcard into a mailbox. There is no handshake, no connection, no notion of a stream. Each datagram either arrives whole or not at all — the network may lose it, deliver two copies, or deliver three datagrams out of order, and UDP will not tell you or fix it. In exchange you get something TCP cannot: no connection setup, no per-byte ordering machinery, and lower latency, because a datagram can leave the instant you hand it over. The postcard is unreliable, but it is fast and it costs nothing to start sending.
Sending and receiving without a connection
Because there is no connection, the UDP API drops the calls that built one. You still call socket(), but with SOCK_DGRAM instead of SOCK_STREAM. There is no connect(), no listen(), no accept() — a UDP server simply bind()s to a port and starts receiving. And because each datagram is addressed on its own, the read/write calls change shape too: instead of send() and recv() on an already-connected peer, you use sendto(), which takes the destination address with each call, and recvfrom(), which hands you the sender's address alongside the data. The address travels with every packet rather than being fixed once at connect time.
/* UDP server core: bind, then loop on recvfrom */
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if (fd == -1) { perror("socket"); return 1; }
struct sockaddr_in me = {0};
me.sin_family = AF_INET;
me.sin_port = htons(9000); /* host -> network byte order */
me.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(fd, (struct sockaddr *)&me, sizeof me) == -1) {
perror("bind"); return 1;
}
char buf[1500]; /* one datagram at a time */
struct sockaddr_in from;
socklen_t fromlen = sizeof from;
ssize_t n = recvfrom(fd, buf, sizeof buf, 0,
(struct sockaddr *)&from, &fromlen);
if (n == -1) { perror("recvfrom"); return 1; }
/* reply straight to whoever sent it: */
sendto(fd, buf, n, 0, (struct sockaddr *)&from, fromlen);Notice the buffer is sized to 1500 bytes and read in a single recvfrom(). That is deliberate, and it is the deepest practical difference from TCP: UDP has message boundaries, whereas TCP does not. When the sender does one sendto() of 100 bytes, the receiver gets exactly one recvfrom() of 100 bytes — one datagram in, one datagram out, never merged with the next and never split. The byte-order rules you learned still apply: ports and addresses go through htons() and htonl() into network byte order exactly as before, because that part of the world has not changed.
Message boundaries: the difference that bites
This boundary difference deserves its own moment, because it cuts the other way too and trips up everyone moving between the two. TCP is a pure byte stream: it preserves the order and the count of your bytes, but not your write() boundaries. If a TCP sender writes "HELLO" then "WORLD", the receiver might read() "HELLOWORLD" in one call, or "HEL" then "LOWORLD", or any other split — the stream remembers ten bytes in order, but has utterly forgotten that you sent them as two messages.
That is why a real TCP server, the kind you built in the previous guide, cannot just assume one read() equals one message. It must impose its own framing on top of the stream — a length prefix, a delimiter like a newline, or a fixed record size — and loop until a whole message has arrived, tolerating short reads along the way. UDP simply does not have this problem: the datagram is the frame. The flip side is the cost we already met — that datagram might never arrive, and there is no stream to smooth over the gap. You trade TCP's framing chore for UDP's reliability chore; one of the two chores is always yours.
Blocking: the quiet pause inside every socket call
Now to the second assumption. Every socket call you have written has been blocking, and this is the default. When you call recv() and no data has arrived yet, your program does not return — it goes to sleep. The kernel parks the whole thread, hands the CPU to someone else, and only wakes you when a byte finally turns up. The same is true of accept() with no pending connection, of connect() during the handshake, and of send() when the kernel's send buffer is full. From your code's point of view the call simply takes a long time; underneath, the blocking versus non-blocking choice is about who waits and how.
For a program talking to one peer, blocking is a blessing: your code reads top to bottom like a story, and the kernel quietly handles the waiting without burning any CPU. The trouble appears the moment you must serve many peers at once. If your single thread is asleep inside recv() waiting on client A, it is deaf to client B, who is trying to send you something right now. A blocking call only watches one socket; while it sleeps, every other connection waits its turn. This is the wall the previous TCP server hit, and there are three classic ways through it.
- One process or thread per connection: simple, and it lets every connection block happily on its own. But threads and processes cost memory and context switches, so this stops scaling somewhere in the thousands.
- Non-blocking sockets plus a polling loop: make the calls return immediately and ask the kernel which sockets are ready, so one thread can tend many. This is what the rest of this guide builds toward.
- Asynchronous or completion-based APIs: hand the kernel a request and a place to put the answer, and be told later when it finished. A different model, hinted at below as readiness versus completion.
Non-blocking sockets and asking "who is ready?"
A non-blocking socket is one you have switched out of the default behaviour, typically with fcntl(fd, F_SETFL, O_NONBLOCK). After that, the rule changes: a call that would have slept instead returns immediately. If recv() has data, you get it; if it has none, it returns -1 and sets errno to EAGAIN (also spelled EWOULDBLOCK), which is the kernel's polite way of saying "nothing right now, do not wait, ask again later." Crucially, EAGAIN is not an error to log and abort on — it is normal flow control, and a non-blocking program treats it as "try this socket again next time round."
But a non-blocking socket alone would tempt you into a busy loop — spinning over every socket calling recv(), getting EAGAIN, and asking again, pinning a CPU core at 100% just to wait. The missing piece is a way to sleep until something, anything across many sockets, becomes ready. That is exactly what I/O multiplexing provides: the calls select(), poll(), and epoll() let you hand the kernel a whole list of file descriptors and say "put me to sleep, and wake me when any of these is readable or writable." One thread, one blocking wait, watching hundreds of sockets at once — the efficiency of blocking with the reach of non-blocking.
Step back and see the whole rung in one frame. You began by meeting the socket as the endpoint idea, learned addresses, ports, and byte order, then built a TCP client and server that trade a reliable stream. This guide widened the lens twice: UDP showed that reliability and ordering are a choice, not a law, paid for in code you write yourself; non-blocking sockets plus multiplexing showed that waiting is a choice too, the hinge between a server that handles one client and one that handles a city of them. Sockets are simply the file-descriptor idea from earlier rungs, stretched across a network — the same read(), write(), and close() you have trusted all along, now reaching another machine.