Introduction
sockpp is a simple, modern, C++ network socket library.
It is a fairly low-level C++ wrapper around the Berkeley sockets library using socket, acceptor, and connector classes that are familiar concepts from other languages.
The base socket class wraps a system socket handle and maintains its lifetime using the familiar RAII pattern. When the C++ object goes out of scope, it closes the underlying socket handle. Socket objects are generally movable but not copyable. A socket can be transferred from one scope (or thread) to another using std::move().
Features
The library currently supports:
- Portable socket classes across Linux, Mac, and Windows.
- Other *nix and POSIX systems are untested, but should work with little or no modification.
- IPv4 and IPv6 on all supported platforms.
- Unix-Domain Sockets on *nix systems that have an OS implementation for them.
- Windows support might be possible, if there was any interest.
connectorclasses for easy client creation.acceptorclasses for easy server creation.addressclasses for easy network address manipulation.- [Experimental] CANbus raw sockets on Linux for SocketCAN.
- [Experimental, Coming Soon] Secure TLS sockets with OpenSSL and MbedTLS
Support for secure sockets using either the OpenSSL or MbedTLS libraries was recently started with basic coverage. This will continue to be expanded in the near future.
There is also some experimental support for CAN bus programming on Linux using the SocketCAN subsystem. SocketCAN creates virtual network interfaces to read and write packets onto a physical CAN bus using raw sockets.
Note that sockpp releases observe semantic versioning, but any features marked as experimental are being actively developed, and are not bound by those constraints. Experimental features may have breaking changes within the same major version.
C++ Language Details
sockpp v2.x targets C++17, and makes use of many of the language features from this version, so a compatible compiler is required. Recent versions of gcc, clang, and MSVC have all been tested and should suffice.
The library provides C++ classes that wrap the Berkeley Socket C API, creating different socket types for the different and protocol amd address families. This makes for easy socket creation depending on the required uses.
All code in the library lives within the sockpp C++ namespace. A number of common collection types from the standard library are imported from the std namespace into the sockpp namespace. Thus, for example, the string type in sockpp is the std::string type. Thus a std::string is a sockpp::string.
The sockpp API generally avoids throwing exceptions, prefering instead to return a generic result type from functions. This is a type of union or variant that can contain the generic return type on success, or an error type on failure. The error is based on a C++ error_code. For functions that can not return a value, such as constructors, there are typically two matching functions: one with the required parameters, and another with the same parameters and an additional parameter which is a reference to an error_code. The first will throw an exception on error. The second is marked as noexcept and sets the error code on failure.
Socket Basics
The socket class forms the base of the class hierarchy for the socket types in the library. It is an RAII wrapper around an integer socket handle, with numerous methods for configuring and using the socket at a base level. An application would typically not use a base socket variable directly, but would create one from a derived class that is specific for clients or servers, or something specific to an address family.
Socket Handle Lifetime
The socket object maintains ownership of a socket handle from the operating system. This is typically an int on *nix systems, and a Winsock HANDLE on Windows. The object maintains the lifetime of the handle and automatically closes it in from the destructor when it goes out of scope.
A socket object does not always contain a valid OS handle. The object can be constructed without a handle, then assigned one later. Similarly, a valid handle can be released from the socket, whereby the object reverts to to initial state without a handle. When a socket object does not contain a handle, it is considered to be in an invalid state. This can be tested in a number of ways, typically with the boolean operators:
sockpp::socket sock;
if (!sock) {
cout << "The socket does not contain an OS handle." << endl;
}
Move Semantics
socket objects do not implement copy semantics by default, but do implement move semantics. Therefore, unless the user does something unsafe with the underlying handle, no two socket objects should contain the same handle. The object has unique ownership.
So, to pass ownership of the socket to another context, such as into a function or to another thread, move the socket to the other context using std::move():
sockpp::socket sock = get_a_socket_from_somewhere();
send_the_socket_somewhere_else(std::move(sock));
The OS Socket Handle
In the library, the OS socket handle is given the platform-agnostic type of socket_t. As previously stated, this is typically an int on *nix systems, and a Winsock HANDLE on Windows. Either way, the socket_t type can be used for portability if the application needs to access the value.
The sockpp API does not fully cover the C socket API (or the Winsock API on Windows). An application is still able to call the C functions by using the object’s handle().
#include "sockpp/socket.h"
#include <sys/stat.h>
int main() {
struct stat sb;
auto sock = sockpp::socket::create(AF_INET, SOCK_STREAM);
if (fstat(sock.handle(), &sb) == 0) {
cout << "I-node number: " << sb.st_ino << endl;
}
}
Error Handling
TCP and Stream Sockets
TCP Sockets
TCP and other “streaming” network applications are usually set up as either servers or clients. An acceptor is used to create a TCP/streaming server. It binds an address and listens on a known port to accept incoming connections. When a connection is accepted, a new, streaming socket is created. That new socket can be handled directly or moved to a thread (or thread pool) for processing.
Conversely, to create a TCP client, a connector object is created and connected to a server at a known address (typically host and socket). When connected, the socket is a streaming one which can be used to read and write, directly.
For IPv4 the tcp_acceptor and tcp_connector classes are used to create servers and clients, respectively. These use the inet_address class to specify endpoint addresses composed of a 32-bit host address and a 16-bit port number.
TCP Server: tcp_acceptor
The tcp_acceptor is used to set up a server and listen for incoming connections.
int16_t port = 12345;
sockpp::tcp_acceptor acc(port);
if (!acc)
report_error(acc.last_error_str());
// Accept a new client connection
sockpp::tcp_socket sock = acc.accept();
The acceptor normally sits in a loop accepting new connections, and passes them off to another process, thread, or thread pool to interact with the client. In standard C++, this could look like:
while (true) {
// Accept a new client connection
sockpp::tcp_socket sock = acc.accept();
if (!sock) {
cerr << "Error accepting incoming connection: "
<< acc.last_error_str() << endl;
}
else {
// Create a thread and transfer the new stream to it.
thread thr(run_echo, std::move(sock));
thr.detach();
}
}
The hazards of a thread-per-connection design is well documented, but the same technique can be used to pass the socket into a thread pool, if one is available.
See the tcpechosvr.cpp example.
TCP Client: tcp_connector
The TCP client is somewhat simpler in that a tcp_connector object is created and connected, then can be used to read and write data directly.
sockpp::tcp_connector conn;
int16_t port = 12345;
if (!conn.connect(sockpp::inet_address("localhost", port)))
report_error(conn.last_error_str());
conn.write_n("Hello", 5);
char buf[16];
ssize_t n = conn.read(buf, sizeof(buf));
See the tcpecho.cpp example.
IPv6
The same style of connectors and acceptors can be used for TCP connections over IPv6 using the classes:
inet6_address
tcp6_connector
tcp6_acceptor
tcp6_socket
udp6_socket
Examples are in the examples/tcp directory.
UDP and Datagram Sockets
UDP Socket: udp_socket
UDP sockets can be used for connectionless communications:
sockpp::udp_socket sock;
sockpp::inet_address addr("localhost", 12345);
std::string msg("Hello there!");
sock.send_to(msg, addr);
sockpp::inet_address srcAddr;
char buf[16];
ssize_t n = sock.recv(buf, sizeof(buf), &srcAddr);
See the udpecho.cpp and udpechosvr.cpp examples.
UNIX Domain Sockets
UNIX domain sockets can be used for local connection on *nix systems that implement them. They operate For that use the classes:
unix_address
unix_connector
unix_acceptor
unix_socket
unix_stream_socket
unix_dgram_socket
Examples are in the examples/unix directory.
Network Addresses
The sockpp library provides wrapper classes for the socket address structures for the supported address families.
The address classes typically wrap the underlying C struct for the family that they represent, and simply provide constructors and field defaults to make for easier and more convenient usage. The address objects act as normal data, implementing copy and move semantics. For simplicity, all multi-byte integer values given or retrieved from the address objects are in native (host) byte order. The classes convert to and from network order as needed.
Note that the C++ classes are specifically given similar, but different, names than their C counterparts to avoid confusion and name clashes, despite being in different namespaces.
The C API
Before explaining the sockpp C++ wrappers for the C API address structures, it would be good to review the C API for network addressing.
Many API functions that expect an address take a pointer and size to a generic, protocol-independent, structure called sockaddr, like this:
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
Here, essentialy, the bind() call takes a socket descriptor and an address. The address is provided as a pointer to a struct and the length of the struct, noting that the addresses from different families can be of different length.
The sockaddr struct is usually defined something like this:
struct sockaddr {
sa_family_t sa_family; // Address family (e.g., AF_INET, AF_INET6, AF_UNIX)
char sa_data[14]; // Protocol address data (variable length)
};
The contains a single, concrete member, sa_family as a short (2-byte) integer, providing the addess family, which defines the actual type that will follow. All addresses simply need this as the first field in their family-specific address structures to properly align with sockaddr.
Addresses for specific protocols have concrete structures defined in the API which overlap with this. For example the IPv4 family address structure often looks like this:
struct sockaddr_in {
sa_family_t sin_family; // Address family is always AF_INET
unsigned short sin_port; // Port number (network byte order)
struct in_addr sin_addr; // IP address (network byte order)
char sin_zero[8]; // Padding to match the size of struct sockaddr
};
Note that the size of sockaddr is 16 bytes, and as sockaddr_in requires less data, it pads the struct to be the same length. But many address structs that were defined later require more than 16 bytes, and thus the length is always required as they can be different sizes.
Since the actual addresses can be larger than sockaddr a new generic type was defined to have enough storage space to contain any address supported on the target platform. This is called sockaddr_storage:
struct sockaddr_storage {
sa_family_t ss_family; // Address family (e.g., AF_INET, AF_INET6)
char padding[SS_PADSIZE]; // Padding to contain any address type
};
This can be used to read in an address when the specific type is not knowd. After it is received, the ss_family member can be checked to see the type of address and determine the actual length.
The Base Address Class: sock_address
In the sockpp library, the sock_address is an abstract base class for all the addresses. It serves a similar use as the sockaddr in the C API, serving as a function parameter when the specific address can be more than one different type. It contains virtual methods to let the derived classes expose their family, and a pointer and length to an underlying C struct, allowing the C++ addresses to be passed to any of the C API functions, as needed.
It looks something like this:
class sock_address
{
public:
virtual socklen_t size() const = 0;
virtual sockaddr* sockaddr_ptr() = 0;
virtual const sockaddr* sockaddr_ptr() const = 0;
virtual sa_family_t family() const { ... }
// ...
};
Since the other addresses are derived from this, they can se sent to any function that takes a reference to a sock_address, like this:
result<size_t> socket::send_to(const string& s, const sock_address& addr);
IPv4 and IPv6 Addresses: inet_address and inet6_address
The inet_address is the address type for the AF_INET family, and wraps the C sockaddr_in struct. It is the IPv4 address type, consisting of a 32-bit address and 16-bit port number. All integer values use native (host) byte ordering going into or out of the objects.
Addresses can be created using the numeric values, like:
inet_address addr{0x7F000001, 80};
They can also be created with string address names, in which case, name resolution will be applied to get the actual address. When done this way, though, the resolution can fail, and the error should be handled. The application can use the presentation form of an address, or the name for lookup:
inet_address addr1{"192.168.1.1", 80};
inet_address addr2{"localhost", 502};
The inet6_address is the address type for the AF_INET6 family, and wraps the C sockaddr_in6 struct. It is the IPv6 address type, consisting of a 128-bit address and 16-bit port number.
Additional Address Families
The sockpp library has additional address types for other supported families, like unix_address for UNIX-domain sockets, can_address for Linux SocketCAN, etc. These are described in the sections about those protocols.
The “Any” Address Type: sock_address_any
The library also provides the generic sock_address_any type, which contains enough storage space for any of the supported addresses on the platform. This wraps the sockaddr_storage type from the C API. Applications can use an address of this type when receiving addresses of an unknown family.
This is also the address type returned by the underlying (generic) socket class when queried for it’s address or the address of its peer:
class socket {
public:
// ...
/**
* Gets the local address to which the socket is bound.
* @return The local address to which the socket is bound.
*/
sock_address_any address() const;
/**
* Gets the address of the remote peer, if this socket is connected.
* @return The address of the remote peer, if this socket is connected.
*/
sock_address_any peer_address() const;
// ...
};
Since it overrides the base address class, sock_address_any can be used whenever an address is required; it does not have to be converted to the specific type to be used. But it can be converted. The different family address types all have constructors that take a sock_address_any, but they are all failable. They only succeed if the address is compatible, which is usually confirmed by checking the family type and length of the source address.
sock_address_any any_addr = sock.peer_address();
error_code ec;
inet_address addr{any_addr, ec};
if (!ec) {
cout << "The peer is not using IPv4" << endl;
}
[Experimental] CAN Bus on Linux with SocketCAN
The Controller Area Network (CAN bus) is a relatively simple protocol typically used by microcontrollers to communicate inside an automobile or industrial machine. Linux has the SocketCAN package which allows processes to share access to a physical CAN bus interface using Raw sockets in user space. See: Linux SocketCAN
The bus is a simple twisted pair of wires that can have multiple “nodes” (devices) attached to them which can communiate with each other using small, individual packets, called “frames”. Each frame is sent to a specific numeric ID (addresses) on the bus. The ID can be a normal one of 11-bits, or an extended ID containing 29 bits. The IDs form a message prioritization where the lower ID is the higher priority. If there is a bus collision, the frame with the lower ID is allowed to proceed, while the other is forced to back-off and try again later.
The two primary flavors of can frames are “standard, CAN 2.0 frames which can contain 8 bytes of data, or CAN-FD frames which can contain up to 64 bytes. In addition, CAN FD can transmit the data payload at a higher bit rate than the framing data, while still allowing the bus to be shared between standard and FD frames.
Example
As an example, consider a device with a temperature sensor which might read the temperature peirodically and write it to the bus as a raw 32-bit integer. Each temperature frame will use the CAN ID of 0x40.
// Use the interface name to get an address, and create a socket for it.
can_address addr("CAN0");
can_socket sock(addr);
// The agreed ID to broadcast temperature on the bus
canid_t canID = 0x40;
while (true) {
this_thread::sleep_for(1s);
// Write the time to the CAN bus as a 32-bit int
int32_t t = read_temperature();
can_frame frame { canID, &t, sizeof(t) };
sock.send(frame);
}
A receiver to get a frame might look like this:
can_address addr("CAN0");
can_socket sock(addr);
can_frame frame;
sock.recv(&frame);
Building Applications with CMake
The library, when installed can normally be discovered with find_package(sockpp). It uses the namespace Sockpp and the library name sockpp.
A simple CMakeLists.txt file might look like this:
cmake_minimum_required(VERSION 3.15)
project(mysock VERSION 1.0.0)
find_package(sockpp REQUIRED)
add_executable(mysock mysock.cpp)
target_link_libraries(mysock Sockpp::sockpp)