Iterative or concurrent, connection-oriented or not
◈ 11 cardsClassify a server into the four-way taxonomy and write the algorithm for each side — including the child reaping the textbook leaves out and the SO_REUSEADDR it never mentions.
Two independent questions
Every socket server answers two questions, and they are independent, which is what makes the taxonomy a 2×2 rather than a list.
Question one: connection-oriented or connectionless? That is the SOCK_STREAM/SOCK_DGRAM choice from the previous lesson. A connection-oriented server calls listen() and accept(); a connectionless one never does, because there is no connection to accept.
Question two: iterative or concurrent? An iterative server handles one client to completion, then the next. A concurrent server hands each client off — classically to a forked slave process — and immediately goes back to waiting.
A warning on vocabulary, because the textbook gets this word wrong in §20.7 and an exam script that copies it loses the mark. The correct term is iterative, not interactive. "Interactive" describes a program a human converses with, which has nothing to do with how a server multiplexes clients.
When each is right
An iterative server is right for a one-shot, short response: DAYTIME returns the time and hangs up, TIME returns a 32-bit integer, ECHO bounces a line back. Serialising those costs nobody anything, and the code is a single loop with no signals in it.
A concurrent server is right for a session — many request/response pairs over one connection. Consider what an iterative server would do to a session-based service: while client A holds a five-minute SSH session, every other client waits five minutes. Average waiting time climbs without bound, and worse, the passive socket's backlog queue overflows and connection requests are simply lost. That is why HTTP, FTP, SSH and TELNET are all concurrent connection-oriented servers.
The algorithms, changed one line at a time
Iterative connectionless (UDP). The simplest server there is:
socket(PF_INET, SOCK_DGRAM, 0)bind()to the service portrecvfrom()a request — this blocks, and it also yields the client's address- build the response and
sendto()it back to that address - go to 3
Concurrent connectionless. Change exactly one thing: fork() after step 3. The master returns immediately to recvfrom(); the slave builds the response, sendto()s it, and exits after that single request. Note what did not change — there is still only one socket, because there is no connection and therefore nothing per-client to hold.
Iterative connection-oriented (TCP). socket → bind → listen → accept → serve the client to completion → close the accepted socket → back to accept. The book subdivides this one usefully: a connection-triggered service treats the arrival of the connection as the request and sends no client message at all (DAYTIME), while a one-shot service reads one request and answers it (TIME, ECHO).
Concurrent connection-oriented, master–slave. This is the important one:
| Master | Slave |
|---|---|
1. socket(SOCK_STREAM) | 1. inherits the socket accept() created |
2. bind() | 2. read() a request |
3. listen() | 3. if it is a quit, go to 5 |
4. accept(), then fork() | 4. build the response, write() it, go to 2 |
| 5. close the accepted socket, go to 4 | 5. close() the active socket and _exit() |
Two details in that table earn marks. The master closes its copy of the accepted socket after forking — the descriptor is duplicated by fork(), and if the master keeps its copy open the connection never fully closes when the slave is done. And the slave may additionally exec() itself into the real service binary, which is exactly how the superserver inetd works.
The step the textbook leaves out: reaping
The algorithm above, as the book prints it, is incomplete in a way that will cost you a running server. Every slave that finishes becomes a zombie: it has exited, but its exit status stays in the process table until its parent collects it. The master never waits for anything — it is busy in accept() — so nothing is ever collected, and the zombie count grows by one per client until the process table fills and no process on the machine can fork.
The fix is a SIGCHLD handler:
static void reap(int sig) {
while (waitpid(-1, NULL, WNOHANG) > 0)
;
}
Two things about that handler are exam-worthy. WNOHANG makes waitpid() return immediately with 0 when no child has exited, instead of blocking — a handler must never block. And the loop is not optional. Standard signals are not queued: if three slaves exit at almost the same instant, the kernel may deliver a single SIGCHLD, and a handler that called waitpid() once would reap one child and leave two zombies behind forever. Install it with sigaction(), not signal(), and pass SA_RESTART so the accept() it interrupts resumes instead of failing with EINTR.
The other thing the textbook never mentions: SO_REUSEADDR
Stop your server and restart it a second later, and bind() fails with EADDRINUSE even though nothing is listening. The reason is TCP's TIME_WAIT state: after a connection closes, the endpoint lingers for twice the maximum segment lifetime so that stray duplicate segments from the dead connection cannot be mistaken for part of a new one. While any such endpoint lingers on your port, the kernel refuses to bind it.
Waiting out TIME_WAIT is not an option for a server you are debugging. The fix is one socket option, set before bind():
int on = 1;
setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &on, sizeof on);
It tells the kernel that a lingering TIME_WAIT endpoint on this port is not an obstacle to binding a new listening socket. Every real server sets it, and it is the answer to the most common question a first socket program produces.
source Stevens & Rago, APUE 3e ch16 (and §10.7 on unqueued signals)
source Stevens & Rago, APUE 3e ch16 (SO_REUSEADDR); POSIX.1-2024 sigaction(), setsockopt()
source Stevens & Rago, APUE 3e ch16, §10.7
source Stevens & Rago, APUE 3e ch16
source Stevens & Rago, APUE 3e §10.7, ch16
source Stevens & Rago, APUE 3e ch16 (SO_REUSEADDR and TIME_WAIT)
source Stevens & Rago, APUE 3e ch16