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
- MDN Web Docs: WebSocket API — Documentação completa da API nativa WebSocket para navegadores, incluindo eventos, métodos e propriedades.
- ws: a Node.js WebSocket library — Biblioteca oficial para implementação de servidores e clientes WebSocket em Node.js, com exemplos e documentação.
- WebSocket Protocol RFC 6455 — Especificação oficial do protocolo WebSocket pelo IETF, detalhando handshake, frames e estados.
- Socket.IO Documentation — Framework que abstrai WebSocket com fallbacks, salas e reconexão automática, amplamente utilizado em produção.
- Redis Pub/Sub para WebSocket Scaling — Guia oficial do Redis sobre Pub/Sub, essencial para sincronizar servidores WebSocket em arquiteturas distribuídas.
- WebSocket Security Best Practices — Artigo sobre segurança em WebSocket, abordando validação de origem, WSS e prevenção de ataques.
- Using WebSocket with JWT Authentication — Tutorial da Auth0 sobre como integrar autenticação JWT com conexões WebSocket.