Unit 2 · Chapter 5 · Book pp. 121–152
TCP Client/Server Example
About 150 lines of code, and the whole rest of the chapter is about what goes wrong. That is deliberate. Writing a client and server that work when everything works is easy; the chapter is really a catalogue of boundary conditions and how each one shows up in your program.
1. The client reads a line of text from its standard input and writes the line to the server.
2. The server reads the line from its network input and echoes the line back to the client.
3. The client reads the echoed line and prints it on its standard output.
netstat output from this very
example and asked which command produced it and what the column headers mean. The
interactive output is below; the full answer is in the
solved paper.
The echo server
str_echo — the server's work
The echo client
Normal startup, watched with netstat
This is the observable half of the chapter. Step the session below and watch the socket table change; click any column header for what it means.
netstat -a — often piped through
grep 9877 to cut the output down. The -a flag is
what makes listening sockets visible; without it you would see the two
ESTABLISHED rows and not the LISTEN row.
netstat shows you both ends of the same connection — rows one
and two are mirror images of each other. Add the still-open listening socket and
you have three. Run the two programs on different machines and each would show only
its own rows.
Normal termination, step by step
The book's seven numbered steps, for an answer you can write out:
- We type the EOF character (Ctrl-D).
fgetsreturns a null pointer andstr_clireturns. str_clireturns to the client'smain, which terminates by callingexit.- Part of process termination is closing all open descriptors, so the client socket is closed by the kernel. This sends a FIN to the server, and the server TCP responds with an ACK. This is the first half of the termination sequence. At this point the server socket is in
CLOSE_WAITand the client socket is inFIN_WAIT_2. - When the server TCP receives the FIN, the server child is blocked in
readline, which returns 0. That causesstr_echoto return to the child'smain, and the child terminates by callingexit. - All open descriptors in the child are closed. Closing the connected socket causes the final two segments: a FIN from server to client, and an ACK from client to server.
- The connection is now completely terminated. The client socket enters
TIME_WAIT. - Finally,
SIGCHLDis sent to the parent when the server child terminates. It is not caught in this version, and the default action is to be ignored — so the child enters the zombie state.
POSIX signal handling
- Signal
- A notification to a process that an event has occurred. Sometimes called a software interrupt. Signals usually occur asynchronously — the process does not know ahead of time exactly when one will arrive.
- Who sends them
-
One process to another process, or to itself; or the kernel to a
process.
SIGCHLDis of the second kind: the kernel sends it to the parent whenever a process terminates. - Disposition
-
Every signal has a disposition, also called the
action associated with it. You set it with
sigaction.
| Disposition | What it means |
|---|---|
| 1 · Catch it |
Provide a function to be called whenever that signal occurs — a
signal handler. Its prototype is always
void handler(int signo); — one integer argument, the signal
number, and no return value.
|
| 2 · Ignore it | Set the disposition to SIG_IGN. |
| 3 · Default |
Set the disposition to SIG_DFL. The default is normally to
terminate the process, with certain signals also
generating a core image. A few signals default to being ignored —
SIGCHLD and SIGURG are the two you meet here.
|
SIGKILL and SIGSTOP can be neither caught nor
ignored. That is the whole point of them — they are the operating system's
guarantee that a process can always be stopped.
sigaction is the POSIX way, but it is fiddly: one of its arguments is
a structure you must allocate and fill in. The signal function is
easier — signal name, plus either a function pointer or SIG_IGN /
SIG_DFL — but it predates POSIX and different
implementations give it different semantics. The book therefore defines its own
signal wrapper on top of sigaction with known behaviour.
SIGCHLD and zombies
It holds no memory and no descriptors. What it does hold is a slot in the process table, and the table is finite. A server that forks a child per client and never reaps them will fill it, and then no process on the machine can fork.
SIGCHLD when forking child processes,
and the handler must call wait or waitpid to prevent the
children from becoming zombies.
void
sig_chld(int signo)
{
pid_t pid;
int stat;
while ( (pid = waitpid(-1, &stat, WNOHANG)) > 0)
printf("child %d terminated\n", pid);
return;
}
wait versus waitpid
#include <sys/wait.h>
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options);
Both return: process ID if OK, 0 or -1 on error
Both return two values: the process ID of the terminated child as
the function's return value, and the child's termination status through the
statloc pointer. Macros — WIFEXITED,
WEXITSTATUS and friends — pick the status apart.
wait | waitpid | |
|---|---|---|
| Which child | Whichever terminates first — no choice | Specified by pid; −1 means the first of our children |
| Blocking | Always blocks if there are running children but none terminated | Optional — WNOHANG says do not block if there is nothing to reap |
| Use it for | A single child | A concurrent server |
SIGCHLD and the handler runs —
but it may run once, not five times, because the other four signals arrive
while the handler is already executing and are collapsed into it.
A handler that calls
wait once therefore reaps one child and
leaves four zombies forever. The fix is the loop above: keep
calling waitpid until it returns 0, and use WNOHANG so
that the last call — when there is nothing left to reap — returns immediately
instead of blocking the whole server inside a signal handler.
SIGCHLD is caught, the parent's blocked accept can
be interrupted by the signal and return -1 with
errno set to EINTR. The server must handle it:
if (errno == EINTR) continue; /* back to accept */
A slow system call is one that can block forever —
accept,
read on a socket, connect. Any of them may return
EINTR, and you must restart it yourself. Notably
connect is the exception you must not restart: if it is
interrupted you have to use select to wait for completion.
A concurrent server catches SIGCHLD and calls wait() once in the handler. Ten clients connect and disconnect at the same instant. What happens?
Somewhere between one and nine zombies are left permanently. Signals are not
queued: the ten SIGCHLDs collapse into far fewer deliveries, the
handler runs a handful of times, and each run reaps exactly one child.
The fix is while ((pid = waitpid(-1, &stat, WNOHANG)) > 0) —
loop until nothing is left, and use WNOHANG so the final call
returns immediately rather than blocking the parent inside a signal handler.
SIGPIPE
SIGPIPE signal is sent to the process. The default action of
this signal is to terminate the process, so the process must catch the signal to
avoid being involuntarily terminated. If the process catches the signal and returns
from the handler, or ignores it, the write operation returns EPIPE.
And note the asymmetry that causes it: it is fine to write to a socket that has received a FIN, but an error to write to a socket that has received an RST. A FIN only means the peer has stopped sending — it may still be reading.
- How to handle it
-
If there is nothing special to do, set the disposition to
SIG_IGNand let the subsequent output operation fail withEPIPE. If special action is needed — writing to a log, say — catch it. But be aware: if multiple sockets are in use, the delivery of the signal does not tell you which socket hit the error. To know which write failed you must either ignore the signal, or return from the handler and handleEPIPEfrom thewrite.
When the server host goes away
Three scenarios that look identical from the outside and produce three different errors. Telling them apart is a favourite question.
| Scenario | What the server sends | What the client sees |
|---|---|---|
| Server host crashes (§5.14) | Nothing at all | ETIMEDOUT after about 9 minutes — or EHOSTUNREACH / ENETUNREACH if a router reported it |
| Server host crashes and reboots (§5.15) | RST | ECONNRESET, promptly |
| Server host is shut down (§5.16) | FIN | read returns 0 — a clean end-of-file |
Crashing of the server host
- When the server host crashes, nothing is sent out on the existing connections. (We are assuming a crash, not an orderly shutdown.)
- We type a line to the client. It is written by
writenand sent as a data segment. The client then blocks inreadline, waiting for the echo. - Watching with
tcpdumpyou would see the client TCP continually retransmitting that data segment, trying to get an ACK. Berkeley-derived implementations retransmit 12 times over about 9 minutes before giving up. - When the client TCP finally gives up, an error is returned to the client process. Since it is blocked in
readline, that call returns the error:ETIMEDOUTif there were no responses at all, orEHOSTUNREACH/ENETUNREACHif some intermediate router sent an ICMP destination-unreachable.
SO_KEEPALIVE
socket option. To detect it faster than nine minutes, put a timeout on the read
(§14.2).
Crashing and rebooting of the server host
The difference from the previous case is that the server host comes back before the client sends data.
- We start the server, then the client, and type a line to verify the connection is established.
- The server host crashes and reboots.
- We type a line of input to the client, which is sent as a TCP data segment to the server host.
- When the server host reboots after crashing, its TCP loses all information about connections that existed before the crash. So it has no idea what this segment belongs to, and the server TCP responds with an RST.
- Our client is blocked in
readlinewhen the RST is received, causingreadlineto return the errorECONNRESET.
Shutdown of the server host
Now the server host is shut down by an operator while the server process is running on it. This is an orderly process, and that changes everything.
- When a Unix system is shut down, the
initprocess sendsSIGTERMto all processes. We can catch this signal. initthen waits some fixed amount of time — often between 5 and 20 seconds — giving every running process a short window to clean up and terminate.- It then sends
SIGKILL, which cannot be caught, to any process still running. - If we do not catch
SIGTERMand terminate ourselves, the server is terminated bySIGKILL. Either way, when the process terminates all open descriptors are closed — so a FIN goes out on the connection, and we follow exactly the same sequence as a normal termination.
read returns 0. If the client happens to be blocked in
fgets waiting for you to type, it will not notice until you do.
To have the client detect the termination of the server process as soon as it occurs, it must wait on both standard input and the socket at the same time — which means
select or poll. This is the exact motivation
for Chapter 6.
The client is blocked in readline and the server host disappears. How does the error tell you what happened?
| Error | What it means |
|---|---|
ETIMEDOUT | Silence for nine minutes. The host crashed and stayed down, or was unreachable and nothing reported it. |
ECONNRESET | An RST came back. The host crashed and rebooted — it is alive again but has forgotten the connection. |
EHOSTUNREACH / ENETUNREACH | An ICMP destination-unreachable arrived. A router told us it could not deliver. |
read returns 0 | A FIN arrived. The process died, or the host was shut down cleanly — someone closed the socket properly. |
The distinction that matters: a FIN is a goodbye, an RST is a denial, and silence is a mystery TCP spends nine minutes trying to resolve.
Data format
The chapter closes with a warning that costs real programs real money. The echo server never looks at the bytes it echoes, so it works between any two machines. As soon as the server starts interpreting the data, two problems appear.
- Passing text strings
- Safe across machines, because both sides agree that a character is a byte. This is why so many Internet protocols — HTTP, SMTP, FTP — are text.
- Passing binary structures
-
Unsafe. Two problems, and both are examinable.
(1) Different implementations store binary numbers in different
formats — the byte-ordering problem again. (2) Different
implementations can use different amounts of padding in a
structure, and different sizes for the same datatype, so the same
structis not the same number of bytes on both machines.
write() a C struct down a socket and
read() it into the same struct at the other end. It appears to work
perfectly — right up until the day the other end is a different compiler, a
different architecture, or a 64-bit build of the same program.
The two real solutions: send everything as text, or define an explicit wire format with fixed-width fields in a fixed byte order.