Writing a UPnP Control Point in JavaScript, Part One

Published on

While writing one of my Node.js packages, I had my first opportunity to work with UPnP. Networking and communication using lower-level network protocols isn’t something that I usually do as a web developer. In part one, we’ll learn how to communicate over UDP with Node.js, solving the first piece in implementing the Discovery phase of UPnP.

If you’re new to this series, or you’re looking for a specific part, have a look at the link(s) below:

  1. Part One, Communicating over UDP
  2. Part Two, Implementing SSDP over UDP

Read the Spec

If you’re not familiar with UPnP (Universal Plug and Play) and how it all works, you can read the UPnP Device Architecture specification. Yes, it’s a lot of reading, but it explains everything you will need to know to get started with UPnP.

The goal of my package, node-sonos-client, is to provide an easy method of controlling Sonos wireless speakers on a network. My first problem to solve was automatic discovery of the speakers. After doing a little digging on the “cyberspace”, it turns out that Sonos speakers operate on UPnP. Bingo!

UPnP, or Universal Plug and Play, is an open standard that allows networked devices to discover each others’ presence and communicate with, or control, one another. The discovery is the bit that I needed, and in order to do it I had to venture into an area that I know next-to-nothing about - networking protocols. UDP, specifically.

What the Heck is UDP?

UDP, or User Datagram Protocol, is a layer on top of the Internet Protocol (ya know, IP?) like TCP, or Transmission Control Protocol. They differ in very big ways, and I won’t go into detail here. To put it simply (I’m terrible at analogies, so I’ll just use one that was given in a Stack Overflow answer):

Think of TCP as a dedicated scheduled UPS/FedEx pickup/dropoff of packages between two locations, while UDP is the equivalent of throwing a postcard in a mailbox.

UPS/FedEx will do their damndest to make sure that the package you mail off gets there, and get it there on time. With the post card, you’re lucky if it arrives at all, and it may arrive out of order or late (how many times have you gotten a postcard from someone AFTER they’re gotten home from the vacation?)

TCP is as close to a guaranteed delivery protocol as you can get, while UDP is just “best effort”.

Marc B, Difference between TCP and UDP? on StackOverflow

UDP is a more efficient protocol, because it’s less reliable and requires less overhead, so it’s commonly used for real-time tasks, such as streaming audio and video. Now that we know what UDP is all about, how do we communicate using UDP with Node.js?

Using UDP

Node.js has a built-in module, dgram (datagram), for communicating over UDP. All communication is sent and received as a Buffer.

To start sending or receiving messages, you first have to create a socket. This can either be an IPv4 socket (udp4), or an IPv6 socket (udp6). Once a socket has been created, you can start sending messages over UDP.

import dgram from 'dgram';

const socket = dgram.createSocket('udp4');
const message = new Buffer('Hello World!');

function send() {
    socket.send(message, 0, message.length, 59999, 'localhost');
}

setInterval(send, 5000);
send();

The code above shows how to send messages over UDP. When sending a message, you must provide the message (as a Buffer), the starting offset in the buffer where the message begins, the message length, the destination port, and the destination IP address. You can also optionally provide a callback that gets called once the message has been sent.

When using send, if a port hasn’t been bound already, the OS will choose a random port to be bound.

import dgram from 'dgram';

const socket = dgram.createSocket('udp4');

socket.on('message', (message, rinfo) => {
    console.log(`${rinfo.address}: ${message.toString()}`);
});
socket.bind(59999);

To listen to messages, you need to explicitly bind a port to listen to. You can provide a specific port, or use 0, which will tell the OS to bind a random port. You can also optionally provide an interface to bind to, otherwise the OS will automatically bind to “all interfaces” (0.0.0.0 for udp4, and ::0 for udp6).

Sockets emit the following events:

  • listening - Emitted right when a socket is bound to a port. Calling bind on a socket will emit this event, for instance.
  • message - Emitted when a new message is available on the socket. This event contains the message received (as a Buffer), as well as an rinfo object, which contains the address and port the message was sent from.
  • close - Emitted when the close method is called.
  • error - Emitted when some form of error occurs with the socket.

Try playing around with the code in sender.js and listener.js and run them concurrently in babel-node.

What’s Next

Now that we know how to communicate over UDP, we need to broadcast and receive SSDP messages over the network so we can discover devices and keep track of their status. We’ll discuss how to do this in the second part of this series, so keep an eye out for it.


Verify the signed markdown version of this article:

curl https://keybase.io/sethlopez/key.asc | gpg --import && \
curl https://sethlopez.me/article/writing-a-upnp-control-point-in-javascript-part-one/ | gpg