Javascript

WebSockets e Comunicação em Tempo Real Já leu

20 min de leitura

WebSockets e Comunicação em Tempo Real
Artigo 50 — WebSockets e Comunicação em Tempo RealPor Prof. Ricardo Matos | Mestre em ProgramaçãoInício do Módulo 9 — Tópicos AvançadosIntroduçãoAté agora todas

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] → Servidor

Socket.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-end

Servidor — 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 WebSockets

Tarefa 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 reload

Conclusã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 useSocket para gerenciar a conexão no React sem vazar conexões
  • Hooks específicos: useProjetoTempoReal, useNotificacoes
  • Componentes de UI: SinoBadge com 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

Comentários

Mais em Javascript

Callbacks: o começo de tudo
Callbacks: o começo de tudo

No artigo anterior entendemos o Event Loop &mdash; o mecanismo que permite ao...

Formulários: validação e coleta de dados
Formulários: validação e coleta de dados

Formul&aacute;rios s&atilde;o a principal forma de comunica&ccedil;&atilde;o...

Fetch API: consumindo dados da internet
Fetch API: consumindo dados da internet

At&eacute; agora simulamos requisi&ccedil;&otilde;es com&nbsp;setTimeout. A p...