Mastering Real-Time Communication: A Comprehensive WebSocket Tutorial

Sergey Dudik on 2024-02-08

WebSocket Node HTML5

WebSocket is a communication protocol that provides full-duplex communication channels over a single TCP connection. It enables bidirectional communication between a client (such as a web browser) and a server, allowing for real-time data transfer. Unlike traditional HTTP communication, which follows a request-response model, WebSocket allows both the client and server to send messages to each other independently, without the need for repeated requests from the client.

WebSocket is designed to overcome some limitations of HTTP, particularly in scenarios where real-time data updates are required, such as chat applications, online gaming, stock market monitoring, and live sports updates. By establishing a persistent connection between the client and server, WebSocket reduces latency and overhead compared to techniques like long-polling or server-sent events.

First of all, let’s take a look at WebSocket’s competitor techniques.

WebSocket’s competitors

  1. Polling involves the client repeatedly sending requests to the server at regular intervals to check for updates. While simple to implement, polling can be inefficient, especially if updates are infrequent or if the client needs to check for updates frequently.
// Define the URL to poll for updates
const apiUrl = 'https://api.example.com/data';

// Function to fetch data from the API
async function fetchData() {
  try {
    const response = await fetch(apiUrl);
    if (!response.ok) {
      throw new Error('Failed to fetch data');
    }
    const data = await response.json();
    // Process the retrieved data
    console.log('Received data:', data);
  } catch (error) {
    console.error('Error fetching data:', error);
  }
}

// Poll for updates every 5 seconds (5000 milliseconds)
const pollingInterval = 5000;
const pollingTimer = setInterval(fetchData, pollingInterval);

// To stop polling after a certain time (e.g., 30 seconds)
// setTimeout(() => clearInterval(pollingTimer), 30000);

2. In long polling, the client sends a request to the server, and the server holds onto the request until new data is available. Once new data is available, the server responds to the request with the updated information. Long polling can simulate real-time communication by keeping a connection open for an extended period, but it can be less efficient and may introduce latency compared to WebSocket.

// Define the URL for long polling
const longPollUrl = 'https://api.example.com/long-poll';

// Function to initiate long polling
async function longPoll() {
  try {
    const response = await fetch(longPollUrl);
    if (!response.ok) {
      throw new Error('Failed to fetch data');
    }
    const data = await response.json();
    // Process the received data
    console.log('Received data:', data);
    // Initiate next long polling request
    longPoll();
  } catch (error) {
    console.error('Error during long polling:', error);
    // Retry long polling after a delay (e.g., 1 second)
    setTimeout(longPoll, 1000);
  }
}

// Start long polling
longPoll();

3. Server-Sent Events (SSE) is a unidirectional communication protocol that allows the server to push updates to the client over a single HTTP connection. With SSE, the client establishes a connection to the server and receives a continuous stream of updates as text-based events. SSE is well-suited for scenarios where the server needs to push updates to the client, such as news feeds or real-time notifications.

// express server example

// Set up a route for SSE
app.get('/events', (req, res) => {
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');

  // Send a message every 2 seconds
  const intervalId = setInterval(() => {
    res.write(`data: ${JSON.stringify({ message: 'Hello, client!' })}\n\n`);
  }, 2000);

  // Clean up on client disconnect
  req.on('close', () => {
    clearInterval(intervalId);
  });
});

app.listen(PORT, () => {
  console.log(`Server listening on port ${PORT}`);
});
const eventSource = new EventSource('/events');

// Listen for messages from the server
eventSource.addEventListener('message', (event) => {
  const data = JSON.parse(event.data);
  console.log('Received message from server:', data.message);
});

// Handle errors
eventSource.addEventListener('error', (error) => {
  console.error('Error with SSE connection:', error);
});

4. HTTP/2 introduces a feature called server push, which allows the server to proactively send resources to the client before they are requested. While not specifically designed for real-time communication, HTTP/2 server push can be used to improve performance by pushing essential resources to the client, potentially reducing the need for frequent requests.

HTTP/2 Server Push is not a notification mechanism from server to client. Instead, pushed resources are used by the client when it may have otherwise produced a request to get the resource anyway.

