Million Miles Technologies

How to Use WebSockets in Node.js to Create Real-time Apps — SitePoint


This tutorial demonstrates how to use WebSockets in Node.js for two-way, interactive communication between a browser and server. The technique is essential for fast, real-time applications such as dashboards, chat apps, and multiplayer games.

Table of Contents

The Web is based on request-response HTTP messages. Your browser makes a URL request and a server responds with data. That may lead to further browser requests and server responses for images, CSS, JavaScript etc. but the server cannot arbitrarily send data to a browser.

Long polling Ajax techniques can make web apps seemingly update in real time, but the process is too limiting for true real-time applications. Polling every second would be inefficient at certain times and too slow at others.

Following an initial connection from a browser, server-sent events are a standard (streamed) HTTP response which can send messages from the server at any time. However, the channel is one-way and the browser cannot send messages back. For true fast two-way communication, you require WebSockets.

WebSockets Overview

The term WebSocket refers to a TCP communications protocol over ws:// or the secure and encrypted wss://. It’s different from HTTP, although it can run over port 80 or 443 to ensure it works in places which block non-web traffic. Most browsers released since 2012 support the WebSocket protocol.

In a typical real-time web application, you must have at least one web server to serve web content (HTML, CSS, JavaScript, images, and so on) and one WebSocket server to handle two-way communication.

The browser still makes an initial WebSocket request to a server, which opens a communication channel. Either the browser or server can then send a message on that channel, which raises an event on the other device.

Communicating with other connected browsers

After the initial request, the browser can send and receive messages to/from the WebSocket server. The WebSocket server can send and receive messages to/from any of its connected client browsers.

Peer-to-peer communication is not possible. BrowserA cannot directly message BrowserB even when they’re running on the same device and connected to the same WebSocket server! BrowserA can only send a message to the server and hope it’s forwarded to other browsers as necessary.

WebSocket Server Support

Node.js doesn’t yet have native WebSocket support, although there are rumors that it’s coming soon! For this article, I’m using the third-party ws module, but there are dozens of others.

Built-in WebSocket support is available in the Deno and Bun JavaScript runtimes.

WebSocket libraries are available for runtimes including PHP, Python, and Ruby. Third-party SaaS options such as Pusher and PubNub also provide hosted WebSocket services.

WebSockets Demonstration Quickstart

Chat apps are the Hello, World! of WebSocket demonstrations, so I apologize for:

  1. Being unoriginal. That said, chat apps are a great to explain the concepts.

  2. Being unable to provide a fully hosted online solution. I’d rather not have to monitor and moderate a stream of anonymous messages!

Clone or download the node-wschat repository from GitHub:

git clone https://github.com/craigbuckler/node-wschat

Install the Node.js dependencies:

cd node-wschat
npm install

Start the chat application:

Open http://localhost:3000/ in a number of browsers or tabs (you can also define your chat name on the query string — such as http://localhost:3000/?Craig). Type something in one window and press SEND or hit Enter you’ll see it appear in all connected browsers.

Node.js real-time chat

Node.js Code Overview

The Node.js application’s index.js entry file starts two servers:

  1. An Express app running at http://localhost:3000/ with an EJS template to serve a single page with client-side HTML, CSS, and JavaScript. The browser JavaScript uses the WebSocket API to make the initial connection then send and receive messages.

  2. A WebSocket server running at ws://localhost:3001/, which listens for incoming client connections, handles messages, and monitors disconnections. The full code:

    
    import WebSocket, { WebSocketServer } from 'ws';
    const ws = new WebSocketServer({ port: cfg.wsPort });
    
    ws.on('connection', (socket, req) => {
      console.log(`connection from ${ req.socket.remoteAddress }`);
      
      socket.on('message', (msg, binary) => {
        
        ws.clients.forEach(client => {
          client.readyState === WebSocket.OPEN && client.send(msg, { binary });
        });
      });
      
      socket.on('close', () => {
        console.log(`disconnection from ${ req.socket.remoteAddress }`);
      });
    });

The Node.js ws library:

  • Raises a "connection" event when a browser wants to connect. The handler function receives a socket object used to communicate with that individual device. It must be retained throughout the lifetime of the connection.

  • Raises a socket "message" event when a browser sends a message. The handler function broadcasts the message back to every connected browser (including the one that sent it).

  • Raises a socket "close" event when the browser disconnects — typically when the tab is closed or refreshed.

Client-side JavaScript Code Overview

The application’s static/main.js file run’s a wsInit() function and passes the address of the WebSocket server (page’s domain plus a port value defined in the HTML page template):

wsInit(`ws://${ location.hostname }:${ window.cfg.wsPort }`);

