Skip to content

Création d'un Serveur WebSocket Universel pour 7u.pl

Remarque : La version polonaise est l'originale. Cette traduction a été générée par l'IA et peut contenir des erreurs mineures.

Serveur WebSocket

À mesure que nous ajoutons de plus en plus de micro-outils interactifs à l'écosystème 7u.pl, le besoin s'est fait sentir de synchroniser l'état en temps réel entre des appareils complètement séparés.

Exemple : notre projet virtual-keyboard doit envoyer des événements ("bouton Q appuyé") depuis un smartphone, vers un navigateur sur un Mac, et directement à un Raspberry Pi physique, en contournant les restrictions de réseau local des deux côtés.

Au lieu de construire une logique WebSocket séparément pour chaque application (ce qui conduirait rapidement à une duplication de l'infrastructure), nous avons décidé de créer un seul serveur de communication indépendant, très "stupide", qui ne fait qu'une seule chose, mais le fait sans faille : il écoute les salles (rooms) et diffuse (broadcast) les messages entre les clients.

L'Architecture d'un Serveur "Stupide"

La clé de l'universalité est l'absence de logique côté serveur. Le serveur ne sait pas ce qu'est un "clavier", ce qu'est une "souris" ni à quoi ressemble la structure du JSON envoyé sur le réseau.

Choix Technologique

Nous avons opté pour Node.js combiné avec Socket.IO. Bien que les WebSockets natifs (ws) soient plus légers, Socket.IO intègre un excellent mécanisme d'organisation des connexions en ce qu'on appelle les Rooms (salles), ce qui correspond parfaitement à nos besoins d'isoler différentes applications. De plus, cela nous soulage du problème des reconnexions automatiques sur les appareils mobiles avec une connexion instable.

Le Code Source en Bref

Le noyau de tout le serveur est contenu dans seulement quelques écouteurs (listeners) sur l'instance io :

javascript
io.on('connection', (socket) => {
  // Rejoint le client à une salle virtuelle
  socket.on('join-room', (roomName) => {
    socket.join(roomName);
  });

  // Diffuse un message aux autres dans la salle (en ignorant l'expéditeur)
  socket.on('broadcast', ({ roomName, eventName, payload }) => {
    socket.to(roomName).emit(eventName, payload);
  });
  
  // Diffuse un message à tout le monde dans la salle (y compris l'expéditeur)
  socket.on('broadcast-all', ({ roomName, eventName, payload }) => {
    io.to(roomName).emit(eventName, payload);
  });
});

C'est littéralement tout. Le serveur accepte n'importe quel objet (payload), trouve une salle (roomName), l'enveloppe dans un événement spécifié par le développeur (eventName), et le lance au reste des clients.

La Sécurité via CORS

Bien que le serveur ne sache rien des données transmises elles-mêmes, nous voulions nous assurer que notre point de terminaison public ne se transformerait pas en serveur proxy gratuit pour la moitié d'Internet.

L'implémentation a nécessité une règle stricte pour valider l'en-tête Origin afin de vérifier la connexion au stade le plus précoce de la négociation (ce qu'on appelle la poignée de main, ou handshake) :

javascript
const io = new Server(httpServer, {
  cors: {
    origin: (origin, callback) => {
      // Tolérer l'absence d'Origin (appareils IoT/scripts)
      if (!origin) return callback(null, true);

      // Autorisés : *.7u.pl et environnements locaux
      const allowedOrigins = [
        /^https?:\/\/((.*)\.)?7u\.pl$/,
        /^http:\/\/localhost(:\d+)?$/,
        /^http:\/\/127\.0\.0\.1(:\d+)?$/
      ];

      if (allowedOrigins.some((regex) => regex.test(origin))) {
        callback(null, true);
      } else {
        callback(new Error('Not allowed by CORS'));
      }
    }
  }
});

Grâce à l'utilisation de Regex au lieu de chaînes de caractères rigides, les domaines tels que sklep.7u.pl, ainsi que les ports de développement (par exemple, localhost:5173 utilisé par l'écosystème Vite) sont autorisés dynamiquement, sans qu'il soit nécessaire de redémarrer et de reconfigurer le serveur pour chaque nouveau projet.

Déploiement et Maintenance (Docker)

Le serveur ne pèse que quelques mégaoctets. L'image Docker a été construite sur la base de l'environnement node:20-alpine.

dockerfile
FROM node:20-alpine
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install --only=production
COPY . .
EXPOSE 80
CMD ["npm", "start"]

Sur la machine de production, l'image est cachée derrière un équilibreur de charge (NGINX/Cloudflare), qui gère la terminaison ultime des certificats SSL (wss://sock.7u.pl). Une telle configuration signifie que la nouvelle infrastructure est devenue complètement invisible ("transparente") pour les modifications futures.

Résumé et Avantages

Déplacer le point de relais des WebSockets dans un dépôt séparé nous a libérés de :

  1. Avoir des backends forcés d'écouter les ports juste pour que les clients "frontend uniquement" puissent se voir,
  2. Recréer des systèmes d'authentification à partir de zéro,
  3. Maintenir une dette technique inutile dans les projets.

Désormais, chaque futur projet interactif sous le domaine *.7u.pl recevra la fonctionnalité temps réel après l'ajout de cinq lignes d'importation. Et c'est exactement le propre d'une bonne architecture.