Como usar WebSockets para aplicações em tempo real

1. Fundamentos do WebSocket: Conceitos e Handshake

WebSocket é um protocolo de comunicação full-duplex sobre uma única conexão TCP, projetado para aplicações que exigem baixa latência e troca contínua de dados. Diferentemente do HTTP tradicional, onde o cliente faz uma requisição e aguarda a resposta (modelo request-response), o WebSocket permite que servidor e cliente enviem mensagens a qualquer momento, sem necessidade de polling.

O handshake de upgrade é o processo que transforma uma requisição HTTP em uma conexão WebSocket. O cliente envia uma requisição HTTP com cabeçalhos específicos:

GET /chat HTTP/1.1
Host: exemplo.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

O servidor responde com status 101 (Switching Protocols) e confirma o upgrade:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

A conexão WebSocket possui quatro estados definidos pela especificação:
- CONNECTING (0): Handshake em andamento
- OPEN (1): Conexão estabelecida, dados podem fluir
- CLOSING (2): Processo de encerramento iniciado
- CLOSED (3): Conexão completamente encerrada

2. Implementação básica de um servidor WebSocket

Utilizando Node.js com a biblioteca ws, podemos criar um servidor WebSocket funcional em poucas linhas:

const WebSocket = require('ws');

const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', (ws) => {
  console.log('Cliente conectado');

  ws.on('message', (message) => {
    console.log(`Recebido: ${message}`);
    // Enviar resposta para o cliente específico
    ws.send(`Echo: ${message}`);
  });

  ws.on('close', () => {
    console.log('Cliente desconectado');
  });

  ws.on('error', (error) => {
    console.error('Erro na conexão:', error);
  });
});

Para broadcast (enviar para todos os clientes conectados):

wss.on('connection', (ws) => {
  ws.on('message', (message) => {
    wss.clients.forEach((client) => {
      if (client.readyState === WebSocket.OPEN) {
        client.send(`Broadcast: ${message}`);
      }
    });
  });
});

3. Implementação de um cliente WebSocket no navegador

A API nativa WebSocket do JavaScript torna trivial conectar-se a um servidor:

const socket = new WebSocket('ws://localhost:8080');

socket.onopen = (event) => {
  console.log('Conexão estabelecida');
  socket.send('Olá servidor!');
};

socket.onmessage = (event) => {
  console.log('Mensagem recebida:', event.data);
};

socket.onclose = (event) => {
  console.log('Conexão fechada:', event.code, event.reason);
};

socket.onerror = (error) => {
  console.error('Erro no WebSocket:', error);
};

Para envio de dados binários, a API suporta Blob e ArrayBuffer:

// Enviar como ArrayBuffer
const buffer = new ArrayBuffer(4);
const view = new DataView(buffer);
view.setUint32(0, 12345);
socket.send(buffer);

// Configurar para receber como ArrayBuffer
socket.binaryType = 'arraybuffer';

Implementando reconexão automática com heartbeat:

function connectWebSocket() {
  const socket = new WebSocket('ws://localhost:8080');
  let heartbeatInterval;

  socket.onopen = () => {
    console.log('Conectado');
    heartbeatInterval = setInterval(() => {
      if (socket.readyState === WebSocket.OPEN) {
        socket.send(JSON.stringify({ type: 'ping' }));
      }
    }, 30000);
  };

  socket.onclose = () => {
    clearInterval(heartbeatInterval);
    setTimeout(connectWebSocket, 5000);
  };

  socket.onmessage = (event) => {
    const data = JSON.parse(event.data);
    if (data.type === 'pong') {
      console.log('Heartbeat recebido');
    }
  };
}

4. Padrões de comunicação em tempo real

Broadcast é o padrão mais simples, como demonstrado anteriormente. Para salas (rooms), agrupamos conexões logicamente:

const rooms = new Map();

wss.on('connection', (ws) => {
  ws.on('message', (message) => {
    const data = JSON.parse(message);

    if (data.action === 'join') {
      if (!rooms.has(data.room)) {
        rooms.set(data.room, new Set());
      }
      rooms.get(data.room).add(ws);
      ws.currentRoom = data.room;
    }

    if (data.action === 'message') {
      const room = rooms.get(ws.currentRoom);
      if (room) {
        room.forEach((client) => {
          if (client.readyState === WebSocket.OPEN) {
            client.send(JSON.stringify({
              user: data.user,
              text: data.text
            }));
          }
        });
      }
    }
  });
});

