Comment utiliser les WebSockets dans Node.js pour créer des applications en temps réel

Ce tutoriel démontre comment utiliser les WebSockets dans Node.js pour une communication bidirectionnelle et interactive entre un navigateur et un serveur. Cette technique est essentielle pour les applications en temps réel rapides telles que les tableaux de bord, les applications de chat et les jeux multijoueurs.

Table of Contents

Le Web repose sur des messages HTTP de type requête-réponse. Votre navigateur fait une demande d’URL et un serveur répond avec des données. Cela peut entraîner d’autres demandes de navigateur et réponses de serveur pour des images, CSS, JavaScript, etc., mais le serveur ne peut pas envoyer de manière arbitraire des données à un navigateur.

Les techniques de long polling Ajax permettent aux applications web de sembler mettre à jour en temps réel, mais le processus est trop limitant pour les applications véritablement en temps réel. Le polling toutes les secondes serait inefficace à certains moments et trop lent à d’autres.

Après une connexion initiale depuis un navigateur, les événements envoyés par le serveur sont une réponse HTTP standard (transmise en flux) qui peut envoyer des messages du serveur à tout moment. Cependant, le canal est unidirectionnel et le navigateur ne peut pas envoyer de messages en retour. Pour une véritable communication bidirectionnelle rapide, vous avez besoin de WebSockets.

Aperçu des WebSockets

Le terme WebSocket fait référence à un protocole de communication TCP sur ws:// ou le wss:// sécurisé et crypté. Il diffère de HTTP, bien qu’il puisse fonctionner sur le port 80 ou 443 pour s’assurer qu’il fonctionne dans les endroits qui bloquent le trafic non web. La plupart des navigateurs sortis depuis 2012 soutiennent le protocole WebSocket.

Dans une application web en temps réel typique, vous devez avoir au moins un serveur web pour servir le contenu web (HTML, CSS, JavaScript, images, etc.) et un serveur WebSocket pour gérer la communication bidirectionnelle.

Le navigateur effectue toujours une requête WebSocket initiale vers un serveur, qui ouvre un canal de communication. Soit le navigateur, soit le serveur peut alors envoyer un message sur ce canal, ce qui déclenche un événement sur l’autre dispositif.

Communication avec d’autres navigateurs connectés

Après la requête initiale, le navigateur peut envoyer et recevoir des messages vers/depuis le serveur WebSocket. Le serveur WebSocket peut envoyer et recevoir des messages vers/depuis n’importe quel de ses navigateurs clients connectés.

La communication de pair à pair est impossible. BrowserA ne peut pas directement envoyer un message à BrowserB, même s’ils sont en cours d’exécution sur le même dispositif et connectés au même serveur WebSocket ! BrowserA ne peut qu’envoyer un message au serveur et espérer qu’il soit relayé aux autres navigateurs si nécessaire.

Support de serveur WebSocket

Node.js ne dispose pas encore du support natif WebSocket, bien qu’il y ait des rumeurs qu’il soit bientôt disponible ! Pour cet article, j’utilise le module tiers ws, mais il existe des dizaines d’autres.

Le support WebSocket intégré est disponible dans les environnements de runtime JavaScript Deno et Bun.

Des bibliothèques WebSocket sont disponibles pour les environnements de runtime tels que PHP, Python et Ruby. Des options SaaS tierces comme Pusher et PubNub offrent également des services WebSocket hébergés.

Démarrage rapide de la démonstration WebSockets

Les applications de chat sont le Bonjour le monde! des démonstrations WebSocket, donc je m’excuse pour :

  1. D’être peu original. Cela étant, les applications de chat sont un excellent moyen d’expliquer les concepts.

  2. De ne pas pouvoir fournir une solution en ligne entièrement hébergée. Je préfère ne pas avoir à surveiller et modérer un flux de messages anonymes!

Clonez ou téléchargez le référentiel node-wschat depuis GitHub:

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

Installez les dépendances Node.js:

cd node-wschat
npm install

Démarrez l’application de chat :

npm start

Ouvrez http://localhost:3000/ dans plusieurs navigateurs ou onglets (vous pouvez également définir votre nom de chat dans la chaîne de requête — par exemple http://localhost:3000/?Craig). Tapez quelque chose dans une fenêtre et appuyez sur ENVOYER ou appuyez sur Entrée vous verrez apparaître dans tous les navigateurs connectés.

Aperçu du code Node.js

Le fichier d’entrée index.js de l’application Node.js démarre deux serveurs:

  1. Un application Express en cours d’exécution à http://localhost:3000/ avec un modèle EJS pour servir une seule page avec HTML, CSS et JavaScript côté client. Le JavaScript du navigateur utilise l’API WebSocket pour établir la connexion initiale puis envoyer et recevoir des messages.

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

    // Serveur WebSocket
    import WebSocket, { WebSocketServer } from 'ws';
    
    const ws = new WebSocketServer({ port: cfg.wsPort });
    
    // connexion client
    ws.on('connection', (socket, req) => {
    
      console.log(`connection from ${ req.socket.remoteAddress }`);
    
      // message reçu
      socket.on('message', (msg, binary) => {
    
        // diffusion à tous les clients
        ws.clients.forEach(client => {
          client.readyState === WebSocket.OPEN && client.send(msg, { binary });
        });
    
      });
    
      // fermé
      socket.on('close', () => {
        console.log(`disconnection from ${ req.socket.remoteAddress }`);
      });
    
    });

Le bibliothèque Node.js ws:

  • Déclenche un événement "connection" lorsqu’un navigateur souhaite se connecter. La fonction de gestionnaire reçoit un objet socket utilisé pour communiquer avec cet appareil individuel. Il doit être conservé pendant toute la durée de la connexion.

  • Déclenche un événement socket "message" lorsqu’un navigateur envoie un message. La fonction de gestionnaire diffuse le message à chaque navigateur connecté (y compris celui qui l’a envoyé).

  • Déclenche un événement socket "close" lorsque le navigateur se déconnecte — généralement lorsque l’onglet est fermé ou rafraîchi.

Vue d’ensemble du code JavaScript côté client

Le fichier static/main.js de l’application exécute une fonction wsInit() et transmet l’adresse du serveur WebSocket (domaine de la page plus une valeur de port définie dans le modèle de page HTML) :

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

// gérer la communication WebSocket
function wsInit(wsServer) {

  const ws = new WebSocket(wsServer);

  // se connecter au serveur
  ws.addEventListener('open', () => {
    sendMessage('entered the chat room');
  });

L’événement open se déclenche lorsque le navigateur se connecte au serveur WebSocket. La fonction de gestionnaire envoie un message entré dans la salle de chat en appelant sendMessage():

// envoyer un message
function sendMessage(setMsg) {

  let
    name = dom.name.value.trim(),
    msg =  setMsg || dom.message.value.trim();

  name && msg && ws.send( JSON.stringify({ name, msg }) );

}

La fonction sendMessage() récupère le nom de l’utilisateur et le message à partir du formulaire HTML, bien que le message puisse être remplacé par tout argument setMsg passé. Les valeurs sont converties en objet JSON et envoyées au serveur WebSocket en utilisant la méthode ws.send().

Le serveur WebSocket reçoit le message entrant qui déclenche le gestionnaire "message" (voir ci-dessus) et le diffuse à nouveau à tous les navigateurs. Cela déclenche un événement "message" sur chaque client:

// recevoir un message
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);
  }

});

Le gestionnaire reçoit les données JSON transmises sur la propriété .data de l’objet événement. La fonction les convertit en objet JavaScript et met à jour la fenêtre de chat.

Enfin, de nouveaux messages sont envoyés en utilisant la fonction sendMessage() chaque fois que le gestionnaire "submit" du formulaire se déclenche :

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

Gestion des erreurs

Un événement "error" se déclenche lorsque la communication WebSocket échoue. Cela peut être géré côté serveur:

// gérer l'erreur WebSocket côté serveur
socket.on('error', e => {
  console.log('WebSocket error:', e);
});

et/ou côté client:

// gérer l'erreur WebSocket côté client
ws.addEventListener('error', e => {
  console.log('WebSocket error:', e);
})

Seul le client peut rétablir la connexion en exécutant à nouveau le constructeur new WebSocket().

Fermeture des connexions

Chaque appareil peut fermer la connexion WebSocket à tout moment en utilisant la méthode .close() de la connexion. Vous pouvez éventuellement fournir un argument entier code et une chaîne reason (maximum 123 octets), qui sont transmis à l’autre appareil avant qu’il ne se déconnecte.

WebSockets avancés

Gérer les WebSockets est facile en Node.js : un appareil envoie un message en utilisant la méthode .send(), ce qui déclenche un événement "message" sur l’autre. La manière dont chaque appareil crée et répond à ces messages peut être plus difficile. Les sections suivantes décrivent les problèmes que vous devrez peut-être prendre en compte.

Sécurité des WebSockets

Le protocole WebSocket ne gère pas l’autorisation ou l’authentification. Vous ne pouvez pas garantir qu’une demande de communication entrante provient d’un navigateur ou d’un utilisateur connecté à votre application web, surtout lorsque le serveur web et les serveurs WebSocket peuvent être sur des appareils différents. La connexion initiale reçoit un en-tête HTTP contenant des cookies et le serveur Origin, mais il est possible de falsifier ces valeurs.

La technique suivante garantit que vous limitez les communications WebSocket aux utilisateurs autorisés :

  1. Avant de faire la demande initiale WebSocket, le navigateur contacte le serveur HTTP (peut-être en utilisant Ajax).

  2. Le serveur vérifie les informations d’identification de l’utilisateur et renvoie un nouveau billet d’autorisation. Le billet ferait généralement référence à un enregistrement de base de données contenant l’ID de l’utilisateur, l’adresse IP, l’heure de la demande, l’heure d’expiration de la session et toute autre donnée requise.

  3. Le navigateur transmet le billet au serveur WebSocket lors du handshake initial.

  4. Le serveur WebSocket vérifie le billet et vérifie des facteurs tels que l’adresse IP, l’heure d’expiration, etc. avant d’autoriser la connexion. Il exécute la méthode WebSocket .close() lorsqu’un billet est invalide.

  5. Le serveur WebSocket peut avoir besoin de vérifier à nouveau l’enregistrement de la base de données de temps en temps pour s’assurer que la session de l’utilisateur reste valide.

De manière cruciale, toujours valider les données entrantes:

  • Comme HTTP, le serveur WebSocket est susceptible aux attaques par injection SQL et autres.

  • Le client ne devrait jamais injecter des valeurs brutes dans le DOM ou évaluer du code JavaScript.

Séparation vs instances multiples de serveur WebSocket

Prenons l’exemple d’un jeu en ligne multijoueur. Le jeu comporte de nombreux univers jouant des instances distinctes du jeu : universeA, universeB, et universeC. Un joueur se connecte à un seul univers:

  • universeA: rejoint par player1, player2, et player3
  • universeB: rejoint par player99

Vous pourriez mettre en œuvre ce qui suit:

  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. Utiliser un seul serveur WebSocket pour tous les univers du jeu.

    Cela utilise moins de ressources et est plus facile à gérer, mais le serveur WebSocket doit enregistrer quel univers chaque joueur rejoint. Lorsque player1 effectue une action, elle doit être diffusée à player2 et player3 mais pas à player99.

Plusieurs serveurs WebSocket

L’exemple d’application de chat peut gérer des centaines d’utilisateurs simultanés, mais elle plante une fois que la popularité et l’utilisation de la mémoire dépassent des seuils critiques. Vous devrez finalement augmenter la capacité en ajoutant d’autres serveurs.

