Installable packages for TCP based client-server communication.\ TLS encryption with two-way authentication is supported.
This project provides installable C++ libraries for setting up TCP based client-server communications.\ Such a connection can be:
Both, the server and the client can send and receive messages asynchronously. To send a message to an established connection, a send method can be called any time. To work on incoming messages, a receive method can be defined to be called immediately in a separate thread.
A regular TCP package has max maximum length of 65536 bytes. Under the hood a TCP package size is limited to 16384 for this library, but it is possible to send and receive messages of any size thanks to built in fragmentation and reassembling.\ A connection can be established for one of the following two modes:
There are some size limitations for this library:
max_size()
on any variable of type string. For most modern systems, this value is such high that it can be treated as infinity.This library is developed on a debian based system, so this manual is specific to it.\ Nevertheless, it only depends on the C++17 standard and the standard library, so it should be possible to use it on other systems.
For the hardware I'm not giving any limitations. It is usable on low level hardware as well as on high performance systems.
The following steps can be applied for the entire project if you want to install both, the server and the client part and it can be applied for each part separately.
Install necessary third party software
sudo apt install build-essential cmake libssl-dev
For creating self-signed certificates and running the tests or examples, the openssl tool is required:
sudo apt install openssl
Create a build directory and move to it
mkdir build
cd build
Build the library
cmake ..
make
Install the library
sudo make install
[optional] Fix shared library dependencies
If you get the following error message when running an application:
error while loading shared libraries: libTcpServer.so.x: cannot open shared object file: No such file or directory
running the following command can solve it:
sudo /sbin/ldconfig
To see a basic example that shows you all functionality, please build and run the example project:
Build both applications
mkdir build
cd build
cmake ..
make
Create self-signed certificates for TLS encryption
./CreateCerts.sh
Start the server and client
Open a terminal and start the server
./server
Open a second terminal and start the client
./client
Compiling the project installs the library on your system.\ To use it in your project, include the header files from the sub-folder tcp:
#include <tcp/TcpServer.h>
#include <tcp/TcpClient.h>
#include <tcp/TlsServer.h>
#include <tcp/TlsClient.h>
using namespace std;
using namespace tcp; // The entire library is in the namespace tcp
The following linker flags are mandatory to be set to tell the system what libraries to use:
For both, an unencrypted TCP and encrypted TLS connection, one of two modes can be selected for exchanging messages.
In the fragmented mode, all messages are text packages with a finite length. When receiving a message, the message is buffered in a string variable that can be processed in the receive worker method. To separate messages, a delimiter must be defined to separate individual messages on the network stream. Please make sure that the delimiter is not part of any message.
In the continuous mode, a continuous stream of data is sent and received. To work on incoming data, an outgoing stream must be defined. For a client, that holds just one active connection to a server, a pointer to an outgoing stream must be defined. For a server, that holds multiple connections, a method returning a pointer to an outgoing stream must be defined based on the sending client ID.
The following examples are done for a TCP server, but they can be used for a TLS server as well.
TcpServer server; // Constructor with no arguments gives a server in continuous mode
TcpServer server{'|'}; // Constructor with delimiter argument gives a server in fragmented mode
TcpServer server{'|', 4096}; // In fragmented mode, the maximum message length (for sending and receiving) can be set
Worker methods can be defined for the following events:
// Worker for established connection to a client
void worker_established(int clientId)
{
// Do stuff immediately after establishing a new connection
// (clientId could be changed if needed)
}
// Worker for closed connection to client
void worker_closed(int clientId)
{
// Do stuff after closing connection
// (clientId could be changed if needed)
}
// Worker for incoming message (Only used in fragmentation-mode)
void worker_message(int clientId, string msg)
{
// Do stuff with message
// (clientId and msg could be changed if needed)
}
// Output stream generator
ofstream *generator_outStream(int clientId)
{
// This method is called when establishing a new connection, even before the worker_established method
// Stream must be generated with new
// This example uses file stream but any other ostream could be used
// (clientId could be changed if needed)
return new ofstream{"FileForClient_"s + to_string(clientId)};
}
// Link worker functions using the following linker methods:
tcpServer.setWorkOnEstablished(&worker_established)
tcpServer.setWorkOnClosed(&worker_closed)
tcpServer.setWorkOnMessage(&worker_message)
tcpServer.setCreateForwardStream(&generator_outStream)
A worker method can be linked to the server on several ways: Using worker_established as example here, this works for all other worker functions similarly.
Standalone function
void worker_established(int clientId)
{
// Do stuff immediately after establishing a new connection
// (clientId could be changed if needed)
}
TcpServer tcpServer;
tcpServer.setWorkOnEstablished(&worker_established);
Member function of this instance
class ExampleClass
{
public:
ExampleClass()
{
tcpServer.setWorkOnEstablished(::std::bind(&ExampleClass::classMember, this, ::std::placeholders::_1));
}
virtual ~ExampleClass() {}
private:
// TCP server as class member
TcpServer tcpServer{};
void classMember(const int clientId)
{
// Some code
}
};
The bind function is used to get the function reference to a method from an object, in this case this
. For each attribute of the passed function, a placeholder with increasing number must be passed.
Member function of foreign class
class ExampleClass
{
public:
ExampleClass() {}
virtual ~ExampleClass() {}
private:
void classMember(const int clientId)
{
// Some code
}
};
// Create object
ExampleClass exampleClass;
// TCP server outside from class
TcpServer tcpServer;
tcpServer.setWorkOnEstablished(::std::bind(&ExampleClass::classMember, exampleClass, ::std::placeholders::_1));
Lambda function
TcpServer tcpServer;
tcpServer.setWorkOnEstablished([](const int clientId)
{
// Some code
});
The following methods are the same for all kinds of servers (TCP or TLS in fragmented or continuous mode):
start():
The start-method is used to start a TCP or TLS server. When this method returns 0, the server runs in the background. If the return value is other that 0, please see Defines.h or Start return codes - server for definition of error codes.
TcpServer tcpServer;
TlsServer tlsServer;
tcpServer.start(8081);
tlsServer.start(8082, "ca_cert.pem", "server_cert.pem", "server_key.pem");
stop():
The stop-method stops a running server.
tcpServer.stop();
sendMsg():
The sendMsg-method sends a message to a connected client (over TCP or TLS). If the return value is true, the sending was successful, if it is false, not.
tcpServer.sendMsg(4, "example message over TCP");
getClientIp():
The getClientIp-method returns the IP address of a connected client (TCP or TLS) identified by its TCP ID. If no client with this ID is connected, the string "Failed Read!" is returned.
TlsServer::getSubjPartFromClientCert():
The getSubjPartFromClientCert-method only exists for TlsServer and returns a given subject part of the client's certificate identified by its TCP ID or its tlsSocket (SSL). If the tlsSocket parameter isnullptr*, the client is identified by its TCP ID, otherwise it is identified by the given tlsSocket parameter.
The subject of a certificate contains information about the certificate owner. Here is a list of all subject parts and how to get them using getSubjPartFromClientCert():
For example
tlsServer.getSubjPartFromClientCert(4, nullptr, NID_localityName);
will return "Stuttgart" if this is the client's city name.
isRunning():
The isRunning-method returns the running flag of the server.\ True means: The server is running\ False means: The server is not running
The following examples are done for a TCP client, but they can be used for a TLS client as well.
myStream ofstream("MyFile.txt");
TcpClient client; // Constructor with no arguments gives a client in continuous mode forwarding to stdout
TcpClient client{myStream}; // Constructor with stream argument gives a client in continuous mode forwarding to a file
TcpClient client{'|'}; // Constructor with delimiter argument gives a client in fragmented mode
TcpClient client{'|', 4096}; // In fragmented mode, the maximum message length (for sending and receiving) can be set
The only worker function a client provides is working on received messages in fragmented mode.
// Worker for incoming message (Only used in fragmentation-mode)
void worker_message(string msg)
{
// Do stuff with message
}
TcpClient tcpClient;
tcpClient.setWorkOnMessage(&worker_message);
This function can be linked to client similarly to server via standalone, member or lambda function.
start():
The start-method is used to start a TCP or TLS client. When this method returns 0, the client runs in the background. If the return value is other that 0, please see Defines.h or Start return codes - client for definition of error codes.
TcpClient tcpClient;
TlsClient tlsClient;
tcpClient.start("serverHost", 8081);
tcpClient.start("serverHost", 8082, "ca_cert.pem", "client_cert.pem", "client_key.pem");
stop():
The stop-method stops a running client.
tcpClient.stop();
sendMsg():
The sendMsg-method sends a message to the server (over TCP or TLS). If the return value is true, the sending was successful, if it is false, not.
tcpClient.sendMsg("example message over TCP");
isRunning():
The isRunning-method returns the running flag of the client.\ True means: The client is running\ False means: The client is not running
When calling the start-method, on server or client, an ineger value is returned. 0 always means success and the server/client is now running in the background until the stop-method is called. Other values indicate the following errors errors (see Defines.h for server and Defines.h for client):
When a client sends a message to the server immediately after the TlsServer::start() method returned, the server program throws a pipe error.
Waiting for a short time after connecting to server will fix it on client side.
To prevent a program crash on server side, a pipe error can simply be ignored:
// Handle pipe error
void signal_handler(int signum)
{
// Ignore pipe error
if (signum == SIGPIPE)
{
::std::cout << "SIGPIPE ignored" << ::std::endl; // Pipe error ignored
return;
}
// For any other error, exit program
exit(signum);
}
// Register pipe error signal to handler
signal(SIGPIPE, signal_handler);
// Start a server regularly
TcpServer server;
server.start(8080);
// Do your stuff here ...
In some gtest executions, the runtime gets stuck right after starting TLS server, see: