Everything You Need To Know About Socket.IO

This article explores Socket.IO, its main use cases and how to get started. We also help identify ideal use cases for Socket.IO, including signs your app has scaled beyond Socket.IO’s scope for support. This article examines where Socket.IO fits into the realtime landscape today, looking into competing technologies/packages, and what the future looks like for the library.

What is Socket.IO?

Socket.IO was created in 2010. It was developed to use open connections to facilitate realtime communication, still a relatively new phenomenon at the time.

Socket.IO allows bi-directional communication between client and server. Bi-directional communications are enabled when a client has Socket.IO in the browser, and a server has also integrated the Socket.IO package. While data can be sent in a number of forms, JSON is the simplest.

To establish the connection, and to exchange data between client and server, Socket.IO uses Engine.IO. This is a lower-level implementation used under the hood. Engine.IO is used for the server implementation and Engine.IO-client is used for the client.


client/server chat message flow

How Socket.IO works

Socket.IO brings to mind WebSockets. WebSockets are also a browser implementation allowing bi-directional communication, however, Socket.IO does not use this as standard. First, Socket.IO creates a long-polling connection using xhr-polling. Then, once this is established, it upgrades to the best connection method available. In most cases, this will result in a WebSocket connection. See how WebSockets fare against long-polling (and why WebSockets are nearly always the better choice), here on the Ably blog. A full overview of WebSockets, their history, how they work and use case, is available to read here.

Socket.IO – In action

A popular way to demonstrate the two-way communication Socket.IO provides is a basic chat app (we talk about some other use cases below). With sockets, when the server receives a new message it will send it to the client and notify them, bypassing the need to send requests between client and server. A simple chat application shows how this works.


Chat example for Socket.io structure

Example – Socket.IO for chat

Server

You will need to have node.js installed. We will be using express to simplify setup.

Create a new folder with:

mkdir socket.io-example
cd socket.io-example
npm install socket.io express

Setup server and import required packages.

const app = require("express")();
const http = require("http").createServer(app);
const io = require("socket.io")(http);

The server root will send our index.html which we will setup shortly.

app.get("/", (req, res) => res.sendFile(__dirname + "/index.html"));

Here is where we setup Socket.IO. It is listening for a ‘connection’ event and will run the provided function anytime this happens.

io.on("connection", function(socket) {
 console.log(“socket connected”);
});

This will setup the server to listen on port 3000.

http.listen(3000, () => console.log("listening on http://localhost:3000"));

Run the application with node index.js and open the page in your browser.

Client

Include the following scripts on your page, before the closing </body> tag. You now have a socket connection setup.

<script src="/socket.io/socket.io.js"></script>
<script>
 const socket = io();
</script>

This is the minimum setup to get the Socket.IO connection working. Let’s go a bit further to get messages sent back and forth.

Server

Inside the function we are using io.emit() to send a message to all the connected clients. This code will notify when a user connects to the server.

io.on("connection", function(socket) {
 io.emit(“user connected”);
});

If you want to broadcast to everyone except the person who connected you can use
socket.broadcast.emit().

We will also add a listener for any new messages received from a client and send a message to all users in response.

io.on("connection", function(socket) {
 io.emit(“user connected”);
 socket.on(“message", function(msg) {
   io.emit("message", msg);
 });
});

How to add these events into the client is shown below.

Client

Here is an index.html file which includes our previous scripts, a simple form with input for new messages and a container for displaying messages.

<!DOCTYPE html>
<html lang="en">
 <head>
   <meta charset="UTF-8" />
   <meta name="viewport" content="width=device-width, initial-scale=1.0" />
   <meta http-equiv="X-UA-Compatible" content="ie=edge" />
   <title>Socket.io Example</title>
 </head>

 <body>
   <h1>Our Socket.io Chat Application</h1>
   <div>
     <h2>Messages</h2>
     <ul></ul>
   </div>
   <form action="">
     <input type="text" />
     <button>Send</button>
   </form>
   <script src="/socket.io/socket.io.js"></script>
   <script>
     const socket = io();
   </script>
 </body>
</html>

Now we will add some additional logic to our <script>.

<script>

 // select relevant elements
 const form = document.querySelector("form");
 const input = document.querySelector("input");
 messageList = document.querySelector("ul");

 // establish socket.io connection
 const socket = io();

 // handle sending message to server & input reset
 function sendMessage(e) {
   // prevent form submission refreshing page
   e.preventDefault();
   // send input value to server as type 'message'
   socket.emit("message", input.value);
   // reset input value
   input.value = "";
 }

 // add listener to form submission
 form.addEventListener("submit", sendMessage);

 // add message to our page
 function addMessageToHTML(message) {
   // create a new li element
   const li = document.createElement("li");
   // add message to the elements text
   li.innerText = message;
   // add to list of messages
   messageList.append(li);
 }
  // watch for socket to emit a 'message'
 socket.on("message", addMessageToHTML);

 // display message when a user connects
 function alertUserConnected() {
   addMessageToHTML("User connected");
 }
  // watch for socket to emit a 'user connected' event
 socket.on("user connected", alertUserConnected);

</script>

The key points here are the socket.on(event, callback) functions. When our server emits events which match the first ‘event’ argument the callback will be run. Inside these callbacks we can take the actions we want on the client-side. In this case, displaying the message on the screen.

Maintaining & Operating Socket.IO