Mensagens privadas exigem identificação única do cliente:

const clients = new Map();

wss.on('connection', (ws) => {
  const clientId = generateUniqueId();
  clients.set(clientId, ws);
  ws.send(JSON.stringify({ type: 'id', id: clientId }));

  ws.on('message', (message) => {
    const data = JSON.parse(message);
    if (data.targetId && clients.has(data.targetId)) {
      clients.get(data.targetId).send(JSON.stringify({
        from: clientId,
        content: data.content
      }));
    }
  });
});

5. Escalabilidade e gerenciamento de estado

Para escalar horizontalmente com múltiplos servidores WebSocket, utilizamos Redis Pub/Sub:

const redis = require('redis');
const publisher = redis.createClient();
const subscriber = redis.createClient();

subscriber.subscribe('chat:messages');

subscriber.on('message', (channel, message) => {
  wss.clients.forEach((client) => {
    if (client.readyState === WebSocket.OPEN) {
      client.send(message);
    }
  });
});

wss.on('connection', (ws) => {
  ws.on('message', (message) => {
    publisher.publish('chat:messages', message);
  });
});

Para autenticação via JWT no handshake:

const jwt = require('jsonwebtoken');

const wss = new WebSocket.Server({ 
  port: 8080,
  verifyClient: (info, callback) => {
    const token = new URL(info.req.url, 'http://localhost').searchParams.get('token');
    try {
      const decoded = jwt.verify(token, 'seu_segredo');
      info.req.user = decoded;
      callback(true);
    } catch (error) {
      callback(false, 401, 'Token inválido');
    }
  }
});

6. Tratamento de erros e resiliência

Estratégia de reconexão com backoff exponencial:

function connectWithBackoff(maxRetries = 10) {
  let retryCount = 0;

  function attemptConnection() {
    const socket = new WebSocket('ws://localhost:8080');

    socket.onclose = () => {
      if (retryCount < maxRetries) {
        const delay = Math.min(1000 * Math.pow(2, retryCount), 30000);
        retryCount++;
        console.log(`Tentando reconectar em ${delay}ms (tentativa ${retryCount})`);
        setTimeout(attemptConnection, delay);
      }
    };
  }

  attemptConnection();
}

7. Casos de uso práticos e exemplos completos

Chat em tempo real com histórico limitado:

const messageHistory = [];

wss.on('connection', (ws) => {
  // Enviar histórico recente
  ws.send(JSON.stringify({ type: 'history', messages: messageHistory.slice(-50) }));

  ws.on('message', (message) => {
    const msg = { user: ws.username, text: message, timestamp: Date.now() };
    messageHistory.push(msg);

    wss.clients.forEach((client) => {
      client.send(JSON.stringify({ type: 'message', ...msg }));
    });
  });
});

Notificações ao vivo para painéis de monitoramento:

// Simular eventos do sistema
setInterval(() => {
  const alert = {
    type: 'alert',
    severity: Math.random() > 0.8 ? 'critical' : 'warning',
    message: `Uso de CPU: ${Math.floor(Math.random() * 100)}%`,
    timestamp: Date.now()
  };

  wss.clients.forEach((client) => {
    client.send(JSON.stringify(alert));
  });
}, 5000);

8. Segurança e boas práticas

Validação de origem e sanitização de mensagens:

const wss = new WebSocket.Server({
  port: 8080,
  verifyClient: (info, callback) => {
    const allowedOrigins = ['https://meusite.com', 'https://admin.meusite.com'];
    const origin = info.origin || info.req.headers.origin;

    if (allowedOrigins.includes(origin)) {
      callback(true);
    } else {
      callback(false, 403, 'Origem não permitida');
    }
  }
});

Para produção, sempre use WSS (WebSocket Secure) com certificados TLS:

const https = require('https');
const fs = require('fs');

const server = https.createServer({
  cert: fs.readFileSync('/caminho/certificado.pem'),
  key: fs.readFileSync('/caminho/chave.pem')
});

const wss = new WebSocket.Server({ server });
server.listen(443);

Encerramento gracioso de conexões:

process.on('SIGINT', () => {
  wss.clients.forEach((client) => {
    client.close(1001, 'Servidor encerrando');
  });

  wss.close(() => {
    console.log('Servidor WebSocket encerrado');
    process.exit(0);
  });
});

Referências