C/C++ Users Journal October, 2005
Have you every wished that writing TCP client or server code could be as simple as writing to the console? Or needed multiple simultaneous I/O operations when using standard C++ I/O streams? In this article, I show how you can use C++ I/O streams to read and write TCP sockets, and how asynchronous input/output can be built on top of the I/O stream mechanism of C++. In the process, I examine DTILIB [1, 2], a platform-independent C++ library developed at the Danish Technological Institute. DTILIB also provides an I/O streams interface to compressed files, serial lines, and TCP sockets, plus a means for handling asynchronous I/O on top of C++ I/O streams.
In the C++ Standard Library, the I/O streams mechanism is found in the std namespace. I/O streams were originally designed to provide input/output to terminals and files. Most C++ programmers are familiar with the cin and cout streams that provide access to the console. Data can be read from and written to the console by applying the operators >> or << to these streams. Thus:
int i;
std::cin >> i;
std::cout << i << " times 2 is "
<< i*2 << std::endl;
reads an integer from the console and writes the integer and two times its value back to the console. The final std::endl outputs a newline and forces the content of the output buffer to be written to the terminal. A similar construct can be used to read and write files.
The stringstream class moves the I/O streams mechanism beyond mere file and terminal I/O. It provides a means to read and write to a string.
The I/O streams mechanism provides a large collection of methods beyond the >> and << operators. Member functions such as getline and put provide additional ways to manipulate I/O streams. Here, I focus on how the I/O streams mechanism can be applied to TCP sockets, and the addition of asynchronous I/O to the streams.
Sockets originated in the BSD version of UNIX. Today, sockets APIs are found in virtually all versions of UNIX and Linux. Under Windows, the Winsock library provides almost identical functionality. Using the sockets API, programs can establish network connections using a number of different transport protocols, including (but not limited to) TCP and UDP.
Sockets APIs are available in different programming languages, but the defining language has always been C. There is, however, a need for a set of C++ classes that provide an interface to the sockets mechanism. Several sockets class libraries exist, but most are thin wrappers around the underlying C library functions. In contrast, the DTILIB classes provide a considerable collection of methods. Currently, DTILIB only supports TCP sockets, but this should be adequate for most uses. The classes all reside in a dti::sockets namespace.
Listing 1(a) illustrates a character-based TCP client. The tcp_client_socket class represents a TCP socket that is capable of establishing a connection. It is a subclass of tcp_socket, which provides I/O functions that operate over an already established connection. Listing 1(a) illustrates how a tcp_client_socket may be constructed. Its constructor has a single argument, which is an object of class endpoint. An endpoint represents an IP address and a TCP port number. The IP address may be specified as a host name. Thus, endpoint("abc.def.com",123) represents TCP port 123 at the host abc.def.com. The construction of the tcp_client_socket object not only creates the object, it also establishes a TCP connection to the IP address and port number specified in the constructor.
Having done this, the client merely copies to cout everything it receives over the TCP connection. The recv method (or, rather, one of the recv methods) takes a string as its argument. It then reads data from the connection and stores that data in the string. The method returns -1 when the peer closes the connection.
The process of establishing a connection may, of course, fail. This is indicated through an exception. However, to make the examples easier to read, I've omitted exception handling. A corresponding server and data provider can look like Listing 1(b).
The tcp_listener class represents a TCP socket that listens for an incoming connection. In Listing 1, a listener is created based simply on the TCP port number 123. Alternatively, a listener can be created based on an endpoint object if the server has more than one network adapter and is required to listen only on a single IP address.
The accept method applied to a tcp_listener accepts incoming connections and returns a tcp_socket object that can be used for further communication. The example code uses the send method to send strings to the client.
When people first learn about C-based sockets, they are often frustrated by the seemingly tedious set of operations that go into setting up a server: First, a socket is created, then a port is bound to the socket, next the socket is placed in the "listening" mode, and finally, incoming connections are accepted. In DTILIB, this is reduced to the simple process illustrated by the first two lines in Listing 1(b).
Programmers who still want to go through the phases of binding, listening, and accepting are free to do so. DTILIB does provide these functions as well.
To be more efficient, most server applications would, of course, start a separate thread to handle the actual input/output while the main thread reverts to listening for more incoming connections.
DTILIB includes classes that let you use the C++ I/O streams mechanism on top of the TCP sockets class library. The basic class here is the tcpstream object. This object is a subclass of the iostream class found in the Standard Library. As such, all the standard methods defined for an iostream are also available on a tcpstream.
One of the advantages achieved by using a tcpstream is that you are relieved of the burden of buffering input. A TCP connection is a bytestream, and if the application wants to read records or lines of data, it is the programmer's duty to provide appropriate buffering and record splitting on input. For a line-oriented protocol, DTILIB does all this for you.
Furthermore, tcpstream provides all the advantages of having formatted I/O. A tcpstream object is constructed from a tcp_socket object. The tcp_socket object can either be a tcp_client_object explicitly created in a client, or it can be the object returned from a call of the accept method on a tcp_listener object in a server.
Once a tcpstream object has been created, it is extremely easy to use. The server code in Listing 1 becomes Listing 2(a), where the return value of the accept method is used as argument for the constructor of a tcpstream object. On the client side, you can, for example, use the getline method to read each line of text from the connection, as in Listing 2(b). But, of course, the >> operator can also be used.
Although I/O streams can be used for both binary and character-based data, they are most convenient for character-based I/O. As many TCP-based protocols, such as HTTP and SMTP, are character based, tcpstream objects are useful in many applications.
In serious network applications, there is almost always a need to handle several sockets simultaneously. For instance, a proxy server may need to forward all data arriving from a client to a server, and vice versa; the problem is that the proxy does not know who is going to transmit nextthe client or the server.
In standard sockets, this problem is solved through a function called select, which lets you monitor the state of several sockets simultaneously. You can, for example, use select to wait for input on one of several sockets. This is a useful mechanism, but it is not easy to apply to an I/O stream. The I/O streams of the Standard Library are inherently synchronous: The program requests input on a single stream, then sleeps until the input is available.
DTILIB provides a mechanism that lets an application have several simultaneous operations on I/O streams. And the syntax is similar to the well-known streams syntax. This mechanism for asynchronous I/O can be used on all C++ I/O streams, not just the TCP streams described so far.
The asynchronous I/O mechanism uses a class called astream, which represents a collection of I/O operations to be executed concurrently. For example, assume you want to simultaneously read from the file hi.txt and from the console input, cin. First, you create an astream object using Listing 3(a). The astream class resides in the dti::async namespace, and its constructor takes no arguments. Next, you add I/O operations to the object, using a syntax similar to the one used on I/O streams; see Listing 3(b). The first line opens the file hi.txt and associates the I/O stream fio with it. The two final lines state our intention to read two integers, a and b, from the console, and an integer and a double, c and d, from the file. Compare this to the normal input operations in Listing 3(c). The similarity in syntax is obvious.
However, when the >> operator is applied to an astream, the actual input operation does not take place immediately. Instead, the astream merely notes which variables are to be read from which input streams.
The actual input operations are started when the method start is called on the astream object:
ast.start();
At this point, several threads are started, one for each input stream. One thread reads two integers from the console and stores them in a and b, another thread reads an integer and a double from fio and stores them in c and d.
You can wait for one of the operations to finish by calling the wait method, which returns a pointer to the I/O stream whose operation was completed; see Listing 4. A call to the start method is frequently not required, as all pending I/O requests are started when wait is called.
The >> and << operators are not the only ones that can be used on astream objects. Just as, for example, the getline method can be used on a normal input stream:
std::cin.getline(buf)
it can also be used on an astream, thusly:
ast(std::cin).getline(buf);
This queues a request to call the getline method on std::cin in parallel to operations on the other I/O streams handled by ast.
Figure 1 is a UML class diagram showing classes related to asynchronous I/O operation. When you write, say, ast(fio), where ast is an astream and fio is an I/O stream, the () operator is being applied to ast. This operator generates a so-called io_operation object, which is responsible for handling a single input or output stream. If more operations are requested on the same stream, the same io_operation object is reused. This may seem strange at first, but remember that although we want the streams to be serviced in parallel, each individual operation on a single stream has to be performed sequentially. If you write:
ast(fio) >> a >> b;
we want a to be read before b. This is obvious when a and b are mentioned on the same line, but, of course, you want the same thing to happen if you write this code:
ast(fio) >> a; ast(fio) >> b;
Therefore, the second ast(fio) must return the same io_operation as the first one.
So, associated with each astream, you have a collection of io_operation objects, one for each underlying stream; these io_operations will be serviced in parallel. Associated with each io_operation is a collection of individual I/O requests that will be serviced sequentially.
Because TCP streams are full duplex streams, it makes sense to send simultaneous input/output requests to a TCP stream. So, if ts is a tcpstream object, and you write:
ast(ts) >> a; ast(ts) << b;
you obviously want the two operations to be executed in parallel. You therefore need each io_operation object to handle not one, but two collections of pending I/O requestsone for input and one for output.
All io_operation objects are subclassed from the super_io_operation class. This class contains a single important member:
half_io hio[2];
which contains two half_io objectsone for input and one for output. A half_io object is responsible for maintaining the list of data that are to be read or written to the I/O stream.
Again, all io_operation objects are subclassed from the super_io_operation class. The io_operation class is a template class. The template parameter is the type of the underlying I/O stream; see Listing 5(a). The io_operation class has a number of methods, one for each "slow" operation that can be performed on an I/O stream. A "slow" operation is an operation that may potentially block because it is accessing a physical device. Examples of slow operations are the get/put methods and the >> and << operators.
To understand what happens here, follow the flow of a getline method call through the system, as in Figure 2. In the code:
char buf[100]; ast(fio).getline(buf, sizeof buf);
fio is an fstream and ast an astream. The call ast(fio) generates (or reuses) an io_operation<fstream> object based on fio. The getline method is then applied to this io_operation object. That method is defined in Listing 5(b).
Remember that the hio array represents the input and outputs half of pending I/O operations. First, the code checks that input is not already active on the stream. It will not allow the programmer to add I/O requests to a stream after start has been called; otherwise, you may run into serious race conditions. This is done by calling the test_running method, which throws an exception if the stream is currently doing input.
Then, the code adds the actual I/O request (that is, the request to call getline) to the collection of I/O requests that hio[input] services. This is done by creating an object of class getline1_target and adding it to a vector on the half_io object.
The getline1_target object represents the getline method when it is called with two arguments. There is another variant of getline that takes three arguments; its corresponding I/O request is an object of class getline2_target.
If instead of calling getline, the programmer had used the >> operator, the corresponding member function of io_operation would be like that in Listing 5(c). Note that the >> operator is a template function, its template parameter being the type of the right-hand side of >>. Other than that, the method is similar to the getline method. Here, the code creates an object of class input_target, which also carries the data type of the right side of >>.
Now the vec vector of the input half_io contains a collection of I/O requests. Each of these requests (getline1_target, input_target, and so on) are subclasses of an abstract class iotarget, which has a single virtual method, invoke. The implementation of this method in each of the subclasses contains the code that performs the actual input or output. For example, the invoke method in getline1_target looks like Listing 6(a), where str, cp, and siz represent the actual I/O stream, buffer address, and buffer size, respectively. Similarly, the invoke method in input_target looks like Listing 6(b), where str and d represent the actual I/O stream and address of the destination variable, respectively. Listing 6(c) is the full definition of input_target. Because d has the type of a pointer to the data type of the right side of the >> operator as it was applied to the astream, the >> operator in the invoke method is the appropriate input operator defined for the particular destination type.
Eventually, the programmer calls the start method on the astream. This creates a number of execution threads, one for each underlying I/O stream and direction (input or output). Each of these streams calls the invoke method in the associated I/O request collections, and this, in turn, performs the actual input or output.
If an exception occurs during the I/O operation, the thread notes this and an async_error exception is thrown when the programmer calls the wait method on the astream.
The template mechanism gracefully handles user-defined classes. If, for example, a programmer creates a class T and defines how the << operator can output T objects to an output stream, the << operator is automatically able to output T objects to an astream as well. Programmers thus need not adapt their class code to asynchronous streams.
Unfortunately, there is no simple way to abort an I/O operation. Operating systems normally do not provide a simple way to cancel a pending I/O operation.
However, if you are writing your own I/O stream, DTILIB provides you with a mechanism for adding an abort command. (Needless to say, the TCP socket streams here support command abortion.)
DTILIB includes an abstract class, ios_abortable, with a single virtual method, abort. If your I/O stream inherits from this class, you call an abort method on your astream, and it tries to abort all I/O operations that have not yet completed.If a pending I/O operation is not abortable, an exception is thrown. Listing 7 aborts an I/O operation that does not complete within five seconds (ts is an I/O stream). If the input operation cannot be aborted, the final lines of code report the fact.
The call to the wait method that follows the abort call is required to clean up the dynamic objects created to handle the asynchronous operation.
The C++ I/O streams mechanism is useful for many things, not just file and console I/O. While working with DTILIB, we found the mechanism to be extremely versatile; and adding an asynchronous capability produced an extremely powerful tool for I/O streams, which has proven useful not just for TCP communication, but also for slow file operations. Instead of focusing on threads and other multitasking issues, you can focus on solving the domain problems, while leaving the burden of thread-awareness to the library developer. The mechanisms described here have been successfully used to develop a speech recognition system for Windows XP.