As explained above, getting started with Socket.IO is relatively simple – all you need is a Node.js server to run it on. If you want to get started with a realtime app for a limited number of users, Socket.IO is a good option. Problems come when working at scale. Say, for example, you want to build a CRM-like app that enables communications between businesses. Socket.IO is built on asynchronous networking libraries and will cause load on your server. Maintaining connections to users as well as sending and receiving messages adds strain, and if clients start sending significant amounts of data via Socket.IO, it streams data in chunks, freeing up resources when the data chunk is transmitted. So when your application attracts more users and your server reaches its maximum load you will need to split connections over multiple servers, or risk losing important information.

Unfortunately this is not as simple as adding another server. Sockets are an open connection between a server and client. The server only knows about the clients who have connected directly with it and not those connected to other servers. Going back to the conversation function, imagine you want to broadcast a message to all users that someone joined the chat. If they are connected to a different server they wouldn’t receive this message.

To solve this problem you need to have a pub/sub store (e.g. Redis). This store will solve the aforementioned problem by notifying all the servers that they need to send the message when someone joins the chat. Unfortunately, this means an additional database to maintain which will most likely require its own server.

Socket.IO have created an adapter socket.io-adapter which works with the pub/sub store and servers to share information. You can write your own implementation of this adapter or you can use the one they have provided for Redis, with which, luckily, Socket.IO is easy to integrate.

Other reliability enhancers for Socket.IO might include CoreOS to break down architecture into units that can be distributed across available hardware, introducing new instances as the load increases.

Another issue with scaling Socket.IO is that whilst WebSockets hold their connection open, if the connection falls back to polling then there are multiple requests during the connection lifetime. When one of these requests goes to a different server you will receive an error `Error during WebSocket handshake: Unexpected response code: 400`.

The two main ways to solve this are by routing clients based on their originating address, or a cookie. Socket.IO have great documentation on how to solve this for different environments.

While Socket.IO does tend to have good documentation for ways round its limitations, these generally count as ‘remedies’ rather than solutions. If you intend to scale further, these suggested ways round end up adding complexity and extra margin for error to your stack.

When does Socket.IO reach its limits?

As with all tech, choosing the right one means being clear on your ambitions for your product. Socket.IO does make many things easier in comparison to setting up sockets yourself, but there are limitations and drawbacks in addition to the scaling issue mentioned above.

The first is that the initial connection is longer compared to WebSockets. This is due to it first establishing a connection using long polling and xhr-polling, and then upgrading to WebSockets if available.

If you don’t need to support older browsers and aren’t worried about client environments which don’t support WebSockets you may not want the added overhead of Socket.IO. You can minimise this impact by specifying to only connect with WebSockets. This will change the initial connection to WebSocket, but remove any fallback.

Client

Const socket = io({transports: [“websocket”], upgrade: false});

Server

io.set("transports", ["websocket"]);

In this scenario, the client will still need to download the 61.2 KB socket.io JavaScript file. This file is 61.2 KB. More information on this process is here.

For streaming that’s data heavy by definition, for example video streaming, sockets are not the answer. If you want to support data exchange on this level a better solution is webRTC or a data-streaming as a service provider, Ably being one of several.

Socket.IO – the future?

Socket.IO doesn’t appear to be actively maintained. The last commit was approximately 3 months ago with most of the codebase free of new commits for much longer. Also, there are currently 384 open issues. For those starting a new project with sockets it is concerning whether Socket.IO will continue to be supported. At the time of writing (July 2019) the situation is unclear beyond the information below. If you have further information do get in touch.

Looking at NPM downloads, Socket.IO use has been increasing but only gradually.


socket.io npm trends

On the other hand, Sockjs and WS have been steadily growing and have outpaced Socket.IO in NPM downloads.


socket.io vs Sockjs vs WS npm trends

This indicates that although use of sockets has increased, developers have chosen alternatives to Socket.IO. Some have chosen packages such as WS or SockJS. Others have opted for a hosted solutions where the complexity of real-time messages is handled for you, and many of whom operate freemium models.

As you can see below, all modern browsers now support WebSockets. This negates some of the need for a package which handles socket connections on the browser and explains the rise in popularity of packages such as WS which handle the server-side socket connection, but relies on the native browser API for client-side connections and communication.


caniuse websockets

Wrap Up

As we have explored, Socket.IO is a great tool for developers wanting to set up bi-directional socket connections between client and server. This makes simple applications such as live chat much simpler to implement. Socket.IO makes many things easier and provides fallbacks for unsupported clients, but has its own trade-offs.

Scaling applications is perhaps the most difficult step in using sockets, and Socket.IO’s implementation for non-WebSocket connections further complicates the process. Socket.IO’s future support is also questionable.

Aside from the question of future support, whether or not to use socket.io really depends on individual use case – for starting out building simple realtime applications, socket.io works well. With WebSocket support widely spread (answering to a huge growth in demand for realtime applications and services since Socket.IO was set up in 2010), there is now more choice to use similar packages closer to the native implementation, so it’s worth comparing Socket.IO to these as well. For more complex apps, or apps you think will scale, be prepared to add other tech to your stack. To help gauge what stage your app is at in terms of future scale, realtime needs, get in touch with Ably’s realtime engineers. We aim to be as impartial as possible.

Further reading

Ably Realtime provides cloud infrastructure and APIs to help developers simplify complex realtime engineering. We make it easy to power and scale realtime features in apps, or distribute data streams to third-party developers as realtime APIs.


Glyn Lewington

Web Developer | Technical Writer | Twitter | Website

Glyn lewington ably