// Create an HTTP/2 server
const server = http2.createSecureServer({
  key: fs.readFileSync(path.join(__dirname, 'certs', 'server.key')),
  cert: fs.readFileSync(path.join(__dirname, 'certs', 'server.crt'))
}, app);

// Define route for the main HTML page
app.get('/', (req, res) => {
  res.sendFile(path.join(__dirname, 'public', 'index.html'));
});

// Push the CSS file when the main HTML page is requested
app.get('/', (req, res) => {
  if (req.httpVersion === '2.0') {
    res.stream.pushStream({ ':path': '/styles.css' }, (pushStream) => {
      pushStream.respondWithFile(path.join(__dirname, 'public', 'styles.css'), {
        'content-type': 'text/css'
      });
    });
  }
});

// Start the server
server.listen(PORT, () => {
  console.log(`Server listening on port ${PORT}`);
});
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>HTTP/2 Server Push Example</title>
  <!-- Include styles.css -->
  <link rel="stylesheet" href="/styles.css">
</head>
<body>
  <h1>HTTP/2 Server Push Example</h1>
  <p>Welcome to the HTTP/2 Server Push Example!</p>
</body>
</html>

On the client side (in the HTML file), we include the CSS file (styles.css) using a <link> tag. This file will be pushed by the server when the main HTML page is requested.

WebSocket overview

WebSocket operates by first establishing an initial handshake between the client and server using HTTP. Once the handshake is complete, the connection is upgraded to the WebSocket protocol, and both parties can exchange messages asynchronously. WebSocket messages are typically lightweight and can be sent in either text or binary format.

One of the key advantages of WebSocket is its efficiency in handling real-time communication. By maintaining an open connection, WebSocket eliminates the need for repeated HTTP requests and reduces network overhead. This makes it particularly well-suited for applications that require instant updates and interactive features.

WebSocket is supported by most modern web browsers and is widely used in web development for building real-time web applications. Many popular web frameworks and libraries provide support for WebSocket, making it relatively easy for developers to integrate into their projects.

Overall, WebSocket is a powerful protocol for enabling real-time communication between clients and servers over the web, offering benefits such as reduced latency, improved efficiency, and enhanced interactivity.

WebSocket platforms on the backend

WebSocket has excellent support in all modern programming languages, including Java, Python, Node.js, and C#. Node.js is often considered a preferred option for building WebSocket servers for several reasons:

  1. Non-blocking I/O: Node.js is built on an event-driven, non-blocking I/O model, which makes it well-suited for handling multiple concurrent connections, such as those required for WebSocket communication. This architecture allows Node.js to efficiently handle high levels of concurrency without blocking the execution of other tasks.
  2. Asynchronous Programming: With its support for asynchronous programming using callbacks, promises, and async/await, Node.js simplifies the development of WebSocket servers. Asynchronous programming enables developers to write code that handles WebSocket events, such as incoming messages or new connections, without blocking the event loop.
  3. Performance: While the performance of a WebSocket server depends on various factors, including the underlying hardware and network infrastructure, Node.js is known for its performance and scalability when handling asynchronous I/O operations. With proper optimization and tuning, Node.js can deliver high performance for WebSocket-based applications.

If you choose Node.js, you have two main options for it:

socket.io npm
ws npm

Both libraries are quite good; however, ws is more popular as it’s written specifically for Node.js, whereas socket.io has ports to all modern languages.

It takes just a few lines of code to implement a WebSocket server using ws.

type TpWebSocket = WebSocket & {
  userId: string;
  connectionId: string;
};

wss.on('connection', (ws: TpWebSocket, req) => {
  LOGGER.debug('New client connected');
  LOGGER.debug(req.url);

  ws.connectionId = uuidv4();
  ws.userId = req.url!.split('=')[1]!;

  LOGGER.debug(`connectionId = ${ws.connectionId}, userId = ${ws.userId}`);

  ws.on('message', (message: string) => {
    ws.send(`Server received your message: ${message}`);
  });

  ws.on('close', () => {
    LOGGER.debug('Client disconnected');
  });

  ws.on('error', LOGGER.error);
});

