The Core Question
When you write a server, you have to ask yourself: what happens when two clients connect at the same time? Do you finish the first client’s request before touching the second one? Or do you handle both at the same time?
This is not a minor detail. It is the most important architectural decision for any server. Linux gives you two classic models: iterative (one at a time) and concurrent (many at a time). Let us understand both deeply.
1. Iterative Server
An iterative server handles clients one at a time. It picks up one client, finishes everything needed for that client, and only then moves on to the next one. Think of it like a single-window bank counter — one customer at a time.
How an Iterative Server Works
When is Iterative the Right Choice?
- Each request is handled very quickly (milliseconds)
- Client sends one request and gets one response (request-reply pattern)
- Low traffic is expected
- Simplicity matters more than performance
- UDP-based services (usually stateless)
- Processing per client takes a long time
- Many clients connect simultaneously
- Client and server exchange many messages (conversation)
- One slow client would block everyone else
The TLPI book gives a perfect example: a UDP echo server. The server receives a datagram and immediately echoes it back. This is almost instant — so iterative design is perfectly fine.
2. Concurrent Server
A concurrent server handles multiple clients at the same time. When a new client arrives, the server creates a separate entity (a child process or a thread) to handle that client, and goes back to listening for more clients. Think of it like a hospital with many doctors — each patient gets their own doctor.
How a Concurrent Server Works (Fork Model)
Handles Client 1
(closes listening socket)
Terminates when done
Closes connected socket
Goes back to accept()
Ready for Client 2
Still running…
Handles Client 2
Back to accept()
When is Concurrent the Right Choice?
- Each client request takes significant time
- Many clients connect at once
- Client and server have a long conversation
- You cannot afford one client to block others
- TCP-based services (usually stateful)
- fork() has memory and CPU overhead
- Must handle SIGCHLD to avoid zombies
- Parent must close connected socket after fork
- Child must close listening socket after fork
- More complex code than iterative
3. Thread-per-Client Variant
Instead of forking a child process, you can create a new POSIX thread for each client. This avoids the heavy cost of fork() but requires careful shared-memory management. The book mentions this as a variation — we will cover it in a later chapter.
Comparison: Three Approaches
| Feature | Iterative | Concurrent (fork) | Concurrent (thread) |
|---|---|---|---|
| Clients at once | 1 | Many (N processes) | Many (N threads) |
| Code complexity | Low | Medium | Medium-High |
| Memory overhead | Very low | High (fork copies memory) | Low (shared memory) |
| Isolation | N/A | Strong (separate memory) | Weak (shared memory) |
| Zombie handling | Not needed | Required (SIGCHLD) | Not needed |
| Typical use | UDP, simple protocols | Traditional TCP servers | Modern high-traffic servers |
- After
fork(), the parent must close the connected socket (cfd) — it is the child’s job to serve that client. - After
fork(), the child must close the listening socket (lfd) — it is the parent’s job to accept new clients. - The server must install a SIGCHLD handler that calls
waitpid()to reap zombie children. - The SIGCHLD handler should use a loop with WNOHANG to reap all terminated children in one go.
Code Skeleton: Concurrent Server
Here is a simplified skeleton showing the structure of a concurrent TCP server:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <netinet/in.h>
/* SIGCHLD handler - reaps zombie children */
static void sigchld_handler(int sig)
{
int saved_errno = errno; /* waitpid may change errno */
/* Loop to reap ALL terminated children, not just one */
while (waitpid(-1, NULL, WNOHANG) > 0)
continue;
errno = saved_errno;
}
int main(void)
{
int lfd, cfd;
struct sigaction sa;
/* Step 1: Install SIGCHLD handler before accepting any clients */
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART; /* restart interrupted system calls */
sa.sa_handler = sigchld_handler;
sigaction(SIGCHLD, &sa, NULL);
/* Step 2: Create listening socket (bind + listen) */
lfd = create_and_bind_socket(); /* your helper function */
/* Step 3: Accept loop */
for (;;) {
cfd = accept(lfd, NULL, NULL);
if (cfd == -1) {
perror("accept");
continue;
}
switch (fork()) {
case -1:
perror("fork");
close(cfd); /* can't fork - close and move on */
break;
case 0: /* CHILD: serve this client */
close(lfd); /* child doesn't need listening socket */
serve_client(cfd);
close(cfd);
_exit(EXIT_SUCCESS);
default: /* PARENT: go back to accepting */
close(cfd); /* parent doesn't need connected socket */
/* child has its own copy via fork - this closes parent's copy */
break;
}
}
/* never reached */
close(lfd);
return 0;
}
After fork(), both parent and child have a file descriptor pointing to the connected socket. The socket will only truly close (send FIN to client) when ALL file descriptors to it are closed. If the parent does not close its copy, the connection stays open even after the child exits.
The Echo Service (RFC 862)
The examples in Chapter 60 implement the echo service, defined in RFC 862. It is the simplest possible service: whatever the client sends, the server sends back exactly. It runs on port 7 for both UDP and TCP. Though simple, it is a perfect teaching tool because:
- UDP echo works naturally with an iterative server (one datagram at a time)
- TCP echo works well with a concurrent server (client may send many lines)
- It requires no application logic — only socket mechanics matter
Interview Questions & Answers
An iterative server handles one client at a time — it completes the entire conversation with one client before accepting the next. A concurrent server handles multiple clients simultaneously, typically by forking a child process or creating a thread for each new client. Iterative servers are simpler but block all other clients while one is being served. Concurrent servers are more complex but offer much better responsiveness under load.
You choose iterative when: (1) each request can be handled very quickly (no long computations or blocking I/O per client), (2) the traffic is low and simultaneous connections are rare, (3) the protocol is simple request-reply (especially UDP-based), and (4) simplicity and low overhead matter. A UDP echo server is a classic example where iterative design is perfectly appropriate.
After fork(), both the parent and child process hold a file descriptor referring to the same connected socket. A TCP connection is only terminated (FIN sent) when all file descriptors pointing to it are closed. If the parent does not close its copy, the client will never see the connection close, even after the child exits and closes its copy. The parent must close its copy immediately after fork() so only the child holds the connection.
After fork(), the child inherits the listening socket. The child’s job is to serve one specific client — it will never call accept(). Keeping the listening socket open in the child wastes a file descriptor, and if the parent later closes it, the socket won’t actually close until all children also close it. Closing it in the child immediately is correct practice.
A zombie (defunct) process is a child that has terminated but whose entry still remains in the process table because the parent has not called wait() or waitpid() to collect its exit status. In a concurrent server, when a child finishes serving a client and exits, it becomes a zombie until the parent reaps it. If the parent never reaps zombies, the process table fills up and no new processes can be created.
Install a SIGCHLD signal handler that calls waitpid(-1, NULL, WNOHANG) in a loop. WNOHANG makes waitpid return immediately instead of blocking. The loop is important because if multiple children exit at nearly the same time, multiple SIGCHLD signals may be merged into one — so a single call to waitpid might miss some. The loop continues until waitpid returns 0 or -1, ensuring all terminated children are reaped.
SA_RESTART is a flag in struct sigaction that tells the kernel to automatically restart certain system calls (like accept(), read(), write()) if they are interrupted by a signal. Without it, when SIGCHLD arrives and interrupts the accept() call in the parent, accept() returns -1 with errno = EINTR, and the server may crash or loop incorrectly. SA_RESTART makes the server more robust by transparently restarting those calls.
Threads share the same memory space as the parent, so creating a thread is much cheaper than fork() which copies the parent’s memory space (even with copy-on-write, the overhead is higher). Threads also have lower per-client memory overhead. However, threads require careful synchronization with mutexes and condition variables when accessing shared data, making the code more complex. Bugs like data races are also harder to detect.
Continue to Part 2
Build a real iterative UDP echo server step by step
