A C++ Socket Library for Linux

SocketCC, the C++ class library Jason presents here, supports both IPv4 and IPv6 network communications using both TCP- and UDP-style sockets. And it's freely available.


June 01, 2002
URL:http://www.drdobbs.com/cpp/a-c-socket-library-for-linux/184405070

Jun02: A C++ Socket Library for Linux

Jason is a senior research fellow at the Centre for Telecommunications and Information Engineering, Monash University. He can be contacted at [email protected].


I was recently working on a network traffic generator that needed to produce both TCP and UDP traffic over both IPv4 and IPv6. No problem. Linux supported IPv6, the standard socket library calls provided the functionality, and W. Richard Stevens's UNIX Network Programming: Volume 1 (Prentice Hall, 1997) covered what I needed to write the software.

But as it turned out, there was a problem. Since I was implementing the program in C++, the design was class based and I wanted the overall program design to utilize a C++ socket class library, rather than use the provided socket function calls. Consequently, I began looking for suitable (free) libraries to use. And although I found a number of libraries, each one had something that made it unsuitable — it wasn't free, it had only TCP or only IPv4 support, it was too complex, and the like.

In the end, I wrote SocketCC, the C++ class library I present here, which supports both IPv4 and IPv6 network communications using both TCP- and UDP-style sockets. SocketCC is not a comprehensive sockets library, nor is it necessarily suitable for all types of applications. However, it is both class based and open source, so you should be able to work around any deficiencies by inheriting classes or rewriting the base class.

The Classes

SocketCC's header file, socketcc.h (available electronically; see "Resource Center," page 5), includes the C++ class definitions for all of the classes defined by the SocketCC library. Some standard socket and pthread library headers are included because they are required by some definitions within the header file; otherwise, this .h file includes the C++ definitions and a series of comments to describe the intended class usage. Since this header file is to be made accessible in the /usr/include directory, it is important for it to be well documented. Figure 1 illustrates the class hierarchy defined in socketcc.h. You can see that there are two standalone classes as well as a family of classes inherited from the SocketBase base class. The family of inherited classes provides the actual socket functionality. The base class provides common socket functions and the inherited socket classes provide the specific functionality of a particular type of socket.

SocketCC uses the SocketException class to pass error messages. If an error occurs at any time within SocketCC, an exception of type SocketException is thrown. The class stores an error code and string representation of the error code. The exception can be queried to determine the error type and be cast to a string value for printing purposes. The class is implemented within socketexception.cpp (available electronically).

The IPAddress class provides encapsulation of both IPv4 and IPv6 addresses within a single type. Within the standard sockets library, IPv4/IPv6 addresses are stored as continuous binary arrays of different lengths and require different function calls to convert both to and from a string representation. Similarly, when using these addresses within a socket call, different structures need to be used depending on which address type is in use. The idea behind the IPAddress class is to allow both address types to be represented within a single type and simplify socket calls, where a variable of type IPAddress is all the user application needs to pass and refer to. A goal in designing this class was to encapsulate DNS functionality — to allow the address to be set either by its binary value or string representation, as well as to retrieve a string representation of the address having set it using the binary representation.

The SocketBase class encapsulates existing socket function calls. My first idea was to store the socket descriptor as a private member variable within the class and provide method calls that identically wrap the socket function calls. I modified this approach to allow use of IPAddress instances and integer port number parameters instead of sockaddr structures. As sockaddr structures are different for both IPv4 and IPv6, they are constructed within the SocketBase methods so that a single method implementation supports both IPv4 and IPv6. The more useful socket function calls are wrapped by SocketBase methods; it is straightforward to include more methods in the definition if more functionality is required. Together with the IPAddress class, the SocketBase class forms the key part of the SocketCC library.

