Understanding WebSocket and Socket.io

Understanding WebSocket and Socket.io

Seamless Connections: Simplifying Real-time Apps with WebSocket and Socket.IO

Featured on Hashnode

Imagine you're building a real-time chat application where users need to send and receive messages instantly, without the delay of refreshing the page or constantly checking for new messages. Traditional HTTP requests would fall short here, creating unnecessary lag and overhead by sending repeated requests to the server just to see if there's something new. This is where WebSockets shine.

WebSocket is a protocol that provides bidirectional communication channels over a single, long-lived connection between a client (typically a web browser) and a server. This technology is particularly useful for real-time applications, such as chat apps, gaming, live notifications, and more.

Above is a simple diagram showing the high-level flow of how Websockets work. Now, let’s dive into a breakdown of each step in the diagram.

  1. Handshake:

The WebSocket communication starts with a handshake process that upgrades an HTTP connection to a WebSocket connection. This is done by the client sending an HTTP request with an Upgrade header, asking the server to upgrade the connection to WebSockets.

  1. Connection Establishment:

If the server supports WebSockets, it responds with a 101 Switching Protocols status code, and the WebSocket connection is established.

  1. Data Exchange:

After the connection is established, both the client and server can send and receive data asynchronously. This data is typically transmitted in frames, which can carry text or binary data.

Something important to note -- Either the client or the server can close the WebSocket connection at any time by sending a close frame. After this, the connection is terminated.

Earlier in the article, we emphasized how traditional HTTP requests fall short when real-time data is needed from the server. Developers have tried various techniques to make HTTP requests deliver real-time data, such as short-polling and long-polling. However, these methods have their limitations, and WebSockets generally provide a better solution for real-time communication.

Short-polling works by repeatedly sending requests to the server at regular intervals, asking if there’s any new data available. It’s like checking your mailbox every few minutes, even if you know the mail usually only comes once a day. This method can lead to a lot of unnecessary requests, which can strain both the server and the network.

Long-polling takes a slightly different approach: the client sends a request to the server, and the server holds onto that request until there’s new data to send back. It’s like leaving your mailbox open and waiting for the mail carrier to drop something in. While long-polling reduces the number of requests compared to short-polling, it still requires reopening a new connection each time, which can add up in terms of overhead.

Use Cases for WebSockets

  • Real-time Chat Applications:

    • Allows for instant messaging between users.
  • Live Updates:

    • For example, real-time cryptocurrency prices, sports scores, or news feeds.
  • Online Gaming:

    • Enables real-time multiplayer interaction.
  • Collaborative Tools:

    • Applications like Figma and Google Docs, where multiple users are editing a document simultaneously.
  • Live Streaming:

    • Video, audio, or data streams that require low latency.

Spinning up a WebSocket server in Nodejs

  1. Install the ws Library

    First, initialize your project and install the ws library, which is a simple and fast WebSocket library for Node.js.

npm install ws
  1. Create WebSocket Server

    Create a server.js file in the root of your project and paste the following code to set up your WebSocket server. As always, we'll walk you through each line of code, so don't worry if it doesn't all make sense at first glance.

const WebSocket = require("ws");

// Create a WebSocket server on port 8000
const wss = new WebSocket.Server({ port: 8000 });

// Event listener for when a client connects to the server
wss.on("connection", (ws) => {
  console.log("A new client connected!");

  // Send a welcome message
  ws.send(
    JSON.stringify({
      message:
        "Welcome to the WebSocket server! You will receive real-time data updates.",
    })
  );

  // Event listener for when the connection is closed
  ws.on("close", () => {
    console.log("A client disconnected.");
  });

  // Event listener for handling errors
  ws.on("error", (error) => {
    console.error("WebSocket error:", error);
  });
});

// Function to send real-time data updates
function sendRealTimeData() {
  console.log("runs every second");
  const data = {
    timestamp: new Date().toISOString(),
    value: Math.random(), // Simulate a real-time data value
  };

  // Broadcast the data to all connected clients
  wss.clients.forEach((client) => {
    if (client.readyState === WebSocket.OPEN) {
      client.send(JSON.stringify(data));
    }
  });
}

// Send real-time data every second
setInterval(sendRealTimeData, 1000);

console.log("WebSocket server is running on ws://localhost:8000");

Explanation of Code

  • Import WebSocket library
const WebSocket = require('ws');

First, we import the required library needed to create our WebSocket server

  • Initialize a new WebSocket server
const wss = new WebSocket.Server({ port: 8000 });

