NetUdp provide a Udp Socket that can send and receive Datagram.
This library is in maintenance mode. It means that only critical bugs will be fixed. No new features will be added. If you want to take over this library, please do so.
The library has some huge design flaw, flanky API and tests. It was part of my experient when I was young, and I learned a lot from it. I don't want to fix the API, because when using Qt you shouldn't rely on third party library like, but rather simply use QUdpSocket.
Keep you dependencies as small as possible, so maintainability is easier.
The two main classes that work out of the box are Socket and RecycledDatagram. Simply create a server, start it. Then send and receive datagrams. The server can join multicast group to receice multicast packets.
The Socket use a Worker that can run on separate thread or in main thread.
Every datagram allocation is stored in std::shared_ptr<Datagram>. This allow to reuse datagram object structure already allocated later without reallocating anything.
ISocket can be inherited to represent a Socket without any functionality.
Socket and Worker can be inherited to implement custom communication between server and worker. For example sending custom objects that can be serialized/deserialized in worker thread.
Datagram can be inherited if a custom data container if required. For example if data is already serialized in a structure. Putting a reference to that structure inside the Datagram avoid a copy to RecycledDatagram.
A Basic Client/Socket can be found in examples/EchoClientServer.cpp.
This example demonstrate how to create a server that send datagram to address 127.0.0.1 on port 9999.
#include <NetUdp/NetUdp.hpp>
int main(int argc, char* argv[])
{
QCoreApplication app(argc, argv);
netudp::Socket server;
server.start();
const std::string data = "Dummy Data";
server.sendDatagram(data.c_str(), data.length()+1, "127.0.0.1", 9999);
return QCoreApplication::exec();
}The datagram is emitted from a random port chosen by the operating system. It can be explicitly specified by calling
setTxPort(uint16_t).If the socket also receive datagram (ie
inputEnabledis true and callsetRxPort), then the rx port will use. To change this default behavior callsetSeparateRxTxSockets(true).
This example demonstrate how to receive a packet on address 127.0.0.1 on port 9999.
#include <NetUdp/NetUdp.hpp>
int main(int argc, char* argv[])
{
QCoreApplication app(argc, argv);
netudp::Socket client;
client.start("127.0.0.1", 9999);
QObject::connect(&client, &netudp::Socket::sharedDatagramReceived,
[](const netudp::SharedDatagram& d)
{
qInfo("Rx : %s", reinterpret_cast<const char*>(d->buffer()));
});
return QCoreApplication::exec();
}Errors can be observed via socketError(int error, QString description) signals. If the socket fail to bind, or if anything happened, the worker will start a watchdog timer to restart the socket.
The default restart time is set to 5 seconds but can be changed via watchdogPeriod property. The property is expressed in milliseconds.
By default, if internal socket is bounded to an interface with a port, the Worker will receive incoming datagram. To avoid receiving those datagram inside Socket, call setInputEnabled(false).
multicastGroups is the list of multicast addresses that are listened.
To join multicast group call joinMulticastGroup(QString), leaveMulticastGroup(QString), leaveAllMulticastGroups.multicastListeningInterfaces: Set the interfaces on which the socket is listening to multicastGroups. By default all interfaces are listened.
Use joinMulticastInterface, leaveMulticastInterface, leaveAllMulticastInterfaces and isMulticastInterfacePresent.multicastLoopback Control if multicast datagram are looping in the system. On windows it should be set on receiver side. On Unix systems, it should be set on sender side.multicastOutgoingInterfaces: Outgoing interfaces for multicast packet. If not specified, then packet is going to all interfaces by default to provide a plug and play experience.Internally the Socket track multiple information to have an idea of what is going on.
isBounded indicate if the socket is currently binded to a network interface.*xBytesPerSeconds is an average value of all bytes received/sent in the last second. This value is updated every seconds. * can be replaced by t and r*xBytesTotal total received/sent bytes since start. * can be replaced by t and r*xPacketsPerSeconds is an average value of all packets received/sent in the last second. This value is updated every seconds. * can be replaced by t and r*xPacketsTotal total received/sent packets since start. * can be replaced by t and rThose property can be cleared with clearRxCounter/clearTxCounter/clearCounters.
When calling any of the following function, a memcpy will happen to a RecycledDatagram.
virtual bool sendDatagram(const uint8_t* buffer, const size_t length, const QHostAddress& address, const uint16_t port, const uint8_t ttl = 0);
virtual bool sendDatagram(const uint8_t* buffer, const size_t length, const QString& address, const uint16_t port, const uint8_t ttl = 0);
virtual bool sendDatagram(const char* buffer, const size_t length, const QHostAddress& address, const uint16_t port, const uint8_t ttl = 0);
virtual bool sendDatagram(const char* buffer, const size_t length, const QString& address, const uint16_t port, const uint8_t ttl = 0);To avoid useless memory copy it's recommended to retrieve a datagram from Socket cache with makeDatagram(const size_t length). Then use this netudp::SharedDatagram to serialize data. And call :
virtual bool sendDatagram(std::shared_ptr<Datagram> datagram, const QString& address, const uint16_t port, const uint8_t ttl = 0);
virtual bool sendDatagram(std::shared_ptr<Datagram> datagram);If you are not satisfied by Socket behavior, or if you want to mock Socket without any dependency to QtNetwork. It's possible to extend ISocket to use it's basic functionality.
isRunning/isBounded.You need to override:
bool start() : Start the socket. Auto restart to survive from error is expected. Don't forget to call ISocket::start at beginning.bool stop() : Stop the socket. Clear all running task, empty cache, buffers, etc... Don't forget to call ISocket::stop at beginning. To ensure maximum cleaning, always stop every even if stopping any part failed.joinMulticastGroup(const QString& groupAddress): Implementation to join a multicast group. Don't forget to call ISocket::joinMulticastGroup.leaveMulticastGroup(const QString& groupAddress): Implementation to leave a multicast group. Don't forget to call ISocket::leaveMulticastGroup.#include <NetUdp/ISocket.hpp>
class MyAbstractSocket : netudp::ISocket
{
Q_OBJECT
public:
MyAbstractSocket(QObject* parent = nullptr) : netudp::ISocket(parent) {}
public Q_SLOTS:
bool start() override
{
if(!netudp::ISocket::start())
return false;
// Do your business ...
return true;
}
bool stop() override
{
auto stopped = netudp::ISocket::stop()
// Do your business ...
return stopped;
}
bool joinMulticastGroup(const QString& groupAddress) override
{
// Join groupAddress ...
return true;
}
bool leaveMulticastGroup(const QString& groupAddress) override
{
// Leave groupAddress ...
return true;
}
}Socket and Worker mainly work in pair, so if overriding one, it make often sense to override the other.
Reasons to override Worker:
Datagram classReasons to override Socket
Worker class.Datagram class.Using a custom Datagram can reduce memory copy depending on your application.
Worker.Socket::sendDatagram(SharedDatagram, ...) with it.Socket to use it when calling with Socket::sendDatagram(const uint8_t*, ...). A memcpy will happen. So don't use a custom Datagram for that purpose.#include <NetUdp/Datagram.hpp>
class MyDatagram : netudp::Datagram
{
uint8_t* myBuffer = nullptr;
size_t myLength = 0;
public:
uint8_t* buffer() { return myBuffer; }
const uint8_t* buffer() const { return myBuffer; }
size_t length() const { return myLength; }
};When inheriting from SocketWorker you can override:
bool isPacketValid(const uint8_t* buffer, const size_t length) const: Called each time a datagram is received. Check if a packet is valid depending on your protocol. Default implementation just return true. You can add a CRC check or something like that. Returning false here will increment the rxInvalidPacketTotal counter in Socket.void onReceivedDatagram(const SharedDatagram& datagram): Called each time a valid datagram arrive. Default implementation emit receivedDatagram signal. Override this function to add a custom messaging system, or a custom deserialization.std::shared_ptr<Datagram> makeDatagram(const size_t length) : Create custom Datagram for rx.Worker, call void onSendDatagram(const SharedDatagram& datagram) to send a datagram to the network.SocketWorker inherit from QObject, so use Q_OBJECT macro to generate custom signals.Example:
#include <NetUdp/Worker.hpp>
class MySocketWorker : netudp::Worker
{
Q_OBJECT
public:
MySocketWorker(QObject* parent = nullptr) : netudp::SocketWorker(parent) {}
public Q_SLOTS:
bool std::unique_ptr<SocketWorker> createWorker() override
{
auto myWorker = std::make_unique<MyWorker>();
// Init your worker with custom stuff ...
// Even keep reference to MyWorker* if you need later access
// It's recommended to communicate via signals to the worker
// Connect here ...
return std::move(myWorker);
}
// This is called before creating a SharedDatagram and calling onDatagramReceived
bool isPacketValid(const uint8_t* buffer, const size_t length) const override
{
// Add your checks, like header, fixed size, crc, etc...
return buffer && length;
}
void onDatagramReceived(const SharedDatagram& datagram) override
{
// Do your business ...
// This super call is optionnal. If not done Socket will never trigger onDatagramReceived
netudp::SocketWorker::onDatagramReceived(datagram);
}
std::shared_ptr<Datagram> makeDatagram(const size_t length) override
{
// Return your custom diagram type used for rx
return std::make_shared<MyDiagram>(length);
}
}Customizing worker mostly make sense when it's running in a separate thread. Otherwise it won't give any performance boost. Don't forget to call
Socket::setUseWorkerThread(true).
When inheriting from Socket you can override:
bool std::unique_ptr<SocketWorker> createWorker() const: Create a custom worker.void onDatagramReceived(const SharedDatagram& datagram) : Handle datagram in there. Default implementation emit datagramReceived signalsstd::shared_ptr<Datagram> makeDatagram(const size_t length) : Create custom Datagram that will be used in Socket::sendDatagram(const uint8_t*, ...).Socket inherit from QObject, so use Q_OBJECT macro to generate custom signals.Example:
#include <NetUdp/Socket.hpp>
class MySocket : netudp::Socket
{
Q_OBJECT
public:
MySocket(QObject* parent = nullptr) : netudp::Socket(parent) {}
public Q_SLOTS:
bool std::unique_ptr<Worker> createWorker() override
{
auto myWorker = std::make_unique<MyWorker>();
// Init your worker with custom stuff ...
// Even keep reference to MyWorker* if you need later access
// It's recommended to communicate via signals to the worker
// Connect here ...
return std::move(myWorker);
}
void onDatagramReceived(const SharedDatagram& datagram) override
{
// Do your business ...
// This super call is optionnal. If not done Socket will never trigger datagramReceived signal
netudp::Socket::onDatagramReceived(datagram);
}
std::shared_ptr<Datagram> makeDatagram(const size_t length) override
{
// Return your custom diagram type used for tx
return std::make_shared<MyDiagram>(length);
}
}This example demonstrate an echo between a server and a client. Socket send a packet to a client, the client reply the same packet. Ctrl+C to quit.
$> NetUdp_EchoClientServer --help
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 "11111".
-d, --dst <port> Port for tx packet. Default "11112".
--src-addr <ip> Ip address for server. Default "127.0.0.1"
--dst-addr <ip> Ip address for client. Default "127.0.0.1"
$> NetUdp_EchoClientServer
> app: Init application
> server: Set Rx Address to 127.0.0.1
> server: Set Rx Port to 11111
> client: Set Rx Address to 127.0.0.1
> client: Set Rx Port to 11112
> app: Start application
> client: Rx : Echo 0
> server: Rx : Echo 0
> client: Rx : Echo 1
> server: Rx : Echo 1
> client: Rx : Echo 2
> server: Rx : Echo 2
> ...This example is also break into 2 examples : NetUdp_EchoClient & NetUdp_EchoServer.
Demonstrate how to join multicast ip group. Send a packet and read it back via loopback.
$> NetUdp_EchoMulticastLoopback --help
Options:
-?, -h, --help Displays this help.
-t Make the worker live in a different thread. Default
false
-p Print available multicast interface name
-s, --src <port> Port for rx packet. Default "11111".
-i, --ip <ip> Ip address of multicast group. Default "239.0.0.1"
--if, --interface <if> Name of the iface to join. Default is os dependent
netudp::registerQmlTypes(); should be called in the main to register qml types.This example show how to send a unicast datagram as a string to 127.0.0.1:9999. Don't forget to start the socket before sending any messages.
import QtQuick 2.0
import QtQuick.Controls 2.0
import NetUdp 1.0 as NetUdp
Button
{
text: "send unicast"
onClicked: () => socket.sendDatagram({
address: "127.0.0.1",
port: 9999,
data: "My Data"
// Equivalent to 'data: [77,121,32,68,97,116,97]'
})
NetUdp.Socket
{
id: socket
Component.onCompleted: () => start()
}
}This example show how to receive the datagram. Don't forget to start listening to an address and a port. The datagram is always received as a string. It can easily be decoded to manipulate a byte array.
import NetUdp 1.0 as NetUdp
NetUdp.Socket
{
onDatagramReceived: function(datagram)
{
console.log(`datagram : ${JSON.stringify(datagram)}`)
console.log(`datagram.data (string) : "${datagram.data}"`)
let byteArray = []
for(let i = 0; i < datagram.data.length; ++i)
byteArray.push(datagram.data.charCodeAt(i))
console.log(`datagram.data (bytes): [${byteArray}]`)
console.log(`datagram.destinationAddress : ${datagram.destinationAddress}`)
console.log(`datagram.destinationPort : ${datagram.destinationPort}`)
console.log(`datagram.senderAddress : ${datagram.senderAddress}`)
console.log(`datagram.senderPort : ${datagram.senderPort}`)
console.log(`datagram.ttl : ${datagram.ttl}`)
}
Component.onCompleted: () => start("127.0.0.1", 9999)
}Send multicast datagram work almost the same as unicast. Only difference is that you control on which interface the data is going.
import NetUdp 1.0 as NetUdp
NetUdp.Socket
{
id: socket
// A Packet will be send to each interface
// The socket monitor for interface connection/disconnection
multicastOutgoingInterfaces: [ "lo", "eth0" ]
// Required in unix world if you want loopback on the same system
multicastLoopback: true
Component.onCompleted: () => start()
}Then send data like in unicast:
socket.sendDatagram({
address: "239.1.2.3",
port: 9999,
data: "My Data"
})To receive it, subscribe the to the multicast group and choose on which interfaces.
import NetUdp 1.0 as NetUdp
NetUdp.Socket
{
multicastGroups: [ "239.1.3.4" ]
multicastListeningInterfaces: [ "lo", "eth0" ]
// Required in the windows world if you want loopback on the same system
multicastLoopback: true
onDatagramReceived: (datagram) => console.log(`datagram : ${JSON.stringify(datagram)}`)
// Listen port 9999
Component.onCompleted: () => start(12999934)
}This library also provide a tool object that demonstrate every Qmls functionality. This is intended for quick debug, or test functionalities if UI isn't built yet.
Dependencies graph can be generated with:
mkdir -p build && cd build cmake --graphviz=dependencies.dot .. dot -Tsvg -o ../docs/dependencies.svg dependencies.dot -Gbgcolor=transparent -Nfillcolor=white -Nstyle=filled
pip install cmakelang)NetUdp use auto-formatting for cpp, cmake. The folder scripts contains helper script. It is recommended to setup auto-format within IDE.
cd scripts
./clangformat.sh
./cmakeformat.sh
? NetUdp -> NetUdp
? netudp -> netudp
➖ remove spdlog dependency in flavor of qCDebug/qCWarning
➕ Manage dependencies via CPM
♻️ Worker: interface -> iface to avoid conflict with MSVC # define interface struct https://stackoverflow.com/questions/25234203/what-is-the-interface-keyword-in-msvc
♻️ pimpl WorkerPrivate
♻️ pimpl SocketPrivate
♻️ pimpl RecycledDatagramPrivate
? Make recycler private since all Recycler include were moved inside pimpl
⚡️ NETUDP_ENABLE_UNITY_BUILD
? InterfaceProvider: Use steady_clock instead of system to avoid rollback
? Print build command at the of cmake
Update Readme with dependencies graph
? include missing QElapsedTimer header in Worker
? Use raw pointer for worker & worker thread. ?️ This should fix issue when port was not completely released.
NETUDP_ENABLE_PCH, NETUDP_ENABLE_EXAMPLES, NETUDP_ENABLE_TESTSresize method.multicastOutgoingInterfaces instead of multicastInterfaceName. If multicastOutgoingInterfaces is empty packets are going to be send on every interfaces.multicastListenOnAllInterfaces and make it the default when multicastListeningInterfaces is empty.