[Roadmap_Node] 14_Real time application

Table of content

Introduction

Node.js shines in the realm of real-time applications. Its event-driven architecture and non-blocking I/O model make it perfectly suited for building dynamic applications that update users instantly without needing page refreshes. Here’s a breakdown of why Node.js excels in this area, some concepts we already saw in previous posts:

Here are some popular examples of real-time applications built with Node.js:

Web sockets

WebSockets are a powerful tool for enabling real-time, two-way communication between a web server (Node.js in our case) and a client (web browser). Unlike traditional HTTP requests, which are one-off exchanges, WebSockets establish persistent connections that allow for continuous data exchange.

Here’s how WebSockets work in Node.js:

  1. Library Setup: You’ll typically use a third-party library like ws to handle WebSocket functionality. Install it using npm install ws.

  2. Server-Side Implementation:

    • Create a WebSocket server using ws.Server({ port: 8080 }), specifying the port to listen on.
    • Define event listeners for:
      • connection: Triggered when a client connects.
      • message: Invoked when a message is received from the client.
      • close: Called when the connection closes.
  3. Client-Side Implementation:

    • Use the browser’s built-in WebSocket object to connect to the server’s WebSocket endpoint (e.g., ws://localhost:8080).
    • Add event listeners for:
      • open: Fired when the connection is established.
      • message: Handles incoming messages from the server.
      • close: Notifies when the connection is closed.

Here’s an example showcasing a simple echo server in Node.js:

const WebSocket = require("ws");

const wss = new WebSocket.Server({ port: 8080 });

wss.on("connection", (ws) => {
  console.log("Client connected");
  ws.on("message", (message) => {
    console.log(`Received message: ${message}`);
    ws.send(`Server received: ${message}`);
  });

  ws.on("close", () => {
    console.log("Client disconnected");
  });
});

console.log("WebSocket server listening on port 8080");

This code creates a WebSocket server that listens on port 8080. When a client connects, the connection event logs a message. Any message received from the client is echoed back to the client after being logged on the server. The close event is triggered when the connection closes.

Client-side example (HTML):

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>WebSocket Client</title>
  </head>
  <body>
    <h1>WebSocket Test</h1>
    <script>
      const ws = new WebSocket("ws://localhost:8080");

      ws.onopen = () => {
        console.log("Connected to server");
        ws.send("Hello from client!");
      };

      ws.onmessage = (message) => {
        console.log(`Received message from server: ${message.data}`);
      };

      ws.onclose = () => {
        console.log("Disconnected from server");
      };
    </script>
  </body>
</html>

This HTML code includes a basic script that connects to the WebSocket server running on port 8080. It sends a message upon successful connection and logs any messages received from the server.

Server-Sent Events (SSE)

Server-Sent Events (SSE) are another technique for real-time communication in Node.js applications. Unlike WebSockets, SSE provides a simpler, one-way communication channel from the server to the client. Here’s a breakdown of SSE in Node.js:

Concepts:

Node.js Implementation:

  1. Express Setup: We’ll use Express.js for a simple server. Install it with npm install express.
  2. SSE Route: Create a route handler that sets the response headers for SSE:
const express = require("express");

const app = express();

app.get("/sse", (req, res) => {
  res.writeHead(200, {
    "Content-Type": "text/event-stream",
    "Cache-Control": "no-cache",
    Connection: "keep-alive",
  });
  // Send data here ...
});
  1. Sending Events: Use res.write to send data to the client. Each message needs two newlines (\n\n) appended.
  const intervalId = setInterval(() => {
    const data = { message: 'Hello from server!' };
    res.write(`data: ${JSON.stringify(data)}\n\n`);
  }, 1000); // Send data every second

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

Client-Side Implementation:

  1. EventSource: Use new EventSource('/sse') to create a connection to the server’s SSE endpoint.
  2. Event Listeners: Attach event listeners to handle:
    • message: Receives data from the server.
    • error: Handles any connection errors.
const eventSource = new EventSource("/sse");

eventSource.onmessage = (event) => {
  console.log(`Received message: ${event.data}`);
};

eventSource.onerror = (error) => {
  console.error("SSE connection error:", error);
};

Complete Example:

Here’s a Node.js server with an SSE endpoint and a basic HTML client:

server.js:

const express = require("express");

const app = express();

app.get("/sse", (req, res) => {
  res.writeHead(200, {
    "Content-Type": "text/event-stream",
    "Cache-Control": "no-cache",
    Connection: "keep-alive",
  });

  const intervalId = setInterval(() => {
    const data = { message: "Hello from server!" };
    res.write(`data: ${JSON.stringify(data)}\n\n`);
  }, 1000);

  req.on("close", () => {
    clearInterval(intervalId);
  });
});

app.listen(3000, () => console.log("Server listening on port 3000"));

index.html:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>SSE Client</title>
  </head>
  <body>
    <h1>Server-Sent Events</h1>
    <script>
      const eventSource = new EventSource("/sse");

      eventSource.onmessage = (event) => {
        console.log(`Received message: ${event.data}`);
      };

      eventSource.onerror = (error) => {
        console.error("SSE connection error:", error);
      };
    </script>
  </body>
</html>

This example demonstrates a basic SSE implementation. In real-world applications, you’d likely send more meaningful data and handle events more elaborately. Remember, SSE is useful for unidirectional server-to-client updates, while WebSockets provide two-way communication.

WebRTC (Web Real-Time Communication)

WebRTC (Web Real-Time Communication) is a free, open-source project that equips web browsers and mobile applications with real-time communication (RTC) capabilities. It achieves this through APIs (Application Programming Interfaces) that enable developers to build features like video calls, voice chats, and data transfers directly within web applications, without requiring additional plugins or software installations.

Here’s a breakdown of WebRTC’s key features:

Applications of WebRTC:

WebRTC has revolutionized web-based communication, making it possible to develop a variety of real-time applications including:

In conclusion, WebRTC is a powerful technology that has transformed web communication by enabling real-time, browser-based features. Its open-source nature and support across platforms make it a valuable tool for developers to create innovative and engaging web applications.

How does Node JS fits in with WebRTC

Node.js itself doesn’t directly handle video calls through WebRTC. However, it plays a crucial role in facilitating WebRTC communication between browsers by acting as a signaling server. WebRTC relies on a signaling mechanism to establish connections between peers and exchange necessary data for the video call. Here’s a breakdown of how Node.js fits in:

WebRTC Roles:

Node.js with WebRTC:

  1. WebSockets or Similar: The signaling server typically uses WebSockets for real-time communication with the clients (browsers).
  2. Peer Discovery/Management: The server can keep track of connected clients and facilitate communication initiation (e.g., listing available users, creating rooms).
  3. SDP/ICE Exchange: The server acts as a relay for SDP offers/answers and ICE candidates exchanged between peers during WebRTC connection negotiation.

Code Example (Simplified):

Here’s a very basic Node.js server with WebSockets to illustrate the concept (not suitable for a production application):

const WebSocket = require("ws");
const clients = new Map(); // Track connected clients

const wss = new WebSocket.Server({ port: 8080 });

wss.on("connection", (ws) => {
  clients.set(ws, { id: Math.random().toString() }); // Assign a random ID

  ws.onmessage = (message) => {
    const data = JSON.parse(message);

    if (data.type === "join") {
      // Broadcast join message to other clients
      wss.clients.forEach((client) => {
        if (client !== ws) {
          client.send(
            JSON.stringify({ type: "peer-joined", data: clients.get(ws).id })
          );
        }
      });
    } else if (data.type === "ice-candidate" || data.type === "sdp") {
      // Relay ICE candidates and SDP to the other peer (identified by data.target)
      wss.clients.forEach((client) => {
        if (clients.get(client).id === data.target) {
          client.send(JSON.stringify(data));
        }
      });
    }
  };

  ws.on("close", () => {
    const clientId = clients.get(ws).id;
    clients.delete(ws);
    // Broadcast leave message to other clients
    wss.clients.forEach((client) => {
      client.send(JSON.stringify({ type: "peer-left", data: clientId }));
    });
  });
});

console.log("Signaling server listening on port 8080");

Explanation:

Remember: This is a simplified example to showcase the concept. Building a robust WebRTC video calling application requires additional libraries for WebRTC functionality in the browser and potentially media servers for recording or streaming.

Conclusion

Most of the time we will use Websockets for RTC, its quite simple in concept and can get quite complex depending on the feature requirements.

See you on the next post.

Sincerely,

Eng. Adrian Beria