The TCPSocket, TCPClientSocket, and TCPServerSocket classes extend SocketBase to provide TCP functionality. TCPSocket forms a base class for TCPClientSocket and TCPServerSocket; it provides functionality that is common to both client and server TCP sockets. The class is protectively inherited from SocketBase; therefore, none of the SocketBase methods are available to TCPSocket users to ensure that only valid TCP operations are available. TCPClientSocket inherits publicly from TCPSocket with all public member methods of TCPSocket becoming public member methods of TCPClientSocket. This class provides for the client side of a TCP connection by letting the socket execute a connection to a remote TCP server type socket. TCPServerSocket also inherits publicly from TCPSocket with the same inheritance of the TCPSocket public member methods. This class provides functionality specific to the server side of a TCP connection by creating a listening TCP socket and allowing connections on that socket to be accepted. When a connection is accepted, a class instance of type TCPSocket is created and returned; this instance refers to the newly created socket and enables communication with the remote TCP client socket. The methods within the TCP socket classes are implemented by calling the methods of the base class SocketBase with the appropriate parameters. Given SocketBase, the implementation of these three classes is straightforward — the classes are implemented within tcpsockets.cpp (available electronically).

UDPSocket, UDPServerSocket, and UDPConnectedSocket extend SocketBase to provide UDP functionality. Unlike TCPSocket (which forms a base class to two other classes), UDPSocket forms a base class only to UDPServerSocket. This is because UDP sockets do not have to be connected as TCP sockets do. UDPSocket provides the base functionality of a UDP unconnected socket, allowing the sending/receiving of datagrams to/from a variety of other UDP sockets. Like TCPSocket, UDPSocket is protectively inherited from SocketBase and, therefore, none of the SocketBase methods are available to UDPSocket users. UDPServerSocket inherits publicly from UDPSocket with the full public inheritance of the UDPSocket public member methods. This class provides functionality specific to the server side of a UDP connection where the UDP socket is bound to an IP address and port number. UDPConnectedSocket inherits from SocketBase. While UDP sockets are connectionless, a UDP socket can be connected to a remote UDP socket using the connect() function call. Once this occurs, datagrams can only be sent to the remotely connected socket and are only received if they are sent by the remote socket. Datagrams arriving from a different socket are ignored and not passed to the application. In this case, there is no need to specify the destination socket address nor to obtain the source address information. UDPConnectedSocket encapsulates this functionality by providing a similar interface to TCPClientSocket, which can find a use in a UDP client application where the socket will only be used to talk with a single UDP server. This simplifies the code as the source/destination address information need not be specified multiple times. Like the TCP socket classes, the implementation of the UDP classes is straightforward and the classes are implemented within udpsockets.cpp (available electronically).

Implementing IPAddress

The IPAddress class is implemented in ipaddress.cpp (available electronically). IPAddress's key feature is that it can be assigned by either a binary IPv4 address, binary IPv6 address, hostname, or another instance of an IPAddress class. Also, there are methods provided to retrieve the stored hostname, address string, a pointer to the binary address, and the length of the binary address. These values are stored internally within private member variables.

One of the key issues involved with the implementation of IPAddress is that the gethostbyname2()/gethostbyaddr() functions (called from within the class) are nonreentrant. This means that if the function is called a second time, it overwrites the result written during the function's prior call. This becomes a problem in multithreaded applications where multiple threads can be executing IPAddress methods simultaneously. You can solve this by using a static mutual exclusion that is common to all instances of IPAddress. MutEx ensures that only one thread has access to the protected functions until all the returned values are safely copied into the internal member variables.

There are four assignment operator overloads, whereby an IPAddress class can be set equal to the following: an existing IPAddress class, IPv4 resolved, IPv4 resolved hostname string, binary IPv4 address, and binary IPv6 address. When copying from an existing IPAddress class, all fields within the class are copied from the original. When copying from a hostname string, the SetHostName() member method is called, setting the class to the IPv4 resolved IP address of the provided hostname. Copying from a binary IP address results in the SetAddress() member method being called to set the class instance to either the provided IPv4 or IPv6 address.

