Artigo 50 — WebSockets e Comunicação em Tempo Real
Por Prof. Ricardo Matos | Mestre em Programação
Início do Módulo 9 — Tópicos Avançados
Introdução
Até agora todas as comunicações entre front-end e back-end seguiram o modelo requisição-resposta: o cliente pergunta, o servidor responde, a conexão encerra. Este modelo funciona bem para a maioria das situações — buscar produtos, criar tarefas, fazer login.
Mas algumas aplicações precisam de algo diferente. Um chat onde mensagens aparecem instantaneamente. Um dashboard onde métricas atualizam em tempo real. Uma ferramenta colaborativa onde múltiplos usuários editam simultaneamente. Um jogo multiplayer. Nestas situações, o cliente não pode ficar perguntando "tem novidade?" a cada segundo — essa abordagem (chamada polling) é ineficiente e cria uma sensação de latência perceptível.
WebSockets resolvem este problema criando um canal de comunicação bidirecional e persistente. Depois que a conexão é estabelecida, tanto o cliente quanto o servidor podem enviar mensagens a qualquer momento, sem que o outro precisasse ter "perguntado" antes. É uma conversa, não uma sequência de perguntas e respostas.
Como WebSockets funcionam
Para entender o que torna WebSockets especiais, precisamos entender a diferença em relação ao HTTP convencional.
No HTTP, cada requisição estabelece uma nova conexão TCP, envia os dados, recebe a resposta e encerra. Há um overhead significativo em cada interação — handshake TCP, headers HTTP, negociação — mesmo para mensagens pequenas.
O WebSocket começa como uma requisição HTTP especial chamada "upgrade request". O servidor responde aceitando o upgrade e, a partir desse momento, a conexão TCP original permanece aberta e os dois lados podem trocar mensagens livremente, com overhead mínimo por mensagem.
HTTP convencional:
Cliente → [Abre TCP] → [Envia HTTP GET] → Servidor
Cliente ← [Recebe HTTP 200] ← Servidor
Cliente → [Fecha TCP] → Servidor
(repete para cada requisição)
WebSocket:
Cliente → [Abre TCP + HTTP Upgrade] → Servidor
Cliente ← [HTTP 101 Switching Protocols] ← Servidor
─────── conexão permanece aberta ────────────────────
Cliente ←→ mensagem ←→ Servidor (qualquer lado, qualquer hora)
Cliente ←→ mensagem ←→ Servidor
Cliente ←→ mensagem ←→ Servidor
Cliente → [Fecha conexão quando quiser] → ServidorSocket.IO — WebSockets com superpoderes
O Socket.IO é a biblioteca mais popular para WebSockets em Node.js. Além do protocolo WebSocket, ele oferece funcionalidades que tornam o desenvolvimento de aplicações em tempo real muito mais simples: reconexão automática, salas de chat, eventos com nome, broadcast seletivo, e fallback para polling quando WebSockets não estão disponíveis.
npm install socket.io # back-end
npm install socket.io-client # front-endServidor — configuração básica
O Socket.IO se integra ao servidor Express existente, compartilhando a mesma porta. A configuração é simples, mas os detalhes de CORS e autenticação são importantes para produção.
// src/index.js — integrando Socket.IO ao Express existente
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
const jwt = require('jsonwebtoken');
const config = require('./config');
const app = express();
// Socket.IO precisa de um servidor HTTP nativo — não o Express diretamente
// O Express é passado para o http.createServer() e ambos compartilham a porta
const servidor = http.createServer(app);
const io = new Server(servidor, {
cors: {
// Mesmas origens permitidas que o CORS do Express
origin: process.env.NODE_ENV === 'production'
? process.env.FRONTEND_URL
: ['http://localhost:5173', 'http://localhost:4173'],
methods: ['GET', 'POST'],
credentials: true,
},
// Configurações de reconexão e timeout
pingTimeout: 60000, // tempo antes de considerar conexão morta (60s)
pingInterval: 25000, // frequência dos pings de heartbeat (25s)
});
// ── Middleware de autenticação do Socket.IO ─────────
// Todo socket que tenta se conectar passa por aqui primeiro
// Se a autenticação falhar, a conexão é recusada imediatamente
io.use(async (socket, next) => {
try {
// O token pode vir no handshake auth ou como query parameter
const token =
socket.handshake.auth?.token ||
socket.handshake.query?.token;
if (!token) {
return next(new Error('Autenticação necessária.'));
}
const payload = jwt.verify(token, config.jwtSecret);
// Busca o usuário e anexa ao socket — disponível em todos os eventos
const usuario = await Usuario.findById(payload.id).select('nome email papel').lean();
if (!usuario) return next(new Error('Usuário não encontrado.'));
socket.usuario = usuario;
next(); // autenticação bem-sucedida — permite a conexão
} catch (erro) {
next(new Error('Token inválido ou expirado.'));
}
});
// ── Gerenciador de conexões ──────────────────────────
// Este é o coração do servidor de tempo real
// Cada socket representa uma conexão de um cliente específico
io.on('connection', (socket) => {
const { usuario } = socket;
console.log(`[Socket] Conectado: ${usuario.nome} (${socket.id})`);
// Entra automaticamente em uma sala com o ID do usuário
// Isso permite enviar mensagens para um usuário específico
// mesmo que ele tenha múltiplas abas abertas
socket.join(`usuario:${usuario._id}`);
// Informa o cliente que a conexão foi estabelecida com sucesso
socket.emit('conectado', {
mensagem: `Bem-vindo, ${usuario.nome}!`,
socketId: socket.id,
});
// Registra os handlers de eventos aqui dentro
// 'socket' já tem o usuário autenticado disponível
registrarEventosTarefas(io, socket);
registrarEventosNotificacoes(io, socket);
// Quando a conexão é encerrada (usuário fechou a aba, perdeu internet, etc.)
socket.on('disconnect', (motivo) => {
console.log(`[Socket] Desconectado: ${usuario.nome} — motivo: ${motivo}`);
});
// Tratamento de erros no socket individual
socket.on('error', (erro) => {
console.error(`[Socket] Erro (${usuario.nome}):`, erro.message);
});
});
// Inicia o servidor na porta configurada
servidor.listen(config.porta, () => {
console.log(`[Server] ✅ HTTP + WebSocket na porta ${config.porta}`);
});Salas e eventos — colaboração em tempo real
As salas do Socket.IO são grupos lógicos de conexões. Um socket pode entrar e sair de salas livremente. Mensagens enviadas para uma sala chegam a todos os membros — perfeito para chats por projeto, notificações por equipe, ou colaboração em documentos.
// src/socketHandlers/tarefas.js
// Handlers para eventos relacionados a tarefas em tempo real
function registrarEventosTarefas(io, socket) {
const { usuario } = socket;
// ── Entrar em uma sala de projeto ────────────────
// Quando o usuário abre a lista de tarefas de um projeto,
// entra na sala daquele projeto para receber atualizações ao vivo
socket.on('entrar:projeto', async ({ projetoId }) => {
try {
// Verifica se o usuário tem acesso ao projeto
const projeto = await Projeto.findOne({
_id: projetoId,
membros: usuario._id,
});
if (!projeto) {
socket.emit('erro', { mensagem: 'Projeto não encontrado ou sem acesso.' });
return;
}
// Entra na sala do projeto
socket.join(`projeto:${projetoId}`);
// Confirma para o cliente que entrou na sala
socket.emit('entrou:projeto', { projetoId, nome: projeto.nome });
// Notifica os outros membros do projeto que este usuário está online
socket.to(`projeto:${projetoId}`).emit('usuario:entrou', {
usuario: { id: usuario._id, nome: usuario.nome },
projetoId,
});
console.log(`[Socket] ${usuario.nome} entrou no projeto ${projetoId}`);
} catch (erro) {
socket.emit('erro', { mensagem: 'Erro ao entrar no projeto.' });
}
});
// ── Sair de uma sala de projeto ──────────────────
socket.on('sair:projeto', ({ projetoId }) => {
socket.leave(`projeto:${projetoId}`);
socket.to(`projeto:${projetoId}`).emit('usuario:saiu', {
usuario: { id: usuario._id, nome: usuario.nome },
projetoId,
});
});
// ── Tarefa criada via HTTP — notifica a sala ─────
// Este handler é chamado internamente (pelo EventEmitter do domínio)
// não pelo cliente — quando uma tarefa é criada via API REST,
// todos na sala recebem a atualização em tempo real
socket.on('tarefa:nova', ({ tarefa, projetoId }) => {
// Envia para todos na sala do projeto EXCETO quem criou (já tem localmente)
socket.to(`projeto:${projetoId}`).emit('tarefa:criada', { tarefa });
});
// ── Usuário está digitando (indicador de atividade) ─
socket.on('tarefa:digitando', ({ projetoId, tarefaId }) => {
// Reencaminha para os outros na sala — sem persistir nada
socket.to(`projeto:${projetoId}`).emit('tarefa:alguem-digitando', {
usuario: { id: usuario._id, nome: usuario.nome },
tarefaId,
});
});
}
module.exports = { registrarEventosTarefas };// src/socketHandlers/notificacoes.js
// Sistema de notificações em tempo real para o usuário individual
function registrarEventosNotificacoes(io, socket) {
const { usuario } = socket;
// Quando o usuário se conecta, busca notificações não lidas
// e envia imediatamente — sem precisar que ele recarregue a página
socket.on('notificacoes:buscar', async () => {
try {
const naoLidas = await Notificacao.find({
destinatario: usuario._id,
lida: false,
})
.sort({ criadoEm: -1 })
.limit(20)
.lean();
socket.emit('notificacoes:lista', { notificacoes: naoLidas });
} catch (erro) {
socket.emit('erro', { mensagem: 'Erro ao buscar notificações.' });
}
});
// Marca uma notificação como lida
socket.on('notificacao:ler', async ({ notificacaoId }) => {
try {
await Notificacao.findOneAndUpdate(
{ _id: notificacaoId, destinatario: usuario._id },
{ lida: true, lidaEm: new Date() }
);
socket.emit('notificacao:lida', { notificacaoId });
} catch (erro) {
socket.emit('erro', { mensagem: 'Erro ao marcar notificação.' });
}
});
}
// Função utilitária exportada para uso em outras partes da aplicação
// Permite que qualquer serviço envie uma notificação para um usuário específico
// mesmo sem ter acesso direto ao socket dele
function notificarUsuario(io, usuarioId, notificacao) {
// A sala 'usuario:ID' garante que a mensagem chega a TODAS as abas
// abertas daquele usuário — mesmo se ele tiver múltiplas sessões
io.to(`usuario:${usuarioId}`).emit('notificacao:nova', { notificacao });
}
module.exports = { registrarEventosNotificacoes, notificarUsuario };Integrando WebSockets com a API REST
Uma das decisões de arquitetura mais importantes em aplicações com tempo real é: quando usar REST e quando usar WebSockets? A resposta prática é usar ambos.
REST continua sendo a escolha certa para operações com estado durável — criar, ler, atualizar e deletar recursos. WebSockets são ideais para propagação de eventos — notificar outros clientes que algo mudou.
// src/container.js — integrando o io nos eventos de domínio
// Quando uma tarefa é criada via API REST, o evento de domínio
// aciona a notificação em tempo real para todos na sala do projeto
const { notificarUsuario } = require('./socketHandlers/notificacoes');
// 'io' é injetado no container para que os ouvintes de eventos possam usá-lo
function criarContainer(io) {
const eventEmitter = new EventEmitter();
// Quando uma tarefa é criada via REST, notifica a sala do projeto em tempo real
eventEmitter.on(EVENTOS.TAREFA_CRIADA, ({ tarefa, usuario }) => {
// Invalida o cache
cache.del(`stats:${usuario._id}`);
// Se a tarefa pertence a um projeto, notifica todos na sala
if (tarefa.projetoId) {
io.to(`projeto:${tarefa.projetoId}`).emit('tarefa:criada', {
tarefa: tarefa.toJSON(),
criadoPor: { id: usuario._id, nome: usuario.nome },
});
}
});
eventEmitter.on(EVENTOS.TAREFA_CONCLUIDA, ({ tarefa, usuario }) => {
cache.del(`stats:${usuario._id}`);
if (tarefa.projetoId) {
io.to(`projeto:${tarefa.projetoId}`).emit('tarefa:concluida', {
tarefaId: tarefa.id,
concluidoPor: { id: usuario._id, nome: usuario.nome },
concluidaEm: tarefa.concluidaEm,
});
}
});
// Casos de uso recebem os mesmos repositórios e event emitter
const tarefaRepository = new MongoTarefaRepository();
const criarTarefa = new CriarTarefa(tarefaRepository, eventEmitter);
// ... resto das instâncias
return { tarefaController: new TarefaController(/* ... */) };
}
module.exports = { criarContainer };Front-end React — conectando ao Socket.IO
No front-end, vamos encapsular toda a lógica do Socket.IO em um hook customizado. Isso mantém os componentes limpos e centraliza o gerenciamento da conexão.
// src/hooks/useSocket.js
// Hook que gerencia a conexão com o servidor de WebSockets
import { useEffect, useRef, useCallback } from 'react';
import { io } from 'socket.io-client';
import useAuthStore from '../stores/authStore';
// Mantemos o socket fora do hook para que seja singleton —
// uma única conexão compartilhada por toda a aplicação
let socketInstancia = null;
export function useSocket() {
const token = useAuthStore((state) => state.token);
const socketRef = useRef(null);
useEffect(() => {
// Não conecta se não há token (usuário não logado)
if (!token) return;
// Reutiliza a instância existente se já estiver conectada
if (socketInstancia?.connected) {
socketRef.current = socketInstancia;
return;
}
// Cria uma nova conexão
const socket = io(import.meta.env.VITE_API_URL || 'http://localhost:3000', {
// Envia o token no handshake — o middleware de auth do servidor usa isso
auth: { token },
// Tenta reconectar automaticamente com backoff exponencial
reconnection: true,
reconnectionDelay: 1000, // aguarda 1s antes da primeira tentativa
reconnectionDelayMax: 30000, // no máximo 30s entre tentativas
reconnectionAttempts: 5, // desiste após 5 tentativas
// Prefere WebSocket, com polling como fallback
transports: ['websocket', 'polling'],
});
socket.on('connect', () => {
console.log('[Socket] Conectado:', socket.id);
});
socket.on('disconnect', (motivo) => {
console.log('[Socket] Desconectado:', motivo);
// Se o servidor encerrou a conexão, não tenta reconectar automaticamente
if (motivo === 'io server disconnect') {
socket.connect();
}
});
socket.on('connect_error', (erro) => {
console.error('[Socket] Erro de conexão:', erro.message);
});
socketInstancia = socket;
socketRef.current = socket;
// Cleanup: desconecta quando o componente que usa o hook desmonta
// Na prática, como é singleton, só desconecta quando o usuário faz logout
return () => {
if (socket.connected) {
socket.disconnect();
socketInstancia = null;
}
};
}, [token]);
// Retorna funções estáveis para emitir e ouvir eventos
const emitir = useCallback((evento, dados) => {
socketRef.current?.emit(evento, dados);
}, []);
const ouvir = useCallback((evento, callback) => {
const socket = socketRef.current;
if (!socket) return () => {};
socket.on(evento, callback);
// Retorna função de cleanup para remover o listener
return () => socket.off(evento, callback);
}, []);
return {
socket: socketRef.current,
emitir,
ouvir,
conectado: socketRef.current?.connected ?? false,
};
}// src/hooks/useProjetoTempoReal.js
// Hook específico para sincronizar tarefas de um projeto em tempo real
import { useEffect } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { useSocket } from './useSocket';
export function useProjetoTempoReal(projetoId) {
const { emitir, ouvir } = useSocket();
const queryClient = useQueryClient();
useEffect(() => {
if (!projetoId) return;
// Entra na sala do projeto ao montar o componente
emitir('entrar:projeto', { projetoId });
// Quando uma nova tarefa chega, invalida o cache do React Query
// para que a lista seja refetchada automaticamente
const removerListenerCriada = ouvir('tarefa:criada', ({ tarefa }) => {
queryClient.invalidateQueries({ queryKey: ['tarefas', { projetoId }] });
});
const removerListenerConcluida = ouvir('tarefa:concluida', ({ tarefaId }) => {
// Atualiza otimisticamente no cache — sem esperar refetch
queryClient.setQueriesData(
{ queryKey: ['tarefas', { projetoId }] },
(cache) => {
if (!cache?.dados) return cache;
return {
...cache,
dados: cache.dados.map((t) =>
t.id === tarefaId ? { ...t, status: 'concluida' } : t
),
};
}
);
});
// Sai da sala e remove listeners ao desmontar
return () => {
emitir('sair:projeto', { projetoId });
removerListenerCriada();
removerListenerConcluida();
};
}, [projetoId, emitir, ouvir, queryClient]);
}// src/hooks/useNotificacoes.js
// Hook para notificações em tempo real do usuário atual
import { useState, useEffect } from 'react';
import { useSocket } from './useSocket';
export function useNotificacoes() {
const [notificacoes, setNotificacoes] = useState([]);
const [naoLidas, setNaoLidas] = useState(0);
const { emitir, ouvir } = useSocket();
useEffect(() => {
// Solicita a lista inicial de notificações não lidas ao conectar
emitir('notificacoes:buscar');
const removerListenerLista = ouvir('notificacoes:lista', ({ notificacoes }) => {
setNotificacoes(notificacoes);
setNaoLidas(notificacoes.filter((n) => !n.lida).length);
});
// Quando uma nova notificação chega, adiciona no topo da lista
const removerListenerNova = ouvir('notificacao:nova', ({ notificacao }) => {
setNotificacoes((prev) => [notificacao, ...prev]);
setNaoLidas((prev) => prev + 1);
// Toca um som sutil de notificação se o usuário tiver dado permissão
if (document.visibilityState === 'hidden') {
new Audio('/notification.mp3').play().catch(() => {});
}
});
const removerListenerLida = ouvir('notificacao:lida', ({ notificacaoId }) => {
setNotificacoes((prev) =>
prev.map((n) => (n._id === notificacaoId ? { ...n, lida: true } : n))
);
setNaoLidas((prev) => Math.max(0, prev - 1));
});
return () => {
removerListenerLista();
removerListenerNova();
removerListenerLida();
};
}, [emitir, ouvir]);
function marcarComoLida(notificacaoId) {
emitir('notificacao:ler', { notificacaoId });
}
return { notificacoes, naoLidas, marcarComoLida };
}Componentes de UI em tempo real
// src/components/SinoBadge.jsx
// Ícone de sino com badge de notificações não lidas
import { useState, useRef, useEffect } from 'react';
import { useNotificacoes } from '../hooks/useNotificacoes';
export function SinoBadge() {
const [aberto, setAberto] = useState(false);
const { notificacoes, naoLidas, marcarComoLida } = useNotificacoes();
const refPainel = useRef(null);
// Fecha o painel ao clicar fora
useEffect(() => {
function handleClickFora(e) {
if (refPainel.current && !refPainel.current.contains(e.target)) {
setAberto(false);
}
}
document.addEventListener('mousedown', handleClickFora);
return () => document.removeEventListener('mousedown', handleClickFora);
}, []);
return (
<div className="sino-container" ref={refPainel}>
<button
className="sino-botao"
onClick={() => setAberto((v) => !v)}
aria-label={`Notificações — ${naoLidas} não lidas`}
>
🔔
{naoLidas > 0 && (
<span className="sino-badge">
{naoLidas > 99 ? '99+' : naoLidas}
</span>
)}
</button>
{aberto && (
<div className="notificacoes-painel">
<div className="notificacoes-header">
<h3>Notificações</h3>
{naoLidas > 0 && (
<span className="notificacoes-contador">{naoLidas} novas</span>
)}
</div>
<ul className="notificacoes-lista">
{notificacoes.length === 0 && (
<li className="notificacoes-vazia">Nenhuma notificação.</li>
)}
{notificacoes.map((n) => (
<li
key={n._id}
className={`notificacao-item ${!n.lida ? 'nao-lida' : ''}`}
onClick={() => !n.lida && marcarComoLida(n._id)}
>
<p className="notificacao-texto">{n.mensagem}</p>
<time className="notificacao-tempo">
{new Date(n.criadoEm).toLocaleTimeString('pt-BR', {
hour: '2-digit',
minute: '2-digit',
})}
</time>
{!n.lida && <span className="notificacao-ponto" aria-hidden />}
</li>
))}
</ul>
</div>
)}
</div>
);
}// src/components/IndicadorDigitando.jsx
// Mostra quem está digitando em tempo real — padrão comum em chats e colaboração
import { useState, useEffect } from 'react';
import { useSocket } from '../hooks/useSocket';
export function IndicadorDigitando({ projetoId, tarefaId }) {
const [digitando, setDigitando] = useState([]);
const { emitir, ouvir } = useSocket();
useEffect(() => {
const remover = ouvir('tarefa:alguem-digitando', (dados) => {
if (dados.tarefaId !== tarefaId) return;
// Adiciona o usuário à lista de quem está digitando
setDigitando((prev) => {
const jaEsta = prev.find((u) => u.id === dados.usuario.id);
if (jaEsta) return prev;
return [...prev, dados.usuario];
});
// Remove após 3 segundos de inatividade — debounce visual
setTimeout(() => {
setDigitando((prev) => prev.filter((u) => u.id !== dados.usuario.id));
}, 3000);
});
return remover;
}, [tarefaId, ouvir]);
function handleDigitando() {
emitir('tarefa:digitando', { projetoId, tarefaId });
}
if (digitando.length === 0) return null;
const nomes = digitando.map((u) => u.nome);
const texto =
nomes.length === 1
? `${nomes[0]} está digitando...`
: `${nomes.slice(0, -1).join(', ')} e ${nomes.at(-1)} estão digitando...`;
return (
<p className="indicador-digitando" aria-live="polite">
<span className="pontos-animados" aria-hidden>···</span>
{texto}
</p>
);
}Escalabilidade — múltiplas instâncias com Redis Adapter
Um servidor Socket.IO armazena as salas e as conexões em memória. Quando você escala horizontalmente para múltiplas instâncias (múltiplos containers no Railway, por exemplo), cada instância tem sua própria memória — um socket conectado na instância A não recebe mensagens emitidas da instância B.
O Redis Adapter resolve isso: as salas e as mensagens são sincronizadas entre todas as instâncias via Redis.
// npm install @socket.io/redis-adapter ioredis
const { createAdapter } = require('@socket.io/redis-adapter');
const { createClient } = require('ioredis');
// Dois clientes Redis: um para publicar, um para subscrever
// O Redis pub/sub é o mecanismo de sincronização entre instâncias
const pubClient = createClient({ url: process.env.REDIS_URL });
const subClient = pubClient.duplicate();
async function configurarRedisAdapter(io) {
await Promise.all([pubClient.connect(), subClient.connect()]);
// Agora qualquer io.to('sala').emit() é sincronizado entre todas as instâncias
io.adapter(createAdapter(pubClient, subClient));
console.log('[Socket] Redis Adapter configurado — escalabilidade horizontal ativa.');
}
// No index.js, após criar o io:
configurarRedisAdapter(io).catch(console.error);Server-Sent Events — alternativa unidirecional
WebSockets são bidirecionais — ambos os lados podem enviar. Mas às vezes você só precisa que o servidor envie dados para o cliente — dashboards de monitoramento, feeds de notícias, progresso de processamento. Para esses casos, Server-Sent Events (SSE) é uma alternativa mais simples que usa HTTP convencional.
// Back-end — SSE com Express
// Não precisa de nenhuma biblioteca adicional — é HTTP puro
app.get('/eventos/tarefas', autenticar, (req, res) => {
// Headers especiais que ativam o modo SSE no navegador
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('X-Accel-Buffering', 'no'); // desabilita buffering no nginx
// Envia um comentário para manter a conexão viva
// Alguns proxies fecham conexões ociosas após 30-60 segundos
res.write(': ping
');
const intervalo = setInterval(() => res.write(': ping
'), 30000);
const usuarioId = req.usuario._id.toString();
// Função que envia um evento SSE formatado
function enviarEvento(tipo, dados) {
// Formato SSE: "event: tipo
data: JSON
"
res.write(`event: ${tipo}
`);
res.write(`data: ${JSON.stringify(dados)}
`);
}
// Registra um ouvinte no event emitter global
// Quando um evento de domínio ocorre, envia via SSE para este cliente
const removerOuvinte = eventEmitter.on(
EVENTOS.TAREFA_CRIADA,
({ tarefa, usuario }) => {
if (usuario._id.toString() === usuarioId) {
enviarEvento('tarefa-criada', tarefa.toJSON());
}
}
);
// Cleanup quando o cliente desconecta
req.on('close', () => {
clearInterval(intervalo);
removerOuvinte();
console.log(`[SSE] Cliente desconectado: ${usuarioId}`);
});
});// Front-end — consumindo SSE com EventSource nativo
// Não precisa de nenhuma biblioteca — é suportado em todos os browsers modernos
function useEventosServidor(url) {
useEffect(() => {
const token = useAuthStore.getState().token;
// EventSource não suporta headers customizados — token na query string
const eventSource = new EventSource(`${url}?token=${token}`);
eventSource.addEventListener('tarefa-criada', (evento) => {
const tarefa = JSON.parse(evento.data);
queryClient.invalidateQueries({ queryKey: ['tarefas'] });
});
eventSource.onerror = () => {
// EventSource reconecta automaticamente após erros
console.warn('[SSE] Conexão interrompida — reconectando...');
};
return () => eventSource.close();
}, [url]);
}Quando usar cada tecnologia
Escolher entre WebSockets, SSE e polling depende dos requisitos da sua aplicação. A tabela abaixo resume as diferenças.
WebSocket (Socket.IO) SSE Polling
─────────────────────────────────────────────────────────────────────
Direção Bidirecional Server → Client Server → Client
Protocolo WebSocket + HTTP HTTP HTTP
Biblioteca extra Socket.IO Nenhuma Nenhuma
Reconexão auto Sim (Socket.IO) Sim (nativo) Controla você
Suporte browsers Excelente Excelente Universal
Escalabilidade Redis Adapter Load balancer Fácil
Complexidade Média Baixa Baixa
Use WebSocket quando:
✓ Comunicação bidirecional (chat, jogos, colaboração)
✓ Baixa latência é crítica (< 100ms)
✓ Muitos eventos por segundo
✓ Salas e grupos complexos
Use SSE quando:
✓ Apenas servidor → cliente (dashboards, feeds, progresso)
✓ Quer simplicidade (sem biblioteca extra)
✓ Os dados são principalmente texto
Use Polling quando:
✓ Atualizações não são críticas (a cada 30s já basta)
✓ Quer implementação mais simples
✓ Backend não suporta WebSocketsTarefa para você
Adicione comunicação em tempo real na SPA do Módulo 6:
# 1. Configure o Socket.IO no servidor
# Integre ao servidor Express existente (sem criar nova porta)
# Adicione autenticação JWT no middleware do Socket.IO
# 2. Crie o hook useSocket.js com:
# - Conexão singleton
# - Reconexão automática
# - Funções emitir() e ouvir() estáveis
# 3. Implemente notificações em tempo real:
# - Quando uma tarefa é criada via REST, emita um socket event
# - No front-end, ouça o evento e invalide o React Query cache
# - Adicione o componente SinoBadge no Layout
# 4. Adicione indicador de "usuário online":
# - Ao conectar, o servidor registra o socket na sala 'usuario:ID'
# - Crie um endpoint REST GET /usuarios/online que retorna
# a lista de usuários com conexão ativa: io.sockets.adapter.rooms
# 5. Implemente um chat simples entre usuários do mesmo projeto:
# - Sala: 'chat:projetoId'
# - Evento: 'chat:mensagem' com { texto, usuario, timestamp }
# - Componente ChatProjeto com lista de mensagens e input
# - Indicador de digitando com debounce de 3 segundos
# 6. Teste com múltiplas abas:
# - Crie uma tarefa em uma aba
# - Observe a lista atualizar na outra aba sem reloadConclusão
Neste artigo você aprendeu:
- A diferença fundamental entre HTTP requisição-resposta e o canal bidirecional persistente do WebSocket
- Como o Socket.IO estende o protocolo WebSocket com reconexão, salas e eventos nomeados
- Configuração do servidor: integração com Express, autenticação JWT no handshake, gerenciamento de salas
- Handlers de eventos para colaboração em tempo real — notificações, indicador de digitando, salas de projeto
- Como integrar WebSockets com a arquitetura REST existente via EventEmitter de domínio
- O hook
useSocketpara gerenciar a conexão no React sem vazar conexões - Hooks específicos:
useProjetoTempoReal,useNotificacoes - Componentes de UI:
SinoBadgecom badge animado,IndicadorDigitando - Escalabilidade horizontal com Redis Adapter
- Server-Sent Events como alternativa mais simples para comunicação unidirecional
- Quando usar cada tecnologia: WebSocket vs SSE vs Polling
No próximo artigo vamos explorar filas de tarefas e processamento assíncrono — como lidar com operações demoradas sem travar a API.
📌 Próximo artigo: Aula 51 — Filas de tarefas e processamento assíncrono
📚 Fontes e Referências
- Socket.IO — Documentação: https://socket.io/docs/v4
- Socket.IO — Redis Adapter: https://socket.io/docs/v4/redis-adapter
- MDN — WebSockets API: https://developer.mozilla.org/pt-BR/docs/Web/API/WebSockets_API
- MDN — Server-Sent Events: https://developer.mozilla.org/pt-BR/docs/Web/API/Server-sent_events
- ioredis: https://github.com/redis/ioredis
- Real-Time Web Application Development — Rami Sayar (Packt)
- Socket.IO Cookbook — Tyson Cadenhead (Packt)
- roadmap.sh — Backend: https://roadmap.sh/backend
Prof. Ricardo Matos
Mestre em Programação | Série: Dominando JavaScript em 1 Ano — Artigo 50/52
Quer a Aula 51 — Filas de tarefas e processamento assíncrono agora?
# you asked
Sim
# claude response
# 📘 Série: Dominando o JavaScript em 1 Ano