Instructivo sobre compilación de un servicio de chat basado en WebSocket para Cloud Run

En este instructivo, se muestra cómo crear un servicio de chat multiusuario y en tiempo real con WebSockets y una conexión persistente para la comunicación bidireccional. Con WebSockets, el cliente y el servidor se pueden enviar mensajes entre sí sin sondear las actualizaciones del servidor.

Aunque puedes configurar Cloud Run para usar la afinidad de sesión, esto proporciona una afinidad de mejor esfuerzo, lo que significa que, potencialmente, cualquier solicitud nueva se puede seguir enrutando a una instancia diferente. Como resultado, los mensajes de los usuarios en el servicio de chat deben sincronizarse entre todas las instancias, no solo entre los clientes conectados a una instancia.

Diseña un servicio de chat en tiempo real

Este servicio de chat de muestra usa una instancia de Memorystore para Redis para almacenar y sincronizar mensajes de usuarios en todas las instancias. Redis usa un mecanismo Pub/Sub, que no debe confundirse con el producto Cloud Pub/Sub, para enviar los datos a los clientes suscritos conectados a cualquier instancia a fin de eliminar el sondeo HTTP para obtener actualizaciones.

Sin embargo, incluso con las actualizaciones push, cualquier instancia que se inicie solo recibirá mensajes nuevos enviados al contenedor. Para cargar mensajes anteriores, el historial de mensajes tendría que almacenarse y recuperarse desde una solución de almacenamiento persistente. En este ejemplo, se usa la funcionalidad convencional de Redis de un depósito de objetos para almacenar en caché y recuperar el historial de mensajes.

La instancia de Redis está protegida de Internet con IPs privadas y acceso controlado y limitado a los servicios que se ejecutan en la misma red privada virtual que la instancia de Redis. Te recomendamos que uses la salida de VPC directa.

Limitaciones

  • En este instructivo, no se muestra la autenticación de usuario final ni el almacenamiento en caché de la sesión. Si deseas obtener más información sobre la autenticación de usuarios finales, consulta el instructivo de Cloud Run para la autenticación de usuarios finales.

  • En este instructivo, no se implementa una base de datos como Firestore para el almacenamiento indefinido y la recuperación del historial de mensajes de chat.

  • Se necesitan elementos adicionales para que este servicio de muestra esté listo para la producción. Se recomienda una instancia de Redis de nivel Estándar para proporcionar alta disponibilidad a través de la replicación y la conmutación por error automática.

Objetivos

  • Escribir, compilar e implementar un servicio de Cloud Run que use WebSockets

  • Conéctate a una instancia de Memorystore para Redis a fin de publicar y suscribirte a mensajes nuevos en todas las instancias.

  • Conectar el servicio de Cloud Run con Memorystore a través de la salida de VPC directa

Costos

En este documento, usarás los siguientes componentes facturables de Google Cloud:

Para obtener una estimación de costos en función del uso previsto, usa la calculadora de precios.

Es posible que los usuarios nuevos de Google Cloud cumplan con los requisitos para acceder a una prueba gratuita.

Antes de comenzar

  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. Instala y, luego, inicializa gcloud CLI.
  8. Roles obligatorios

    Si quieres obtener los permisos que necesitas para completar el instructivo, pídele a tu administrador que te otorgue los siguientes roles de IAM en tu proyecto:

    Para obtener más información sobre cómo otorgar roles, consulta Administra el acceso a proyectos, carpetas y organizaciones.

    También puedes obtener los permisos necesarios mediante roles personalizados o cualquier otro rol predefinido.

Configura los valores predeterminados de gcloud

A fin de configurar gcloud con los valores predeterminados para el servicio de Cloud Run, sigue estos pasos:

  1. Configura el proyecto predeterminado:

    gcloud config set project PROJECT_ID

    Reemplaza PROJECT_ID por el nombre del proyecto que creaste para este instructivo.

  2. Configura gcloud en la región que elegiste:

    gcloud config set run/region REGION

    Reemplaza REGION por la región de Cloud Run compatible que prefieras.

Ubicaciones de Cloud Run

Cloud Run es regional, lo que significa que la infraestructura que ejecuta los servicios se ubica en una región específica, y Google la administra para que esté disponible de manera redundante en todas las zonas de esa región.

