Criar um serviço de chat WebSocket para o tutorial do Cloud Run


Este tutorial mostra como criar um serviço de chat em tempo real com várias salas através de WebSockets com uma ligação persistente para comunicação bidirecional. Com os WebSockets, o cliente e o servidor podem enviar mensagens entre si sem sondar o servidor para atualizações.

Embora possa configurar o Cloud Run para usar a afinidade de sessão, esta oferece uma afinidade de melhor esforço, o que significa que qualquer novo pedido pode continuar a ser potencialmente encaminhado para uma instância diferente. Consequentemente, as mensagens dos utilizadores no serviço de chat têm de ser sincronizadas em todas as instâncias e não apenas entre os clientes ligados a uma instância.

Vista geral do design

Este serviço de chat de exemplo usa uma instância do Memorystore para Redis para armazenar e sincronizar mensagens do utilizador em todas as instâncias. O Redis usa um mecanismo Pub/Sub, que não deve ser confundido com o produto Cloud Pub/Sub, para enviar dados para clientes subscritos ligados a qualquer instância, de modo a eliminar a sondagem HTTP para atualizações.

No entanto, mesmo com as atualizações push, qualquer instância que seja iniciada só recebe novas mensagens enviadas para o contentor. Para carregar mensagens anteriores, o histórico de mensagens teria de ser armazenado e obtido a partir de uma solução de armazenamento persistente. Este exemplo usa a funcionalidade convencional de uma loja de objetos do Redis para colocar em cache e obter o histórico de mensagens.

Diagrama arquitetónico
O diagrama mostra várias ligações de clientes a cada instância do Cloud Run. Cada instância liga-se a uma instância do Memorystore para Redis através de um conetor do Acesso a VPC sem servidor.

A instância do Redis está protegida da Internet através de IPs privados com acesso controlado e limitado a serviços executados na mesma rede privada virtual que a instância do Redis. Por conseguinte, é necessário um conector de acesso à VPC sem servidor para que o serviço do Cloud Run se ligue ao Redis. Saiba mais sobre o acesso a VPC sem servidor.

Limitações

  • Este tutorial não mostra a autenticação do utilizador final nem o armazenamento em cache da sessão. Para saber mais sobre a autenticação do utilizador final, consulte o tutorial do Cloud Run sobre a autenticação do utilizador final.

  • Este tutorial não implementa uma base de dados, como o Firestore, para o armazenamento e a obtenção indefinidos do histórico de mensagens do chat.

  • São necessários elementos adicionais para que este serviço de exemplo esteja pronto para produção. Recomendamos uma instância do Redis de nível padrão para oferecer alta disponibilidade através da replicação e da comutação por falha automática.

Objetivos

  • Escreva, crie e implemente um serviço do Cloud Run que use WebSockets.

  • Estabeleça ligação a uma instância do Memorystore for Redis para publicar e subscrever novas mensagens em várias instâncias.

  • Associe o serviço do Cloud Run ao Memorystore através de conetores do Acesso a VPC sem servidor.

Custos

Neste documento, usa os seguintes componentes faturáveis do Google Cloud:

Para gerar uma estimativa de custos com base na sua utilização projetada, use a calculadora de preços.

Os novos Google Cloud utilizadores podem ser elegíveis 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. 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.

    Go to project selector

  5. Verify 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.
  8. Funções necessárias

    Para receber as autorizações de que precisa para concluir o tutorial, peça ao seu administrador para lhe conceder as seguintes funções da IAM no seu projeto:

    Para mais informações sobre a atribuição de funções, consulte o artigo Faça a gestão do acesso a projetos, pastas e organizações.

    Também pode conseguir as autorizações necessárias através de funções personalizadas ou outras funções predefinidas.

Configurar predefinições do gcloud

Para configurar o gcloud com as predefinições do seu serviço do Cloud Run:

  1. Defina o projeto predefinido:

    gcloud config set project PROJECT_ID

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

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

    gcloud config set run/region REGION

    Substitua REGION pela região do Cloud Run suportada à sua escolha.

