Tutorial: como criar um serviço de chat WebSocket para o Cloud Run


Neste tutorial, você verá como criar um serviço de chat em tempo real com várias salas usando WebSockets com conexão persistente para comunicação bidirecional. Com o WebSockets, o cliente e o servidor podem enviar mensagens uns para os outros sem procurar atualizações no servidor.

Embora seja possível configurar o Cloud Run para usar a afinidade da sessão, isso fornece uma afinidade de melhor esforço, o que significa que qualquer nova solicitação ainda poderá ser possivelmente roteada para uma instância diferente. Consequentemente, as mensagens dos usuários no serviço de chat precisam ser sincronizadas em todas as instâncias, não apenas entre os clientes conectados a uma instância.

Visão geral do design

Este serviço de chat de amostra usa uma instância do Memorystore para Redis para armazenar e sincronizar mensagens de usuários em todas as instâncias. O Redis usa um mecanismo Pub/Sub para não confundir com o produto Cloud Pub/Sub, para enviar dados a clientes inscritos conectados a qualquer instância, visando eliminar a pesquisa de HTTP para atualizações.

No entanto, mesmo com atualizações push, qualquer instância de contêiner ativada receberá apenas novas mensagens enviadas ao contêiner. Para carregar mensagens anteriores, o histórico de mensagens precisaria ser armazenado e recuperado de uma solução de armazenamento permanente. Neste exemplo, usamos a funcionalidade convencional do Redis de um armazenamento de objetos para armazenar em cache e recuperar o histórico de mensagens.

Diagrama de arquitetura
O diagrama mostra várias conexões de cliente para cada instância do Cloud Run. Cada instância se conecta a uma instância do Memorystore para Redis por meio de um conector de acesso VPC sem servidor.

A instância do Redis é protegida da Internet usando IPs privados com acesso controlado e limitado a serviços em execução na mesma rede privada virtual que a instância do Redis. Portanto, é necessário um conector de acesso VPC sem servidor para que o serviço do Cloud Run se conecte ao Redis. Saiba mais sobre o acesso VPC sem servidor.

Limitações

  • Neste tutorial, não mostramos a autenticação do usuário final ou o armazenamento em cache da sessão. Para saber mais sobre a autenticação de usuários finais, consulte o tutorial do Cloud Run sobre autenticação do usuário final.

  • Neste tutorial, não implementamos um banco de dados como o Firestore para armazenamento e recuperação indefinidos do histórico de mensagens do chat.

  • São necessários elementos adicionais para que este serviço de amostra esteja pronto para produção. Uma instância do Redis de nível padrão é recomendada para fornecer alta disponibilidade usando a replicação e o failover automático.

Objetivos

  • Gravar, criar e implantar um serviço do Cloud Run que usa WebSockets.

  • Conecte-se a uma instância do Memorystore para Redis para publicar e assinar novas mensagens em instâncias de contêiner.

  • Conecte o serviço do Cloud Run com o Memorystore usando conectores de acesso VPC sem servidor.

Custos

Neste documento, você usará os seguintes componentes faturáveis do Google Cloud:

Para gerar uma estimativa de custo baseada na projeção de uso deste tutorial, use a calculadora de preços. Novos usuários do Google Cloud podem estar qualificados para uma avaliação gratuita.

Antes de começar

  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.

    Go to project selector

  3. Make sure 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.

    Go to project selector

  5. Make sure that billing is enabled for your Google Cloud project.

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

    Enable the APIs

  7. Instale e inicialize a CLI gcloud.

Funções exigidas

Para conseguir as permissões necessárias para concluir o tutorial, peça ao administrador para conceder a você os seguintes papéis do IAM no seu projeto:

Para mais informações sobre a concessão de papéis, consulte Gerenciar o acesso a projetos, pastas e organizações.

Também é possível conseguir as permissões necessárias por meio de papéis personalizados ou de outros papéis predefinidos.

Configuração dos padrões gcloud

Para configurar a gcloud com os padrões do serviço do Cloud Run, realize as etapas a seguir:

  1. Defina seu projeto padrão:

    gcloud config set project PROJECT_ID

    Substitua PROJECT_ID pelo nome do projeto que você criou para este tutorial.

  2. Configure a gcloud para a região escolhida:

    gcloud config set run/region REGION

    Substitua REGION pela região compatível do Cloud Run.

Locais do Cloud Run

O Cloud Run é regional, o que significa que a infraestrutura que executa seus serviços do Cloud Run está localizada em uma região específica e é gerenciada pelo Google para estar disponível de maneira redundante em todas as zonas da região.

Atender aos seus requisitos de latência, disponibilidade ou durabilidade são os principais fatores para selecionar a região em que seus serviços do Cloud Run são executados. Geralmente, é possível selecionar a região mais próxima de seus usuários, mas considere a localização dos outros produtos do Google Cloud usados pelo serviço do Cloud Run. O uso de produtos do Google Cloud em vários locais pode afetar a latência e o custo do serviço.