There are also four comparison operator overloads, allowing the IPAddress instance to be compared against another IPAddress class, a hostname string value, and a binary IPv4 or IPv6 address. When comparing to another IPAddress class, you only compare the iAddressType, iAddressLength, and pcAddress fields. This is because if the pcAddress fields match, then the pcStrAddress fields must also match since it is the string representation of pcAddress. pcHostName fields are not compared because the same IP address can have more than one possible hostname. You are comparing the IP address, not the related hostname. The other three comparison overload methods create an instance of IPAddress based on the provided information and then compare this newly created address with itself.

The advantages of using IPAddress are twofold: An IPAddress is set and calculated via a single operation, and an IPAddress class is passed to SocketBase methods, which takes the appropriate action based on whether it is an IPv4 or IPv6 address, respectively. This hides the details of IPv4 or IPv6 from programmers who can then use a single code base that functions with both IPv4 and IPv6 addresses.

Implementing SocketBase

When I first devised the interface for SocketCC, my intention was to hide the details of the multiple sockaddr structures from user applications, letting it communicate using IPAddress classes and integer values for port numbers. I intended to construct the sockaddr structures as required within methods of the SocketBase class. As it turned out, more socket functions used sockaddr structures than I thought. I first thought about moving this functionality to a private method, but decided that the range of sockaddr structures needed to be encapsulated into a single class, just as I had encapsulated IPv4 and IPv6 binary addresses into a single class. However, I still did not want this to be part of the library.

My solution was to create a private class called SocketAddress, defined and implemented entirely within socketbase.cpp (also available electronically). As such, I could use this class freely within my SocketBase implementation, but since it is not exported outside this source file, it is unavailable to other classes within the SocketCC library as well as to users of the library.

The SocketAddress Class

The SocketAddress class encapsulates the structures sockaddr, sockaddr_in, and sockaddr_in6 into a single class. While all these structures can be passed as parameters to socket function calls, they are not defined as a family tree and cannot be simply interchanged for one another. The SocketAddress class overcomes this by creating a memory space for a sockaddr_in6 structure (the largest of the three structures), and defining three pointers of each structure type to point to the single allocated structure. This saves memory allocation and lets you access fields retrieved via a socket function call by casting to the correct structure type. The class has been defined to simplify code in the SocketBase class, where a SocketAddress class is created, used in a socket function call, and then destroyed.

The public interface of the SocketAddress class consists of a constructor that clears and sets the internal variables (a series of methods that allow setting the IP address) and port number fields of the SocketAddress class as well as a series of methods to retrieve these values from the class. Finally, there is a method to return the size of the sockaddr structure currently being represented, as well as being able to cast the class to a (sockaddr *). This cast lets you use the class directly in calls to socket functions.

SocketAddress is straightforward. Each method branches to one of two instructions to either set or retrieve information from the encapsulated sockaddr structure. The instructions differ, depending on whether the structure represents an IPv4 or IPv6 address/port pair.

The SocketBase Class

If any of the socket function calls return an error within SocketBase, the error code in the global variable errno is examined and the matching SocketException is thrown, causing the method to terminate. Each method in SocketBase corresponds to a matching socket function call, with the class constructor implementing the socket() function call and the class destructor implementing close(). The class constructor creates the socket via socket(), the result of which is the socket descriptor that is then assigned to the internal member variable iSockDesc for future reference.

Where a socket function requires a sockaddr structure to be filled and passed as a parameter, the corresponding method requires an IPAddress and port number as parameters. A SocketAddress instance is created and its values are set to the provided values. This instance is then cast to a (sockaddr *) pointer when calling the socket function. Where a socket function call returns data by filling in a sockaddr structure, a SocketAddress instance is created and cast to a (sockaddr *) pointer for the socket function call. The IPAddress and port number stored within the SocketAddress instance is then extracted and returned to the caller.

