Tutoriel sur la création d'un service WebSocket Chat pour Cloud Run


Ce tutoriel explique comment créer un service de chat en plusieurs parties et en temps réel à l'aide de WebSockets avec une connexion persistante pour la communication bidirectionnelle. Avec WebSockets, le client et le serveur peuvent envoyer des messages sans interroger le serveur.

Bien que vous puissiez configurer Cloud Run pour utiliser l'affinité de session, il s'agit d'une affinité optimale, ce qui signifie que toute nouvelle requête peut toujours être acheminée vers une instance différente. Par conséquent, les messages des utilisateurs du service de chat doivent être synchronisés entre toutes les instances, et pas seulement entre les clients connectés à une instance.

Présentation de la conception

Cet exemple de service de chat utilise une instance Memorystore pour Redis afin de stocker et de synchroniser les messages des utilisateurs sur toutes les instances. Redis utilise un mécanisme Pub/Sub, à ne pas confondre avec le produit Cloud Pub/Sub, pour transmettre les données aux clients abonnés connectés à une instance, et ainsi éliminer les interrogations HTTP pour les mises à jour.

Toutefois, même avec les mises à jour push, toute instance créée ne reçoit que les nouveaux messages envoyés au conteneur. Pour charger les messages précédents, l'historique des messages doit être stocké et récupéré à partir d'une solution de stockage persistant. Cet exemple utilise les fonctionnalités traditionnelles de Redis d'un magasin d'objets pour mettre en cache et récupérer l'historique des messages.

Schéma de l'architecture
Le schéma illustre plusieurs connexions clientes vers chaque instance Cloud Run. Chaque instance se connecte à une instance Memorystore pour Redis via un connecteur d'accès au VPC sans serveur.

L'instance Redis est protégée contre Internet à l'aide d'adresses IP privées avec un accès contrôlé et limitée aux services exécutés sur le même réseau privé virtuel que l'instance Redis. Par conséquent, un connecteur d'accès au VPC sans serveur est nécessaire pour que le service Cloud Run se connecte à Redis. En savoir plus sur l'accès au VPC sans serveur.

Limites

  • Ce tutoriel ne montre pas l'authentification de l'utilisateur final ni la mise en cache de session. Pour en savoir plus sur l'authentification des utilisateurs finaux, consultez le tutoriel Cloud Run sur l'authentification des utilisateurs finaux.

  • Ce tutoriel ne met pas en œuvre une base de données telle que Firestore pour le stockage et la récupération illimités de l'historique des messages de chat.

  • Des éléments supplémentaires sont nécessaires pour que cet exemple de service soit prêt pour la production. Une instance Redis de niveau standard est recommandée pour fournir une haute disponibilité à l'aide de la réplication et du basculement automatique.

Objectifs

  • Écrire, créer et déployer un service Cloud Run utilisant WebSockets

  • Connectez-vous à une instance Memorystore pour Redis pour publier de nouveaux messages et vous y abonner sur plusieurs instances.

  • Connectez le service Cloud Run à Memorystore à l'aide de connecteurs d'accès au VPC sans serveur.

Coûts

Dans ce document, vous utilisez les composants facturables suivants de Google Cloud :

Obtenez une estimation des coûts en fonction de votre utilisation prévue à l'aide du simulateur de coût. Les nouveaux utilisateurs de Google Cloud peuvent bénéficier d'un essai gratuit.

Avant de commencer

  1. Connectez-vous à votre compte Google Cloud. Si vous débutez sur Google Cloud, créez un compte pour évaluer les performances de nos produits en conditions réelles. Les nouveaux clients bénéficient également de 300 $ de crédits gratuits pour exécuter, tester et déployer des charges de travail.
  2. Dans Google Cloud Console, sur la page de sélection du projet, sélectionnez ou créez un projet Google Cloud.

    Accéder au sélecteur de projet

  3. Vérifiez que la facturation est activée pour votre projet Google Cloud.

  4. Dans Google Cloud Console, sur la page de sélection du projet, sélectionnez ou créez un projet Google Cloud.

    Accéder au sélecteur de projet

  5. Vérifiez que la facturation est activée pour votre projet Google Cloud.

  6. Activer les API Cloud Run, Memorystore for Redis, Serverless VPC Access, Artifact Registry, and Cloud Build .

    Activer les API

  7. Installez et initialisez gcloud CLI.