O Cloud Run está disponível nas regiões a seguir:

Sujeitas aos preços do nível 1

  • asia-east1 (Taiwan)
  • asia-northeast1 (Tóquio)
  • asia-northeast2 (Osaka)
  • asia-south1 (Mumbai, Índia)
  • europe-north1 (Finlândia) Ícone de folha Baixo CO2
  • europe-southwest1 (Madri) Ícone de folha Baixo CO2
  • europe-west1 (Bélgica) Ícone de folha Baixo CO2
  • europe-west4 (Países Baixos) Ícone de folha Baixo CO2
  • europe-west8 (Milão)
  • europe-west9 (Paris) Ícone de folha Baixo CO2
  • me-west1 (Tel Aviv)
  • us-central1 (Iowa) Ícone de folha Baixo CO2
  • us-east1 (Carolina do Sul)
  • us-east4 (Norte da Virgínia)
  • us-east5 (Columbus)
  • us-south1 (Dallas) Ícone de folha Baixo CO2
  • us-west1 (Oregon) Ícone de folha Baixo CO2

Sujeitas aos preços do nível 2

  • africa-south1 (Johannesburgo)
  • asia-east2 (Hong Kong)
  • asia-northeast3 (Seul, Coreia do Sul)
  • asia-southeast1 (Singapura)
  • asia-southeast2 (Jacarta)
  • asia-south2 (Déli, Índia)
  • australia-southeast1 (Sydney)
  • australia-southeast2 (Melbourne)
  • europe-central2 (Varsóvia, Polônia)
  • europe-west10 (Berlim) Ícone de folha Baixo CO2
  • europe-west12 (Turim)
  • europe-west2 (Londres, Reino Unido) Ícone de folha Baixo CO2
  • europe-west3 (Frankfurt, Alemanha) Ícone de folha Baixo CO2
  • europe-west6 (Zurique, Suíça) Ícone de folha Baixo CO2
  • me-central1 (Doha)
  • me-central2 (Damã)
  • northamerica-northeast1 (Montreal) Ícone de folha Baixo CO2
  • northamerica-northeast2 (Toronto) Ícone de folha Baixo CO2
  • southamerica-east1 (São Paulo, Brasil) Ícone de folha Baixo CO2
  • southamerica-west1 (Santiago, Chile) Ícone de folha Baixo CO2
  • us-west2 (Los Angeles)
  • us-west3 (Salt Lake City)
  • us-west4 (Las Vegas)

Se você já criou um serviço do Cloud Run, é possível visualizar a região no painel do Cloud Run no console do Google Cloud.

Como recuperar o exemplo de código

Para recuperar o exemplo de código para uso, siga estas etapas:

  1. Clone o repositório de amostra na máquina local:

    Node.js

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

    Outra alternativa é fazer o download da amostra como um arquivo ZIP e extraí-lo.

  2. Mude para o diretório que contém o código de amostra do Cloud Run:

    Node.js

    cd nodejs-docs-samples/run/websockets/

Noções básicas sobre o código

O Socket.io é uma biblioteca que permite a comunicação bidirecional em tempo real entre o navegador e o servidor. Embora o Socket.io não seja uma implementação WebSocket, ele une a funcionalidade para fornecer uma API mais simples para vários protocolos de comunicação com recursos adicionais, como maior confiabilidade, reconexão automática e transmitir para todos ou um subconjunto de clientes.

Integração no lado do cliente

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

O cliente instancia uma nova instância de soquete para cada conexão. Como este exemplo é renderizado no servidor, o URL do servidor não precisa ser definido. A instância de soquete pode emitir e ouvir 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');
});

Integração no servidor

No servidor, o servidor Socket.io é inicializado e anexado ao servidor HTTP. Semelhante ao lado do cliente, uma vez que o servidor Socket.io faz uma conexão com o cliente, uma instância de soquete é criada para cada conexão que pode ser usada para emitir e ouvir mensagens. Ele também oferece uma interface fácil para criar "salas" ou um canal arbitrário em que os soquetes podem entrar e sair.

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

O Socket.io também fornece um adaptador Redis para transmitir eventos para todos os clientes, independentemente de qual servidor está veiculando o soquete. O Socket.io usa apenas o mecanismo Pub/Sub do Redis e não armazena dados.

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

O adaptador Redis do Socket.io pode reutilizar o cliente Redis usado para armazenar o histórico de mensagens da sala. Cada contêiner cria uma conexão com a instância do Redis, e o Cloud Run pode criar um grande número de instâncias de contêiner. Isso está dentro das 65.000 conexões compatíveis com o Redis. Se você precisa oferecer suporte a essa quantidade de tráfego, também precisa avaliar a capacidade do conector de acesso VPC sem servidor.