Chaque serveur WebSocket ne peut gérer que ses propres clients connectés. Un message envoyé d’un navigateur à serverX ne pourrait pas être diffusé à ceux connectés à serverY. Il peut devenir nécessaire d’implémenter des systèmes de messagerie backend pub-sub (éditeur-abonné). Par exemple :

  1. Le serveur WebSocket serverX souhaite envoyer un message à tous les clients. Il publie le message sur le système pub-sub.

  2. Tous les serveurs WebSocket abonnés au système pub-sub reçoivent un événement de nouveau message (y compris serverX). Chacun peut gérer le message et le diffuser à leurs clients connectés.

Efficacité de la messagerie WebSocket

La communication WebSocket est rapide, mais le serveur doit gérer tous les clients connectés. Il faut prendre en compte les mécaniques et l’efficacité des messages, surtout lors de la création de jeux d’action multijoueur :

  • Comment synchroniser les actions d’un joueur sur tous les appareils clients ?

  • Si player1 est à un endroit différent de player2, est-il nécessaire d’envoyer à player2 des informations sur des actions qu’il ne peut pas voir ?

  • Comment gérer la latence réseau — ou le retard de communication ? Est-ce qu’une personne avec une machine rapide et une connexion aura un avantage injuste ?

Les jeux rapides doivent faire des compromis. Pensez-y comme si vous jouiez au jeu sur votre appareil local, mais certains objets sont influencés par les activités des autres. Plutôt que d’envoyer la position exacte de chaque objet en tout temps, les jeux envoient souvent des messages plus simples et moins fréquents. Par exemple :

  • objectX est apparu à pointX
  • objectY a une nouvelle direction et vitesse
  • objectZ a été détruit

Chaque jeu client comble les lacunes. Lorsque objectZ explose, peu importe si l’explosion a l’air différente sur chaque appareil.

Conclusion

Node.js facilite la gestion des WebSockets. Cela ne rend pas nécessairement les applications en temps réel plus faciles à concevoir ou à coder, mais la technologie ne vous freinera pas!

Les principaux inconvénients:

  • Les WebSockets nécessitent leur propre instance de serveur distincte. Les requêtes Ajax Fetch() et événements envoyés par le serveur peuvent être gérées par le serveur web que vous utilisez déjà.

  • Les serveurs WebSocket nécessitent leurs propres vérifications de sécurité et d’autorisation.

  • Les connexions WebSocket interrompues doivent être rétablies manuellement.

Mais ne laissez pas cela vous décourager!

Foire aux questions (FAQ) sur les applications en temps réel avec WebSockets et événements envoyés par le serveur

En quoi les WebSockets diffèrent-ils d’HTTP en termes de performance et de fonctionnalité?

Les WebSockets fournissent un canal de communication full-duplex sur une seule connexion TCP, ce qui signifie que les données peuvent être envoyées et reçues simultanément. Ceci constitue une amélioration significative par rapport à HTTP, où chaque requête nécessite une nouvelle connexion. Les WebSockets permettent également le transfert de données en temps réel, ce qui les rend idéaux pour les applications nécessitant des mises à jour instantanées, telles que les applications de chat ou les mises à jour en direct des sports. D’un autre côté, HTTP est sans état et chaque paire requête-réponse est indépendante, ce qui peut être plus approprié pour les applications où les mises à jour en temps réel ne sont pas nécessaires.

Pouvez-vous expliquer le cycle de vie d’une connexion WebSocket?

Le cycle de vie d’une connexion WebSocket débute par un échange de messages de bienvenue, qui transforme une connexion HTTP en une connexion WebSocket. Une fois la connexion établie, les données peuvent être échangées entre le client et le serveur jusqu’à ce que l’une des parties décide de fermer la connexion. La connexion peut être fermée par le client ou le serveur en envoyant un cadre de fermeture, suivi de la partie adverse qui confirme ce cadre de fermeture.

Comment puis-je implémenter les WebSockets dans une application Android?