Next, we create a WebSocket server using the imported library and assign it to port 8000. This means the WebSocket server will be accessible on port 8000.

  • Handle New Connections
wss.on("connection", (ws) => {
  console.log("A new client connected!");

  // Send a welcome message
  ws.send(
    JSON.stringify({
      message:
        "Welcome to the WebSocket server! You will receive real-time data updates.",
    })
  );

  // Event listener for when the connection is closed
  ws.on("close", () => {
    console.log("A client disconnected.");
  });

  // Event listener for handling errors
  ws.on("error", (error) => {
    console.error("WebSocket error:", error);
  });
});

Once the WebSocket server is set up, we can handle incoming connections using the on("connection", ...) event listener. When a new client connects, the server logs a message and sends a welcome message to the client.

We also set up event listeners for when a client disconnects and if any errors occur during the connection. These are handled by the on("close", ...) and on("error", ...) event listeners, respectively.

  • Sending Real-time Data
// Function to send real-time data updates
function sendRealTimeData() {
  const data = {
    timestamp: new Date().toISOString(),
    value: Math.random(), // Simulate a real-time data value
  };

  // Broadcast the data to all connected clients
  wss.clients.forEach((client) => {
    if (client.readyState === WebSocket.OPEN) {
      client.send(JSON.stringify(data));
    }
  });
}

// Send real-time data every second
setInterval(sendRealTimeData, 1000);

After establishing the connection and handling basic events, the next step is to broadcast real-time data to all connected clients.

First, we define a sendRealTimeData function that simulates real-time data by generating a random value and timestamp every second. This data is then broadcasted to all clients connected to the WebSocket server.

With this setup, each connected client receives a new piece of real-time data every second, mimicking a real-world scenario where data streams in from a server.

Testing our Real-Time Server App With a Simple Frontend(HTML and Javascript)

To visualize the real-time data sent from our WebSocket server, we'll create a simple HTML page that connects to the server and displays the data.

Here's the basic structure of the HTML file:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>WebSocket Real-Time Data</title>
  </head>
  <body>
    <h1>Real-Time Data from WebSocket Server</h1>
    <div id="data"></div>

    <script>
      // Create a WebSocket connection to the server
      const socket = new WebSocket("ws://localhost:8000");

      // Event listener for when the connection is open
      socket.addEventListener("open", (event) => {
        console.log("Connected to the server");
      });

      // Event listener for receiving messages from the server
      socket.addEventListener("message", (event) => {
        console.log(event.data);
        const data = JSON.parse(event.data);
        document.getElementById(
          "data"
        ).innerText = `Time: ${data.timestamp}, Value: ${data.value}`;
      });
    </script>
  </body>
</html>

Explanation of Code

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WebSocket Real-Time Data</title>
</head>
<body>
    <h1>Real-Time Data from WebSocket Server</h1>
    <div id="data"></div>

This part sets up a basic webpage with a title and a div element where the real-time data will be displayed.

Next, we add a script that creates a WebSocket connection to the server running on ws://localhost:8000:

<script>
    // Create a WebSocket connection to the server
    const socket = new WebSocket('ws://localhost:8080');

    // Event listener for when the connection is open
    socket.addEventListener('open', (event) => {
        console.log('Connected to the server');
    });

This code establishes the WebSocket connection and logs a message if the connection is successfully opened.

We also need to handle the incoming data from the server. This is done by listening for the message event and updating the content of the div with the received data:

        // Event listener for receiving messages from the server
        socket.addEventListener('message', (event) => {
            const data = JSON.parse(event.data);
            document.getElementById('data').innerText = `Time: ${data.timestamp}, Value: ${data.value}`;
        });
    </script>
</body>
</html>

When the server sends data, it's parsed and displayed on the webpage, showing the current timestamp and the generated value.

Now, start your WebSocket server in the background, and then load up the HTML page on your browser:

Websocket server demo

And just like that my simple webpage is constantly displaying new data from the backend without sending multiple http requests. Cool right?

Socket.io

While WebSocket provides a solid foundation for real-time communication, there are scenarios where developers might need more than just basic communication. This is where Socket.IO comes into play. Imagine a scenario where you need to support real-time communication across different platforms and networks, handle automatic reconnection, or manage namespaces and rooms for chat applications. Implementing these features with plain WebSocket can be complex and time-consuming.