Reconexão

O Cloud Run tem um tempo limite máximo de 60 minutos. Portanto, você precisa adicionar a lógica de reconexão para possíveis tempos limite. Em alguns casos, o Socket.io tenta se reconectar automaticamente após eventos de desconexão ou erro de conexão. Não há garantia de que o cliente se reconectará à mesma instância de contêiner.

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

As instâncias de contêiner persistirão se houver uma conexão ativa até que todas as solicitações sejam encerradas ou expirem. Mesmo que você use a afinidade da sessão do Cloud Run, é possível que novas solicitações tenham a carga balanceada para contêineres ativos. Isso permite a redução do escalonamento horizontal. Se você estiver preocupado com um grande número de contêineres que persistem após um pico de tráfego, diminua o valor máximo de tempo limite para que os soquetes não utilizados sejam limpos com mais frequência.

Como enviar o serviço

  1. Crie uma instância do Memorystore para Redis:

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

    Substitua INSTANCE_ID pelo nome da instância, ou seja, my-redis-instance, e REGION_ID pela região de todos os seus recursos e serviços, ou seja, us-central1.

    A instância receberá automaticamente um intervalo de IP do intervalo de rede de serviço padrão. Neste tutorial, 1 GB de memória é usado para o cache local de mensagens na instância do Redis. Saiba mais sobre Como determinar o tamanho inicial de uma instância do Memorystore para seu caso de uso.

  2. Configure um conector de acesso VPC sem servidor:

    Para se conectar à instância do Redis, o serviço do Cloud Run (totalmente gerenciado) precisa acessar a rede VPC autorizada da instância do Redis.

    Cada conector VPC requer a própria sub-rede /28 para colocar instâncias de conector. Ele não pode sobrepor nenhuma reserva de endereço IP atual na rede VPC. Por exemplo, 10.8.0.0 (/28) funcionará na maioria dos novos projetos, ou é possível especificar outro intervalo de IP personalizado não utilizado, como 10.9.0.0 (/28). É possível ver quais intervalos de IP estão reservados no console do Google Cloud.

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

    Substitua CONNECTOR_NAME pelo nome do conector.

    Esse comando cria um conector na rede VPC padrão, igual à instância do Redis, com tamanho de máquina e2-micro. Aumentar o tamanho da máquina do conector melhora a capacidade do conector, mas também aumenta o custo. O conector também precisa estar na mesma região da instância do Redis. Saiba mais sobre Como configurar o acesso VPC sem servidor.

  3. Defina uma variável de ambiente com o endereço IP da rede autorizada da instância do Redis:

     export REDISHOST=$(gcloud redis instances describe INSTANCE_ID --region REGION --format "value(host)")
  4. Crie uma conta de serviço para servir como a identidade de serviço. Por padrão, isso não tem privilégios além da associação ao projeto.

    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. Crie e implante a imagem do contêiner no 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

    Responda a todas as solicitações para instalar as APIs necessárias respondendo ao y quando solicitado. Você só precisa fazer isso uma vez para um projeto. Responda a outras solicitações fornecendo a plataforma e a região, se você não tiver definido os padrões delas, conforme descrito na página de configuração. Saiba mais sobre como implantar a partir do código-fonte.

Testar

Para testar o serviço completo:

  1. Navegue até o URL fornecido pela etapa de implantação acima.

  2. Adicione seu nome e uma sala de chat para fazer login.

  3. Envie uma mensagem para a sala.

Se você optar por continuar desenvolvendo esses serviços, lembre-se de que eles têm acesso restrito do gerenciamento de identidade e acesso (IAM, na sigla em inglês) ao restante do Google Cloud e precisarão receber mais papéis do IAM para acessar muitos outros serviços.

Limpar

Se você criou um novo projeto para este tutorial, exclua o projeto. Se você usou um projeto atual e quer mantê-lo sem as alterações incluídas neste tutorial, exclua os recursos criados para o tutorial.

Como excluir o projeto

O jeito mais fácil de evitar cobranças é excluindo o projeto que você criou para o tutorial.

Para excluir o projeto:

  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.

Como excluir recursos do tutorial

  1. Exclua o serviço do Cloud Run que você implantou neste tutorial:

    gcloud run services delete SERVICE-NAME

    SERVICE-NAME é o nome escolhido do serviço.

    Também é possível excluir os serviços do Cloud Run no Console do Google Cloud.

  2. Remova a configuração da região padrão da gcloud que você adicionou durante a configuração do tutorial:

     gcloud config unset run/region
    
  3. Remova a configuração do projeto:

     gcloud config unset project
    
  4. Exclua outros recursos do Google Cloud criados neste tutorial:

A seguir