In this article, I don’t want to discuss how we can distinguish between clients or how we can authenticate them. These topics will be covered in future articles. The focus of this article is solely to demonstrate that WebSocket implementation is indeed straightforward.

What is wscat?

wscat is a command-line tool for WebSocket communication testing and debugging. It allows you to interact with WebSocket servers directly from the terminal, making it useful for debugging WebSocket connections, testing server responses, and experimenting with WebSocket APIs.

npm install -g wscat

Then you can connect to your websocket server:

wscat -c 'ws://app.targpatrol.com/ws'  //HTTP
wscat -c 'wss://app.targpatrol.com/ws' //HTTPS

If you are working with a local server and using self-signed certificates, you may need to disable SSL certificate verification.

wscat -c 'wss://app.targpatrol.local/ws' --no-check

WebSocket on the frontend

This example demonstrates a basic implementation of WebSocket communication in a web browser. You’ll need a WebSocket server running on ws://localhost:3000 to handle the WebSocket connections and messages sent from the browser.

  <script>
    const messagesContainer = document.getElementById('messages');
    const messageForm = document.getElementById('messageForm');
    const messageInput = document.getElementById('messageInput');

    // Create WebSocket connection
    const socket = new WebSocket('ws://localhost:3000');

    // Listen for messages from the WebSocket server
    socket.addEventListener('message', function (event) {
      const message = document.createElement('div');
      message.textContent = event.data;
      messagesContainer.appendChild(message);
    });

    // Send message when the form is submitted
    messageForm.addEventListener('submit', function (event) {
      event.preventDefault();
      const message = messageInput.value;
      socket.send(message);
      messageInput.value = '';
    });
  </script>

Using RxJS with WebSockets provides a powerful combination for handling real-time data streams in web applications. RxJS (Reactive Extensions for JavaScript) is a library for reactive programming using Observables, which are a powerful way to manage asynchronous data streams. When combined with WebSockets, RxJS enables developers to handle WebSocket events in a reactive and composable manner, simplifying the management of real-time data.

Here’s an example of how you can use RxJS with WebSockets:

// Create a WebSocketSubject
const socket$ = new WebSocketSubject('ws://localhost:3000');

// Subscribe to WebSocket events
socket$
  .pipe(

    map(event => JSON.parse(event.data)) // Parse JSON data from WebSocket event
  )
  .subscribe(
    data => console.log('Received data:', data), // Handle incoming data
    error => console.error('WebSocket error:', error), // Handle WebSocket errors
    () => console.log('WebSocket closed') // Handle WebSocket closure
  );

// Send data over WebSocket
socket$.next(JSON.stringify({ message: 'Hello WebSocket!' }));

// Close WebSocket connection
socket$.complete();

One of the issues with WebSockets is that the client needs to handle reconnecting to the server if the connection is closed. This can be accomplished using RxJS in just one line:

const events$ = webSocket(url).pipe(retry({ count: 100, delay: 10000 }));

Conclusion

In this comprehensive WebSocket tutorial, we have explored the fundamental concepts and practical implementation of real-time communication using WebSocket technology. From understanding the WebSocket protocol to building a WebSocket server and client, we have covered everything you need to know to harness the power of real-time communication in your web applications.

By mastering WebSocket, you can create highly interactive and responsive web applications that deliver seamless user experiences. Whether it’s building chat applications, live data dashboards, multiplayer games, or collaborative editing tools, WebSocket provides the foundation for real-time communication that can elevate your projects to the next level.

As you continue your journey with WebSocket, remember to leverage the rich ecosystem of libraries, frameworks, and tools available to streamline development and enhance functionality. Stay updated with the latest advancements in WebSocket technology and explore new possibilities for real-time communication in your applications.

With the knowledge and skills gained from this tutorial, you are well-equipped to embark on exciting projects that leverage the power of real-time communication to delight users and transform the web landscape. Embrace the potential of WebSocket and unlock endless possibilities for innovation and creativity in your web development endeavors.