Localizações do Cloud Run

O Cloud Run é regional, o que significa que a infraestrutura que executa os seus serviços do Cloud Run está localizada numa região específica e é gerida pela Google para estar disponível de forma redundante em todas as zonas dessa região.

O cumprimento dos requisitos de latência, disponibilidade ou durabilidade são fatores principais para selecionar a região onde os seus serviços do Cloud Run são executados. Geralmente, pode selecionar a região mais próxima dos seus utilizadores, mas deve considerar a localização dos outros Google Cloudprodutos usados pelo seu serviço do Cloud Run. A utilização Google Cloud de produtos em conjunto em várias localizações pode afetar a latência do seu serviço, bem como o custo.

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

Sujeito aos preços de Nível 1

  • asia-east1 (Taiwan)
  • asia-northeast1 (Tóquio)
  • asia-northeast2 (Osaca)
  • asia-south1 (Mumbai, Índia)
  • europe-north1 (Finlândia) ícone de folha Baixo CO2
  • europe-north2 (Estocolmo) ícone de folha Baixo CO2
  • europe-southwest1 (Madrid) í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 (Telavive)
  • northamerica-south1 (México)
  • us-central1 (Iowa) ícone de folha Baixo CO2
  • us-east1 (Carolina do Sul)
  • us-east4 (Virgínia do Norte)
  • us-east5 (Columbus)
  • us-south1 (Dallas) ícone de folha Baixo CO2
  • us-west1 (Oregão) ícone de folha Baixo CO2

Sujeito aos preços de Nível 2

  • africa-south1 (Joanesburgo)
  • asia-east2 (Hong Kong)
  • asia-northeast3 (Seul, Coreia do Sul)
  • asia-southeast1 (Singapura)
  • asia-southeast2 (Jacarta)
  • asia-south2 (Deli, Í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)
  • europe-west6 (Zurique, Suíça) ícone de folha Baixo CO2
  • me-central1 (Doha)
  • me-central2 (Dammam)
  • 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 já criou um serviço do Cloud Run, pode ver a região no painel de controlo do Cloud Run na Google Cloud consola.

Obter o exemplo de código

Para obter o exemplo de código para utilização:

  1. Clone o repositório de exemplo para a sua máquina local:

    Node.js

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

    Em alternativa, pode transferir o exemplo como um ficheiro ZIP e extraí-lo.

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

    Node.js

    cd nodejs-docs-samples/run/websockets/

Compreender 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 do WebSocket, envolve a funcionalidade para fornecer uma API mais simples para vários protocolos de comunicação com funcionalidades adicionais, como fiabilidade melhorada, reconexão automática e transmissão para todos ou um subconjunto de clientes.

Integração do lado do cliente

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

O cliente instancia uma nova instância de Socket para cada ligação. Uma vez que este exemplo é renderizado do lado do servidor, não é necessário definir o URL do servidor. A instância de socket 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 do lado do servidor

Do lado do servidor, o servidor Socket.io é inicializado e anexado ao servidor HTTP. Tal como no lado do cliente, assim que o servidor Socket.io estabelece uma ligação ao cliente, é criada uma instância de socket para cada ligação, que pode ser usada para emitir e ouvir mensagens. O Socket.io também oferece uma interface simples para criar "salas" ou um canal arbitrário ao qual os sockets podem aderir 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 a todos os clientes, independentemente do servidor que está a servir o socket. 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 contentor cria uma ligação à instância do Redis e o Cloud Run pode criar um grande número de instâncias. Este valor está muito abaixo das 65 000 ligações que o Redis pode suportar. Se precisar de suportar esta quantidade de tráfego, também tem de avaliar o débito do conetor do Acesso a VPC sem servidor.

Nova associação