Rôles requis

Pour obtenir les autorisations nécessaires pour suivre le tutoriel, demandez à votre administrateur de vous accorder les rôles IAM suivants sur votre projet :

Pour en savoir plus sur l'attribution de rôles, consultez la section Gérer les accès.

Vous pouvez également obtenir les autorisations requises via des rôles personnalisés ou d'autres rôles prédéfinis.

Configurer les valeurs par défaut pour gcloud

Pour configurer gcloud avec les valeurs par défaut pour votre service Cloud Run, procédez comme suit :

  1. Définissez le projet par défaut :

    gcloud config set project PROJECT_ID

    Remplacez PROJECT_ID par le nom du projet que vous avez créé pour ce tutoriel.

  2. Configurez gcloud pour la région choisie :

    gcloud config set run/region REGION

    Remplacez REGION par la région Cloud Run compatible de votre choix.

Emplacements Cloud Run

Cloud Run est régional, ce qui signifie que l'infrastructure qui exécute vos services Cloud Run est située dans une région spécifique et gérée par Google pour être disponible de manière redondante dans toutes les zones de cette région.

Lors de la sélection de la région dans laquelle exécuter vos services Cloud Run, vous devez tout d'abord considérer vos exigences en matière de latence, de disponibilité et de durabilité. Vous pouvez généralement sélectionner la région la plus proche de vos utilisateurs, mais vous devez tenir compte de l'emplacement des autres produits Google Cloud utilisés par votre service Cloud Run. L'utilisation conjointe de produits Google Cloud dans plusieurs emplacements peut avoir une incidence sur la latence et le coût de votre service.

Cloud Run est disponible dans les régions suivantes :

Soumis aux tarifs de niveau 1

Soumis aux tarifs de niveau 2

  • africa-south1 (Johannesburg)
  • asia-east2 (Hong Kong)
  • asia-northeast3 (Séoul, Corée du Sud)
  • asia-southeast1 (Singapour)
  • asia-southeast2 (Jakarta)
  • asia-south1 (Mumbai, Inde)
  • asia-south2 (Delhi, Inde)
  • australia-southeast1 (Sydney)
  • australia-southeast2 (Melbourne)
  • europe-central2 (Varsovie, Pologne)
  • europe-west10 (Berlin)
  • europe-west12 (Turin)
  • europe-west2 (Londres, Royaume-Uni) icône feuille Faibles émissions de CO2
  • europe-west3 (Francfort, Allemagne) icône feuille Faibles émissions de CO2
  • europe-west6 (Zurich, Suisse) icône feuille Faibles émissions de CO2
  • me-central1 (Doha)
  • me-central2 (Dammam)
  • northamerica-northeast1 (Montréal) icône feuille Faibles émissions de CO2
  • northamerica-northeast2 (Toronto) icône feuille Faibles émissions de CO2
  • southamerica-east1 (São Paulo, Brésil) icône feuille Faibles émissions de CO2
  • southamerica-west1 (Santiago, Chili) icône feuille Faibles émissions de CO2
  • us-west2 (Los Angeles)
  • us-west3 (Salt Lake City)
  • us-west4 (Las Vegas)

Si vous avez déjà créé un service Cloud Run, vous pouvez afficher la région dans le tableau de bord Cloud Run de la console Google Cloud.

Récupérer l'exemple de code

