Como Usar WebSockets no Node.js para Criar Aplicações em Tempo Real

Este tutorial demonstra como usar WebSockets no Node.js para comunicação bidirecional e interativa entre um navegador e um servidor. A técnica é essencial para aplicativos rápidos e em tempo real, como painéis de instrumentos, aplicativos de chat e jogos multiplayer.

Table of Contents

A Web baseia-se em mensagens HTTP de requisição-resposta. Seu navegador faz uma solicitação de URL e um servidor responde com dados. Isso pode levar a mais solicitações do navegador e respostas do servidor para imagens, CSS, JavaScript etc., mas o servidor não pode enviar dados arbitrariamente para um navegador.

Técnicas de long polling Ajax podem fazer com que aplicativos web pareçam atualizar em tempo real, mas o processo é muito limitante para aplicações de tempo real verdadeiras. Polling a cada segundo seria ineficiente em certos momentos e muito lento em outros.

Após uma conexão inicial do navegador, eventos enviados pelo servidor são uma resposta HTTP padrão (transmitida) que pode enviar mensagens do servidor a qualquer momento. No entanto, o canal é unidirecional e o navegador não pode enviar mensagens de volta. Para uma comunicação bidirecional verdadeiramente rápida, você precisa de WebSockets.

Visão geral de WebSockets

O termo WebSocket refere-se a um protocolo de comunicação TCP sobre ws:// ou o wss:// seguro e criptografado. É diferente de HTTP, embora possa ser executado sobre a porta 80 ou 443 para garantir que funcione em locais que bloqueiam o tráfego não-web. A maioria dos navegadores lançados desde 2012 suporta o protocolo WebSocket.

Em uma aplicação web de tempo real típica, você deve ter pelo menos um servidor web para servir conteúdo web (HTML, CSS, JavaScript, imagens, etc.) e um servidor WebSocket para lidar com a comunicação bidirecional.

O navegador ainda faz uma solicitação inicial de WebSocket a um servidor, o que abre um canal de comunicação. Então, tanto o navegador quanto o servidor podem enviar uma mensagem por esse canal, o que aciona um evento no outro dispositivo.

Comunicação com outros navegadores conectados

Após a solicitação inicial, o navegador pode enviar e receber mensagens para/do servidor WebSocket. O servidor WebSocket pode enviar e receber mensagens para/de qualquer um de seus navegadores cliente conectados.

A comunicação peer-to-peer é não possível. O BrowserA não pode enviar mensagens diretamente ao BrowserB, mesmo quando ambos estão rodando no mesmo dispositivo e conectados ao mesmo servidor WebSocket! O BrowserA só pode enviar uma mensagem ao servidor e esperar que seja encaminhada para outros navegadores conforme necessário.

Suporte ao Servidor WebSocket

O Node.js ainda não possui suporte nativo a WebSocket, embora haja rumores de que isso está chegando em breve! Para este artigo, estou usando o módulo de terceiros ws, mas existem dezenas de outros.

O suporte a WebSocket embutido está disponível nos ambientes de execução JavaScript Deno e Bun.

Bibliotecas WebSocket estão disponíveis para ambientes de execução, incluindo PHP, Python e Ruby. Opções de SaaS de terceiros, como Pusher e PubNub, também oferecem serviços hospedados de WebSocket.

Demonstração Rápida de WebSockets

Aplicativos de chat são o Olá, Mundo! das demonstrações de WebSocket, então peço desculpas por:

  1. Ser pouco original. Dito isto, aplicativos de chat são ótimos para explicar os conceitos.

  2. Não ser capaz de fornecer uma solução online totalmente hospedada. Prefiro não ter que monitorar e moderar um fluxo de mensagens anônimas!

Clone ou faça o download do repositório node-wschat no GitHub:

git clone https://github.com/craigbuckler/node-wschat

Instale as dependências do Node.js:

cd node-wschat
npm install

Inicie o aplicativo de chat:

npm start

