What We Are Building
The UDP echo server is the simplest real server you can write. It listens on a UDP port, reads any datagram it receives, and immediately sends an exact copy back to the sender. That is it. No application logic, no parsing — just network mechanics.
From TLPI Chapter 60, this server implements RFC 862 (Echo Protocol) on UDP port 7. It becomes a proper Linux daemon using becomeDaemon(), logs errors via syslog(), and uses the internet socket helper library from Chapter 59. Let us understand every piece.
Why Iterative Works for UDP Echo
Remember from Part 1: iterative servers are fine when each request is handled quickly. The UDP echo server receives a datagram (a few hundred bytes max) and sends it right back. This takes microseconds. No client needs to wait more than a tiny fraction of a millisecond for the previous client’s datagram to be echoed. So iterative is perfect.
UDP Echo: The flow per datagram
The entire processing between recvfrom and sendto is near-instant — no file I/O, no heavy computation. This is why iterative is completely fine here.
Step 1 — The Header File (id_echo.h)
TLPI keeps the shared definitions between server and client in a header file. Two things are defined: the service name and the buffer size.
/* id_echo.h - shared header for UDP echo server and client */
#include "inet_sockets.h" /* Internet socket helper functions from Ch. 59 */
#include "tlpi_hdr.h" /* TLPI error-handling macros: errExit, fatal, etc. */
#define SERVICE "echo" /* Service name - maps to port 7 via /etc/services */
#define BUF_SIZE 500 /* Max size of a datagram we will read or send */
/* UDP datagrams in this example are small */
/etc/services to get the actual port number 7. This is more maintainable than hardcoding 7 directly. The inetBind() and inetConnect() helper functions both accept a service name string and do the getaddrinfo() lookup internally.Step 2 — The UDP Echo Server (id_echo_sv.c)
Here is the full server from Listing 60-2, with detailed comments explaining every decision:
/* id_echo_sv.c - Iterative UDP echo server (from TLPI Listing 60-2) */
#include <syslog.h>
#include "id_echo.h"
#include "become_daemon.h"
int
main(int argc, char *argv[])
{
int sfd;
ssize_t numRead;
socklen_t addrlen, len;
struct sockaddr_storage claddr; /* Holds client address - IPv4 or IPv6 */
char buf[BUF_SIZE];
char addrStr[IS_ADDR_STR_LEN]; /* For printing client address in logs */
/*
* Step 1: Become a daemon.
*
* becomeDaemon() does: fork() → setsid() → fork() → chdir("/") →
* close stdin/stdout/stderr → redirect them to /dev/null.
*
* After this call, we run as a background daemon with no controlling
* terminal. We cannot use printf() for errors — must use syslog().
*/
if (becomeDaemon(0) == -1)
errExit("becomeDaemon");
/*
* Step 2: Create and bind a UDP socket.
*
* inetBind() is a helper from Section 59.12. It calls getaddrinfo()
* to resolve "echo" to port 7, creates a SOCK_DGRAM socket, and
* binds it. It also fills 'addrlen' with the address length.
*/
sfd = inetBind(SERVICE, SOCK_DGRAM, &addrlen);
if (sfd == -1) {
syslog(LOG_ERR, "Could not create server socket (%s)", strerror(errno));
exit(EXIT_FAILURE);
}
/*
* Step 3: The infinite receive-echo loop.
*
* This is the core of the iterative server. We never accept() because
* UDP has no connections. Each call to recvfrom() gets one datagram
* and tells us which client sent it (via claddr).
*/
for (;;) {
len = sizeof(struct sockaddr_storage);
/*
* recvfrom() blocks until a datagram arrives.
* It fills claddr with the sender's address and port.
* Returns number of bytes received.
*/
numRead = recvfrom(sfd, buf, BUF_SIZE, 0,
(struct sockaddr *) &claddr, &len);
if (numRead == -1)
errExit("recvfrom");
/*
* Echo the datagram back to whoever sent it.
* We use the claddr filled by recvfrom() to address the reply.
* If sendto() fails, we log a warning but keep running.
*
* In production: rate-limit syslog() calls here because:
* (a) each syslog() calls fsync() by default — expensive
* (b) an attacker sending bad packets could flood the log
*/
if (sendto(sfd, buf, numRead, 0,
(struct sockaddr *) &claddr, len) != numRead)
syslog(LOG_WARNING, "Error echoing response to %s (%s)",
inetAddressStr((struct sockaddr *) &claddr, len,
addrStr, IS_ADDR_STR_LEN),
strerror(errno));
}
/* Never reached */
}
Breaking Down the Key Points
A daemon is a background process with no controlling terminal. becomeDaemon() from Section 37.2 performs these steps automatically:
fork() — parent exits, child continues. Child is now orphaned and adopted by init.setsid() — creates a new session, detaches from controlling terminal.fork() again — ensures the process can never re-acquire a controlling terminal./, clear umask, close all open file descriptors, redirect stdin/stdout/stderr to /dev/null.After becomeDaemon() returns, you can no longer use printf(), perror(), or fprintf(stderr,...) for error reporting. You must use syslog() instead.
For UDP, you use recvfrom() because you need to know who sent the datagram in order to send the reply back. recv() only gives you the data; it discards the sender’s address. The function signatures:
/* recv() — does NOT tell you who sent the data */
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
/* recvfrom() — fills 'src_addr' with the sender's address and port */
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
In the echo server, the claddr (client address) filled by recvfrom() is passed directly to sendto() to send the reply back to the exact same client.
You might wonder: why not use struct sockaddr_in (for IPv4) or struct sockaddr_in6 (for IPv6)? Because a modern server should work with both. struct sockaddr_storage is large enough to hold any socket address family. It is the standard way to write address-family-agnostic code.
struct sockaddr_storage claddr; /* Big enough for IPv4 or IPv6 */
socklen_t len = sizeof(struct sockaddr_storage);
/* recvfrom fills claddr with whatever the client is (IPv4 or IPv6) */
recvfrom(sfd, buf, BUF_SIZE, 0, (struct sockaddr *) &claddr, &len);
/* sendto sends back using the same claddr - works for both IPv4 and IPv6 */
sendto(sfd, buf, numRead, 0, (struct sockaddr *) &claddr, len);
Since a daemon has no terminal, all log messages go to the system logger via syslog(). Two priority levels are used here:
LOG_ERR— fatal error (can’t create socket): server exitsLOG_WARNING— non-fatal error (can’t send reply): server continues running
/* Log a fatal error and exit */
syslog(LOG_ERR, "Could not create server socket (%s)", strerror(errno));
exit(EXIT_FAILURE);
/* Log a warning but keep running */
syslog(LOG_WARNING, "Error echoing response to %s (%s)",
client_address_string, strerror(errno));
Step 3 — The UDP Echo Client (id_echo_cl.c)
The client program tests the server. It takes the server hostname as the first argument, then sends each remaining argument as a separate UDP datagram, reading and printing the echo.
/* id_echo_cl.c - Client for the UDP echo service (from TLPI Listing 60-3) */
#include "id_echo.h"
int
main(int argc, char *argv[])
{
int sfd, j;
size_t len;
ssize_t numRead;
char buf[BUF_SIZE];
if (argc < 2 || strcmp(argv[1], "--help") == 0)
usageErr("%s: host msg...\n", argv[0]);
/*
* inetConnect() helper from Section 59.12:
* - Calls getaddrinfo() to resolve argv[1] (hostname) and "echo" (port 7)
* - Creates a SOCK_DGRAM socket
* - Calls connect() on it
*
* Calling connect() on a UDP socket does NOT send anything to the server.
* It just sets the default destination address, so we can use write()
* instead of sendto(). It also filters incoming packets to only those
* from the server's address.
*/
sfd = inetConnect(argv[1], SERVICE, SOCK_DGRAM);
if (sfd == -1)
fatal("Could not connect to server socket");
/*
* Send each command-line argument as a separate datagram.
* Read and print the echo response after each send.
*/
for (j = 2; j < argc; j++) {
len = strlen(argv[j]);
/* write() works here because we called connect() on the socket */
if (write(sfd, argv[j], len) != len)
fatal("partial/failed write");
/* read() the echoed datagram back */
numRead = read(sfd, buf, BUF_SIZE);
if (numRead == -1)
errExit("read");
/* Print: [N bytes] the message */
printf("[%ld bytes] %.*s\n", (long) numRead, (int) numRead, buf);
}
exit(EXIT_SUCCESS);
}
Normally UDP is connectionless — you send datagrams to different destinations each time. But calling
connect() on a UDP socket “associates” it with a specific remote address. After connect(): (1) you can use write()/read() instead of sendto()/recvfrom(), and (2) the kernel filters incoming datagrams — only those from the connected address are delivered. No actual handshake happens — it is purely a local kernel setting.Running the Example
Here is what the book shows when running the server and two clients:
$ su # Need root to bind port 7 (privileged port)
Password:
# ./id_echo_sv # Start server — it daemonizes itself, returns to shell
# exit # Drop root privileges
$ ./id_echo_cl localhost hello world # Client sends two datagrams: "hello" and "world"
[5 bytes] hello # Server echoed "hello" back
[5 bytes] world # Server echoed "world" back
$ ./id_echo_cl localhost goodbye # Second client sends one datagram
[7 bytes] goodbye # Server echoed it back
claddr). Even though the server is iterative, both clients get responses because the processing per datagram is nearly instant.Complete Standalone UDP Echo Server (No Helper Library)
Here is a self-contained version without the TLPI helper functions, showing you exactly what inetBind() does under the hood:
/*
* udp_echo_server.c - standalone iterative UDP echo server
* Compile: gcc -o udp_echo_server udp_echo_server.c
* Run as root: sudo ./udp_echo_server 7777 (use non-privileged port for testing)
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define BUF_SIZE 500
int main(int argc, char *argv[])
{
int sfd;
struct sockaddr_in svaddr, claddr;
socklen_t cllen;
ssize_t numRead;
char buf[BUF_SIZE];
char addrStr[INET_ADDRSTRLEN];
int port;
if (argc != 2) {
fprintf(stderr, "Usage: %s port\n", argv[0]);
exit(EXIT_FAILURE);
}
port = atoi(argv[1]);
/* Create UDP socket */
sfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sfd == -1) {
perror("socket");
exit(EXIT_FAILURE);
}
/* Bind to the given port on all interfaces */
memset(&svaddr, 0, sizeof(svaddr));
svaddr.sin_family = AF_INET;
svaddr.sin_addr.s_addr = htonl(INADDR_ANY);
svaddr.sin_port = htons(port);
if (bind(sfd, (struct sockaddr *) &svaddr, sizeof(svaddr)) == -1) {
perror("bind");
exit(EXIT_FAILURE);
}
printf("UDP echo server listening on port %d\n", port);
/* Iterative receive-echo loop */
for (;;) {
cllen = sizeof(struct sockaddr_in);
numRead = recvfrom(sfd, buf, BUF_SIZE, 0,
(struct sockaddr *) &claddr, &cllen);
if (numRead == -1) {
perror("recvfrom");
continue;
}
/* Log which client sent data */
inet_ntop(AF_INET, &claddr.sin_addr, addrStr, INET_ADDRSTRLEN);
printf("Received %ld bytes from %s:%d\n",
(long) numRead, addrStr, ntohs(claddr.sin_port));
/* Echo it back */
if (sendto(sfd, buf, numRead, 0,
(struct sockaddr *) &claddr, cllen) != numRead)
perror("sendto - partial/failed");
}
close(sfd); /* never reached */
return 0;
}
/* udp_echo_client.c - simple UDP echo client for testing
* Compile: gcc -o udp_echo_client udp_echo_client.c
* Usage: ./udp_echo_client 127.0.0.1 7777 hello world
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define BUF_SIZE 500
int main(int argc, char *argv[])
{
int sfd, j;
struct sockaddr_in svaddr;
ssize_t numRead;
size_t len;
char buf[BUF_SIZE];
if (argc < 4) {
fprintf(stderr, "Usage: %s server-ip port msg...\n", argv[0]);
exit(EXIT_FAILURE);
}
sfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sfd == -1) { perror("socket"); exit(EXIT_FAILURE); }
memset(&svaddr, 0, sizeof(svaddr));
svaddr.sin_family = AF_INET;
svaddr.sin_port = htons(atoi(argv[2]));
if (inet_pton(AF_INET, argv[1], &svaddr.sin_addr) <= 0) {
fprintf(stderr, "Invalid address: %s\n", argv[1]);
exit(EXIT_FAILURE);
}
/* connect() so we can use write()/read() instead of sendto()/recvfrom() */
if (connect(sfd, (struct sockaddr *) &svaddr, sizeof(svaddr)) == -1) {
perror("connect"); exit(EXIT_FAILURE);
}
for (j = 3; j < argc; j++) {
len = strlen(argv[j]);
if (write(sfd, argv[j], len) != (ssize_t)len) {
perror("write"); continue;
}
numRead = read(sfd, buf, BUF_SIZE);
if (numRead == -1) { perror("read"); continue; }
printf("[%ld bytes] %.*s\n", (long) numRead, (int) numRead, buf);
}
close(sfd);
return 0;
}
Interview Questions & Answers
recv() receives data but does not tell you who sent it — the sender’s address is discarded. recvfrom() fills a struct sockaddr buffer with the sender’s IP address and port. For a UDP echo server (or any UDP server that needs to reply to the sender), you must use recvfrom() because you need the client’s address to pass to sendto() for the reply.
Calling connect() on a UDP socket sets the default destination address for that socket. After connecting, you can use write() and read() instead of sendto() and recvfrom(). It also makes the kernel filter incoming packets — only datagrams from the connected remote address are delivered to the socket. No actual network connection or handshake occurs; it is a purely local kernel setting.
becomeDaemon() transforms a process into a proper daemon: it forks twice, calls setsid(), changes the working directory to /, and redirects stdin, stdout, and stderr to /dev/null. After this, the process has no terminal and no standard streams. Any printf() or perror() calls would write to /dev/null and be silently discarded. So syslog() is the only correct way to report errors from a daemon.
struct sockaddr_storage is large enough to hold any socket address — IPv4 (sockaddr_in), IPv6 (sockaddr_in6), or any future family. Using it allows you to write address-family-agnostic code. If you used sockaddr_in and a client connected via IPv6, the address would overflow the buffer, causing memory corruption. The standard practice is to use sockaddr_storage and cast to the specific type only when you need to access family-specific fields.
Yes, this is the correct approach for a UDP server. UDP is inherently unreliable — packet loss is expected. A single failed sendto() (perhaps the client disappeared or the network had a hiccup) should not crash the server. The server logs a warning (so operators can monitor for problems) and continues serving other clients. If it exited on every failed send, a single bad client or temporary network issue would take down the service for everyone.
By default, syslog() calls fsync() after each write, making it an expensive operation. In a high-traffic server, if an attacker sends many malformed packets that trigger log entries, the server could become extremely slow due to constant disk I/O. Mitigation strategies include: (1) configuring syslogd to not fsync on every write (e.g., prefix the log file path with - in /etc/rsyslog.conf), (2) implementing a rate limiter in the server code that caps how many log entries are written per second, or (3) using a logging framework with asynchronous buffered writes.
Yes. An iterative UDP server can use select() or poll() to implement timeouts — for example, to exit after a period of inactivity, or to handle multiple UDP sockets (e.g., IPv4 and IPv6) in a single loop. The recvfrom() would then only be called on a socket that select() reported as readable, preventing unnecessary blocking. This is still iterative (one datagram at a time) but more flexible than a plain blocking recvfrom().
Continue to Part 3
Learn how to build a concurrent TCP echo server with fork()
