An Iostream-Compatible Socket Wrapper

With suitable scaffolding, writing to a socket is as easy as cout << "Hello, world";.


December 01, 2001
URL:http://www.drdobbs.com/an-iostream-compatible-socket-wrapper/184401470

December 2001/An Iostream-Compatible Socket Wrapper/Figure 1

Figure 1: Structure of ostream objects

December 2001/An Iostream-Compatible Socket Wrapper/Listing 1

Listing 1: Skeleton of the simple socket wrapper

class TCPSocketWrapper
{
    class TCPAcceptedSocket
    {
        // ...
    };

public:
    enum sockstate_type { CLOSED, LISTENING,
        ACCEPTED, CONNECTED };

    TCPSocketWrapper();
    ~TCPSocketWrapper();

    // this is provided for syntax
    // TCPSocketWrapper s2(s2.accept());
    TCPSocketWrapper(const TCPAcceptedSocket &as);

    // server methods

    // binds and listens on a given port number
    void listen(int port, int backlog = 100);
    
    // accepts the new connection
    // it requires the earlier call to listen
    TCPAcceptedSocket accept();

    // client methods

    // creates the new connection
    void connect(const char *address, int port);

    // general methods

    // get the current state of the socket wrapper
    sockstate_type state() const { return sockstate; }

    // get the network address
    // and port number of this socket
    const char * address() const;
    int port() const;

    // write data to the socket
    void write(const void *buf, int buflen);

    // read data from the socket
    // returns the number of bytes read
    int read(void *buf, int buflen);

    // close socket
    void close();

private:
    // copy is not supported
    TCPSocketWrapper(const TCPSocketWrapper&);
    TCPSocketWrapper& operator=(const TCPSocketWrapper&);
};
— End of Listing —
December 2001/An Iostream-Compatible Socket Wrapper/Listing 2

Listing 2: Simple server program that prints a message received from the client

#include "Sockets.h"
#include <iostream>
using namespace std;

int main()
{
    if (socketsInit() == false)
    {
        cout << "cannot initialize sockets library"
            << endl;
        return 0;
    }

    try
    {
        char buf[100];
        int readn;

        TCPSocketWrapper serversocket;

        // listen on some port
        serversocket.listen(12345);

        while (1)
        {
            cout << "server is ready" << endl;

            TCPSocketWrapper sock(sockserver.accept());

            cout << "got connection from: "
                << sock.address() << endl;

            readn = sock.read(buf, 100);

            cout << "read " << readn << " bytes: "
                << buf << endl;
        }
    }
    catch (const SocketRunTimeException &e)
    {
        cout << "socket exception: "
            << e.what() << endl;
    }

    socketsEnd();

    return 0;
}
— End of Listing —
December 2001/An Iostream-Compatible Socket Wrapper/Listing 3

Listing 3: Simple client program that sends a message using the TCPSocketWrapper class

#include "Sockets.h"
#include <iostream>
#include <cstring>

using namespace std;

int main()
{
    if (socketsInit() == false)
    {
    cout << "cannot initialize sockets library"
        << endl;
    return 0;
    }

    try
    {
        const char *message = "Hello, Socket!";
        int msglen = strlen(message) + 1;

        TCPSocketWrapper sock;

        // connect to the same machine
        // and the IP port of the server process
        sock.connect("127.0.0.1", 12345);

        sock.write(message, msglen);
    }
    catch (const SocketRunTimeException &e)
    {
        cout << "socket exception: "
            << e.what() << endl;
    }

    socketsEnd();

    return 0;
}
— End of Listing —
December 2001/An Iostream-Compatible Socket Wrapper/Listing 4

Listing 4: Skeleton of the custom socket-based stream buffer

template
<
    class charT,
    class traits = std::char_traits<charT>
>
class TCPStreamBuffer :
    public std::basic_streambuf<charT, traits>
{
    typedef std::basic_streambuf<charT, traits> sbuftype;
    typedef typename sbuftype::int_type         int_type;
    typedef charT                               char_type;
    // ...

public:
    // the buffer will take ownership of the socket
    // (ie. it will close it in the destructor)
    // if takeowner == true
    explicit TCPStreamBuffer (TCPSocketWrapper &sock,
                bool takeowner = false,
                int_type bufsize = 512);

    // ...

protected:
    int_type overflow(int_type c = traits::eof());
    int_type underflow();

    // ...

private:
    // copy not supported
    TCPStreamBuffer(const TCPStreamBuffer&);
    TCPStreamBuffer& operator=(const TCPStreamBuffer&);

    TCPSocketWrapper &rsocket_;
    bool ownsocket_;

    // ...
};
— End of Listing —
December 2001/An Iostream-Compatible Socket Wrapper/Listing 5