For instance, consider the following challenges:

  • Reconnection Handling: What happens when a client loses connection due to network issues? With WebSocket, you'd need to write custom logic to detect and handle reconnections.

  • Fallback Mechanisms: Not all environments support WebSocket. In such cases, you need to implement fallbacks to other protocols, like long polling, which can be complicated.

  • Event-Based Communication: Managing multiple events in a WebSocket application requires extra effort to structure and maintain.

Socket.IO solves these issues out of the box by offering:

  • Automatic Reconnection: If a client gets disconnected, Socket.IO will automatically attempt to reconnect without requiring extra code.

  • Fallback to Long Polling: Socket.IO can automatically downgrade to long polling if WebSocket is not supported, ensuring compatibility across different environments.

  • Event-Based API: Socket.IO allows you to emit and listen for named events, making it easier to structure your code around different actions and responses.

  • Namespaces and Rooms: Socket.IO simplifies the creation of namespaces and rooms, which are useful for implementing features like chat rooms or segmented real-time data streams.

Working with SocketIO

While the differences between using the ws and socket.io packages might not be apparent in simple applications like the one we built earlier.

For now, let’s take the simple application we built earlier and rebuild it using Socket.IO, so you can get familiar with its basic syntax and features. With Socket.IO, you can easily create rooms where connected clients can join and leave, and broadcast messages to all clients in a specific room. This feature allows clients to join specific "rooms" or groups, where they can receive messages targeted only at that room.

We'll be adding this room-based messaging feature to our existing WebSocket application to showcase one of the features that comes out of the box with Socket.IO, but is a bit trickier to implement with ws.

This is what our revised code will look like using socket.io ⬇️

const http = require("http").createServer();
const io = require("socket.io")(http, {
  cors: { origin: "*" },
});

// Event listener for when a client connects to the server
io.on("connection", (socket) => {
  console.log("A new client connected!");

  // Join a room based on client-provided room name
  socket.on("joinRoom", (room) => {
    socket.join(room);
    console.log(`Client joined room: ${room}`);

    // Send a welcome message to the specific room
    socket.to(room).emit("message", {
      message: `Welcome to room ${room}! You will receive real-time data updates.`,
    });
  });

  socket.on("leaveRoom", (room) => {
    socket.leave(room);
    console.log(`Client left room: ${room}`);
  });

  // Event listener for when the connection is closed
  socket.on("disconnect", () => {
    console.log("A client disconnected.");
  });

  // Event listener for handling errors
  socket.on("error", (error) => {
    console.error("Socket.IO error:", error);
  });
});

// Function to send real-time data updates to a specific room
function sendRealTimeDataToRoom(room) {
  if (room == "room1") {
    const data = {
      timestamp: new Date().toISOString(),
    };
    io.to(room).emit("realTimeData", data);
  } else if (room == "room2") {
    const data = {
      value: Math.random(),
    };
    io.to(room).emit("realTimeData", data);
  }
}

//Send real-time data to different rooms every second
setInterval(() => {
  sendRealTimeDataToRoom("room1");
  sendRealTimeDataToRoom("room2");
}, 1000);

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

Key Changes:

  1. Switch to Socket.IO:

    • We set up an HTTP server and attach a Socket.IO instance to it.

    • Socket.IO automatically handles many things that require manual setup with the ws library, like CORS handling.

  2. Room Functionality:

    • Instead of broadcasting data to all clients, we introduced the concept of rooms.

    • Clients can join specific rooms using the joinRoom event. This allows for more control over which clients receive which data.

  3. Targeted Data Broadcasting:

    • The server sends different data to different rooms (room1 and room2) using io.to(room).emit() based on the room name.

    • This demonstrates the power of Socket.IO’s room functionality, where you can send customized data to different groups of clients.

  4. Enhanced Functionality:

    • Clients can leave rooms with the leaveRoom event.

    • This feature allows dynamic control of room membership, making the application more interactive and flexible.