Abra http://localhost:3000/ em vários navegadores ou abas (você também pode definir seu nome de bate-papo na cadeia de consulta, como http://localhost:3000/?Craig). Digite algo em uma janela e pressione ENVIAR ou aperte Enter você verá aparecer em todos os navegadores conectados.

Visão geral do código Node.js

O arquivo de entrada index.js do aplicativo Node.js inicia dois servidores:

  1. Um aplicativo Express rodando em http://localhost:3000/ com um modelo EJS para servir uma única página com HTML, CSS e JavaScript do lado do cliente. O JavaScript do navegador usa a API WebSocket para fazer a conexão inicial e enviar e receber mensagens.

  2. A WebSocket server running at ws://localhost:3001/, which listens for incoming client connections, handles messages, and monitors disconnections. The full code:

    // Servidor WebSocket
    import WebSocket, { WebSocketServer } from 'ws';
    
    const ws = new WebSocketServer({ port: cfg.wsPort });
    
    // conexão do cliente
    ws.on('connection', (socket, req) => {
    
      console.log(`connection from ${ req.socket.remoteAddress }`);
    
      // mensagem recebida
      socket.on('message', (msg, binary) => {
    
        // transmitir para todos os clientes
        ws.clients.forEach(client => {
          client.readyState === WebSocket.OPEN && client.send(msg, { binary });
        });
    
      });
    
      // fechado
      socket.on('close', () => {
        console.log(`disconnection from ${ req.socket.remoteAddress }`);
      });
    
    });

O biblioteca ws do Node.js:

  • Dispara um evento "connection" quando um navegador deseja se conectar. A função manipuladora recebe um objeto socket usado para se comunicar com esse dispositivo individual. Deve ser mantido durante toda a vida útil da conexão.

  • Dispara um evento socket "message" quando um navegador envia uma mensagem. A função manipuladora transmite a mensagem de volta para todos os navegadores conectados (incluindo o que a enviou).

  • Dispara um evento socket "close" quando o navegador desconecta — geralmente quando a aba é fechada ou atualizada.

Visão Geral do Código JavaScript no Cliente

O arquivo static/main.js da aplicação executa a função wsInit() e passa o endereço do servidor WebSocket (domínio da página mais um valor de porta definido no modelo da página HTML):

wsInit(`ws://${ location.hostname }:${ window.cfg.wsPort }`);