L’implémentation des WebSockets dans une application Android implique la création d’un client WebSocket capable de se connecter à un serveur WebSocket. Cela peut être réalisé en utilisant des bibliothèques telles que OkHttp ou Scarlet. Une fois le client configuré, vous pouvez ouvrir une connexion au serveur, envoyer et recevoir des messages, et gérer différents événements tels que l’ouverture de la connexion, la réception de messages et la fermeture de la connexion.

Quels sont les Server-Sent Events et comment se comparent-ils aux WebSockets?

Les Server-Sent Events (SSE) sont une norme qui permet à un serveur d’envoyer des mises à jour à un client via HTTP. Contrairement aux WebSockets, les SSE sont unidirectionnels, ce qui signifie qu’ils ne permettent que l’envoi de données du serveur vers le client. Cela les rend moins adaptés pour les applications qui nécessitent une communication bidirectionnelle, mais ils peuvent constituer une solution plus simple et plus efficace pour les applications qui n’ont besoin que de mises à jour provenant du serveur.

Quels sont certains cas d’utilisation courants pour les WebSockets et les Server-Sent Events?

Les WebSockets sont couramment utilisés dans les applications qui nécessitent une communication en temps réel et bidirectionnelle, telles que les applications de chat, les jeux multijoueurs et les outils collaboratifs. Les Server-Sent Events, en revanche, sont souvent utilisés dans les applications qui ont besoin de mises à jour en temps réel provenant du serveur, telles que les mises à jour de nouvelles en direct, les mises à jour de cours de bourse ou les rapports de progression pour des tâches longues.

Comment puis-je gérer les connexions WebSocket dans une application Spring Boot?

Spring Boot fournit un support pour la communication WebSocket via le module Spring WebSocket. Vous pouvez utiliser l’annotation @EnableWebSocket pour activer le support WebSocket, puis définir un WebSocketHandler pour gérer le cycle de vie de la connexion et la gestion des messages. Vous pouvez également utiliser le SimpMessagingTemplate pour envoyer des messages aux clients connectés.

Quelles sont les considérations de sécurité lors de l’utilisation des WebSockets?

Comme toute autre technologie web, les WebSockets peuvent être vulnérables à diverses menaces de sécurité, telles que l’Hameçonnage Cross-Site WebSocket (CSWSH) et les attaques par Déni de Service (DoS). Pour atténuer ces risques, vous devriez toujours utiliser des connexions WebSocket sécurisées (wss://) et valider et nettoyer toutes les données entrantes. Vous devriez également envisager d’utiliser des mécanismes d’authentification et d’autorisation pour contrôler l’accès à votre serveur WebSocket.

Puis-je utiliser les WebSockets avec une API REST?

Oui, vous pouvez utiliser les WebSockets en conjonction avec une API REST. Alors que les API REST sont idéales pour la communication sans état de type requête-réponse, les WebSockets peuvent être utilisés pour une communication en temps réel, bidirectionnelle. Cela peut être particulièrement utile dans les applications qui nécessitent des mises à jour instantanées, telles que les applications de chat ou les mises à jour en direct des sports.

Comment puis-je tester un serveur WebSocket?

Il existe plusieurs outils disponibles pour tester les serveurs WebSocket, tels que le Test d’Écho de WebSocket.org, ou Postman. Ces outils vous permettent d’ouvrir une connexion WebSocket à un serveur, d’envoyer des messages et de recevoir des réponses. Vous pouvez également écrire des tests automatisés pour votre serveur WebSocket en utilisant des bibliothèques telles que Jest ou Mocha.

Quelles sont les limitations des WebSockets et des Server-Sent Events?

Bien que les WebSockets et les Server-Sent Events offrent des capacités puissantes pour la communication en temps réel, ils ont aussi leurs limitations. Par exemple, tous les navigateurs et réseaux ne prennent pas en charge ces technologies, et ils peuvent consommer une quantité importante de ressources si elles ne sont pas gérées correctement. De plus, ils peuvent être plus complexes à mettre en œuvre et à gérer par rapport à la communication HTTP traditionnelle.

Source:
https://www.sitepoint.com/real-time-apps-websockets-server-sent-events/