Pour récupérer l’exemple de code à utiliser, procédez comme suit :

  1. Clonez l'exemple de dépôt sur votre ordinateur local :

    Node.js

    git clone https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git

    Vous pouvez également télécharger l'exemple en tant que fichier ZIP et l'extraire.

  2. Accédez au répertoire contenant l'exemple de code Cloud Run :

    Node.js

    cd nodejs-docs-samples/run/websockets/

Comprendre le code

Socket.io est une bibliothèque qui permet une communication bidirectionnelle en temps réel entre le navigateur et le serveur. Bien que Socket.io ne soit pas une implémentation WebSocket, il encapsule la fonctionnalité pour fournir une API plus simple pour plusieurs protocoles de communication avec des fonctionnalités supplémentaires, telles qu'une fiabilité améliorée, une reconnexion automatique et la diffusion à tous ou à un sous-ensemble de clients.

Intégration côté client

<script src="/socket.io/socket.io.js"></script>

Le client instancie une nouvelle instance de socket pour chaque connexion. Comme cet exemple est côté serveur, il n'est pas nécessaire de définir l'URL du serveur. L'instance de socket peut émettre et écouter des événements.

// Initialize Socket.io
const socket = io('', {
  transports: ['websocket'],
});
// Emit "sendMessage" event with message
socket.emit('sendMessage', msg, error => {
  if (error) {
    console.error(error);
  } else {
    // Clear message
    $('#msg').val('');
  }
});
// Listen for new messages
socket.on('message', msg => {
  log(msg.user, msg.text);
});

// Listen for notifications
socket.on('notification', msg => {
  log(msg.title, msg.description);
});

// Listen connect event
socket.on('connect', () => {
  console.log('connected');
});

Intégration côté serveur

Du côté du serveur, le serveur Socket.io est initialisé et associé au serveur HTTP. Comme pour le client, une fois que le serveur Socket.io établit une connexion avec le client, une instance de socket est créée pour chaque connexion pouvant être utilisée pour émettre et écouter des messages. Socket.io fournit également une interface simple pour créer des "salles" ou un canal arbitraire que les sockets peuvent rejoindre et quitter.

// Initialize Socket.io
const server = require('http').Server(app);
const io = require('socket.io')(server);

const {createAdapter} = require('@socket.io/redis-adapter');
// Replace in-memory adapter with Redis
const subClient = redisClient.duplicate();
io.adapter(createAdapter(redisClient, subClient));
// Add error handlers
redisClient.on('error', err => {
  console.error(err.message);
});

subClient.on('error', err => {
  console.error(err.message);
});

// Listen for new connection
io.on('connection', socket => {
  // Add listener for "signin" event
  socket.on('signin', async ({user, room}, callback) => {
    try {
      // Record socket ID to user's name and chat room
      addUser(socket.id, user, room);
      // Call join to subscribe the socket to a given channel
      socket.join(room);
      // Emit notification event
      socket.in(room).emit('notification', {
        title: "Someone's here",
        description: `${user} just entered the room`,
      });
      // Retrieve room's message history or return null
      const messages = await getRoomFromCache(room);
      // Use the callback to respond with the room's message history
      // Callbacks are more commonly used for event listeners than promises
      callback(null, messages);
    } catch (err) {
      callback(err, null);
    }
  });

  // Add listener for "updateSocketId" event
  socket.on('updateSocketId', async ({user, room}) => {
    try {
      addUser(socket.id, user, room);
      socket.join(room);
    } catch (err) {
      console.error(err);
    }
  });

  // Add listener for "sendMessage" event
  socket.on('sendMessage', (message, callback) => {
    // Retrieve user's name and chat room  from socket ID
    const {user, room} = getUser(socket.id);
    if (room) {
      const msg = {user, text: message};
      // Push message to clients in chat room
      io.in(room).emit('message', msg);
      addMessageToCache(room, msg);
      callback();
    } else {
      callback('User session not found.');
    }
  });

  // Add listener for disconnection
  socket.on('disconnect', () => {
    // Remove socket ID from list
    const {user, room} = deleteUser(socket.id);
    if (user) {
      io.in(room).emit('notification', {
        title: 'Someone just left',
        description: `${user} just left the room`,
      });
    }
  });
});