O Cloud Run tem um limite de tempo máximo de 60 minutos. Por isso, tem de adicionar uma lógica de nova ligação para possíveis limites de tempo. Em alguns casos, o Socket.io tenta automaticamente restabelecer a ligação após eventos de erro de ligação ou de desligamento. Não existe garantia de que o cliente volte a estabelecer ligação à mesma instância.

// 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 persistem se existir uma ligação ativa até que todos os pedidos sejam fechados ou excedam o tempo limite. Mesmo que use a afinidade de sessão do Cloud Run, é possível que os novos pedidos sejam equilibrados para contentores ativos, o que permite o dimensionamento dos contentores. Se estiver preocupado com a persistência de um grande número de contentores após um pico de tráfego, pode diminuir o valor máximo de tempo limite para que os soquetes não usados sejam limpos com mais frequência.

Envio do serviço

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

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

    Substitua INSTANCE_ID pelo nome da instância, como my-redis-instance, e REGION_ID pela região de todos os seus recursos e serviços, por exemplo, europe-west1.

    À instância é automaticamente atribuído um intervalo de IP do intervalo de rede de serviços predefinido. Este tutorial usa 1 GB de memória para a cache local de mensagens na instância do Redis. Saiba como determinar o tamanho inicial de uma instância do Memorystore para o seu exemplo de utilização.

  2. Configure um conetor do Acesso a VPC sem servidor:

    Para se ligar à sua instância do Redis, o serviço do Cloud Run precisa de acesso à rede VPC autorizada da instância do Redis.

    Cada conetor de VPC requer a sua própria sub-rede /28 para colocar instâncias do conetor. Este intervalo de IP não pode sobrepor-se a nenhuma reserva de endereço IP existente na sua rede VPC. Por exemplo, 10.8.0.0 (/28) funciona na maioria dos novos projetos ou pode especificar outro intervalo de IP personalizado não usado, como 10.9.0.0 (/28). Pode ver que intervalos de IP estão atualmente reservados na Google Cloud consola.

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

    Substitua CONNECTOR_NAME pelo nome do conetor.

    Este comando cria um conetor na rede da VPC predefinida, tal como a instância do Redis, com o tamanho da máquina e2-micro. Aumentar o tamanho da máquina do conector pode melhorar o débito do conector, mas também aumenta o custo. O conetor também tem de estar na mesma região que a instância do Redis. Saiba mais sobre a configuração do acesso a VPC sem servidor.

  3. Defina uma variável de ambiente com o endereço IP da rede autorizada da sua 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 identidade do serviço. Por predefinição, 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 implemente a imagem de contentor 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 instruções para instalar as APIs necessárias respondendo y quando lhe for pedido. Só tem de fazer isto uma vez por projeto. Responda a outras instruções fornecendo a plataforma e a região, se não tiver definido predefinições para estas, conforme descrito na página de configuração. Saiba mais sobre a implementação a partir do código fonte.

Experimentar

Para experimentar o serviço completo:

  1. Navegue no navegador para o URL fornecido no passo de implementação acima.

  2. Adicione o seu nome e uma sala de chat para iniciar sessão.

  3. Envie uma mensagem para a sala!

Se optar por continuar a desenvolver estes serviços, lembre-se de que têm acesso restrito à gestão de identidade e acesso (IAM) ao resto do Google Cloud e terão de receber funções adicionais de IAM para aceder a muitos outros serviços.

Limpar

Se criou um novo projeto para este tutorial, elimine o projeto. Se usou um projeto existente e quer mantê-lo sem as alterações adicionadas neste tutorial, elimine os recursos criados para o tutorial.

Eliminar o projeto

A forma mais fácil de eliminar a faturação é eliminar o projeto que criou para o tutorial.

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

Eliminar recursos do tutorial

  1. Elimine o serviço do Cloud Run que implementou neste tutorial:

    gcloud run services delete SERVICE-NAME

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

    Também pode eliminar serviços do Cloud Run a partir da Google Cloud consola.

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

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

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

O que se segue?