🚀 Quick Start
This library have 3 main classes:
Server
: Listen for incoming connection from a client. Create a new socket for each client.Socket
: Allow to send data and receive data from a client. Or to connect to server and send/receive data.SocketWorker
: Object that actually handle data stream and is supposed to reconstruct from it. This object can live in a worker thread to do deserialization work.
The library won’t do much out of the box. Inheritance from those class is required. The goal of this library is to provide easy to use object with all the required functionality to only focus on data transmission.
Custom Protocol for example
Most of the time when using TCP, a custom packet protocol needs to be implemented. In the following example, the protocol used will be very simple. The protocol goal is to send strings.
First byte will be header, and will indicate the length of the string. Maximum string length is 128. The string will follow. Data stream will look like that:
The header + payload stream structure is the most efficient way to transmit data because depending on the protocol (if packet size is known, or packet size have a limit), one or two read is only necessary to retrieve a full packet.
Create a Client
Creating a client is the easiest way to start, because only Socket
and SocketWorker
needs to be customize.
Socket Worker
Let’s start by implementing a custom SocketWorker
.
- Inherit from
net::tcp::SocketWorker
.- It’s a QObject, so don’t forget the Q_OBJECT macro.
- Override
void onDataAvailable()
. This function will be called each time new data is available to poll.- This function run in the worker thread. It’ can either be a thread created by owning
Server
, orServer
thread. - Call
size_t bytesAvailable()
to know how many bytes are in system buffer. - Call
size_t read(uint8_t* buffer, size_t max)
to read at maximummax
bytes. The function return the real number of byte read. - If any problem happened call
closeAndRestart()
. Socket will try to restart later.- If the owning
Socket
have been created as a client, then it will reconnect to remote server later. - If the owning
Socket
have been created by aServer
, theSocket
will be completely destroy. It’s the client responsibility to reconnect.
- If the owning
- This function run in the worker thread. It’ can either be a thread created by owning
- Call
size_t write(const uint8_t* buffer, const size_t length)
to write data to the stream. The function returned the number of byte written. If byte written is 0 then retry later. Every buffer are full. - Don’t forget to reset the State Machine when server disconnect or reconnect.
The example is self explanatory.
- The function that write the data first write the header then write the payload (the string)
- The function that read wait for a header to read. Then poll every possible byte until the full string got read.
- Custom signals & slots are present to communicate with
Socket
.
#include <Net/Tcp/SocketWorker.hpp>
class MySocketWorker : public net::tcp::SocketWorker
{
Q_OBJECT
public:
MySocketWorker(QObject* parent = nullptr) : net::tcp::SocketWorker(parent) {}
private:
bool waitingForData = false;
uint8_t buffer[128] = {};
uint8_t bufferLength = 0;
uint8_t expectedSize = 0;
void readHeader()
{
// Only read if something is available
if (!bytesAvailable())
return;
// Only read if in correct state
if (waitingForData)
return;
// Read 1 byte
if (!read(&expectedSize, 1))
return closeAndRestart();
// Check header is valid
if (expectedSize == 0 || expectedSize >= 128)
return closeAndRestart();
// Go to next state waiting for data
waitingForData = true;
}
protected Q_SLOTS:
// !! DONT FORGET TO RELEASE BUFFER !! IMPORTANT !! //
void onConnected() override final
{
net::tcp::SocketWorker::onConnected();
waitingForData = false;
bufferLength = 0;
}
void onDataAvailable() override final
{
// Read header if not done
if (!waitingForData)
readHeader();
// Otherwise read maximum number of bytes expected
while(waitingForData && bytesAvailable())
{
// Read maximum data until the whole packet have been read.
const auto bytesRead = read(buffer + bufferLength, expectedSize - bufferLength);
bufferLength += uint8_t(bytesRead);
// Emit the received string when read is complete
if(bufferLength == expectedSize)
{
QString s(reinterpret_cast<char*>(buffer));
Q_EMIT stringAvailable(s);
waitingForData = false;
bufferLength = 0;
readHeader();
}
}
}
public Q_SLOTS:
void onSendString(const QString& s)
{
const auto data = s.toStdString();
// Max packet size is 128
if (data.length() >= 128)
return;
uint8_t size = uint8_t(data.length() + 1);
// Write header
if (!write(&size, 1))
return closeAndRestart();
// Write data
if (!write(data.c_str(), size))
return closeAndRestart();
}
Q_SIGNALS:
void stringAvailable(const QString& s);
};
Create Socket
Now let’s implement a custom Socket
. It will be responsible of:
- Sending string to the worker
- Receiving string from the worker
- Create the wanted custom worker by overriding
net::tcp::SocketWorker* createWorker()
.
#include <Net/Tcp/Socket.hpp>
#include "MySocketWorker.hpp"
class MySocket : public net::tcp::Socket
{
Q_OBJECT
public:
MySocket(QObject* parent = nullptr) : net::tcp::Socket(parent) {}
protected:
net::tcp::SocketWorker* createWorker() override
{
auto worker = new MySocketWorker;
// Send string to worker
connect(this, &MySocket::sendString, worker, &MySocketWorker::onSendString);
// Receive string from worker
connect(worker, &MySocketWorker::stringAvailable, this, &MySocket::stringReceived);
return worker;
}
Q_SIGNALS:
void sendString(const QString& s);
void stringReceived(const QString& s);
};
Start the client
#include "MySocket.hpp"
int main(int argc, char* argv[])
{
QCoreApplication app(argc, argv);
MySocket client;
client.start("127.0.0.1", 9999);
client.sendString("My String");
return QCoreApplication::exec();
}
If anything fail in the connection or during an exchange, a watchdog will try to reconnect to server.
- You can customize the watchdog with
bool setWatchdogPeriod(const quint64& ms)
. - Or call
restart
to force a restart before watchdog end.
Create a Server
Let’s create a custom server than can receive strings for multiple clients. Because packet formatting is the same than on client side, let’s reuse MySocketWorker
. Let’s also reuse MySocket
that can already send and receive strings.
Create a custom Server
Let’s create a MyServer
that create MySocket
for each client that connect. When a string is received by the server, the message is echoed to the client.
- Override
net::tcp::AbstractSocket* newTcpSocket(QObject* parent)
to create a new socket each time a client connects. - Override
bool canAcceptNewClient()
to give condition to accept a client.
#include <Net/Tcp/Server.hpp>
class MyServer : public net::tcp::Server
{
Q_OBJECT
protected:
net::tcp::Socket* newTcpSocket(QObject* parent) override
{
const auto s = new MySocket(parent);
connect(s, &MySocket::stringReceived, [this, s](const QString& string)
{
qInfo("RX \"%s\" from client %s:%d", qPrintable(string), qPrintable(s->peerAddress()), int(s->peerPort()));
Q_EMIT s->sendString(string);
});
return s;
}
bool canAcceptNewClient() const
{
if(!::net::tcp::Server::canAcceptNewClient())
return false;
// Do your business here
return true;
}
};
Then simply start the server by giving the port that it needs to listen to.
int main(int argc, char* argv[])
{
QCoreApplication app(argc, argv);
MyServer server;
server.start(9999);
return QCoreApplication::exec();
}
By default the server will listen on any interface. It’s also possible to listen only a specified interface by calling bool start(const QString& address, const quint16 port)
.
A watchdog will take care a rebinding to the interface/port if something failed.
- You can customize the watchdog with
bool setWatchdogPeriod(const quint64& ms)
.
React to Server
It’s possible to react to multiple signals from the Server
.
isListeningChanged
tell if the Server is correctly listening toport
andaddress
.void acceptError(int error, const QString description)
indicate an error occurred with a client connection.void newClient(const QString& address, const quint16 port)
tell when a new client is connectedvoid clientLost(const QString& address, const quint16 port);
tell when a client got disconnected
Handle Logs
NetTcp library use spdlog
as a logging backend. To listen to logs, you need to install spdlog::sink
. The registerSink
function needs to be called before any logs.
// ...
#include <spdlog/sinks/stdout_color_sinks.h>
// ... Log NetTcp to stdout
const auto sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
net::tcp::Logger::registerSink(sink);
// ...
NetTcp
use 4 main category:
net.tcp.server
: Log fromnet::tcp::Server
objectsnet.tcp.socket
: Log fromnet::tcp::Socket
objectsnet.tcp.socket.worker
: Log fromnet::tcp::SocketWorker
objectsnet.tcp.utils
: Register type logs
Register types
To use the type from qml you need to register them. NetTcp also provide a qml debug namespace NetTcp.Debug 1.0
that contain out of the box qml widget that are ready to use based on Qaterial
library.
// Register types to QML
net::tcp::registerQmlTypes();
// Load NetTcp.qrc
net::tcp::loadQmlResources();
Examples
An example that implement a client/server connection that exchange string is available in examples
folder.
NetTcp_EchoServer
: Start a server that wait for client to connect and reply to the string they send.NetTcp_EchoClient
: Connect to a server and send a string every 1 second.NetTcp_EchoClientServer
: Implement both example into one executable.
> NetTcp_EchoClientServer -h
Options:
-?, -h, --help Displays this help.
-t Make the worker live in a different thread. Default false
-s, --src <port> Port for rx packet. Default "9999".
-i, --ip <ip> Ip address of multicast group. Default "127.0.0.1"
You can also check NetTcp_EchoServer
that implement only the server code that reply echo to client that will connect.
And check NetTcp_EchoClient
, example of a client that will connect to a server and send a string.