Listing 5: Complete stream class

template
<
    class charT,
    class traits = std::char_traits<charT>
>
class TCPGenericStream :
    private TCPStreamBuffer<charT, traits>,
    public std::basic_iostream<charT, traits>
{
public:
    explicit TCPGenericStream(TCPSocketWrapper &sock,
                              bool takeowner = false)
        : TCPStreamBuffer<charT, traits>(sock, takeowner),
        std::basic_iostream<charT, traits>(this)
    {
    }

private:
    // copy not provided
    TCPGenericStream(const TCPGenericStream&);
    TCPGenericStream& operator=(const TCPGenericStream&);
};
— End of Listing —
December 2001/An Iostream-Compatible Socket Wrapper/Listing 6

Listing 6: Complete socket-based stream class designed for client-side use

template
<
    class charT,
    class traits = std::char_traits<charT>
>
class TCPGenericClientStream :
    private TCPSocketWrapper,
    public TCPGenericStream<charT, traits>
{
public:
    TCPGenericClientStream(const char *address, int port)
        : TCPGenericStream<charT, traits>(*this, false)
    {
        TCPSocketWrapper::connect(address, port);
    }

private:
    // copy not provided
    TCPGenericClientStream(const TCPGenericClientStream&);
    TCPGenericClientStream&
        operator=(const TCPGenericClientStream&);
};
— End of Listing —
December 2001/An Iostream-Compatible Socket Wrapper/Listing 7

Listing 7: Simple test server program

#include "Sockets.h"
#include <iostream>
#include <string>
#include <complex>

using namespace std;

const int IPportnumber = 12345;

int main()
{
    string message("Hello Socket!");
    complex<double> cplx(1.0, 2.0);
    int integer(1234);

    if (socketsInit() == false)
    {
        cout << "cannot initialize sockets" << endl;
        return 0;
    }

    try
    {
        TCPSocketWrapper sockserver;

        // listen on some port
        sockserver.listen(IPportnumber);

        cout << "server is ready" << endl;

        // accept connection from client
        TCPSocketWrapper sock(sockserver.accept());

        cout << "accepted connection from: "
            << sock.address() << endl;

        // make the stream around the socket wrapper
        TCPStream stream(sock);

        bool oncemore = true;
        int command;
        while (oncemore)
        {
            // read the command
            stream >> command;
            switch (command)
            {
            case 1:
                cout << "command 1" << endl;
                stream << message << endl;
                break;
            case 2:
                cout << "command 2" << endl;
                stream << integer << endl;
                break;
            case 3:
                cout << "command 3" << endl;
                stream << cplx << endl;
                break;
            default:
                cout << "END command" << endl;
                oncemore = false;
                break;
            }
        }
    }
    catch (const SocketRunTimeException &e)
    {
        cout << "socket exception: "
            << e.what() << endl;
    }

    socketsEnd();

    return 0;
}
— End of Listing —
December 2001/An Iostream-Compatible Socket Wrapper/Listing 8

Listing 8: Test client program

#include "Sockets.h"
#include <iostream>
#include <string>
#include <complex>

using namespace std;

const int IPportnumber = 12345;
const char *IPserveraddress = "127.0.0.1";

int main()
{
    string message;
    complex<double> cplx;
    int integer;

    if (socketsInit() == false)
    {
        cout << "cannot initialize sockets" << endl;
        return 0;
    }

    try
    {
        TCPClientStream stream(IPserveraddress,
            IPportnumber);

        bool oncemore = true;
        int command;
        while (oncemore)
        {
            cout << "1. request for a string" << endl;
            cout << "2. request for a number" << endl;
            cout << "3. request for a complex number"
                << endl;
            cout << "other - end" << endl;

            cin >> command;
            stream << command << endl;

            switch (command)
            {
            case 1:
                do
                {
                    getline(stream, message);
                } while (message.empty());
                cout << "received: " << message << endl;
                break;
            case 2:
                stream >> integer;
                cout << "received: " << integer << endl;
                break;
            case 3:
                stream >> cplx;
                cout << "received: " << cplx << endl;
                break;
            default:
                oncemore = false;
                break;
            }
        }
    }
    catch (const SocketRunTimeException &e)
    {
        cout << "socket exception: " << e.what() << endl;
    }

    socketsEnd();

    return 0;

}


— End of Listing —
December 2001/An Iostream-Compatible Socket Wrapper

An Iostream-Compatible Socket Wrapper

Maciej Sobczak

With suitable scaffolding, writing to a socket is as easy as cout << "Hello, world";.


Introduction

When I was attending the C++ course at my university (which followed the C course), I was told that now there are better tools to do the job than before, and that, for example, the following form should no longer be used:

fprintf(stdout, "Hello World!");

Instead, the object-based approach is preferred:

cout << "Hello World!" << endl;

I know a lot of people who would argue that the second line is not a bit more object-oriented than the first; the former is even better, because it clearly states what operation is performed on what entity. What is the improvement, then? The improvement comes from code reuse and the polymorphism that the iostream-compatible objects expose. Consider this:

void printHello(ostream &stream)
{
    stream << "Hello World!" << endl;
}

printHello is reusable, because it treats its arguments polymorphically [1]. You can use it with the standard cout object, or with any other stream you can invent, whether it is connected to a file (by std::ofstream) or to a string buffer (by std::ostringstream) or to something else derived from ostream. Indeed, with a slight change you can make it work with all of the standard streams, including the wide-character versions. Beyond that, it will also work with anything else that supports operator<< and understands endl, whether the stream happens to be derived from a standard stream class or not:

template<typename T>
void printHello(T &stream)
{
    stream << "Hello World!" << endl;
}

The possibilities are countless, and there is tons of code like the above to reuse. There is one minor problem, though. The standard library cannot provide all the classes needed for all I/O, because I/O programming is not limited to terminals, files, and memory devices like strings. Computers are used also for network communication where sockets are the popular abstraction. Is there any possibility to use the existing, iostream-compatible code in programs based on socket networking? Of course, there is.

Iostream Mechanics Revisited

Each iostream object (for example the standard cout) is composed of three parts:

Those three components are glued together by the tools available in the language: inheritance, aggregation, and containment. For example, Figure 1 shows the structure of ostream objects (only the key components are shown). Above the horizontal dashed line, there are framework classes, which provide the interface of the stream. Below that line, two concrete classes are shown, which together implement the ultimate ostream object (file-based stream in this example). The istream objects have conceptually the same structure, where the basic_istream class is on the bottom of the hierarchy instead of basic_ostream.

These classes are in fact templates. This was introduced a few years ago by the Standard and distinguishes “new” from “old” iostreams [2]. The charT template parameter represents the character type used to transmit the data. When the term “ostream” is used, it means the basic_ostream<charT> instantiated with the char type. In fact, ostream is merely a typedef for basic_ostream<char>; istream and iostream are typedefed in a similar way. There is also a second template parameter for this template, called traits, but its default value is good enough for most needs. (It is good enough to be used in the istream and ostream typedefs, for example.)

The ios_base and basic_ios classes make up the state component of the ostream object.

The basic_streambuf object is responsible for providing the uniform view over the physical I/O device, which means that it provides the access to the underlying device through its public methods. basic_ostream objects keep the pointer to the basic_streambuf components and use its methods when they need to write or read some bytes as objects of type charT.

The streambuf component manages its own buffer area. In order to present the abstract notion of unlimited-size buffer, streambuf has to properly react when the overflow (when the user puts more bytes than fit into the buffer — this can happen in ostream objects) or underflow (when the user wants to extract more bytes than are currently in memory — in istream objects) condition occurs.

For example, for file I/O, when overflow occurs while performing the insertion operation (most frequently by operator<<), the ofstream object has to flush some or all of its buffer to the file, making room in the buffer area for new bytes. Similarly, when underflow happens, the ifstream object has to read a new portion of data from the file into the buffer area, making them available for future extraction operations (usually by operator>>).

Writing a new stream buffer class that will work with a new I/O device (for example network sockets) concentrates on those two unusual conditions. For socket-based I/O, the overflow condition should trigger writing the buffer’s contents to the socket, and for the underflow condition, the new portion of data should be read from the network connection.

The Simple Socket Wrapper

Before implementing the full solution in the context of iostream classes, I will present a simple wrapper for the socket functionality, which can be used on its own without streams. The iostream classes will then be implemented in terms of this wrapper.

The wrapper defines two of its own exception classes for error reporting:

class SocketLogicException :
    public std::logic_error
{
    // ...
};
class SocketRunTimeException :
    public std::runtime_error
{
    // ...
};

The first class is used to report design errors. For example, when the programmer wants to read from a socket object that was not connected, it is a serious design flaw. The instances of the second class are thrown when the error related to the network happened. For example, if the IP address cannot be resolved, it can mean that the network is unavailable. The what method in both classes gives a human-readable message describing the error.

Listing 1 presents the interface of the socket wrapper class, TCPSocketWrapper. This class can be used only for TCP sockets (stream mode), opened in blocking mode. In fact, the user does not have any control over the internal socket details, but this is not a restriction for the purpose of writing an iostream-compatible class.

Listing 2 presents the simple server program that uses TCPSocketWrapper. It waits for a connection, prints the message read from the socket, then closes its end, and waits for another connection. Listing 3 presents a simple example client program that connects to the server process running on the same machine, sends the message to the server, and quits [3]. (Note: both programs are illustrative examples; production versions would be more complex and perform robust error checking and handling.)

The Iostream-Compatible Socket Wrapper

A basic understanding of iostream mechanics is enough to implement a custom iostream-compatible class. The key component is the stream buffer object, and for custom streams, you need to write your own buffer class, derived (possibly indirectly) from the basic_streambuf template. Listing 4 presents the skeleton of the custom, stream buffer class. To be fully compatible with the “new” iostreams, TCPStreamBuffer is a template, which accepts two template parameters, as does basic_streambuf. Both parameters are used to instantiate the base class basic_streambuf.

The buffer object keeps a reference to the socket wrapper. It will use this wrapper to perform all the physical I/O operations. It is possible to declare that the buffer object should be responsible for the socket wrapper. The second argument to the buffer’s constructor is a boolean flag. If it is true, the buffer object will automatically close the socket in its own destructor. The third parameter of the constructor is the size of the buffer area that will be allocated if the user did not provide his own buffer. You can experiment with different default values to obtain the best performance. The buffer’s size has an influence on how often it should be flushed (or reloaded) and, thus, how often the physical I/O is performed.

Note the overflow and underflow methods in TCPStreamBuffer. They are virtual methods (they are declared so in the basic_streambuf base class) and will be called when there is a need for flushing or reloading the buffer area of the iostream object’s stream buffer component. The core part of the underflow method looks like this (for details please refer to the complete source code available for download at <www.cuj.com/code>):

int readn = rsocket_.read(inbuf_,
    bufsize_ * sizeof(char_type));
if (readn == 0)
    return (int_type)traits::eof();
setg(inbuf_, inbuf_,
    inbuf_ + readn / sizeof(char_type));
return sgetc();

Here, the socket wrapper is used to read the next data packet from the network. The number of bytes to read is the buffer size expressed in bytes, not in the units used to instantiate the template, hence the scaling. After reading the data, underflow makes a call to setg to reset the basic_streambuf internal pointers to the buffer area.

Note the return value from the underflow method: it is the next character from the buffer area, taken by the call to basic_streambuf::sgetc. If the underflow function cannot successfully fill the buffer area, it should return the special end-of-file value.

The overflow method is written in essentially similar style.

You can use the stream buffer class to build a fully-functional stream class. Listing 5 shows the essence of the socket-based stream. Using containment, it manages the stream buffer class (a TCPStreamBuffer object) and instantiates the base class basic_iostream with the pointer to the stream buffer. Private inheritance is used to ensure that the TCPStreamBuffer object is constructed before passing its pointer (by implicitly upcasting the this pointer) to the basic_iostream component. The layout of the whole stream object is still compatible with Figure 1, except for the fact that the stream buffer object is physically contained by the most-derived class, TCPGenericStream, as an instance of its private base class. Note, that TCPGenericStream is still a template.

The constructor of TCPGenericStream accepts the socket wrapper object. This means that, before constructing the stream object (and finally reusing all that iostream-based code), the socket wrapper has to be created. These semantics are useful for server-side code; on the server-side, the usual steps of setting up the connection are:

After these two steps, the server-side process has an open socket through which it can communicate with the client. TCPGenericStreams semantics make it very easy and intuitive. Its constructor takes that open socket and turns it into stream object.

On the client-side, however, the steps taken to set up the communication are much simpler:

Here, there is always just one socket. It could be more intuitive not to split the responsibilities between two different objects: one socket wrapper (for making the connection) and one stream built around the socket (for inserting and extracting data). TCPGenericClientStream is designed exactly to combine those two tasks. Listing 6 presents its complete implementation. Again, the private inheritance ensures that TCPSocketWrapper is created before passing its reference to TCPGenericStream’s constructor.

TCPGenericClientStream’s constructor takes two arguments: the IP address of the server to which it should connect (as either a raw IP address "xxx.xxx.xxx.xxx", or as a symbolic host name "comp.company.com") and its IP port number. In the constructor’s body, the private socket wrapper connects to the server. Note that the base class, TCPGenericStream, is instantiated with the socket wrapper contained in the TCPGenericClientStream object. No other socket object is necessary to set up the communication on the client side.

All the classes are templates in the spirit of the iostream library. To make life easier, you can use typedefs similar to the ones found in the standard library for the istream and ostream classes:

typedef
TCPGenericStream<char>
TCPStream;

typedef
TCPGenericStream<wchar_t>
TCPWStream;

typedef
TCPGenericClientStream<char>
TCPClientStream;

typedef
TCPGenericClientStream<wchar_t>
TCPWClientStream;

Sample Application

Listing 7 shows a sample server application. This application opens the listening socket and waits for a connection from the client; once established, it reads the commands sent by the client. The command is just an integer value:

  1. The server sends back a string message.
  2. The server sends back an integer value.
  3. The server sends back a complex number.

For any other command, the server just quits. Of course, a normal full-blown server would fork or at least spawn a thread for each client, but here it is not needed.

These different commands show that the formatting capabilities of the stream object are preserved, even for new, custom stream classes. If you can write this:

complex<double> cplx(1, 2);
cout << cplx << endl;

you can also write this:

complex<double> cplx(1, 2);
yourstream << cplx << endl;

This is the beauty of the iostream class hierarchy.

Listing 8 presents the example client program. It allows the user to choose which command should be sent to the server, sends it, and (in case of commands 1, 2, and 3) reads the response from the server. Again, the formatting abilities of stream classes are preserved because the TCPStream and TCPClientStream classes have appropriate base classes. The server can insert whatever it wants to its stream, and if the client knows how to extract it properly, it can do it with a simple call to the extraction operator. Of course, this applies not only to standard classes (like string and complex), but also to any user-defined class with operator<< and operator>> overloaded for iostreams.

Acknowledgements

I would like to thank James Dennett for his tips on porting the code to Linux (although it was not tested on this platform) and to Marta Jakubowska for her support while I was working on this article.

Special thanks goes to Herb Sutter for his instructive comments on the initial draft.

Notes and References

[1] In fact, this kind of overloading is to some extent possible in C. For example, POSIX read and write functions accept the file descriptor, but from the programmer’s point of view, it does not matter if the descriptor denotes file, pipe, socket, or anything else. This kind of “polymorphism” is present in many places in different operating systems, which confirms that OO is a way of programming rather than any language construct typical to C++ or Java.

[2] It also distinguishes “new” compilers from “old” ones if we take into account that the compiler and library are usually shipped together. I have written the code and successfully tested it on Microsoft Visual C++ 7.0 beta. You may have problems running the examples if you compile them with Microsoft Visual C++ 6.0 — this is the result of a bug in the library shipped with this compiler. See <http://support.microsoft.com/support/kb/articles/Q240/0/15.ASP> to learn about the bug and how to fix it. You may also need some recent version of g++ if you want to use the code on Linux.

[3] Note the use of socketsInit and socketsEnd. These functions are required on the Microsoft Windows system to properly initialize the sockets library (when compiling the code on Microsoft Windows, be sure to link with Ws2_32.lib). This is not required on Unix-like systems, but for compatibility it is a good idea to use them. On Unix systems, these functions do nothing.

[4] Angelika Langer and Klaus Kreft. Standard C++ IOStreams and Locales: Advanced Programmer’s Guide and Reference (Addison-Wesley, 2000).

[5] Cameron Hughes and Tracey Hughes. Mastering the Standard C++ Classes (John Wiley & Sons, 1999).

[6] “ISO/IEC 14882:1998(E), Programming Languages — C++.” A very cryptic name for the C++ Standard, it is the final reference in case (and also source) of confusion.

Maciej Sobczak is a student at the Institute of Computer Science, Warsaw University of Technology. He is passionate about C++ (and experiments with other technologies, too) with two years working experience. Maciej is interested in distributed, object-oriented computing. You can visit him at < www.maciejsobczak.com>.

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.