// lidar com comunicação WebSocket
function wsInit(wsServer) {

  const ws = new WebSocket(wsServer);

  // conectar ao servidor
  ws.addEventListener('open', () => {
    sendMessage('entered the chat room');
  });

O evento open é acionado quando o navegador se conecta ao servidor WebSocket. A função manipuladora envia uma mensagem entrou na sala de bate-papo chamando sendMessage():

// enviar mensagem
function sendMessage(setMsg) {

  let
    name = dom.name.value.trim(),
    msg =  setMsg || dom.message.value.trim();

  name && msg && ws.send( JSON.stringify({ name, msg }) );

}

A função sendMessage() busca o nome do usuário e a mensagem do formulário HTML, embora a mensagem possa ser substituída por qualquer argumento setMsg passado. Os valores são convertidos em um objeto JSON e enviados ao servidor WebSocket usando o método ws.send().

O servidor WebSocket recebe a mensagem recebida que aciona o manipulador "message" (veja acima) e a transmite de volta a todos os navegadores. Isso aciona um evento "message" em cada cliente:

// receber mensagem
ws.addEventListener('message', e => {

  try {

    const
      chat = JSON.parse(e.data),
      name = document.createElement('div'),
      msg  = document.createElement('div');

    name.className = 'name';
    name.textContent = (chat.name || 'unknown');
    dom.chat.appendChild(name);

    msg.className = 'msg';
    msg.textContent = (chat.msg || 'said nothing');
    dom.chat.appendChild(msg).scrollIntoView({ behavior: 'smooth' });

  }
  catch(err) {
    console.log('invalid JSON', err);
  }

});

O manipulador recebe os dados JSON transmitidos no atributo .data do objeto de evento. A função analisa-os para um objeto JavaScript e atualiza a janela do bate-papo.

Finalmente, novas mensagens são enviadas usando a função sendMessage() sempre que o manipulador "submit" do formulário é acionado:

// envio de formulário
dom.form.addEventListener('submit', e => {
  e.preventDefault();
  sendMessage();
  dom.message.value = '';
  dom.message.focus();
}, false);

Tratamento de erros

Um evento "error" é acionado quando a comunicação WebSocket falha. Isso pode ser tratado no servidor:

// lidar com erro WebSocket no servidor
socket.on('error', e => {
  console.log('WebSocket error:', e);
});

e/ou no cliente:

// lidar com erro WebSocket no cliente
ws.addEventListener('error', e => {
  console.log('WebSocket error:', e);
})

Apenas o cliente pode reestabelecer a conexão executando novamente o construtor new WebSocket().

Encerramento de conexões

Qualquer dispositivo pode fechar o WebSocket a qualquer momento usando o método .close() da conexão. Você pode optar por fornecer argumentos inteiro code e string reason (máximo de 123 bytes), que são transmitidos ao outro dispositivo antes de desconectar.

WebSockets avançados

Gerenciar WebSockets é fácil no Node.js: um dispositivo envia uma mensagem usando o método .send(), que aciona um evento "message" no outro. Como cada dispositivo cria e responde a essas mensagens pode ser mais desafiador. As seções a seguir descrevem questões que você pode precisar considerar.

Segurança WebSocket

O protocolo WebSocket não lida com autorização ou autenticação. Você não pode garantir que uma solicitação de comunicação recebida venha de um navegador ou de um usuário conectado ao seu aplicativo web — especialmente quando os servidores web e WebSocket podem estar em dispositivos diferentes. A conexão inicial recebe um cabeçalho HTTP contendo cookies e o servidor Origin, mas é possível falsificar esses valores.

A técnica a seguir garante que você restrinja as comunicações WebSocket a usuários autorizados:

  1. Antes de fazer a solicitação inicial do WebSocket, o navegador entra em contato com o servidor HTTP (talvez usando Ajax).

  2. O servidor verifica as credenciais do usuário e retorna um novo ticket de autorização. O ticket normalmente faz referência a um registro no banco de dados contendo o ID do usuário, endereço IP, hora da solicitação, tempo de expiração da sessão e quaisquer outros dados necessários.

  3. O navegador passa o ticket para o servidor WebSocket na mão inicial.

  4. O servidor WebSocket verifica o ticket e verifica fatores como o endereço IP, tempo de expiração, etc. antes de permitir a conexão. Ele executa o método WebSocket .close() quando um ticket é inválido.

  5. O servidor WebSocket pode precisar verificar o registro do banco de dados de vez em quando para garantir que a sessão do usuário permaneça válida.

Importante, sempre validar dados de entrada:

  • Como o HTTP, o servidor WebSocket é suscetível a injeção de SQL e outros ataques.

  • O cliente nunca deve injetar valores brutos no DOM ou avaliar código JavaScript.

Separar vs múltiplas instâncias do servidor WebSocket

Considere um jogo multiplayer online. O jogo tem muitos universos jogando instâncias separadas do jogo: universeA, universeB e universeC. Um jogador se conecta a um único universo:

  • universeA: ingressado por player1, player2 e player3
  • universeB: ingressado por player99

Você poderia implementar o seguinte:

  1. A separate WebSocket server for each universe.

    A player action in universeA would never be seen by those in universeB. However, launching and managing separate server instances could be difficult. Would you stop universeC because it has no players, or continue to manage that resource?

  2. Utilizar um único servidor WebSocket para todos os universos do jogo.

    Isso consome menos recursos e é mais fácil de gerenciar, mas o servidor WebSocket deve registrar em qual universo cada jogador se junta. Quando player1 realiza uma ação, ela deve ser transmitida para player2 e player3 mas não para player99.

Múltiplos servidores WebSocket

O exemplo de aplicativo de chat pode lidar com centenas de usuários simultâneos, mas falhará quando a popularidade e o uso de memória ultrapassarem limites críticos. Eventualmente, será necessário escalar horizontalmente adicionando mais servidores.

Cada servidor WebSocket pode gerenciar apenas seus próprios clientes conectados. Uma mensagem enviada de um navegador para serverX não poderia ser transmitida aos conectados a serverY. Pode ser necessário implementar sistemas de mensagens de editor–assinante (pub-sub) no backend. Por exemplo:

  1. O WebSocket serverX deseja enviar uma mensagem a todos os clientes. Ele publica a mensagem no sistema pub–sub.

  2. Todos os servidores WebSocket inscritos no sistema pub–sub recebem um evento de nova mensagem (incluindo serverX). Cada um pode lidar com a mensagem e transmiti-la aos seus clientes conectados.

Eficiência de mensagens WebSocket

A comunicação WebSocket é rápida, mas o servidor deve gerenciar todos os clientes conectados. Você deve considerar a mecânica e a eficiência das mensagens, especialmente ao criar jogos de ação multijogador:

  • Como você sincroniza as ações de um jogador em todos os dispositivos dos clientes?

  • Se player1 estiver em uma localização diferente de player2, é necessário enviar a player2 informações sobre ações que eles não podem ver?

  • Como lidar com a latência da rede — ou atraso na comunicação? Alguém com uma máquina rápida e conexão teria uma vantagem injusta?

Jogos rápidos devem fazer compromissos. Pense nisso como jogar o jogo em seu dispositivo local, mas alguns objetos são influenciados pelas atividades dos outros. Em vez de enviar a posição exata de cada objeto o tempo todo, os jogos geralmente enviam mensagens mais simples e menos frequentes. Por exemplo:

  • objectX apareceu no pontoX
  • objectY tem uma nova direção e velocidade
  • objectZ foi destruído

Cada jogo de cliente preenche as lacunas. Quando objectZ explode, não importará se a explosão parecer diferente em cada dispositivo.

Conclusão

Node.js facilita o gerenciamento de WebSockets. Não necessariamente torna aplicativos em tempo real mais fáceis de projetar ou codificar, mas a tecnologia não o impedirá!

As principais desvantagens:

  • WebSockets exigem sua própria instância de servidor separada. Requisições Ajax Fetch() e eventos enviados pelo servidor podem ser tratados pelo servidor web que você já está executando.

  • Os servidores WebSocket exigem suas próprias verificações de segurança e autorização.

  • Conexões WebSocket perdidas devem ser reestabelecidas manualmente.

Mas não deixe isso desanimá-lo!

Perguntas Frequentes (FAQs) sobre Aplicativos em Tempo Real com WebSockets e Eventos Enviados pelo Servidor

De que forma WebSockets diferem do HTTP em termos de desempenho e funcionalidade?

WebSockets oferecem um canal de comunicação full-duplex sobre uma única conexão TCP, o que significa que dados podem ser enviados e recebidos simultaneamente. Isso representa uma melhoria significativa em relação ao HTTP, onde cada requisição requer uma nova conexão. WebSockets também permitem a transferência de dados em tempo real, tornando-os ideais para aplicativos que exigem atualizações instantâneas, como aplicativos de chat ou atualizações de esportes ao vivo. Por outro lado, o HTTP é sem estado e cada par de requisição-resposta é independente, o que pode ser mais adequado para aplicativos onde atualizações em tempo real não são necessárias.

Você pode explicar o ciclo de vida de uma conexão WebSocket?

O ciclo de vida de uma conexão WebSocket começa com um handshake, que atualiza uma conexão HTTP para uma conexão WebSocket. Uma vez estabelecida a conexão, dados podem ser enviados e recebidos entre o cliente e o servidor até que qualquer uma das partes decida fechar a conexão. A conexão pode ser fechada pelo cliente ou servidor enviando um frame de fechamento, seguido pela outra parte reconhecendo o frame de fechamento.

Como posso implementar WebSockets em um aplicativo Android?

Implementar WebSockets em um aplicativo Android envolve criar um cliente WebSocket que possa conectar-se a um servidor WebSocket. Isso pode ser feito usando bibliotecas como OkHttp ou Scarlet. Uma vez configurado o cliente, você pode abrir uma conexão com o servidor, enviar e receber mensagens e lidar com diferentes eventos, como abertura da conexão, recebimento de mensagens e fechamento da conexão.

O que são Eventos Enviados por Servidor e como eles se comparam aos WebSockets?

Os Eventos Enviados pelo Servidor (SSE) são um padrão que permite que um servidor envie atualizações para um cliente por meio do HTTP. Ao contrário dos WebSockets, os SSE são unidirecionais, o que significa que permitem apenas que os dados sejam enviados do servidor para o cliente. Isso os torna menos adequados para aplicações que requerem comunicação bidirecional, mas podem ser uma solução mais simples e eficiente para aplicações que precisam apenas de atualizações do servidor.

Quais são alguns casos de uso comuns para WebSockets e Eventos Enviados pelo Servidor?

Os WebSockets são comumente utilizados em aplicações que exigem comunicação em tempo real e bidirecional, como aplicativos de chat, jogos multiplayer e ferramentas colaborativas. Por outro lado, os Eventos Enviados pelo Servidor são frequentemente usados em aplicações que precisam de atualizações em tempo real do servidor, como atualizações de notícias ao vivo, atualizações de preços das ações ou relatórios de progresso para tarefas de longa execução.

Como posso lidar com conexões WebSocket em uma aplicação Spring Boot?

O Spring Boot oferece suporte para comunicação WebSocket através do módulo Spring WebSocket. Você pode usar a anotação @EnableWebSocket para habilitar o suporte a WebSocket e, em seguida, definir um WebSocketHandler para gerenciar o ciclo de vida da conexão e o tratamento de mensagens. Você também pode usar o SimpMessagingTemplate para enviar mensagens aos clientes conectados.

Quais são as considerações de segurança ao usar WebSockets?

Como qualquer outra tecnologia web, os WebSockets podem ser vulneráveis a várias ameaças de segurança, como o Hijacking de WebSocket Cross-Site (CSWSH) e ataques de Negação de Serviço (DoS). Para mitigar esses riscos, você deve sempre usar conexões WebSocket seguras (wss://) e validar e sanitizar todos os dados recebidos. Também é recomendável considerar o uso de mecanismos de autenticação e autorização para controlar o acesso ao seu servidor WebSocket.

Posso usar WebSockets com uma API REST?

Sim, você pode usar WebSockets em conjunto com uma API REST. Embora as APIs REST sejam ótimas para comunicação sem estado de solicitação-resposta, os WebSockets podem ser usados para comunicação em tempo real, bidirecional. Isso pode ser particularmente útil em aplicativos que exigem atualizações instantâneas, como aplicativos de chat ou atualizações de esportes ao vivo.

Como posso testar um servidor WebSocket?

Existem várias ferramentas disponíveis para testar servidores WebSocket, como o Echo Test do WebSocket.org ou o Postman. Essas ferramentas permitem abrir uma conexão WebSocket a um servidor, enviar mensagens e receber respostas. Você também pode escrever testes automatizados para seu servidor WebSocket usando bibliotecas como Jest ou Mocha.

Quais são as limitações dos WebSockets e Eventos Enviados por Servidor?

Embora os WebSockets e Eventos Enviados por Servidor ofereçam capacidades poderosas para comunicação em tempo real, eles também têm suas limitações. Por exemplo, nem todos os navegadores e redes suportam essas tecnologias, e elas podem consumir uma quantidade significativa de recursos se não forem gerenciadas adequadamente. Além disso, elas podem ser mais complexas de implementar e gerenciar em comparação com a comunicação HTTP tradicional.

Source:
https://www.sitepoint.com/real-time-apps-websockets-server-sent-events/