Here's the rewritten frontend code to work with socket.io server:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Socket.IO Real-Time Data with Rooms</title>
    <!-- Include Socket.IO client library -->
    <!-- <script src="/socket.io/socket.io.js"></script> -->
    <script src="https://cdn.socket.io/4.0.0/socket.io.min.js"></script>
  </head>
  <body>
    <h1>Real-Time Data from Socket.IO Server</h1>

    <!-- Room selection and message display -->
    <div>
      <label for="room">Select Room:</label>
      <select id="room">
        <option value="room1">Room 1</option>
        <option value="room2">Room 2</option>
      </select>
      <button id="joinRoom">Join Room</button>
    </div>

    <div id="data"></div>

    <script>
      // Create a Socket.IO connection to the server
      const socket = io("http://localhost:8000");

      // Event listener for when the connection is established
      socket.on("connect", () => {
        console.log("Connected to the server");
      });

      let currentRoom = null;

      // Function to join a room based on selection
      document.getElementById("joinRoom").addEventListener("click", () => {
        const room = document.getElementById("room").value;
        if (currentRoom) {
          socket.emit("leaveRoom", currentRoom);
          console.log(`Left room: ${currentRoom}`);
        }
        socket.emit("joinRoom", room);
        currentRoom = room;
        console.log(`Joined room: ${room}`);
      });

      // Event listener for receiving messages (e.g., welcome message, room messages)
      socket.on("message", (data) => {
        console.log(data.message);
        const messageElement = document.createElement("p");
        messageElement.innerText = data.message;
        document.body.insertBefore(
          messageElement,
          document.getElementById("data")
        );
      });

      // Event listener for receiving real-time data from the server
      socket.on("realTimeData", (data) => {
        console.log(data);
        if (data.timestamp) {
          document.getElementById("data").innerText = `Time: ${data.timestamp}`;
        } else if (data.value) {
          document.getElementById("data").innerText = ` Value: ${data.value}`;
        }
      });
    </script>
  </body>
</html>

Key Changes:

<script src="/socket.io/socket.io.js"></script>

The socket.io client library is required to establish a connection to the socket.io server.

  • Room Selection UI:
<div>
  <label for="room">Select Room:</label>
  <select id="room">
    <option value="room1">Room 1</option>
    <option value="room2">Room 2</option>
  </select>
  <button id="joinRoom">Join Room</button>
</div>

Since we will be displaying the Real-time data based on the rooms joined, this div block adds a dropdown (<select>) for the user to choose a room and a button to join the selected room.

  • Joining and Leaving Rooms:
document.getElementById("joinRoom").addEventListener("click", () => {
  const room = document.getElementById("room").value;
  if (currentRoom) {
    socket.emit("leaveRoom", currentRoom);
  }
  socket.emit("joinRoom", room);
  currentRoom = room;
});
  • Receiving and displaying data(Room based)
socket.on("realTimeData", (data) => {
  if (data.timestamp) {
    document.getElementById("data").innerText = `Time: ${data.timestamp}`;
  } else if (data.value) {
    document.getElementById("data").innerText = ` Value: ${data.value}`;
  }
});

Here we are using Socket.io's built-in event listeners to handle real-time data from the server. You might notice the custom event realTimeData we've set up to push our data. This is a handy feature that comes straight out of the box with Socket.io, making it much simpler compared to the extra steps you'd need to take if you were using the ws library.

Socket.io app demo 👷‍♂️

Socket.io app demo

In the video, I demonstrated the functionality of our Socket.IO application by connecting to the server and interacting with the room-based features. Initially, I selected a room from the dropdown menu and clicked the "Join Room" button. I then observed the real-time data updates, which dynamically changed based on the room I was in. Switching between rooms effectively updated the data displayed, confirming that the room management and data broadcasting were working seamlessly. ✅

In this section of the article we explored the transition from a basic WebSocket implementation to a more feature-rich Socket.IO setup. The initial WebSocket demo established a straightforward connection and broadcast real-time data to all clients, showcasing basic real-time capabilities. In contrast, the Socket.IO demo introduced advanced features such as room-based messaging, enabling clients to join and leave specific rooms, and receive tailored data based on their room. This transition highlights Socket.IO’s enhanced functionality, including automatic reconnections, room management, and event-driven communication, offering a more scalable and flexible approach to real-time data handling compared to the basic WebSocket setup.

Summary

In this article, we explored the fundamentals of WebSocket technology and then explored into Socket.IO, showing its enhanced capabilities. We began by understanding WebSocket's core principles for establishing persistent, bidirectional connections between clients and servers. Building on this foundation, we introduced Socket.IO, highlighting its out-of-the-box improvements, such as automatic reconnections, room-based messaging, and a more robust event-driven architecture. These features showcase how Socket.IO extends the basic functionality of WebSocket, offering a more powerful and flexible solution for real-time communication, making it an ideal choice for developing scalable and interactive applications.

I hope this guide has clarified how to leverage Socket.IO for building dynamic and scalable applications. If you have any questions or comments, feel free to leave a comment below. Don’t forget to like the article if you found it useful and check out the code on GitHub via the link below. Happy coding! 🫡

Github repo - https://github.com/gcadigwe/websocket-tutorial