El cumplimiento de los requisitos de latencia, disponibilidad o durabilidad es el factor principal para seleccionar la región en la que se ejecutan los servicios de Cloud Run. Por lo general, puedes seleccionar la región más cercana a tus usuarios, pero debes considerar la ubicación de los otros productos Google Cloud que usa tu servicio de Cloud Run. Si usas Google Cloud productos en varias ubicaciones, la latencia y el costo del servicio pueden verse afectados.

Cloud Run está disponible en las siguientes regiones:

Sujetas a los Precios del nivel 1

Sujetas a los Precios del nivel 2

  • africa-south1 (Johannesburgo)
  • asia-east2 (Hong Kong)
  • asia-northeast3 (Seúl, Corea del Sur)
  • asia-southeast1 (Singapur)
  • asia-southeast2 (Yakarta)
  • asia-south2 Delhi (India)
  • australia-southeast1 (Sídney)
  • australia-southeast2 (Melbourne)
  • europe-central2 (Varsovia, Polonia)
  • europe-west10 (Berlín)
  • europe-west12 (Turín)
  • europe-west2 (Londres, Reino Unido) ícono de hoja Bajo nivel de CO2
  • europe-west3 (Fráncfort, Alemania)
  • europe-west6 (Zúrich, Suiza) ícono de hoja Bajo nivel de CO2
  • me-central1 (Doha)
  • me-central2 (Dammam)
  • northamerica-northeast1 (Montreal) ícono de hoja Bajo nivel de CO2
  • northamerica-northeast2 (Toronto) ícono de hoja Bajo nivel de CO2
  • southamerica-east1 (São Paulo, Brasil) ícono de hoja Bajo nivel de CO2
  • southamerica-west1 (Santiago, Chile) ícono de hoja Bajo nivel de CO2
  • us-west2 (Los Ángeles)
  • us-west3 (Salt Lake City)
  • us-west4 (Las Vegas)

Si ya creaste un servicio de Cloud Run, puedes ver la región en el panel de Cloud Run en la consola.Google Cloud

Recupera la muestra de código

A fin de recuperar la muestra de código para su uso, haz lo siguiente:

  1. Clona el repositorio de muestra en tu máquina local:

    Node.js

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

    De manera opcional, puedes descargar la muestra como un archivo ZIP y extraerla.

  2. Ve al directorio que contiene el código de muestra de Cloud Run:

    Node.js

    cd nodejs-docs-samples/run/websockets/

Comprende el código de WebSockets

Socket.io es una biblioteca que permite la comunicación bidireccional en tiempo real entre el navegador y el servidor. Si bien Socket.io no es una implementación de WebSocket, sí ajusta la funcionalidad a fin de proporcionar una API más simple para varios protocolos de comunicación con funciones adicionales, como una mayor confiabilidad, una reconexión automática y hacer una transmisión a todos los clientes o a un subconjunto de ellos.

Integración del cliente

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

El cliente crea una instancia de socket nueva para cada conexión. Debido a que este ejemplo es el procesamiento del servidor, no es necesario definir la URL del servidor. La instancia de socket puede emitir y escuchar eventos.

// 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');
});

Integración del servidor

Del lado del servidor, el servidor de Socket.io se inicializa y se adjunta al servidor HTTP. Al igual que en el lado del cliente, una vez que el servidor Socket.io establece una conexión con el cliente, se crea una instancia de socket para cada conexión que se puede utilizar para emitir y escuchar mensajes. Socket.io también proporciona una interfaz para crear “salas” o un canal arbitrario al que los sockets pueden unirse y salir.

// 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 también proporciona un adaptador de Redis para transmitir eventos a todos los clientes, sin importar qué servidor entrega el socket. Socket.io solo usa el mecanismo Pub/Sub de Redis y no almacena ningún dato.

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

El adaptador de Redis de Socket.io puede reutilizar el cliente de Redis que se usa para almacenar el historial de mensajes de la sala. Cada contenedor creará una conexión a la instancia de Redis y Cloud Run puede crear una gran cantidad de instancias. Esto está muy por debajo de las 65,000 conexiones que admite Redis.

Reconexión

Cloud Run tiene un tiempo de espera máximo de 60 minutos. Por lo tanto, debes agregar la lógica de reconexión para los posibles tiempos de espera. En algunos casos, Socket.io se vuelve a conectar automáticamente después de los eventos de error de conexión o de desconexión. No hay garantía de que el cliente se volverá a conectar a la misma instancia.

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

