WebSockets do zero ao deploy com autenticação e reconexão automática
1. Fundamentos do WebSocket e seu lugar na comunicação em tempo real
O WebSocket é um protocolo de comunicação bidirecional full-duplex sobre uma única conexão TCP, definido pela RFC 6455. Diferente do HTTP, que segue o modelo request-response, o WebSocket permite que servidor e cliente troquem mensagens a qualquer momento, sem polling constante.
Diferenças cruciais entre tecnologias de tempo real:
- HTTP Polling: Cliente faz requisições repetidas ao servidor. Ineficiente, alta latência e overhead de cabeçalhos.
- Server-Sent Events (SSE): Comunicação unidirecional (servidor → cliente). Ideal para notificações, feeds de dados, mas não permite que o cliente envie dados.
- WebSocket: Bidirecional, baixa latência, overhead mínimo após o handshake. Ideal para chats, jogos multiplayer, dashboards financeiros, colaboração em tempo real.
O handshake de upgrade:
O WebSocket começa como uma requisição HTTP, depois é "upgraded" para o protocolo WebSocket. Exemplo do handshake do lado do servidor:
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Resposta do servidor:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Estrutura de frames: Cada mensagem WebSocket é encapsulada em frames com opcode (texto, binário, close, ping, pong), payload e máscara (cliente → servidor). O controle de fluxo é feito via frames de close e ping/pong.
2. Implementação do servidor WebSocket com autenticação
A autenticação no WebSocket deve ocorrer durante o handshake, pois após o upgrade não há mais cabeçalhos HTTP. Utilizamos middleware que valida tokens JWT ou cookies antes de aceitar a conexão.
Exemplo de servidor com autenticação JWT (Node.js + ws):
const WebSocket = require('ws');
const jwt = require('jsonwebtoken');
const wss = new WebSocket.Server({ port: 8080 });
const connections = new Map(); // userId -> WebSocket
wss.on('connection', (ws, req) => {
// Extrair token da query string ou cookie
const token = new URL(req.url, 'http://localhost').searchParams.get('token');
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
ws.userId = decoded.userId;
connections.set(ws.userId, ws);
ws.on('message', (message) => {
console.log(`Recebido de ${ws.userId}: ${message}`);
});
ws.on('close', () => {
connections.delete(ws.userId);
});
} catch (err) {
ws.close(4001, 'Autenticação falhou');
}
});
Gerenciamento de sessões e mapas de conexões: Mantenha um mapa userId → WebSocket para enviar mensagens diretas. Para múltiplas conexões por usuário, use userId → Set<WebSocket>.
Tratamento de erros de autenticação: Feche a conexão com código 4001 (ou 4000-4999 para códigos personalizados) e mensagem descritiva. Nunca exponha detalhes internos do erro.
3. Cliente WebSocket com suporte a reconexão automática inteligente
Reconexão automática é essencial para resiliência. Estratégias comuns incluem backoff exponencial com jitter para evitar "tempestade de reconexões" quando o servidor cai.
Implementação da classe cliente:
class ReconnectingWebSocket {
constructor(url, options = {}) {
this.url = url;
this.maxRetries = options.maxRetries || 10;
this.baseDelay = options.baseDelay || 1000;
this.maxDelay = options.maxDelay || 30000;
this.retryCount = 0;
this.connect();
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
this.retryCount = 0;
this.startHeartbeat();
};
this.ws.onclose = (event) => {
this.stopHeartbeat();
if (this.retryCount < this.maxRetries) {
const delay = this.getBackoffDelay();
setTimeout(() => {
this.retryCount++;
this.connect();
}, delay);
}
};
this.ws.onerror = (err) => {
console.error('Erro WebSocket:', err);
};
}
getBackoffDelay() {
const exponential = Math.min(this.maxDelay, this.baseDelay * Math.pow(2, this.retryCount));
const jitter = Math.random() * 1000;
return exponential + jitter;
}
startHeartbeat() {
this.heartbeatInterval = setInterval(() => {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.ping();
}
}, 30000);
}
stopHeartbeat() {
clearInterval(this.heartbeatInterval);
}
send(data) {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(data);
}
}
}
Heartbeat e ping/pong: Envie pings periódicos (ex: a cada 30s). Se o servidor não responder com pong dentro de um timeout (ex: 10s), considere a conexão perdida e reinicie.
4. Roteamento, canais e broadcast de mensagens
Organize conexões em canais/salas para enviar mensagens seletivamente. Implemente um sistema pub/sub interno.
Exemplo de roteamento de mensagens:
class RoomManager {
constructor() {
this.rooms = new Map(); // roomName -> Set<WebSocket>
}
join(ws, roomName) {
if (!this.rooms.has(roomName)) {
this.rooms.set(roomName, new Set());
}
this.rooms.get(roomName).add(ws);
}
leave(ws, roomName) {
const room = this.rooms.get(roomName);
if (room) {
room.delete(ws);
if (room.size === 0) this.rooms.delete(roomName);
}
}
broadcast(roomName, message, senderWs = null) {
const room = this.rooms.get(roomName);
if (!room) return;
const data = JSON.stringify({ type: 'broadcast', room: roomName, message });
room.forEach(ws => {
if (ws !== senderWs && ws.readyState === WebSocket.OPEN) {
ws.send(data);
}
});
}
unicast(userId, message) {
const ws = connections.get(userId);
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'unicast', message }));
}
}
}
Tipos de mensagens: Use um campo type no JSON para rotear: chat, notification, system, error. O servidor pode rejeitar mensagens de tipos não autorizados.
5. Segurança e boas práticas no deploy
Proteção contra ataques comuns:
- CSRF em WebSocket: Utilize tokens únicos por sessão (enviados via cookie HTTP-only e validados no handshake). Nunca confie apenas na origem.
- Injeção de payload: Valide e sanitize todas as mensagens recebidas. Limite tamanho máximo (ex: 1MB por mensagem).
- DoS via conexões: Implemente rate limiting por IP e por conexão ativa.
Exemplo de rate limiting:
const rateLimit = new Map(); // IP -> { count, resetTime }
function checkRateLimit(ip) {
const now = Date.now();
const entry = rateLimit.get(ip);
if (!entry || now > entry.resetTime) {
rateLimit.set(ip, { count: 1, resetTime: now + 60000 });
return true;
}
if (entry.count >= 100) { // Max 100 conexões por minuto
return false;
}
entry.count++;
return true;
}
wss.on('connection', (ws, req) => {
const ip = req.socket.remoteAddress;
if (!checkRateLimit(ip)) {
ws.close(4002, 'Rate limit excedido');
return;
}
// ... resto do código
});
WSS (WebSocket over TLS): Use certificados SSL/TLS. Configure no servidor ou no proxy reverso. URLs começam com wss://.
Validação de origem: Verifique o header Origin no handshake e rejeite origens não autorizadas.
6. Deploy em produção e monitoramento
Proxy reverso com Nginx:
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /etc/ssl/certs/example.crt;
ssl_certificate_key /etc/ssl/private/example.key;
location /ws/ {
proxy_pass http://localhost:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# Timeout longo para WebSocket
proxy_read_timeout 86400s;
}
}
Escalabilidade horizontal com Redis Pub/Sub:
Quando você tem múltiplas instâncias do servidor WebSocket, use Redis para propagar mensagens entre instâncias:
const redis = require('redis');
const publisher = redis.createClient();
const subscriber = redis.createClient();
subscriber.subscribe('chat:messages');
subscriber.on('message', (channel, message) => {
// Broadcast para todas as conexões locais
const data = JSON.parse(message);
roomManager.broadcast(data.room, data.message);
});
// Ao receber mensagem de um cliente local, publique no Redis
ws.on('message', (message) => {
const data = JSON.parse(message);
publisher.publish('chat:messages', JSON.stringify(data));
});
Métricas essenciais para monitoramento:
- Conexões ativas: Número atual de WebSockets abertos (por instância e total).
- Taxa de reconexão: Percentual de conexões que foram restabelecidas (ideal < 5%).
- Latência das mensagens: Tempo entre envio e recebimento (P95 < 100ms para chats).
- Throughput: Mensagens por segundo (envio e recebimento).
- Erros de autenticação: Número de handshakes rejeitados.
Ferramentas como Prometheus + Grafana podem coletar essas métricas via endpoints HTTP expostos pelo servidor WebSocket.
Referências
- MDN Web Docs: WebSocket API — Documentação oficial da API WebSocket no navegador, com exemplos de uso e eventos.
- RFC 6455 - The WebSocket Protocol — Especificação oficial do protocolo WebSocket, detalhando handshake, frames e controle de fluxo.
- Socket.IO Documentation — Biblioteca popular que implementa WebSocket com fallback, reconexão automática e salas, ideal para produção.
- Nginx WebSocket proxying — Guia oficial da Nginx para configurar proxy reverso de WebSocket com terminação TLS.
- Redis Pub/Sub with WebSocket — Documentação do Redis sobre Pub/Sub, usado para escalar servidores WebSocket horizontalmente.