DEV Community

Cover image for Websockets with Socket.IO
eachampagne
eachampagne

Posted on

Websockets with Socket.IO

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. I did make sure duplicate colors would work in sync, however.) The /admin user can turn primary colors on or off. Here’s the app in action:

Demo of websocket app

The clients aren’t all constantly making requests to the server. How do they know to update?

Establishing Connections

When each client runs its <script>, 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
Enter fullscreen mode Exit fullscreen mode

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();
});
Enter fullscreen mode Exit fullscreen mode

Meanwhile, the server detects the new connection. It assigns the client a color, sends that color and 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, but I didn’t end up using this for anything

  socket.on('set-color', (color, value) => {
    colorSettings[color] = value;
    colorNamespace.emit('set-color', color, value);
  });

  socket.on('invert', () => {
    socket.broadcast.emit('invert');
  });
});

Enter fullscreen mode Exit fullscreen mode

The /admin page follows 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);
});

Enter fullscreen mode Exit fullscreen mode

That event 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

But 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 when the server receives and retransmits the invert message:

// server.js
socket.on('invert', () => {
  socket.broadcast.emit('invert'); // broadcast
});

Enter fullscreen mode Exit fullscreen mode

This flag means that that the server will send the event to every socket except the one it’s called on. Here this is just a neat trick, but in practice, it might be useful to avoid sending a post to the user who originally wrote it, because their client already has that information.

Namespaces

You may have noticed that the admin tab isn’t changing color with the other three. For simplicity, I didn’t set up any handlers for the admin page. But 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
const colorNamespace = io.of('/color');
const adminNamespace = io.of('/admin');



colorNamespace.emit('set-color', color, value); // the admin page doesn’t receive this event
Enter fullscreen mode Exit fullscreen mode

(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 matched the server.)

Namespaces provide a convenient way to target a subset of sockets. However, namespaces can communicate with each other:

// admin.html
const toggleFunction = (color) => {
  socket.emit('toggle-active', color);
};

// =======================

// index.js
// 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
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();
});

Enter fullscreen mode Exit fullscreen mode

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. However, 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 get information from some API and pass it on. This demo is only a small showcase of what I’ve been learning and hope to keep applying in my projects going 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, plus the articles on writing your own clients and servers (Deno version)

Cover Photo by Scott Rodgerson on Unsplash

Top comments (2)

Collapse
 
art_light profile image
Art light

Wow, this is an incredibly clear and practical explanation! I really appreciate how you broke down the client-server flow with Socket.IO—it makes even the trickier concepts like namespaces and broadcasting feel approachable.

Collapse
 
spock123 profile image
Lars Rye Jeppesen

Great article.
A question though: why use Socket.IO when NodeJs now has it natively built in?