Socket.io fournit également un adaptateur Redis pour diffuser des événements à tous les clients, quel que soit le serveur qui diffuse le socket. Socket.io n'utilise que le mécanisme Pub/Sub de Redis et ne stocke aucune donnée.

const {createAdapter} = require('@socket.io/redis-adapter');
// Replace in-memory adapter with Redis
const subClient = redisClient.duplicate();
io.adapter(createAdapter(redisClient, subClient));

L'adaptateur Redis de Socket.io peut réutiliser le client Redis utilisé pour stocker l'historique des messages de la salle. Chaque conteneur crée une connexion à l'instance Redis et Cloud Run peut créer un grand nombre d'instances. Ce nombre est bien inférieur aux 65 000 connexions que Redis peut prendre en charge. Si vous devez gérer cette quantité de trafic, vous devez également évaluer le débit du connecteur d'accès au VPC sans serveur.

Reconnexion

Le délai avant expiration maximal de Cloud Run est de 60 minutes. Vous devez donc ajouter une logique de reconnexion pour les délais d'expiration possibles. Dans certains cas, Socket.io tente automatiquement de se reconnecter après les événements de déconnexion ou d'erreur de connexion. Rien ne garantit que le client se reconnectera à la même instance.

// Listen for reconnect event
socket.io.on('reconnect', () => {
  console.log('reconnected');
  // Emit "updateSocketId" event to update the recorded socket ID with user and room
  socket.emit('updateSocketId', {user, room}, error => {
    if (error) {
      console.error(error);
    }
  });
});
// Add listener for "updateSocketId" event
socket.on('updateSocketId', async ({user, room}) => {
  try {
    addUser(socket.id, user, room);
    socket.join(room);
  } catch (err) {
    console.error(err);
  }
});

Les instances seront conservées si une connexion active est établie jusqu'à ce que toutes les requêtes soient fermées ou expirent. Même si vous utilisez l'affinité de session Cloud Run, vous pouvez équilibrer la charge des nouvelles requêtes dans des conteneurs actifs, ce qui permet aux conteneurs d'effectuer un scaling vertical. Si vous craignez qu'un grand nombre de conteneurs persistent après un pic de trafic, vous pouvez réduire la valeur du délai maximal pour que les sockets inutilisés soient nettoyés plus fréquemment.

