You sent three messages. Two arrived, backwards.
You write a tiny program. It calls sendto three times with
"HELLO", "WORLD!!", and "BYE".
Every call returns the number of bytes you passed. No error.
On the other machine, recvfrom returns "BYE",
then "HELLO", then blocks forever.
"WORLD!!" never shows up.
Nothing is broken. This is UDP doing exactly what it promises, which is
almost nothing. The return value of sendto means one thing:
the kernel copied your bytes into a packet and handed the packet to the network
interface. That is where UDP's responsibility ends.
A datagram is a packet you can see
In the first lesson you built a frame by hand: payload, 8-byte UDP header, 20-byte IP header, Ethernet header. That 8-byte header is the entire protocol. Source port, destination port, length, checksum. Nothing else. There is no field for "which message is this" and no field for "did you get the last one". The protocol cannot retransmit because it has no way to know anything was lost.
So a UDP socket is a very direct thing. socket(AF_INET, SOCK_DGRAM, 0)
gives you an fd whose kernel object has a receive buffer and almost no state.
Each sendto becomes exactly one IP packet. Each arriving packet
becomes exactly one recvfrom. The packet is the unit, and the unit
is called a datagram.
Message boundaries are kept
This is the one guarantee UDP does make, and it is a useful one.
If you sendto 5 bytes, the receiver's recvfrom returns
those 5 bytes and nothing else. It will not return 3 of them. It will not glue
them to the next datagram. The boundary you created when you called
sendto is the boundary the receiver sees.
That boundary comes from the length field in the header. The kernel reads it,
knows exactly where the datagram ends, and queues it as one unit in the socket's
receive buffer. Your recvfrom dequeues one unit.
The visualization shows all of this: three datagrams, one dropped on the way,
the two survivors arriving in the wrong order, each one coming out whole.
What UDP does not do
- No connection. There is no handshake. The first packet you send is data. The remote port can be closed; you may get an ICMP error back, or nothing at all.
- No ordering. Two datagrams can take different routes. The second can arrive first.
- No delivery guarantee. A congested router drops the packet and nobody is told.
sendtohas already returned success. - No congestion control. You can send as fast as you like. The network will drop what it cannot carry, and it will drop other people's traffic too.
- No duplicate suppression. Rare, but a datagram can arrive twice.
If your program needs any of those properties, you build them yourself on top of UDP: sequence numbers in your payload, acknowledgements, retransmit timers. That sounds like a lot of work, and it is. It is also exactly what the next protocol you will meet does for you. The reason to choose UDP anyway is that sometimes you want different rules than TCP's.
The truncation trap
recvfrom(fd, buf, 8, ...) on a 16-byte datagram copies 8 bytes
into buf, returns 8, and throws the other 8 away.
They are not waiting in the buffer for your next call. The next call returns
the next datagram. There is no error. The only hint is the MSG_TRUNC
flag in msg_flags if you use recvmsg, which almost nobody does.
An echo server and client
A UDP server has no listen, no accept, and no per-client
socket. One fd, bound to one port, receives from everybody. The address of each
sender comes back in the recvfrom out-parameter, and you reply to it
with sendto. That is the whole server.
The client is the mirror image. It does not bind: the kernel assigns an
ephemeral port on the first sendto, and the server's reply comes back to it.
Notice the client has to decide for itself how long to wait, because nothing in
UDP will ever tell it the datagram was lost.
SO_RCVTIMEO makes a blocking recvfrom fail with
EAGAIN after the interval. Without it, one lost datagram
leaves your program asleep forever. Treat "no reply" as a normal outcome,
not an exception.
Who wants this?
UDP looks like the bad option until you notice who uses it. DNS: one question, one answer, a connection would cost more than the lookup. Games and voice: a late packet is useless, so retransmitting it is worse than dropping it. QUIC, the transport under HTTP/3, runs over UDP so it can implement its own reliability without the kernel's TCP in the way.
And VPNs. WireGuard carries encrypted IP packets inside UDP datagrams. The packets it carries are already somebody else's TCP or UDP, with their own retransmission. Wrapping them in a second reliable layer would mean retransmitting retransmissions. The datagram model, one inner packet per outer packet, is the honest fit. When you build a mesh VPN, this is the socket you will build it on.
One sendto is one datagram and one recvfrom returns one datagram — whole, in any order, or never; everything else is your problem.