Websockets with Socket.IO
Source: Dev.to
This post contains a flashing gif.
HTTP requests have taken me pretty far, but I’m starting to run into their limits. How do I tell a client that the server updated at midnight, and it needs to fetch the newest data? How do I notify one user when another user makes a post? In short, how do I get information to the client without it initiating the request?
Websockets
One possible solution is to use WebSockets, which establish a persistent connection between the client and server. This will allow us to send data to the client when we want to, without waiting for the client’s next request. WebSockets have their own protocol (though the connection is initiated with HTTP requests) and are language‑agnostic.
We could, if we wanted, implement a WebSocket client and its corresponding server from scratch or with Deno… or we could use one of the libraries that’s already done the hard work for us. I’ve used Socket.IO in a previous project, so we’ll go with that. I enjoyed working with it before, and it even has the advantage of a fallback in case the WebSocket fails.
Colorsocket
For immediate visual feedback, we’ll make a small demo where any one client can affect the colors displayed on all. Each client on the /color endpoint has a slider to control one primary color, plus a button to invert all the other /color clients.
- The server assigns a color in order to each client when the client connects, so you just have to refresh a few times until you get all three colors.
- Duplicate colors work in sync.
- The
/adminuser can turn primary colors on or off.
Here’s the app in action:
The clients aren’t all constantly making requests to the server. How do they know to update?
Establishing Connections
When each client runs its io() call, it creates a new socket, which opens a connection to the server.
// color.html
const socket = io('/color'); // we’ll come back to the argument
The script then assigns handlers on the new socket for the various events we expect to receive from the server:
// color.html
socket.on('assign-color', (color, colorSettings, activeSettings) => {
document.getElementById('color-name').innerText = color;
controllingColor = color;
currentBackground = colorSettings;
active = activeSettings;
colorSlider.disabled = !active[controllingColor];
document.getElementById('active').innerText = active[controllingColor] ? 'active' : 'inactive';
colorSlider.value = colorSettings[controllingColor];
updateBackground();
});
socket.on('set-color', (color, value) => {
currentBackground[color] = value;
if (controllingColor === color) {
colorSlider.value = value;
}
updateBackground();
});
socket.on('invert', () => {
inverted = !inverted;
document.getElementById('inverted').innerText = inverted ? '' : 'not ';
updateBackground();
});
socket.on('toggle-active', (color) => {
active[color] = !active[color];
if (controllingColor === color) {
colorSlider.disabled = !active[color];
}
document.getElementById('active').innerText = active[controllingColor] ? 'active' : 'inactive';
updateBackground();
});
Meanwhile, the server detects the new connection, assigns the client a color, sends that color and the current state of the application to the client, and sets up its own handlers for events received through the socket:
// index.js
colorNamespace.on('connection', (socket) => {
const color = colors[colorCount % 3]; // pick the next color in the list, then loop
colorCount++;
socket.emit('assign-color', color, colorSettings, activeSettings); // synchronize the client with the application state
socket.data.color = color; // you can save information to a socket’s data key
socket.on('set-color', (color, value) => {
colorSettings[color] = value;
colorNamespace.emit('set-color', color, value);
});
socket.on('invert', () => {
socket.broadcast.emit('invert');
});
});
The /admin page follows a similar setup.
Sending Information to the Client
Let’s follow how user interaction on one page changes all the others.
When a user on the blue page moves the slider, the slider emits a change event, which is caught by the slider’s event listener:
// color.html
colorSlider.addEventListener('change', (event) => {
socket.emit('set-color', controllingColor, event.target.value);
});
That listener emits a new set-color event with the color and new value. The server receives the client’s set-color, then emits its own to transmit that data to all clients. Each client receives the message and updates its blue value accordingly.
Broadcasting to Other Sockets
Clicking the “Invert others” button affects the other /color users, but not the user who actually clicked the button. The key here is the broadcast flag:
socket.on('invert', () => {
socket.broadcast.emit('invert'); // sends to everyone except the sender
});
Using socket.broadcast.emit ensures the originating client does not receive its own broadcast, while all other connected sockets do. This pattern can be applied to any event where you want to update other clients without echoing the change back to the initiator.
Broadcasting Events – “to all connected clients except the sender”
Socket.IO documentation – Broadcasting events
When the server receives and retransmits the invert message:
// server.js
socket.on('invert', () => {
socket.broadcast.emit('invert'); // broadcast
});
This flag means that the server will send the event to every socket except the one it’s called on.
In this simple example it’s just a neat trick, but in practice it can be useful to avoid sending a post back to the user who originally created it, since their client already has that information.
Namespaces
You may have noticed that the admin tab isn’t changing color with the other three pages. For simplicity, I didn’t set up any handlers for the admin page. Even if I had, they wouldn’t do anything because the admin socket isn’t receiving those events at all. This is because the admin tab is in a different namespace.
// color.html
const socket = io('/color');
// admin.html
const socket = io('/admin');
// index.js (server)
const colorNamespace = io.of('/color');
const adminNamespace = io.of('/admin');
…
Only sockets in the /color namespace receive this:
colorNamespace.emit('set-color', color, value);
(For clarity, I gave my two namespaces the same names as the two endpoints the pages are located at, but I didn’t have to. The namespaces could have had arbitrary names with no change in functionality, as long as the client matches the server.)
Namespaces provide a convenient way to target a subset of sockets, but they can communicate with each other:
// admin.html
const toggleFunction = (color) => {
socket.emit('toggle-active', color);
};
// index.js (server)
// Clicking the buttons on the admin page triggers changes on the color pages
adminNamespace.on('connection', (socket) => {
socket.on('toggle-active', (color) => {
activeSettings[color] = !activeSettings[color];
colorNamespace.emit('toggle-active', color);
});
});
// color.html (client)
socket.on('toggle-active', (color) => {
active[color] = !active[color];
if (controllingColor === color) {
colorSlider.disabled = !active[color];
}
document.getElementById('active').innerText = active[controllingColor] ? 'active' : 'inactive';
updateBackground();
});
In all of the examples, events were caused by some interaction on one of the clients. An event was emitted to the server, and a second message was emitted by the server to the appropriate clients. This is only a small sample of the possibilities. For example, a server could use WebSockets to update all clients on a regular cycle, or fetch information from an API and push it to clients. This demo is just a showcase of what I’ve been learning and hope to keep applying in my projects moving forward.
References and Further Reading
- Socket.IO – especially the tutorial, which got me up and running very quickly.
- WebSockets on MDN – API reference and glossary.
- Articles on writing your own client and server implementations.
- Deno version of a WebSocket server.