Las instancias se conservarán si hay una conexión activa hasta que todas las solicitudes se cierren o se agote el tiempo de espera. Incluso si usas la afinidad de sesión de Cloud Run, es posible que las cargas de las solicitudes nuevas se balanceen en contenedores activos, lo que permite que los contenedores reduzcan la escala. Si te preocupa que haya una gran cantidad de contenedores persistentes después de un aumento repentino de tráfico, puedes reducir el valor de tiempo de espera máximo para que los sockets sin usar se borren con más frecuencia.

Envía el servicio

  1. Crea una instancia de Memorystore para Redis

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

    Reemplaza lo siguiente:

    • INSTANCE_ID: Es el nombre de la instancia, por ejemplo, my-redis-instance.
    • REGION_ID: Es la región para todos tus recursos y servicios, por ejemplo, europe-west1.

    Se asignará automáticamente un rango de IP a la instancia desde el rango de red del servicio predeterminado. En este instructivo, se usa 1 GB de memoria para la caché local de mensajes en la instancia de Redis. Obtén más información a fin de determinar el tamaño inicial de una instancia de Memorystore para tu caso de uso.

  2. Define una variable de entorno con la dirección IP de la red autorizada de tu instancia de Redis:

     export REDISHOST=$(gcloud redis instances describe INSTANCE_ID --region REGION --format "value(host)")
  3. Crea una cuenta de servicio para que funcione como la identidad del servicio. De forma predeterminada, esta no tiene otros privilegios más que la membresía del proyecto.

    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. Para encontrar el nombre de la red de VPC autorizada de tu instancia de Redis, ejecuta el siguiente comando:

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

    Reemplaza lo siguiente:

    • INSTANCE_ID: Es el nombre de la instancia, por ejemplo, my-redis-instance.
    • REGION_ID: Es la región para todos tus recursos y servicios, por ejemplo, europe-west1.

    Toma nota del nombre de la red de VPC.

  5. Compila e implementa la imagen de contenedor en 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

    Reemplaza lo siguiente:

    • NETWORK es el nombre de la red de VPC autorizada a la que está conectada tu instancia de Redis.
    • SUBNET es el nombre de la subred. La subred debe ser /26 o mayor. La salida de VPC directa es compatible con los rangos IPv4 RFC 1918, RFC 6598 y clase E.

    Responde a cualquier solicitud para instalar las API obligatorias. Para ello, responde y cuando se te solicite. Solo debes hacer esto una vez en un proyecto. Para responder a otras solicitudes, suministra la plataforma y la región si no configuraste los valores predeterminados de estas como se describe en la página de configuración. Obtén más información para implementar desde el código fuente.

Prueba el servicio

Para probar el servicio completo, haz lo siguiente:

  1. Dirige tu navegador a la URL proporcionada en el paso de implementación.

  2. Agrega tu nombre y una sala de chat para acceder.

  3. Envía un mensaje a la sala.

Si eliges seguir desarrollando estos servicios, recuerda que tienen acceso restringido de Identity and Access Management (IAM) al resto de Google Cloud y necesitarán tener funciones de IAM adicionales para acceder a muchos otros servicios.

Realiza una limpieza

Para evitar cargos adicionales en tu cuenta de Google Cloud , borra todos los recursos que implementaste con este instructivo.

Borra el proyecto

Si creaste un proyecto nuevo para este instructivo, bórralo. Si usaste un proyecto existente y necesitas conservarlo sin los cambios que agregaste en este instructivo, borra los recursos que creaste para el instructivo.

La manera más fácil de eliminar la facturación es borrar el proyecto que creaste para el instructivo.

Para borrar el proyecto, sigue estos pasos:

  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.

Elimina recursos de instructivos

  1. Borra el servicio de Cloud Run que implementaste en este instructivo. Los servicios de Cloud Run no generan costos hasta que reciben solicitudes.

    Para borrar tu servicio de Cloud Run, ejecuta el siguiente comando:

    gcloud run services delete SERVICE-NAME

    SERVICE-NAME por el nombre del servicio

    También puedes borrar los servicios de Cloud Run desde la consola deGoogle Cloud .

  2. Quita la configuración de región predeterminada de gcloud que agregaste durante la configuración del instructivo:

     gcloud config unset run/region
    
  3. Quita la configuración del proyecto:

     gcloud config unset project
    
  4. Borra otros recursos Google Cloud que creaste en este instructivo:

¿Qué sigue?