Dynamic Namespaces in Socket.io
I’m teaching socket.io as a convenient WebRTC signaling channel in my WebRTC class this semester. As part of prepping that, I finally had to sit down and figure out dynamic namespaces in socket.io. There is some really tricky business to namespaces, dynamic or otherwise, particularly when it comes to listening for messages and events server-side sent from connected clients on the namespace.
In short, namespaced sockets work differently on the client from how they do on the server.
On the client, we simply create a namespaced socket connection (ns
, here
with a Google Meet–like namespace of /jkl-mnop-qrs
) and both listen and emit
events on it directly:
// Client-side code (site.js)
var ns = io('/jkl-mnop-qrs'); // ordinarily set dynamically in JavaScript, somehow
ns.on('message', function(data) {
console.log('Message received: ' + data);
});
document.querySelector('body').addEventListener('click', function(e) {
console.log('Body was clicked');
// the `send()` method is essentially shorthand for `emit('message', data)`;
// that is, the `send()` method emits the `message` event for us:
ns.send('Someone clicked the body element');
});
In that example, the listener ns.on(...)
handles incoming messages and sends
a pre-determined message to the socket server when someone clicks anywhere in the
<body>
element. Both use the ns
object created from
calling the io()
constructor.
On the server, though, it’s a completely different, more complicated story.
// Server-side code (app.js)
const namespaces = io.of(/^\/[a-z]{3}-[a-z]{4}-[a-z]{3}$/);
namespaces.on('connection', function(socket) {
const namespace = socket.nsp;
// You can emit messages directly on the `namespace` object...
namespace.emit('message', `Successfully connected on namespace: ${namespace.name}`);
// ...BUT--BUT, BUT, BUT--you must listen for messages coming from the clients
// on the socket (`socket`) object, NOT the namespace:
socket.on('message', function(data) {
console.log('A message was received from a client: ', data);
// AND if you want to do a broadcast emit -- which sends the message to
// all the connected clients *except* for the sender -- you MUST use the
// socket object (`socket`), as the `namespace` does not understand the
// `broadcast` method:
socket.broadcast.emit('message', data);
});
});
To summarize the content of the comments: you listen for connections on the namespaces
that match the pattern in socket.of(...)
. You can emit messages on the
namespace
returned by namespaces.on(...)
, but you
cannot listen for incoming messages or any other events on
namespace
. Instead, you listen on the socket object (socket
)
created on the connection event.
Additionally, if you want to broadcast a message (which sends the message to all
connected clients except the sending client), you must use
socket.broadcast.emit
.
So a simplified version of the code above looks like this:
// Server-side code (app.js)
const namespaces = io.of(/^\/[a-z]{3}-[a-z]{4}-[a-z]{3}$/);
namespaces.on('connection', function(socket) {
const namespace = socket.nsp;
socket.emit('message', `Successfully connected on namespace: ${namespace.name}`);
socket.on('message', function(data) {
console.log('A message was received from a client: ', data);
socket.broadcast.emit('message', data);
});
});
Now the namespace
variable is only being used for diagnostic purposes
(Successfully connected on namespace: ${namespace.name}
).
Everything else is listening or emitting on the socket object (socket
)
returned to the initial namespaces.on(...)
callback on each client
connection.
The thing that is tricky to grasp (and that cost me about 3 hours of my life) is that
that socket
object is unique to the connection on each namespace. This is
not properly reflected ANYWHERE in socket.io’s documentation, which makes me crazy.
And yes, I should write something and submit a pull request. I know.