Network Programming

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.

§5.1 · p.121 Our simple example is an echo server that performs the following steps:

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.
An echo server does nothing useful on purpose. Stripping the application logic to "send it straight back" means everything you then observe — the states, the queues, the zombies, the signals — is about the network machinery and not about the task.
Q2(b) [5] gave the 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.

The command is 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.
Because the client and the server are running on the same host, so 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:

  1. We type the EOF character (Ctrl-D). fgets returns a null pointer and str_cli returns.
  2. str_cli returns to the client's main, which terminates by calling exit.
  3. 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_WAIT and the client socket is in FIN_WAIT_2.
  4. When the server TCP receives the FIN, the server child is blocked in readline, which returns 0. That causes str_echo to return to the child's main, and the child terminates by calling exit.
  5. 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.
  6. The connection is now completely terminated. The client socket enters TIME_WAIT.
  7. Finally, SIGCHLD is 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. SIGCHLD is 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.
DispositionWhat 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 ignoredSIGCHLD 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

When a child terminates, the kernel cannot throw everything away immediately: the parent might still want to know how the child died. So it keeps a small record — the process ID, the termination status, some resource usage — and discards everything else. That record is the zombie.

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.
§5.9 · p.132 We must catch 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

§5.10 · p.135
#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.

waitwaitpid
Which childWhichever terminates first — no choiceSpecified by pid; −1 means the first of our children
BlockingAlways blocks if there are running children but none terminatedOptionalWNOHANG says do not block if there is nothing to reap
Use it forA single childA concurrent server
Unix signals are not queued. If five children terminate at almost the same moment, the parent receives 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.
Once 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

§5.13 · p.142 When a process writes to a socket that has received an RST, the 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.
"How do I get this signal on the first write instead of the second?" You cannot. The first write elicits the RST; the second write elicits the signal. There is no way to compress the two.

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.
what it looks like when it happens
tcpcli11 127.0.0.1 hi there we type this line hi there this is echoed by the server here we kill the server child bye then we type this line Broken pipe this is printed by the shell
How to handle it
If there is nothing special to do, set the disposition to SIG_IGN and let the subsequent output operation fail with EPIPE. 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 handle EPIPE from the write.

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.

ScenarioWhat the server sendsWhat 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

  1. When the server host crashes, nothing is sent out on the existing connections. (We are assuming a crash, not an orderly shutdown.)
  2. We type a line to the client. It is written by writen and sent as a data segment. The client then blocks in readline, waiting for the echo.
  3. Watching with tcpdump you 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.
  4. 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: ETIMEDOUT if there were no responses at all, or EHOSTUNREACH / ENETUNREACH if some intermediate router sent an ICMP destination-unreachable.
The client discovers this only because it sent data. If it is idle it will never notice — the connection just sits there. To detect a dead peer without sending anything you need the 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.

  1. We start the server, then the client, and type a line to verify the connection is established.
  2. The server host crashes and reboots.
  3. We type a line of input to the client, which is sent as a TCP data segment to the server host.
  4. 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.
  5. Our client is blocked in readline when the RST is received, causing readline to return the error ECONNRESET.
A reboot wipes the kernel's connection table. So the returning host is not being hostile — it genuinely does not recognise the connection, and RST is exactly the right answer to a segment for a connection that does not exist. This is the third of the three conditions that generate an RST.

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.

  1. When a Unix system is shut down, the init process sends SIGTERM to all processes. We can catch this signal.
  2. init then waits some fixed amount of time — often between 5 and 20 seconds — giving every running process a short window to clean up and terminate.
  3. It then sends SIGKILL, which cannot be caught, to any process still running.
  4. If we do not catch SIGTERM and terminate ourselves, the server is terminated by SIGKILL. 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.
So from the client's point of view, a server host shutdown is indistinguishable from the server child exiting normally: a FIN arrives, and 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?
ErrorWhat it means
ETIMEDOUTSilence for nine minutes. The host crashed and stayed down, or was unreachable and nothing reported it.
ECONNRESETAn RST came back. The host crashed and rebooted — it is alive again but has forgotten the connection.
EHOSTUNREACH / ENETUNREACHAn ICMP destination-unreachable arrived. A router told us it could not deliver.
read returns 0A 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 struct is not the same number of bytes on both machines.
This is why you do not simply 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.