2.1 The UDP API — two new calls
No connections: each datagram carries its destination / reveals its source.
ssize_t recvfrom(int fd, void *buf, size_t n, int flags,
struct sockaddr *from, socklen_t *fromlen); /* value-result! */
ssize_t sendto(int fd, const void *buf, size_t n, int flags,
const struct sockaddr *to, socklen_t tolen);
No listen, no accept, no fork needed — one socket serves every client, datagrams from all of them interleave through the same recvfrom. The typical UDP server is iterative, not concurrent.
/* the UDP echo server's whole loop */
void dg_echo(int sockfd, struct sockaddr *cli, socklen_t clilen) {
ssize_t n; socklen_t len; char buf[MAXLINE];
for (;;) {
len = clilen;
n = recvfrom(sockfd, buf, MAXLINE, 0, cli, &len);
sendto(sockfd, buf, n, 0, cli, len);
}
}
A 0-byte datagram is legal (recvfrom returns 0 — not EOF; there is no EOF on UDP).
2.2 The lost-datagram problem
Client sends; the datagram (or the reply) is lost → client blocks in recvfrom forever. UDP has no retransmission — the application must add timeouts (SIGALRM, select with a timeout, or SO_RCVTIMEO). Subtlety: a timeout alone can't tell whether the request or the reply was lost — an important distinction for operations that aren't idempotent (debiting an account twice ≠ reading a counter twice). Reliable-over-UDP needs sequence numbers + retransmission + (ideally) RTT estimation — at which point you have rebuilt half of TCP; know when that effort is worth it.
Verifying the reply's source: recvfrom accepts datagrams from anyone; a careful client compares the reply's source address with the server's (or simply connects — below).
2.3 No flow control — UDP's foot-gun
Blast 2000 large datagrams at a slow receiver and watch most vanish: the receiving socket buffer fills, the kernel silently drops the excess. No backpressure exists. Demonstration numbers in the classic experiment: 2000 sent, ~30 received. Cures: application-level pacing/acks, bigger SO_RCVBUF (palliative), or just use TCP. Check drops with netstat -s -p udp ("receive buffer errors").
2.4 connect() on a UDP socket — the connected UDP socket
connect on UDP sends nothing — it merely records the peer address in the kernel:
| Aspect | Unconnected UDP | Connected UDP |
|---|---|---|
| send | sendto(..., to, len) | plain write/send |
| receive | from anyone | only from the connected peer |
| async errors (ICMP port unreachable) | not delivered | delivered → ECONNREFUSED on next call |
| repeated sends to same peer | route looked up each time | cached — measurably faster |
The famous asymmetry explained: send to a dead port from an unconnected socket and the ICMP error has no associated socket to report to — your client just times out; a connected socket gets ECONNREFUSED. Always connect a request-reply UDP client.
2.5 Determining the outgoing interface
On a multihomed host, which local IP will a datagram leave from? A connected UDP socket answers without sending a byte:
sock = socket(AF_INET, SOCK_DGRAM, 0);
connect(sock, (struct sockaddr *)&dest, sizeof(dest)); /* no packets sent */
getsockname(sock, (struct sockaddr *)&local, &len); /* kernel's chosen */
printf("would leave from %s\n", inet_ntop(...)); /* source IP+port */
The kernel runs its routing decision at connect time and getsockname reads the result — a standard trick (and lab exercise) for "what's my outbound IP toward host X?".
2.6 TCP vs UDP server structure — summary table
| Feature | TCP server | UDP server |
|---|---|---|
| sockets | listening + one per client | one |
| calls | bind, listen, accept, read/write | bind, recvfrom/sendto |
| concurrency | fork / threads / select | usually iterative |
| client identity | implicit in connected socket | from-address of each datagram |
| connection state in kernel | yes | none |
2.7 recvfrom and sendto, argument by argument
The two prototypes deserve the same per-argument treatment the TCP calls got:
fd— a SOCK_DGRAM socket (bound for servers; binding is optional for clients — the first sendto triggers an implicit bind to an ephemeral port).buf, n— the datagram buffer. Crucial UDP semantics: exactly one datagram per call, and if the datagram is larger thann, the excess is silently truncated and lost (unlike TCP, where the rest waits for the next read). Always size buffers to the largest expected datagram.flags— same flag set as recv/send (MSG_PEEK, MSG_DONTWAIT...).from/fromlen(recvfrom) — out: who sent this datagram; the value-result length again. Pass NULL/NULL if you don't care.to/tolen(sendto) — in: the destination of this one datagram.
And the matching client loop, for completeness next to dg_echo:
void dg_cli(FILE *fp, int sockfd, const struct sockaddr *servaddr, socklen_t servlen) {
int n; char sendline[MAXLINE], recvline[MAXLINE + 1];
while (fgets(sendline, MAXLINE, fp) != NULL) {
sendto(sockfd, sendline, strlen(sendline), 0, servaddr, servlen);
n = recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL); /* trusting! */
recvline[n] = 0;
fputs(recvline, stdout);
}
}
The two marked flaws — it never verifies the reply's source, and it blocks forever on loss — are exactly what §2.2 and the connected-socket discussion fix; exam questions often hand you this code and ask for the critique.
2.8 Adding reliability over UDP — the checklist
When the question asks "how would you make a UDP application reliable?", enumerate the machinery (each item maps to something TCP gives free):
- Sequence numbers in each request, echoed in the reply — detects duplicates, matches replies to requests.
- Timeout and retransmission of unanswered requests.
- Adaptive RTT estimation — fixed timeouts are wrong on every network but one; keep a smoothed RTT and variance (exactly TCP's RTO algorithm) per peer.
- Karn's rule — never update RTT estimates from a retransmitted exchange's timing (you can't tell which transmission the reply answers); back the timer off exponentially instead.
- Idempotence or a duplicate cache — a retransmitted request may execute twice at the server unless requests are idempotent or the server caches recent (client, seq) → reply.
Conclude with the judgement sentence: for one-shot request/response, this is a few dozen lines and worth it (DNS does it); for anything stream-like, you are reimplementing TCP badly — switch protocols.
2.9 Worked failure: UDP datagram to a closed port
Mirror of TCP's Case B, and a favourite compare-question. Client sendto a port nobody owns:
Why the asymmetry exists is worth two sentences: the ICMP error arrives carrying the offending datagram's headers; for a connected socket the kernel can match (peer addr, ports) to exactly one socket and post the error there. An unconnected socket may have sent to many destinations — the 4-tuple match fails, and the kernel (per the classic BSD behaviour) drops the error rather than guess. This is also the gap the icmpd daemon of Unit 4 closes.
Exam pointers
- "Compare TCP and UDP socket programming" — the §2.6 table plus the ladder diagrams from both lessons; mention that UDP needs neither listen/accept nor fork.
- "What is a connected UDP socket? What are its advantages?" — §2.4's four-row table; the async-error row and the always-connect-request-reply rule are the scoring lines.
- "Explain the lost-datagram problem and its solution" — loss → infinite block → timeout; then the request-vs-reply ambiguity and idempotence (that second half is what separates grades).
- Numeric bait: a 0-return from recvfrom is a zero-length datagram, not EOF — there is no EOF on UDP.
Check yourself
- A 2000-byte datagram is received with recvfrom(buf, 1024). What does the application get, and where did the rest go?
- Why does an unconnected UDP client never see ECONNREFUSED?
- Your UDP query client occasionally debits a customer twice. Which reliability ingredient is missing — and which two designs fix it?
- How can a program discover its outgoing interface toward 8.8.8.8 without sending a packet? Name all three calls involved.
- 2000 datagrams sent on a LAN, 1970 lost, no congestion. Explain the mechanism and name the netstat counter that proves it.