Winding up Communications
The astute reader might have been wondering about the shutdown(2) call that was introduced earlier. How should this function call be exercised when it is needed? With the dual stream approach, you might be tempted to misuse the shutdown(2) function, based on a bad assumption. For example, because there are actually two underlying file descriptors being used in Listing 10.2, it might be tempting to call shutdown(2) on each of the file descriptors. On one, you might shut down the read side, whereas on the other file descriptor, you might shut down the write side. Do not do this!
Here we will discuss the issues that can occur when you are going to close the streams that are associated with the socket.
Recall from the Basic Tutorials of the socket first Post, "Introducing Sockets," that the shutdown(2) function was described. There, it was stated that one of the advantages of its use was that "it disregards the number of open references on the socket." Consequently, calling shutdown(2) on duplicated sockets will affect all references to the same socket. Consequently, it also affects all existing streams you have connected with that socket!
When winding up communications between your process and the remote process over a socket, there are three basic scenarios to be considered:
- The process is not going to write any further data, but is expecting to receive more data (shutdown of the write side only).
- The process is not going to receive any further data, but is expecting to write more data (shutdown of the read side only).
- The process is not going to read or write any further data (shutdown of reading and writing).
Using the two streams shown in Listing 10.2, the scenarios will be described in the following sections.
Shutting down the Write Side Only
The shutdown(2) function is called upon to indicate to the Linux kernel that the calling process intends no further writes of data, in this particular case. Because the shutdown(2) call affects the socket and not the file descriptor, either file descriptor could actually be used. However, for program clarity, I would encourage you to use the writing stream to accomplish this task. The procedure for this task consists of the following steps:
- Flush any data that might exist in the stream buffers using fflush(3).
- Shut down the write side of the socket using shutdown(2).
- Close the stream, using fclose(3).
Before shutting down the write side, you must always flush the output stream. This is important because there might be some unwritten data that is sitting in a buffer. This can be accomplished as follows:
(tx); /* Flush buffer out */
To accomplish the shutdown(2) step, you need to obtain the underlying file descriptor of the stream tx. To access it, you can use the following C language macro:
int fileno(FILE *stream);
You simply pass the stream pointer to the macro as input, and it returns the underlying integer file descriptor that it is using. This is a portable and the only acceptable way of doing this. Applying this macro, you can perform the shutdown step as follows:
The last step of this procedure is to simply fclose(3) the tx stream that you no longer need:
Putting the procedure all together, the shutdown procedure looks like this in C code:
This sequence will leave the rx stream intact for reading, but forces all buffered data in the tx stream to be written out to the socket. The shutdown(2) call tells the kernel to expedite the
sending of the socket data because there will be no more data to send. Finally, the fclose(3) call on the tx stream closes the file descriptor and releases the memory resources associated with the stream.
Shutting down the Read Side Only
This procedure is similar to shutting down the write side only. The procedure does vary slightly:
- Call shutdown(2) to indicate that there is no more receive data expected.
- Close the stream using fclose(3).
You'll notice that there is no fflush(3) step required in this case. The procedure can be summarized in code as follows:
Note again the portable use of the fileno(3) macro to fetch the underlying file descriptor for the stream rx. Although Listing 10.2 shows the original socket number is available in variable s, for program clarity it is probably preferred to use the fileno(3) macro after the rx stream has been created.
This procedure accomplishes the indication of no further reads to the Linux kernel, as well as the closing and releasing of all stream resources for rx. However, the application will still be able to write to stream tx unhindered.
Shutting down Both Read and Write Sides
This procedure might be perceived as being more complex, but it actually turns out to be quite simple:
- Close the write stream by calling fclose(3).
- Close the read stream by calling fclose(3).
No fflush(3) is required in step 1 because the fclose(3) function for the write stream will implicitly ensure that this flush takes place. Closing the read stream in step 2 closes the last open file descriptor for the socket, so the socket is implicitly shut down for both reading and writing.
One exception to the rule, which might prove to be a sticking point, depends upon your application design. If your process has forked, then there might be other open file descriptors to your socket. You'll recall that only when the last close(2) takes place will the socket actually be shut down. If there is some doubt about this, you might want to follow a more elaborate procedure as follows:
- Close the write stream using fclose(3). This will force unwritten data out to the socket, and release the write stream's resources.
- Call shutdown(2) to terminate both reading and writing to this socket.
- Close the read stream using fclose(3) to close the read file descriptor, and to release the stream's buffer and FILE structure.
The only real change to the procedure is that the shutdown(2) function is called as follows:
The entire procedure boils down to this:
I will submit to you that this procedure is the best one to use, even if you do not expect to have problems with the two -step procedure. This procedure will always accomplish your task, regardless of any future program modifications that might otherwise impact the other procedure.