Using UDP multicast With Node.js
With all the modern languages and tools we use, it’s easy to forget how simple yet powerful some low-level protocols are. Recently, I’ve been thinking about building a blockchain system as an exercise to better understand distributed ledger technology. As part of this thought exercise, I’ve been thinking about simplifying and optimizing process of sharing state between many nodes in a network which has led me to playing around with the UDP protocol.
UDP is considered part of the transport layer alongside TCP. Unlike TCP, UDP is a unreliable and connectionless protocol that does not guarantee delivery or order of messages. The protocol is, however, much simpler and faster and supports broadcast and multicast. Because of these properties, UDP should be an excellent protocol to use for any system that must emit its state to multiple subscribers. Examples of these systems are online game engines that must share state between clients, distributed systems where components must be discoverable, and a blockchain network.
I will use Node.js as an example of how to connect to and send messages to a UDP multicast group.
const PORT = 20000;
const MULTICAST_ADDR = "233.255.255.255";
Here, choose the port that we will bind the socket to and the multicast address we will use. The multicast group address must be within the appropriate multicast address space. Much of the space is reserved, but you can still find some unassigned ad-hoc space such as 233.252.18.0-233.255.255.255.
const dgram = require("dgram");
const process = require("process");
const socket = dgram.createSocket({ type: "udp4", reuseAddr: true });
Then, import some modules we will use and create the socket reference.
socket.bind(PORT);
We bind the socket to our port specified above.
The socket is an instance of EventEmitter, so we can listen to a couple events:
socket.on("listening", function() {
socket.addMembership(MULTICAST_ADDR);
setInterval(sendMessage, 2500);
const address = socket.address();
console.log(
`UDP socket listening on ${address.address}:${address.port} pid: ${
process.pid
}`
);
});
function sendMessage() {
const message = Buffer.from(`Message from process ${process.pid}`);
socket.send(message, 0, message.length, PORT, MULTICAST_ADDR, function() {
console.info(`Sending message "${message}"`);
});
}
The ‘listening’ event is the first event that is fired after binding to the port. We catch this event and add ourselves to the multicast group. Then, we call sendMessage
every 2.5 seconds which sends a message to the multicast group.
socket.on("message", function(message, rinfo) {
console.info(`Message from: ${rinfo.address}:${rinfo.port} - ${message}`);
});
Finally, we listen for the message
event and print any messages we see to the console.
Here’s the whole script:
const PORT = 20000;
const MULTICAST_ADDR = "233.255.255.255";
const dgram = require("dgram");
const process = require("process");
const socket = dgram.createSocket({ type: "udp4", reuseAddr: true });
socket.bind(PORT);
socket.on("listening", function() {
socket.addMembership(MULTICAST_ADDR);
setInterval(sendMessage, 2500);
const address = socket.address();
console.log(
`UDP socket listening on ${address.address}:${address.port} pid: ${
process.pid
}`
);
});
function sendMessage() {
const message = Buffer.from(`Message from process ${process.pid}`);
socket.send(message, 0, message.length, PORT, MULTICAST_ADDR, function() {
console.info(`Sending message "${message}"`);
});
}
socket.on("message", function(message, rinfo) {
console.info(`Message from: ${rinfo.address}:${rinfo.port} - ${message}`);
});
I can then run this script in 3 separate terminal sessions to see each process bouncing a message off my home router and communicating with one-another.
$ node udp.js
UDP socket listening on 0.0.0.0:20000 pid: 6637
Message from: 192.168.2.214:20000 - Message from process 6626
Message from: 192.168.2.214:20000 - Message from process 6619
Sending message "Message from process 6637"
Message from: 192.168.2.214:20000 - Message from process 6637
Message from: 192.168.2.214:20000 - Message from process 6626
Message from: 192.168.2.214:20000 - Message from process 6619
Sending message "Message from process 6637"
Message from: 192.168.2.214:20000 - Message from process 6637
Message from: 192.168.2.214:20000 - Message from process 6626
Message from: 192.168.2.214:20000 - Message from process 6619
Sending message "Message from process 6637"
Message from: 192.168.2.214:20000 - Message from process 6637
Message from: 192.168.2.214:20000 - Message from process 6626
Message from: 192.168.2.214:20000 - Message from process 6619
Sending message "Message from process 6637"
Message from: 192.168.2.214:20000 - Message from process 6637
Message from: 192.168.2.214:20000 - Message from process 6626
Message from: 192.168.2.214:20000 - Message from process 6619
Sending message "Message from process 6637"
Message from: 192.168.2.214:20000 - Message from process 6637
Message from: 192.168.2.214:20000 - Message from process 6626
Message from: 192.168.2.214:20000 - Message from process 6619
Now for the bad news: UDP multicast is not supported by the vast majority of ISPs and cloud hosting providers which means it likely won’t work over the internet. There are workarounds, but given the added complexity, it is unlikely that UDP multicast is the best approach for sending information in a one-to-many and zero configuration scenario. UDP unicast is still a viable option, but unless we are sending massive amounts of data continuously, TCP (or HTTP) may be a better option.
The fact that UDP multicast is not viable on the global internet is quite unfortunate because it is clearly a very powerful tool for sharing state between systems. I’m sure there are bandwidth and security concerns to overcome in allowing this traffic but, if there were a financial incentive, I have no doubt that ISPs and hosting providers could figure out a way to make it work.
Discussion