function wsInit(wsServer) {
  const ws = new WebSocket(wsServer);
  
  ws.addEventListener('open', () => {
    sendMessage('entered the chat room');
  });

The open event triggers when the browser connects to the WebSocket server. The handler function sends an entered the chat room message by calling sendMessage():


function sendMessage(setMsg) {
  let
    name = dom.name.value.trim(),
    msg =  setMsg || dom.message.value.trim();
  name && msg && ws.send( JSON.stringify({ name, msg }) );
}

The sendMessage() function fetches the user’s name and message from the HTML form, although the message can be overridden by any passed setMsg argument. The values are converted to a JSON object and sent to the WebSocket server using the ws.send() method.

The WebSocket server receives the incoming message which triggers the "message" handler (see above) and broadcasts it back to all browsers. This triggers a "message" event on each client:


ws.addEventListener('message', e => {
  try {
    const
      chat = JSON.parse(e.data),
      name = document.createElement('div'),
      msg  = document.createElement('div');
    name.className = 'name';
    name.textContent = (chat.name || 'unknown');
    dom.chat.appendChild(name);
    msg.className = 'msg';
    msg.textContent = (chat.msg || 'said nothing');
    dom.chat.appendChild(msg).scrollIntoView({ behavior: 'smooth' });
  }
  catch(err) {
    console.log('invalid JSON', err);
  }
});

The handler receives the transmitted JSON data on the event object’s .data property. The function parses it to a JavaScript object and updates the chat window.

Finally, new messages are sent using the sendMessage() function whenever the form’s "submit" handler triggers:


dom.form.addEventListener('submit', e => {
  e.preventDefault();
  sendMessage();
  dom.message.value = '';
  dom.message.focus();
}, false);

Handling errors

An "error" event triggers when WebSocket communication fails. This can handled on the server:


socket.on('error', e => {
  console.log('WebSocket error:', e);
});

and/or the client:


ws.addEventListener('error', e => {
  console.log('WebSocket error:', e);
})

Only the client can re-establish the connection by running the new WebSocket() constructor again.

Closing connections

Either device can close the WebSocket at any time using the connection’s .close() method. You can optionally provide a code integer and reason string (max 123 bytes) arguments, which are transmitted to the other device before it disconnects.

Advanced WebSockets

Managing WebSockets is easy in Node.js: one device sends a message using a .send() method, which triggers a "message" event on the other. How each device creates and responds to those messages can be more challenging. The following sections describe issues you may need to consider.

WebSocket security

The WebSocket protocol doesn’t handle authorization or authentication. You can’t guarantee an incoming communication request originates from a browser or a user logged in to your web application — especially when the web and WebSocket servers could be on a different devices. The initial connection receives an HTTP header containing cookies and the server Origin, but it’s possible to spoof these values.

The following technique ensures you restrict WebSocket communications to authorized users:

  1. Before making the initial WebSocket request, the browser contacts the HTTP web server (perhaps using Ajax).

  2. The server checks the user’s credentials and returns a new authorization ticket. The ticket would typically reference a database record containing the user’s ID, IP address, request time, session expiry time, and any other required data.

  3. The browser passes the ticket to the WebSocket server in the initial handshake.

  4. The WebSocket server verifies the ticket and checks factors such as the IP address, expiry time, etc. before permitting the connection. It executes the WebSocket .close() method when a ticket is invalid.

  5. The WebSocket server may need to re-check the database record every so often to ensure the user session remains valid.

Importantly, always validate incoming data:

  • Like HTTP, the WebSocket server is prone to SQL injection and other attacks.

  • The client should never inject raw values into the DOM or evaluate JavaScript code.

Separate vs multiple WebSocket server instances

Consider an online multiplayer game. The game has many universes playing separate instances of the game: universeA, universeB, and universeC. A player connects to a single universe:

  • universeA: joined by player1, player2, and player3
  • universeB: joined by player99

You could implement the following:

  1. A separate WebSocket server for each universe.

    A player action in universeA would never be seen by those in universeB. However, launching and managing separate server instances could be difficult. Would you stop universeC because it has no players, or continue to manage that resource?

  2. Use a single WebSocket server for all game universes.

    This uses fewer resources and be easier to manage, but the WebSocket server must record which universe each player joins. When player1 performs an action, it must be broadcast to player2 and player3 but not player99.

Multiple WebSocket servers

The example chat application can cope with hundreds of concurrent users, but it’ll crash once popularity and memory usage rises above critical thresholds. You’ll eventually need to scale horizontally by adding further servers.

Each WebSocket server can only manage its own connected clients. A message sent from a browser to serverX couldn’t be broadcast to those connected to serverY. It may become necessary to implement backend publisher–subscriber (pub-sub) messaging systems. For example:

  1. WebSocket serverX wants to send a message to all clients. It publishes the message on the pub–sub system.

  2. All WebSocket servers subscribed to the pub–sub system receive a new message event (including serverX). Each can handle the message and broadcast it to their connected clients.

WebSocket messaging efficiency

WebSocket communication is fast, but the server must manage all connected clients. You must consider the mechanics and efficiency of messages, especially when building multiplayer action games:

  • How do you synchronize a player’s actions across all client devices?

  • If player1 is in a different location from player2, is it necessary to send player2 information about actions they can’t see?

  • How do you cope with network latency — or communication lag? Would someone with a fast machine and connection have an unfair advantage?

Fast games must make compromises. Think of it as playing the game on your local device but some objects are influenced by the activities of others. Rather than sending the exact position of every object at all times, games often send simpler, less frequent messages. For example:

  • objectX has appeared at pointX
  • objectY has a new direction and velocity
  • objectZ has been destroyed

Each client game fills in the gaps. When objectZ explodes, it won’t matter if the explosion looks different on each device.

Conclusion

Node.js makes it easy to handle WebSockets. It doesn’t necessarily make real-time applications easier to design or code, but the technology won’t hold you back!

The main downsides:

  • WebSockets require their own separate server instance. Ajax Fetch() requests and server-sent events can be handled by the web server you’re already running.

  • WebSocket servers require their own security and authorization checks.

  • Dropped WebSocket connections must be manually re-established.

But don’t let that put you off!

Frequently Asked Questions (FAQs) about Real-Time Apps with WebSockets and Server-Sent Events

How do WebSockets differ from HTTP in terms of performance and functionality?

WebSockets provide a full-duplex communication channel over a single TCP connection, which means data can be sent and received simultaneously. This is a significant improvement over HTTP, where each request requires a new connection. WebSockets also allow for real-time data transfer, making them ideal for applications that require instant updates, such as chat apps or live sports updates. On the other hand, HTTP is stateless and each request-response pair is independent, which can be more suitable for applications where real-time updates are not necessary.

Can you explain the lifecycle of a WebSocket connection?

The lifecycle of a WebSocket connection begins with a handshake, which upgrades an HTTP connection to a WebSocket connection. Once the connection is established, data can be sent back and forth between the client and the server until either party decides to close the connection. The connection can be closed by either the client or the server sending a close frame, followed by the other party acknowledging the close frame.

How can I implement WebSockets in an Android application?

Implementing WebSockets in an Android application involves creating a WebSocket client that can connect to a WebSocket server. This can be done using libraries such as OkHttp or Scarlet. Once the client is set up, you can open a connection to the server, send and receive messages, and handle different events such as connection opening, message receiving, and connection closing.

What are Server-Sent Events and how do they compare to WebSockets?

Server-Sent Events (SSE) are a standard that allows a server to push updates to a client over HTTP. Unlike WebSockets, SSE are unidirectional, meaning that they only allow for data to be sent from the server to the client. This makes them less suitable for applications that require two-way communication, but they can be a simpler and more efficient solution for applications that only need updates from the server.

What are some common use cases for WebSockets and Server-Sent Events?

WebSockets are commonly used in applications that require real-time, two-way communication, such as chat apps, multiplayer games, and collaborative tools. Server-Sent Events, on the other hand, are often used in applications that need real-time updates from the server, such as live news updates, stock price updates, or progress reports for long-running tasks.

How can I handle WebSocket connections in a Spring Boot application?

Spring Boot provides support for WebSocket communication through the Spring WebSocket module. You can use the @EnableWebSocket annotation to enable WebSocket support, and then define a WebSocketHandler to handle the connection lifecycle and message handling. You can also use the SimpMessagingTemplate for sending messages to connected clients.

What are the security considerations when using WebSockets?

Like any other web technology, WebSockets can be vulnerable to various security threats, such as Cross-Site WebSocket Hijacking (CSWSH) and Denial of Service (DoS) attacks. To mitigate these risks, you should always use secure WebSocket connections (wss://) and validate and sanitize all incoming data. You should also consider using authentication and authorization mechanisms to control access to your WebSocket server.

Can I use WebSockets with a REST API?

Yes, you can use WebSockets in conjunction with a REST API. While REST APIs are great for stateless request-response communication, WebSockets can be used for real-time, two-way communication. This can be particularly useful in applications that require instant updates, such as chat apps or live sports updates.

How can I test a WebSocket server?

There are several tools available for testing WebSocket servers, such as WebSocket.org’s Echo Test, or Postman. These tools allow you to open a WebSocket connection to a server, send messages, and receive responses. You can also write automated tests for your WebSocket server using libraries such as Jest or Mocha.

What are the limitations of WebSockets and Server-Sent Events?

While WebSockets and Server-Sent Events provide powerful capabilities for real-time communication, they also have their limitations. For example, not all browsers and networks support these technologies, and they can consume a significant amount of resources if not managed properly. Additionally, they can be more complex to implement and manage compared to traditional HTTP communication.

Related blogs