Communicating with External Systems via TCP/UDP¶
How to use raw TCP and UDP sockets to talk to non-NUClear systems.
When to Use This vs Network\¶
| Feature | Network<T> |
TCP / UDP |
|---|---|---|
| Peer type | Other NUClear nodes | Any external system |
| Serialization | Automatic | Manual (raw bytes) |
| Discovery | Built-in multicast | You manage connections |
| Use case | Inter-node messaging | Protocols, hardware, third-party services |
TCP¶
Accepting Connections¶
Use on<TCP>(port) to listen for incoming TCP connections.
When a client connects, you receive a TCP::Connection object with the file descriptor for the stream:
#include "nuclear"
class TCPServer : public NUClear::Reactor {
public:
TCPServer(std::unique_ptr<NUClear::Environment> environment)
: Reactor(std::move(environment)) {
// Listen on port 9000
on<TCP>(9000).then([this](const TCP::Connection& connection) {
log("Client connected from port", ntohs(connection.remote.ipv4.sin_port));
// Set up ongoing IO on the connection's file descriptor
on<IO>(connection.fd, IO::READ | IO::CLOSE).then([this](IO::Event event) {
if (event.events & IO::READ) {
char buffer[1024];
ssize_t bytes = ::recv(event.fd, buffer, sizeof(buffer), 0);
if (bytes > 0) {
std::string msg(buffer, bytes);
log("Received:", msg);
// Echo back
::send(event.fd, buffer, bytes, 0);
}
}
if (event.events & IO::CLOSE) {
log("Client disconnected");
::close(event.fd);
}
});
});
}
};
Key Points¶
on<TCP>(port)triggers once per new connection- The
connection.fdis a raw file descriptor — useIOto monitor reads/writes - You are responsible for reading, writing, and closing the socket
- Omit the port argument to bind to an automatically assigned port (returned from
bind)
Binding to a Specific Interface¶
// Listen on a specific address — returns (handle, port, fd)
auto [handle, port, fd] = on<TCP>(9000, "192.168.1.100").then(/* ... */);
UDP¶
Receiving Unicast Packets¶
class UDPReceiver : public NUClear::Reactor {
public:
UDPReceiver(std::unique_ptr<NUClear::Environment> environment)
: Reactor(std::move(environment)) {
on<UDP>(5000).then([this](const UDP::Packet& packet) {
if (packet) {
log("Received", packet.payload.size(), "bytes from port",
ntohs(packet.remote.ipv4.sin_port));
}
});
}
};
Receiving Broadcast Packets¶
on<UDP::Broadcast>(5000).then([this](const UDP::Packet& packet) {
log("Broadcast received:", packet.payload.size(), "bytes");
});
Receiving Multicast Packets¶
on<UDP::Multicast>("239.0.0.1", 5000).then([this](const UDP::Packet& packet) {
log("Multicast received:", packet.payload.size(), "bytes");
});
Sending UDP Packets¶
Use emit<Scope::UDP> to send serialized data as a UDP datagram:
// Send to a specific address and port
emit<Scope::UDP>(std::make_unique<MyMessage>(/* ... */), "192.168.1.50", 5000);
// Send from a specific local address/port
emit<Scope::UDP>(std::make_unique<MyMessage>(/* ... */), "192.168.1.50", 5000, "192.168.1.1", 6000);
The data type must be serializable (same rules as Network<T>).
Complete Example: UDP Echo Server¶
#include "nuclear"
struct EchoRequest {
std::array<char, 256> message;
uint32_t length;
};
class UDPEcho : public NUClear::Reactor {
public:
UDPEcho(std::unique_ptr<NUClear::Environment> environment)
: Reactor(std::move(environment)) {
on<UDP>(7000).then([this](const UDP::Packet& packet) {
if (packet) {
log("Echo:", packet.payload.size(), "bytes back to sender");
// Use raw socket to reply (UDP::Packet gives us the remote address)
util::FileDescriptor fd = ::socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
::sendto(fd,
packet.payload.data(),
packet.payload.size(),
0,
&packet.remote.sock,
packet.remote.size());
}
});
}
};
UDP Packet Structure¶
The UDP::Packet received in callbacks contains:
| Field | Type | Description |
|---|---|---|
valid |
bool |
Whether the packet contains data |
local |
sock_t |
Local address the packet arrived on |
remote |
sock_t |
Remote address of the sender |
payload |
std::vector<uint8_t> |
Raw bytes of the received datagram |