Some minor differences are evident in the Bind(), Recv(), and RecvFrom() methods. Bind() has two implementations, one that accepts both an IPAddress and port number to bind the socket to. The second implementation accepts just a port number. In this case, the socket is bound to all local IP addresses; this is reflected in the SetIPAddressWildcard() method of SocketAddress. As for the standard socket function calls, a port number of 0 indicates that the server selects any available port number to bind the socket to. Like the socket recvfrom() function call, the two data retrieval methods return the actual number of bytes read from the socket. However, a function result of 0 indicates the socket has been closed at the remote end. In keeping with the basic design of the SocketCC library, these methods throw a NotConnected SocketException when this occurs, saving the calling code from checking the return value of these methods.

The Accept() method accepts a new connection on a listening socket. The underlying function call returns a new socket descriptor that refers to the newly connected client socket, while the original descriptor refers to the still listening socket. Since the aim of SocketBase is to hide the socket descriptor from the user as an abstraction, the Accept() method should instead return a new SocketBase instance that refers to the newly created socket. The problem is that SocketBase is intended as a base class and you may want to return an inherited class instance for the new connection. My approach is to require a pointer to a SocketBase (or inherited class) instance to be provided to the Accept() method; once the new connection is accepted, the SocketBase instance is modified to represent the newly created socket. This procedure involves closing the socket that is already represented and then setting the value of its internal variables. In the Accept() method, this is achieved by calling the protected NewSocketDetails() method on the second SocketBase instance. To understand the code structure involved in accepting a connection, see Listing One.

Using SocketCC

Once the SocketCC library has been compiled and installed to the system (installation tips are available electronically), it is available for use in all applications. Myecho.cpp (also available electronically) is a sample app that can be compiled with the command gcc -lsocketcc -o myecho myecho.cpp.

The most interesting parts of this application are the functions TCPEchoClient(), TCPEchoServer(), UDPEchoClient(), and UDPEchoServer(). The rest of the source code is concerned with parsing the command-line parameters and calling one of these four functions with the appropriate parameters. If you ignore the code that prints information to the screen, you can see that the function implementations are simple. For example, TCPEchoClient() creates an instance of TCPClientSocket, connects to the specified server, sends a character string, and waits for a response before disconnecting the socket. In contrast, the server creates an instance of TCPServerSocket that is bound to the provided port number. The AcceptClient() method returns a pointer to a newly created instance of TCPSocket. You wait for data to arrive on the TCPSocket instance and echo it back to the sender. This socket is then closed and destroyed. The cycle repeats, waiting for the next connection. While this server cannot support multiple concurrent clients (which requires the use of multiple threads), it does provide a simple example of the usage of the SocketCC library.

The UDP client and server implementations are similar and you can use the sample application to experiment with TCP and UDP data transfer over both IPv4 and IPv6 networks based on the command-line parameters of the myecho program.

DDJ

Listing One

try
{
    IPAddress       cClientIP;
    int             iClientPort;
    SocketBase      cListenSock(), cCommsSock;

    // Bind listening socket to port 80 and start listening for a connection
    cListenSock.Bind(80);
    cListenSock.Listen(5);
4
    // Block until a client connects, cCommsSock now refers to 
    //    the newly connected socket
    cListenSock.Accept(&cCommsSock, cClientIP, iClientPort);
printf("Client (%s) connected from port %d\n", 
                         (const char *) cClientIP, iClientPort);
    // Data transfer using cCommsSock
    cCommsSock.Recv(..);
    cCommsSock.Send(..);

    // cListenSock and cCommsSock are closed when they go out of scope
}
catch (SocketException &excep);
{
    printf("Error in Socket Call : %s\n", (const char *) excep);
}

Back to Article

Jun02: AC++ Socket Libraryfor Linux

Figure 1: Class hierarchy defined in socketcc.h.

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