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