Como usar server-sent events para streaming de dados ao cliente

1. Fundamentos de Server-Sent Events (SSE)

Server-Sent Events (SSE) representam uma tecnologia de streaming unidirecional que permite ao servidor enviar dados automaticamente para o cliente através de uma conexão HTTP persistente. Diferentemente dos WebSockets, que oferecem comunicação bidirecional completa, o SSE é otimizado para cenários onde apenas o servidor precisa enviar atualizações contínuas ao cliente.

A principal diferença entre SSE, WebSockets e polling tradicional está no modelo de comunicação:

  • Polling tradicional: O cliente faz requisições periódicas ao servidor, mesmo quando não há dados novos, gerando overhead desnecessário.
  • WebSockets: Conexão bidirecional full-duplex, ideal para aplicações como chats e jogos multiplayer.
  • SSE: Conexão unidirecional do servidor para o cliente, utilizando protocolo HTTP padrão, com reconexão automática nativa.

O mecanismo de funcionamento do SSE baseia-se em uma conexão HTTP persistente onde o servidor mantém o response aberto e envia dados no formato text/event-stream. O navegador oferece suporte nativo através da API EventSource, que gerencia automaticamente a reconexão em caso de falha.

2. Configuração do Servidor para SSE

Para implementar SSE no servidor, três headers HTTP são obrigatórios:

Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

A estrutura de mensagens SSE segue um formato específico:

event: atualizacao
data: {"mensagem": "Nova atualização disponível"}
id: 12345
retry: 3000

Cada campo tem função específica:
- event: Nome do evento personalizado (opcional)
- data: Conteúdo da mensagem (obrigatório)
- id: Identificador único para controle de reconexão (opcional)
- retry: Tempo em milissegundos para tentar reconexão (opcional)

O gerenciamento de conexões abertas exige implementação de heartbeat para evitar timeouts de proxy e balanceadores de carga. Um heartbeat típico envia um comentário a cada 30 segundos:

:heartbeat

3. Implementação de um Endpoint SSE Básico

Exemplo de implementação em Node.js com Express:

const express = require('express');
const app = express();

app.get('/stream', (req, res) => {
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive'
  });

  const intervalId = setInterval(() => {
    const data = {
      timestamp: new Date().toISOString(),
      valor: Math.random() * 100
    };
    res.write(`data: ${JSON.stringify(data)}\n\n`);
  }, 2000);

  const heartbeatId = setInterval(() => {
    res.write(':heartbeat\n\n');
  }, 30000);

  req.on('close', () => {
    clearInterval(intervalId);
    clearInterval(heartbeatId);
    res.end();
  });
});

app.listen(3000, () => {
  console.log('SSE server running on port 3000');
});

O tratamento de desconexão é crucial para evitar vazamento de memória. O evento close na requisição permite limpar os intervalos e encerrar a resposta adequadamente.

4. Cliente: Consumindo SSE no Frontend

O consumo de SSE no frontend utiliza a API EventSource:

const eventSource = new EventSource('/stream');

eventSource.onmessage = (event) => {
  const data = JSON.parse(event.data);
  console.log('Dados recebidos:', data);
};

eventSource.onopen = () => {
  console.log('Conexão SSE estabelecida');
};

eventSource.onerror = (error) => {
  console.error('Erro na conexão SSE:', error);
};

Para manipular eventos nomeados, utilize addEventListener:

eventSource.addEventListener('atualizacao', (event) => {
  const data = JSON.parse(event.data);
  atualizarDashboard(data);
});

A reconexão automática é gerenciada pelo navegador, mas você pode controlar o ponto de retomada usando last-event-id. O servidor deve verificar este header e retransmitir eventos perdidos:

app.get('/stream', (req, res) => {
  const lastId = req.headers['last-event-id'];
  if (lastId) {
    // Retransmitir eventos a partir do ID especificado
  }
  // Continuar com o stream normal
});

5. Padrões Avançados de Streaming com SSE

Para enviar dados estruturados complexos, utilize JSON:

event: usuario_atualizado
data: {"id": 42, "nome": "Maria", "ultimo_acesso": "2024-01-15T10:30:00Z"}
id: 789

O uso de IDs de evento permite garantir entrega ordenada e retomada de conexão. O servidor deve incrementar o ID a cada evento e armazenar os últimos eventos em buffer:

let eventId = 0;
const eventBuffer = [];

app.get('/stream', (req, res) => {
  // Verificar last-event-id e retransmitir eventos perdidos
  const lastId = parseInt(req.headers['last-event-id']) || 0;
  const missedEvents = eventBuffer.filter(e => e.id > lastId);

  missedEvents.forEach(event => {
    res.write(`id: ${event.id}\ndata: ${event.data}\n\n`);
  });

  // Continuar com novos eventos
});

Implemente filtros por tipo de evento no cliente para processamento seletivo:

const filtros = {
  critico: (event) => mostrarAlerta(event.data),
  informativo: (event) => atualizarLog(event.data),
  metrica: (event) => atualizarGrafico(event.data)
};

eventSource.addEventListener('mensagem', (event) => {
  const data = JSON.parse(event.data);
  if (filtros[data.tipo]) {
    filtros[data.tipo](event);
  }
});

6. Segurança e Performance em SSE

A autenticação em SSE apresenta limitações porque o EventSource não suporta headers personalizados. As alternativas incluem:

  • Token na URL: new EventSource('/stream?token=seu_token')
  • Cookies: O servidor pode validar cookies de sessão
  • Autenticação inicial: Estabelecer sessão via POST antes de abrir o SSE

Para limitar conexões simultâneas, implemente um contador no servidor:

let activeConnections = 0;
const MAX_CONNECTIONS = 100;

app.get('/stream', (req, res) => {
  if (activeConnections >= MAX_CONNECTIONS) {
    res.status(503).end('Servidor sobrecarregado');
    return;
  }

  activeConnections++;

  req.on('close', () => {
    activeConnections--;
  });

  // Configurar stream
});

Estratégias de cache e compressão melhoram a performance:

app.get('/stream', (req, res) => {
  res.setHeader('Content-Encoding', 'gzip');
  // Configurar stream com compressão
});

7. Casos de Uso Reais e Comparação com Alternativas

SSE é ideal para cenários específicos:

  • Notificações em tempo real: Alertas de sistema, notificações push
  • Feeds de log: Streaming de logs de aplicação para dashboards
  • Atualizações de cotação: Preços de ações, criptomoedas
  • Métricas de servidor: CPU, memória, tráfego de rede

Comparação com alternativas:

Característica SSE WebSockets Long Polling
Direção Servidor → Cliente Bidirecional Cliente → Servidor
Reconexão automática Nativa Manual Manual
Suporte HTTP/2 Sim Limitado Sim
Complexidade Baixa Alta Média
Latência Baixa Muito baixa Alta

Exemplo completo de streaming de métricas para dashboard:

// Servidor
app.get('/metricas', (req, res) => {
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache'
  });

  const monitor = setInterval(() => {
    const metricas = {
      cpu: os.loadavg()[0],
      memoria: process.memoryUsage().heapUsed / 1024 / 1024,
      conexoes: activeConnections
    };
    res.write(`event: metrica\ndata: ${JSON.stringify(metricas)}\n\n`);
  }, 1000);

  req.on('close', () => clearInterval(monitor));
});

// Cliente
const metricasSSE = new EventSource('/metricas');
metricasSSE.addEventListener('metrica', (event) => {
  const dados = JSON.parse(event.data);
  atualizarDashboard(dados);
});

Referências