NetUdp provide a Udp Socket that can send and receive Datagram.
- Support Unicast/Multicast/Broadcast Datagram.
- C++ and Qml API.
- Watcher that restart internal socket if something went wrong with the operating system.
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.
-
ISocketcan be inherited to represent aSocketwithout any functionality. -
SocketandWorkercan be inherited to implement custom communication between server and worker. For example sending custom objects that can be serialized/deserialized in worker thread. -
Datagramcan 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 theDatagramavoid a copy toRecycledDatagram.
- The library depends on C++ 14 STL.
- Recycler library to reuse allocated datagram (The dependency is private).
- Qt dependencies:
- Core, Network, Qml
- CMake v3.14 or greater.
- C++14 compliant compiler or greater.
- Internet connection to download dependencies from Github during configuration.
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).
multicastGroupsis the list of multicast addresses that are listened. To join multicast group calljoinMulticastGroup(QString),leaveMulticastGroup(QString),leaveAllMulticastGroups.multicastListeningInterfaces: Set the interfaces on which the socket is listening tomulticastGroups. By default all interfaces are listened. UsejoinMulticastInterface,leaveMulticastInterface,leaveAllMulticastInterfacesandisMulticastInterfacePresent.multicastLoopbackControl 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.
isBoundedindicate if the socket is currently binded to a network interface.*xBytesPerSecondsis 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*xBytesTotaltotal received/sent bytes since start.* can be replaced by t and r*xPacketsPerSecondsis 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*xPacketsTotaltotal received/sent packets since start.* can be replaced by t and r
Those 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.
- Managing list of multicast ip.
- start/stop behavior that clear counter and
isRunning/isBounded.
You need to override:
bool start(): Start the socket. Auto restart to survive from error is expected. Don't forget to callISocket::startat beginning.bool stop(): Stop the socket. Clear all running task, empty cache, buffers, etc... Don't forget to callISocket::stopat 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 callISocket::joinMulticastGroup.leaveMulticastGroup(const QString& groupAddress): Implementation to leave a multicast group. Don't forget to callISocket::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:
- Implement a serialization/deserialization in a worker thread.
- Check if a datagram is valid with computation of crc, hash, etc... on every received datagram in worker thread.
- Compute crc, hash, ... for every outgoing datagram in worker thread.
- Use a custom
Datagramclass - ...
Reasons to override Socket
- Use a custom
Workerclass. - Use a custom
Datagramclass. - ...
Using a custom Datagram can reduce memory copy depending on your application.
- To use custom datagram for Rx packet, customize
Worker. - To use custom datagram for Tx packet:
- Call
Socket::sendDatagram(SharedDatagram, ...)with it. - Customize
Socketto use it when calling withSocket::sendDatagram(const uint8_t*, ...). Amemcpywill happen. So don't use a customDatagramfor that purpose.
- Call
#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 therxInvalidPacketTotalcounter inSocket.void onReceivedDatagram(const SharedDatagram& datagram): Called each time a valid datagram arrive. Default implementation emitreceivedDatagramsignal. Override this function to add a custom messaging system, or a custom deserialization.std::shared_ptr<Datagram> makeDatagram(const size_t length): Create customDatagramfor rx.- If you implement a custom serialization via a custom message system in
Worker, callvoid onSendDatagram(const SharedDatagram& datagram)to send a datagram to the network. - Don't forget that
SocketWorkerinherit fromQObject, so useQ_OBJECTmacro 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 emitdatagramReceivedsignalsstd::shared_ptr<Datagram> makeDatagram(const size_t length): Create customDatagramthat will be used inSocket::sendDatagram(const uint8_t*, ...).- Don't forget that
Socketinherit fromQObject, so useQ_OBJECTmacro 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.
In order to use this qml object into another qml file, multiple steps are required.
- Call
netudp::registerQmlTypes(...)to registerSocket,SharedDatagram, ... to the qml system - Call
netudp::loadQmlResources()to load everyNetUdpresources into theqrc.
Then simply to something like that:
import NetUdp.Debug 1.0 as NetUdpDebug
import NetUdp 1.0 as NetUdp
Rectangle
{
property NetUdp.Socket socket
NetUdpDebug.Socket
{
object: socket
}
}NetUdp.Debug.Socket is a Qaterial.DebugObject. If you want the raw content to display it somewhere else, then use NetUdp.Debug.SocketContent that is a Column.
This library use CMake for configuration.
git clone https://github.com/OlivierLDff/NetUdp
cd NetUdp
mkdir build && cd build
cmake ..The CMakeLists.txt will download every dependencies for you.
Simply use integrated cmake command:
cmake --build . --config "Release"Adding NetUdp library in your library is really simple if you use CMake 3.14.
In your CMakeLists.txt:
# ...
include(FetchContent)
FetchContent_Declare(
NetUdp
GIT_REPOSITORY "https://github.com/OlivierLDff/NetUdp"
GIT_TAG "master"
)
# ...
FetchContent_MakeAvailable(NetUdp)
# ...
target_link_libraries(MyTarget PRIVATE NetUdp)Then you just need to #include <NetUdp/NetUdp.hpp>. You should also call in your main : netudp::registerQmlTypes();.
All dependencies are managed in cmake/Dependencies.cmake.
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
- clang-format : format cpp
- cmake-format : format cmake (
pip install cmakelang) - js-beautify: format qml
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.
- 🐛 Fix reset Datagram. (ttl is now always reset to 0)
- 🔊 Log more info about fail send datagram
- 🐛 Fix compilation with pch disabled (cmake < 3.17)
- 🔊 Log missing cmake info :
NETUDP_ENABLE_PCH,NETUDP_ENABLE_EXAMPLES,NETUDP_ENABLE_TESTS
- Allow to resize datagram with
resizemethod. - Update NetUdp.Debug to comply with Qaterial v1.4
- 🐛 SocketWorker: Fix potential nullptr access
- 🐛 Fix compilation with -DNETUDP_ENABLE_QML=OFF
- Introduce
multicastOutgoingInterfacesinstead ofmulticastInterfaceName. IfmulticastOutgoingInterfacesis empty packets are going to be send on every interfaces. - Remove
multicastListenOnAllInterfacesand make it the default whenmulticastListeningInterfacesis empty. - QML API/Examples.
- Unit Tests.
- Initial work
