Programação Por Equipe DevBras

Capa do artigo: Chat na página: polling, SSE ou WebSocket e a ordem das mensagens

Chat na página: polling, SSE ou WebSocket e a ordem das mensagens

Escolha entre polling, Server-Sent Events e WebSocket pelo uso real, grave a mensagem antes de mostrar, evite duplicata no reenvio e recupere o que perdeu quando a rede volta.

O chat de suporte da loja pergunta ao servidor, a cada segundo, se há mensagem nova. Com 10 atendentes e clientes com a aba aberta, são 864 mil requisições por dia, quase todas respondendo "nada". Quando a conexão de um cliente oscila, a tela continua parada como se ninguém tivesse respondido, e a mensagem que ele mandou duas vezes aparece duplicada para o atendente.

A resposta curta: polling rápido não transforma a página em conversa em tempo real. Para recado interno de baixo volume, polling a cada poucos segundos, com recuo quando falha, resolve. Para conversa de verdade, use um canal persistente: Server-Sent Events para receber, ou WebSocket quando os dois lados falam o tempo todo. Em qualquer caso, a mensagem é gravada no banco antes de aparecer, a ordem vem do id do banco, o reenvio usa um identificador do cliente para não duplicar, e reconectar busca o que ficou para trás.

Por que o polling engana

No polling, o navegador pergunta de tempos em tempos. Se o intervalo é longo, a conversa atrasa. Se é curto, o servidor gasta CPU, conexões e consultas ao banco respondendo vazio. Um segundo de intervalo ainda significa até um segundo de atraso, mais o tempo da requisição. Você paga caro e continua não tendo tempo real.

Há outro problema, mais grave que o custo: quando uma requisição falha, muitos chats caseiros simplesmente ignoram o erro. A tela fica igual, sem indicar que perdeu a conexão. A pessoa acha que o outro lado está calado, quando na verdade ela está desconectada. O mesmo vale para requisições que nunca respondem; sem tempo limite, elas ficam penduradas, como explica timeout antes do infinito.

Qual transporte usar

  • Polling com intervalo de alguns segundos: bom para mural de recados, status de pedido, painel interno com poucos usuários. Simples, funciona em qualquer hospedagem.
  • Server-Sent Events (SSE): uma conexão HTTP que fica aberta e por onde o servidor empurra mensagens. O envio continua sendo um POST comum. O EventSource do navegador reconecta sozinho e, se as mensagens tiverem id, manda o cabeçalho Last-Event-ID na reconexão, o que facilita retomar de onde parou.
  • WebSocket: canal bidirecional, ideal quando há muito tráfego nos dois sentidos, como indicador de "digitando". Bibliotecas como SignalR e Socket.IO cuidam de reconexão e de alternativas quando o WebSocket não está disponível.

Para chat de suporte, SSE costuma ser suficiente e mais simples de operar que WebSocket. Para a parte de servidor com SignalR, veja notificação em tempo real com SignalR.

Como gravar e ordenar as mensagens

O desenho abaixo vale para qualquer transporte. O banco é a fonte da verdade; o canal só avisa.

CREATE TABLE mensagens (
  id          BIGINT AUTO_INCREMENT PRIMARY KEY,
  sala_id     INT          NOT NULL,
  autor_id    INT          NOT NULL,
  id_cliente  CHAR(36)     NOT NULL,
  texto       VARCHAR(2000) NOT NULL,
  criada_em   DATETIME(3)  NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
  UNIQUE KEY uq_reenvio (autor_id, id_cliente),
  KEY ix_sala (sala_id, id)
) DEFAULT CHARSET=utf8mb4;
  1. O navegador gera um id_cliente com crypto.randomUUID() antes de enviar.
  2. Mostra a mensagem como "enviando", em cinza, sem fingir que foi entregue.
  3. O servidor grava. Se o par autor e id_cliente já existe, devolve a mensagem existente em vez de criar outra.
  4. O navegador troca o estado para enviado ao receber o id do banco.
  5. Se falhar, reenvia com o mesmo id_cliente. A restrição única garante uma mensagem só, o mesmo princípio de idempotência no pagamento.
  6. A tela ordena pelo id do banco, nunca pela ordem em que as respostas chegaram.

Um cliente com polling que não mente

Mesmo no polling simples, dá para fazer o certo: intervalo razoável, recuo exponencial quando falha, aviso visível de desconexão e remoção de duplicatas.

const SALA = 7;
const recebidas = new Map();
let ultimoId = 0;
let espera = 3000;

async function buscar() {
  try {
    // volta alguns ids para pegar mensagens confirmadas fora de ordem; o Map descarta repetidas
    const desde = Math.max(0, ultimoId - 50);
    const resposta = await fetch('/api/salas/' + SALA + '/mensagens?depois=' + desde, {
      signal: AbortSignal.timeout(10000)
    });
    if (!resposta.ok) throw new Error('HTTP ' + resposta.status);
    for (const m of await resposta.json()) {
      recebidas.set(m.id, m);
      if (m.id > ultimoId) ultimoId = m.id;
    }
    desenhar();
    espera = document.visibilityState === 'visible' ? 3000 : 15000;
    mostrarStatus('');
  } catch (erro) {
    espera = Math.min(espera * 2, 60000);
    mostrarStatus('Sem conexão. Nova tentativa em ' + Math.round(espera / 1000) + ' s.');
  }
  setTimeout(buscar, espera);
}

function desenhar() {
  const ordenadas = [...recebidas.values()].sort((a, b) => a.id - b.id);
  const lista = document.getElementById('mensagens');
  lista.replaceChildren(...ordenadas.map((m) => {
    const li = document.createElement('li');
    li.textContent = m.autor + ': ' + m.texto;
    return li;
  }));
}

function mostrarStatus(texto) {
  document.getElementById('status').textContent = texto;
}

buscar();

O textContent não é detalhe: montar a mensagem com innerHTML deixa qualquer participante injetar script na tela dos outros. A página precisa de um ul com id mensagens e de um elemento com id status, de preferência com aria-live="polite" para que o aviso de desconexão seja anunciado.

Por que voltar alguns ids

Com várias pessoas enviando ao mesmo tempo, o banco pode reservar o id 104 para uma transação e o 105 para outra, e a 105 terminar primeiro. Se o cliente buscou "depois de 104" nesse intervalo, recebeu a 105, avançou o marcador e nunca mais pede a 104. Buscar com uma pequena sobreposição e descartar repetidas fecha esse buraco em chats de volume baixo. Em volume alto, a solução é uma sequência por sala atribuída de forma serializada, ou o canal persistente entregando cada mensagem confirmada.

Reconectar é recuperar a história

Com qualquer transporte, a rede vai cair. Ao reconectar, o cliente pede ao servidor as mensagens depois do último id que conhece, junta com as que já tem e redesenha em ordem. No SSE, o Last-Event-ID chega pronto; no WebSocket, o cliente envia o último id ao reconectar. O que nunca pode acontecer é a tela voltar a receber mensagens novas sem as que se perderam no meio.

Erros comuns

  • Polling a cada segundo para todos: centenas de milhares de requisições vazias por dia, banco ocupado e ainda assim atraso perceptível.
  • Ignorar erro de rede: a pessoa acha que o outro lado não responde quando, na verdade, está desconectada.
  • Mostrar a mensagem como enviada antes de gravar: se o servidor recusa, a conversa na tela não bate com a do outro lado.
  • Reenvio sem identificador do cliente: cada nova tentativa cria uma mensagem duplicada.
  • Ordenar pela chegada da resposta: duas mensagens enviadas juntas aparecem invertidas para pessoas diferentes.
  • Montar a bolha com innerHTML: qualquer participante executa script no navegador dos outros.

Checklist

  • O intervalo de polling é de segundos, aumenta quando falha e quando a aba está oculta, ou o chat usa SSE ou WebSocket.
  • A tela mostra claramente quando está sem conexão.
  • Toda mensagem é gravada antes de aparecer como enviada.
  • O reenvio usa o mesmo identificador gerado no cliente e o banco tem restrição única.
  • A ordem na tela é a do id do banco.
  • Ao reconectar, o cliente busca as mensagens perdidas.
  • O texto das mensagens é inserido com textContent.
  • Toda requisição tem tempo limite.

Abra o chat em duas abas, desligue a rede de uma por trinta segundos enquanto a outra envia três mensagens e religue. As três aparecem, na mesma ordem nas duas abas, ou o chat montou a história na corrida?

Avalie este artigo

Referência: http://criarestilosnet.com/criar-chat-em-jquery/