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.

Concevoir un service de chat en temps réel

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 store d'objets pour mettre en cache et récupérer l'historique des messages.

L'instance Redis est protégée contre Internet à l'aide d'adresses IP privées avec un accès contrôlé et limité aux services exécutés sur le même réseau privé virtuel que l'instance Redis. Nous vous recommandons d'utiliser la sortie VPC directe.

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 la sortie VPC directe.

Coûts

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

Vous pouvez obtenir 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 sans frais.

Avant de commencer

  1. Sign in to your Google Cloud account. If you're new to Google Cloud, create an account to evaluate how our products perform in real-world scenarios. New customers also get $300 in free credits to run, test, and deploy workloads.
  2. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Roles required to select or create a project

    • Select a project: Selecting a project doesn't require a specific IAM role—you can select any project that you've been granted a role on.
    • Create a project: To create a project, you need the Project Creator role (roles/resourcemanager.projectCreator), which contains the resourcemanager.projects.create permission. Learn how to grant roles.

    Go to project selector

  3. Verify that billing is enabled for your Google Cloud project.

  4. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Roles required to select or create a project

    • Select a project: Selecting a project doesn't require a specific IAM role—you can select any project that you've been granted a role on.
    • Create a project: To create a project, you need the Project Creator role (roles/resourcemanager.projectCreator), which contains the resourcemanager.projects.create permission. Learn how to grant roles.

    Go to project selector

  5. Verify that billing is enabled for your Google Cloud project.

  6. Enable the Cloud Run, Memorystore for Redis, Artifact Registry, and Cloud Build APIs.

    Roles required to enable APIs

    To enable APIs, you need the Service Usage Admin IAM role (roles/serviceusage.serviceUsageAdmin), which contains the serviceusage.services.enable permission. Learn how to grant roles.

    Enable the APIs

  7. Installez et initialisez la gcloud CLI.
  8. 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 page Gérer l'accès aux projets, aux dossiers et aux organisations.

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

Configurer les paramètres par défaut de 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 Google Cloudproduits 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-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)
  • 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 consoleGoogle 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 WebSockets

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 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.

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 les éléments suivants :

    • INSTANCE_ID : nom de l'instance, par exemple my-redis-instance.
    • REGION_ID : région de toutes vos ressources et de tous vos services (par exemple, europe-west1).

    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. 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)")
  3. 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
  4. Recherchez le nom du réseau VPC autorisé de votre instance Redis en exécutant la commande suivante :

      gcloud redis instances describe INSTANCE_ID --region REGION --format "value(authorizedNetwork)"
    

    Remplacez les éléments suivants :

    • INSTANCE_ID : nom de l'instance, par exemple my-redis-instance.
    • REGION_ID : région de toutes vos ressources et de tous vos services (par exemple, europe-west1).

    Notez le nom du réseau VPC.

  5. Créez et déployez l'image de conteneur dans Cloud Run :

    gcloud run deploy chat-app --source . \
        --allow-unauthenticated \
        --timeout 3600 \
        --service-account chat-identity \
        --network NETWORK \
        --subnet SUBNET \
        --update-env-vars REDISHOST=$REDISHOST

    Remplacez les éléments suivants :

    • NETWORK est le nom du réseau VPC autorisé auquel votre instance Redis est associée.
    • SUBNET est le nom de votre sous-réseau. Le sous-réseau doit être /26 ou supérieur. La sortie VPC directe est compatible avec les plages IPv4 RFC 1918, RFC 6598 et Classe E.

    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 du code source.

Essayer le service

Pour tester le service complet :

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

  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.

Effectuer un nettoyage

Pour éviter des frais supplémentaires sur votre compte Google Cloud , supprimez toutes les ressources que vous avez déployées avec ce tutoriel.

Supprimer le projet

Si vous avez créé un projet pour ce tutoriel, supprimez-le. Si vous avez utilisé un projet existant et que vous devez le conserver sans les modifications que vous avez apportées dans ce tutoriel, supprimez les ressources que vous avez créées pour ce tutoriel.

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. In the Google Cloud console, go to the Manage resources page.

    Go to Manage resources

  2. In the project list, select the project that you want to delete, and then click Delete.
  3. In the dialog, type the project ID, and then click Shut down to delete the project.

Supprimer les ressources du tutoriel

  1. Supprimez le service Cloud Run que vous avez déployé dans ce tutoriel. Les services Cloud Run n'entraînent pas de coûts tant qu'ils ne reçoivent pas de requêtes.

    Pour supprimer votre service Cloud Run, exécutez la commande suivante :

    gcloud run services delete SERVICE-NAME

    Remplacez SERVICE-NAME par le nom du service.

    Vous pouvez également supprimer des services Cloud Run à partir de la consoleGoogle Cloud .

  2. Supprimez la configuration régionale par défaut gcloud 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 :

Étapes suivantes