WebSocket no Node.js: servidor, cliente, heartbeat e reconexão
WebSocket no Node.js: servidor ws, cliente nativo, heartbeat | We.Inc
Crie um servidor WebSocket no Node.js com o pacote ws, conecte com o cliente WebSocket nativo do Node, detecte sockets mortos com ping e pong e reconecte com backoff.
O Node.js não tem um servidor WebSocket nativo, então a maioria dos projetos instala o pacote ws e cria um WebSocketServer. Para o lado do cliente, o Node traz um WebSocket global compatível com o do navegador, adicionado na v21.0.0 e na v20.10.0, liberado sem flag na v22.0.0 e marcado como estável na v22.4.0. Adicione um heartbeat com ping e pong e reconexão com backoff antes de ir para produção.
Qual peça faz o quê
Tarefa
Use
De onde vem
Aceitar conexões
new WebSocketServer({ port })
pacote ws (npm install ws)
Conectar a partir do Node
new WebSocket(url)
Global no Node 22 e posteriores
Conectar a partir de um navegador
new WebSocket(url)
Nativo em todo navegador moderno
Encontrar sockets mortos
ws.ping() mais um listener de pong
pacote ws
Sobreviver a reinícios
Reconectar com backoff exponencial
Seu próprio código de cliente
Este guia monta um pequeno servidor no estilo chat e os clientes que conversam com ele, nesta ordem: um servidor que ecoa e transmite mensagens, um script Node que conecta sem nenhuma dependência, um heartbeat que descarta conexões que morreram em silêncio e um cliente que volta depois que o servidor reinicia. A calculadora abaixo mostra como os atrasos entre tentativas crescem antes de você colocá-los no código.
Rode npm install ws em um projeto cujo package.json tenha "type": "module", para que a sintaxe de import dos exemplos funcione.
Inicie o arquivo do servidor com node server.js. Ele escuta na porta 8080 e registra cada mensagem.
Rode o arquivo do cliente em um segundo terminal com Node 22 ou mais recente, ou abra o console do navegador e cole o mesmo código.
Pare o servidor com Ctrl+C e inicie de novo para ver o cliente de reconexão esperar, tentar de novo e se recuperar.
Sempre adicione um listener de error a cada socket do lado do servidor. O ws emite eventos de erro, e um não tratado pode derrubar o processo.
Verifique req.headers.origin no handler de conexão e feche sockets de origens que você não atende; os navegadores enviam esse cabeçalho, inclusive em páginas de atacantes.
Proxies reversos cortam sockets ociosos. Por padrão, o nginx fecha um WebSocket intermediado se o backend não enviar nada por 60 segundos, então mantenha o intervalo de ping abaixo disso.
Adicione jitter aleatório aos atrasos de reconexão. Sem ele, todos os clientes tentam no mesmo instante depois de um reinício e atingem o servidor juntos.
Loops de broadcast devem pular clientes cujo readyState não seja OPEN, porque enviar para um socket fechando não serve para nada.
Perguntas frequentes
O Node.js tem WebSocket nativo?
Só o cliente. A classe global WebSocket apareceu na v21.0.0 e na v20.10.0 atrás da flag --experimental-websocket, perdeu a flag na v22.0.0 e deixou de ser experimental na v22.4.0. Ainda não existe um servidor nativo, e por isso o ws continua sendo a escolha usual.
Devo usar ws ou Socket.IO?
O ws fala o protocolo WebSocket puro, então qualquer cliente padrão, incluindo o navegador e o global do Node, consegue conectar. O Socket.IO adiciona o próprio protocolo por cima, com salas e reconexão automática, mas exige a própria biblioteca de cliente do outro lado.
Por que conexões morrem sem um evento close?
Se um caminho de rede desaparece, como quando a tampa de um notebook é fechada, nenhum frame de close é enviado e o servidor mantém um socket meio aberto. O heartbeat encontra esses casos: um socket que não respondeu ao último ping é encerrado na rodada seguinte.
Um cliente no navegador pode enviar pings?
Não. A API do navegador só tem send() e close(). Os navegadores respondem sozinhos aos pings do servidor com pongs, e por isso o heartbeat do lado do servidor também funciona com clientes no navegador.
Como compartilho uma porta com um servidor HTTP?
Crie o servidor HTTP você mesmo, por exemplo com Express ou node:http, e passe-o como new WebSocketServer({ server }). Requisições de upgrade vão para o ws e requisições normais vão para a sua aplicação.
Como uma conexão começa
Um WebSocket nasce como um HTTP GET com Upgrade: websocket. O servidor ws verifica a requisição, responde 101 Switching Protocols e dispara o evento connection com o novo socket e a requisição original. Qualquer coisa que você queira do handshake, como um token na query string ou o cabeçalho Origin, é lida dessa requisição.
Heartbeats em palavras simples
O protocolo diz que um endpoint que recebe um ping precisa responder com um pong com os mesmos dados. O servidor marca cada socket como vivo quando um pong chega e, em um timer, limpa a marca e envia ping de novo. Qualquer socket ainda sem marca no tick seguinte nunca respondeu, então o servidor o encerra.
O README do ws usa um timer de 30 segundos para isso. Escolha um valor com folga abaixo do timeout de inatividade de qualquer proxy na sua frente.
Reconectar sem martelar o servidor
Um bom cliente espera um pouco depois da primeira queda, depois mais a cada falha, até um teto, e zera o contador quando uma conexão abre. Espalhar as esperas de forma aleatória evita que uma multidão de clientes volte toda ao mesmo tempo.
Passo 1: um servidor com ws
server.js: eco e broadcast
Cada mensagem é reenviada para todos os clientes abertos. O listener de error impede que um socket com problema derrube o processo.
Passo 2: um cliente sem dependências
client.js: o WebSocket global do Node
Roda sem mudanças no Node 22 ou posterior e no console do navegador, porque os dois expõem a mesma classe WebSocket.
Passo 3: descarte conexões mortas com ping e pong
Nossa versão do padrão do README do ws: marca como vivo no pong, encerra o que perdeu a última rodada.
Passo 4: um cliente que reconecta
reconnect.js: backoff exponencial com jitter
O atraso dobra a cada tentativa falha até um teto, recebe uma variação aleatória e zera quando uma conexão abre.
Atrás do nginx
O nginx só repassa o upgrade quando você mesmo encaminha os cabeçalhos Upgrade e Connection. Antes do nginx 1.29.7 você também precisa de proxy_http_version 1.1.
location do nginx para o socket
proxy_read_timeout aumenta o limite de inatividade de 60 segundos; o heartbeat acima mantém o tráfego fluindo de qualquer forma.
Calculadora de atraso de reconexão
Defina um primeiro atraso, um multiplicador e um teto para ver quanto tempo um cliente espera antes de cada nova tentativa, e a menor e a maior espera quando o jitter é adicionado.
Primeiro atraso (ms)
Multiplicador
Teto (ms)
Tentativas a mostrar
Adicionar jitter (esperar entre metade e o atraso inteiro)