Transmettre le service

  1. Créer une instance Memorystore pour Redis :

    gcloud redis instances create INSTANCE_ID --size=1 --region=REGION

    Remplacez INSTANCE_ID par le nom de l'instance, c'est-à-dire my-redis-instance, et REGION_ID par la région pour toutes vos ressources et tous vos services, c'est-à-dire us-central1.

    Une plage d'adresses IP de la plage de réseau de service par défaut sera automatiquement attribuée à l'instance. Ce tutoriel utilise 1 Go de mémoire pour le cache local de messages dans l'instance Redis. En savoir plus sur la détermination de la taille initiale d'une instance Memorystore pour votre cas d'utilisation.

  2. Configurer un connecteur d'accès au VPC sans serveur :

    Pour se connecter à votre instance Redis, votre service Cloud Run doit avoir accès au réseau VPC autorisé de votre instance Redis.

    Chaque connecteur VPC nécessite son propre sous-réseau /28 pour y placer des instances. Cette plage d'adresses IP ne doit pas chevaucher les réservations d'adresses IP existantes sur votre réseau VPC. Par exemple, 10.8.0.0 (/28) fonctionnera dans la plupart des nouveaux projets ou vous pourrez spécifier une autre plage d'adresses IP personnalisée inutilisée, telle que 10.9.0.0 (/28). Vous pouvez afficher les plages d'adresses IP actuellement réservées dans Google Cloud Console.

    gcloud compute networks vpc-access connectors create CONNECTOR_NAME \
      --region REGION \
      --range "10.8.0.0/28"
    

    Remplacez CONNECTOR_NAME par le nom de votre connecteur.

    Cette commande crée un connecteur dans le réseau VPC par défaut, identique à l'instance Redis, avec la taille de machine e2-micro. L'augmentation de la taille de la machine du connecteur peut améliorer son débit, mais également augmenter les coûts. Le connecteur doit également se trouver dans la même région que l'instance Redis. Découvrez comment configurer l'accès au VPC sans serveur.

  3. Définissez une variable d'environnement avec l'adresse IP du réseau autorisé de votre instance Redis :

     export REDISHOST=$(gcloud redis instances describe INSTANCE_ID --region REGION --format "value(host)")
  4. Créez un compte de service qui servira d'identité de service. Par défaut, il ne dispose d'aucun autre droit que l'abonnement au projet.

    gcloud iam service-accounts create chat-identity
    gcloud projects add-iam-policy-binding PROJECT_ID \
    --member=serviceAccount:chat-identity@PROJECT_ID.iam.gserviceaccount.com \
    --role=roles/serviceusage.serviceUsageConsumer
    
  5. Créez et déployez l'image de conteneur dans Cloud Run :

    gcloud run deploy chat-app --source . \
        --vpc-connector CONNECTOR_NAME \
        --allow-unauthenticated \
        --timeout 3600 \
        --service-account chat-identity \
        --update-env-vars REDISHOST=$REDISHOST

    Répondez aux invites pour installer les API requises en répondant y lorsque vous y êtes invité. Vous ne devez procéder à cette opération qu'une fois par projet. Répondez aux autres invites en fournissant la plate-forme et la région, si vous n'avez pas défini les paramètres par défaut pour celles-ci, comme décrit sur la page de configuration. En savoir plus sur le déploiement à partir de code source.

Essayez-le !

Pour tester le service complet :

  1. Accédez dans votre navigateur à l'URL fournie par l'étape de déploiement ci-dessus.

  2. Ajoutez votre nom et un salon de discussion pour vous connecter.

  3. Envoie un message au salon !

Si vous décidez de poursuivre le développement de ces services, n'oubliez pas qu'ils ont un accès IAM restreint au reste de Google Cloud. Des rôles IAM supplémentaires devront donc leur être attribués afin de pouvoir accéder à de nombreux autres services.

Nettoyer

Si vous avez créé un projet pour ce tutoriel, supprimez-le. Si vous avez utilisé un projet existant et que vous souhaitez le conserver sans les modifications du présent tutoriel, supprimez les ressources créées pour ce tutoriel.

Supprimer le projet

Le moyen le plus simple d'empêcher la facturation est de supprimer le projet que vous avez créé pour ce tutoriel.

Pour supprimer le projet :

  1. Dans la console Google Cloud, accédez à la page Gérer les ressources.

    Accéder à la page Gérer les ressources

  2. Dans la liste des projets, sélectionnez le projet que vous souhaitez supprimer, puis cliquez sur Supprimer.
  3. Dans la boîte de dialogue, saisissez l'ID du projet, puis cliquez sur Arrêter pour supprimer le projet.

Supprimer les ressources du tutoriel

  1. Supprimez le service Cloud Run que vous avez déployé dans ce tutoriel :

    gcloud run services delete SERVICE-NAME

    SERVICE-NAME est le nom de service que vous avez choisi.

    Vous pouvez également supprimer des services Cloud Run à partir de Google Cloud Console.

  2. Supprimez la configuration régionale gcloud par défaut que vous avez ajoutée lors de la configuration du tutoriel :

     gcloud config unset run/region
    
  3. Supprimez la configuration du projet :

     gcloud config unset project
    
  4. Supprimez les autres ressources Google Cloud créées dans ce tutoriel :

Étape suivante