5.12 Termination of Server Process
We will now start our client/server and then kill the server child process. This simulates the crashing of the server process, so we can see what happens to the client. (We must be careful to distinguish between the crashing of the server process, which we are about to describe, and the crashing of the server host, which we will describe in Section 5.14.) The following steps take place:
We start the server and client and type one line to the client to verify that all is okay. That line is echoed normally by the server child.
We find the process ID of the server child and kill it. As part of process termination, all open descriptors in the child are closed. This causes a FIN to be sent to the client, and the client TCP responds with an ACK. This is the first half of the TCP connection termination.
The SIGCHLD signal is sent to the server parent and handled correctly (Figure 5.12).
Nothing happens at the client. The client TCP receives the FIN from the server TCP and responds with an ACK, but the problem is that the client process is blocked in the call to fgets waiting for a line from the terminal.
Running netstat at this point shows the state of the sockets.
linux % netstat -a | grep 9877
tcp 0 0 *:9877 *:* LISTEN
tcp 0 0 localhost:9877 localhost:43604 FIN_WAIT2
tcp 1 0 localhost:43604 localhost:9877 CLOSE_WAIT
From Figure 2.4, we see that half of the TCP connection termination sequence has taken place.
We can still type a line of input to the client. Here is what happens at the client starting from Step 1:
linux % tcpcli01 127.0.0.1
the first line that we type
is echoed correctly here we kill the server child on the server host
we then type a second line to the client
str_cli : server terminated prematurely
When we type "another line," str_cli calls writen and the client TCP sends the data to the server. This is allowed by TCP because the receipt of the FIN by the client TCP only indicates that the server process has closed its end of the connection and will not be sending any more data. The receipt of the FIN does not tell the client TCP that the server process has terminated (which in this case, it has). We will cover this again in Section 6.6 when we talk about TCP's half-close.
When the server TCP receives the data from the client, it responds with an RST since the process that had that socket open has terminated. We can verify that the RST was sent by watching the packets with tcpdump.
The client process will not see the RST because it calls readline immediately after the call to writen and readline returns 0 (EOF) immediately because of the FIN that was received in Step 2. Our client is not expecting to receive an EOF at this point (Figure 5.5) so it quits with the error message "server terminated prematurely."
When the client terminates (by calling err_quit in Figure 5.5), all its open descriptors are closed.
What we have described also depends on the timing of the example. The client's call to readline may happen before the server's RST is received by the client, or it may happen after. If the readline happens before the RST is received, as we've shown in our example, the result is an unexpected EOF in the client. But if the RST arrives first, the result is an ECONNRESET ("Connection reset by peer") error return from readline.
The problem in this example is that the client is blocked in the call to fgets when the FIN arrives on the socket. The client is really working with two descriptors—the socket and the user input—and instead of blocking on input from only one of the two sources (as str_cli is currently coded), it should block on input from either source. Indeed, this is one purpose of the select and poll functions, which we will describe in Chapter 6. When we recode the str_cli function in Section 6.4, as soon as we kill the server child, the client is notified of the received FIN.