Presenting Socket.IO: Building a chat in 70 lines

May 30, 2020 17:00 · 1125 words · 6 minute read

In my latest post, I have presented WebSockets as the modern way to realize “server push”, and generally for anything that involves much communication between client and server.

But: While it is generally very easy to use WebSockets directly (like using the native browser API, or using libraries that implement the reference protocol), there are some annoyances. Here are the main ones for me:

  1. There is no automatic reconnection logic. Imagine a web application that is connected to a server. You are using the web application on your laptop while sitting in a train & using the WiFi onboard. If your laptop temporarily looses the connection, e.g. because you closed the lid or because the WiFi stopped working, the connection won’t be automatically resumed.
  2. You cannot send arbitrary data like JavaScript objects & arrays through the WebSocket connection. It is either text strings, or binary data (see the documentation about WebSocket.send() for the exact support types).

These are no big problems. For (1), you could implement a retry logic: After the connection is lost, the web application could try to reconnect every X seconds - and additionally when the network connection changes (see here for the Network Information API - not supported in all browsers). Or you use a small wrapper library like reconnecting-websocket. For (2), you could implement send and receive wrapper functions that serialize the data before sending, and de-serialize it on receival.

Still, for my past projects, I preferred not to fiddle with these annoyances, but to use a library like Socket.IO. Socket.IO is not a WebSocket implementation. Instead, it is a client-server communication framework that abstracts away they actual transport technology, and offers easy-to-use send and receival methods. Under the hood, it starts with long-polling, and then tries to upgrade the connection to WebSockets. On top of that, it adds features like automatic reconnection logic, and sending arbitrary data structures (as long they are serializable). By using Socket.IO, you won’t have to implement all things on your own and can focus on your actual application logic.

Because Socket.IO is not a pure WebSocket implementation, you need to use a Socket.IO-compatible library in both your server and client. The Socket.IO GitHub repository already ships with a Node.js server and a JavaScript client library, so we can directly use that.

Building a simple chat

Let’s dive into it with a simple chat! You can try out the live chat here: https://chat.sukram21.repl.co The full code is public on Repl.it - you can directly run the code, modify it, and play with it! 😀

Server

Let’s start with the server. What should the server do? Probably something like this:

  1. Whenever a new client connects, it should broadcast a “New client has joined” notice to all clients (the new client included).
  2. Whenever a client sends a message to the server, it should broadcast this message to all clients (the sender inluded).
  3. Whenever a client disconnects, it should broadcast a “Client has left” notice to all remaining clients.

And that’s all! Besides setting up the server, we just have to implement three event listeners and their respective handlers. Around 20 LoC (lines of code) including (!) the white space:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// index.js (the Node.js server)
const express = require('express');
const app = express();
const server = require('http').createServer(app);

const io = require('socket.io')(server);

app.use(express.static('public'));

io.on('connection', (client) => {
  io.emit('notice', { text: "A new client joined the chat"});

  client.on('message', (message) => {
    io.emit('message', message);
  });
  client.on('disconnect', () => {
    io.emit('notice', { text: "A client left the chat"});
  })
});

server.listen(3000, () => {
  console.log('server started');
});

Please note that using the express package is not strictly necessary. Instead, you could simply use the built-in Node.js web server. The reason I am using express here is so my Node.js webserver can serve the static website files as well (app.use(express.static('public')), meaning the client app.

If this was a larger project, I would rather split my project into an api and an app part. And then serve the static app files directly through a much faster web server like nginx or Caddy.

That’s already all for the server, now getting to the client…

Client

The client code is a litte bit more complex than the server code. Not regarding the actual logic, but because we have to deal with HTML elements. My client code assumes that we have an HTML form with an input field and a submit button, and a readonly textarea that will act as a chatlog. Please see the code on Repl.it for the exact HTML & CSS code.

So, what should the client exactly do?

  1. Whenever a user submits the form, the form’s input value should be sent to the server.
  2. Whenever a new chat message from the server is incoming, the message should be appended to the chatlog.
  3. Whenever a new notice from the server is incoming, the notice should be appended to the chatlog.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// public/script.js (web application)
window.onload = () => {
  const socket = io('https://chat.sukram21.repl.co');

  initSender(socket);
  initReceivers(socket);
  initAutofocus();
}

function initSender(socket) {
  const user = `user${Math.round(Math.random() * 100)}`;
  const form = document.getElementById('form');
  const input = document.getElementById('input');

  const onSubmit = (event) => {
    event.preventDefault();
    const text = input.value;

    socket.emit("message", { user, text });
    input.value = ''; // clear the input
  }

  form.onsubmit = onSubmit;
}

function initReceivers(socket) {
  socket.on("message", (message) => {
    const chatlog = document.getElementById('chatlog');
    const { user, text } = message;

    chatlog.textContent += `${user}: ${text}\n`;
    chatlog.scrollTop = chatlog.scrollHeight; // scroll to bottom
  });

  socket.on("notice", (serverEvent) => {
    const chatlog = document.getElementById('chatlog');
    const { text } = serverEvent;

chatlog.textContent += `${text}\n`;
    chatlog.scrollTop = chatlog.scrollHeight; // scroll to bottom
  });
}

function initAutofocus() {
  const input = document.getElementById('input');
  input.focus();
}

This code does what we were planning, and it does a bit more:

  • On load, it makes the browser focus the input element.
  • It generates a random user name that is used for sending the chat messages.
  • After the message was sent, it clears the input.
  • On new messages, it makes the chatlog scroll to the bottom.

Under 50 LoC - together with the server JS code, we are at exactly 70 LoC, including whitespace & comments. Pretty neat!

I hope this blog post gave you a bit of an inspiration what is possible using Socket.IO (of course, building on an exchellent technology like WebSockets). There is much to do, and great applications to be built! 😍