Transmitir atualizações com eventos enviados pelo servidor

Eventos enviados pelo servidor (SSEs) enviam atualizações automáticas a um cliente usando um servidor com uma uma conexão com a Internet. Depois que a conexão é estabelecida, os servidores podem iniciar transmissão.

É possível usar SSEs para enviar notificações push do seu app da Web. Os SSEs enviam informações em uma única direção. Por isso, você não vai receber atualizações do para o cliente.

Você já conhece o conceito de SSEs. Um app da Web "se inscreve" a um fluxo geradas por um servidor e, sempre que ocorre um novo evento, uma notificação é enviada ao cliente. Mas, para realmente entender os eventos enviados pelo servidor, compreender as limitações de seus antecessores de AJAX. Isso inclui:

  • Pesquisa: o aplicativo pesquisa repetidamente um servidor em busca de dados. Essa técnica é usado pela maioria dos aplicativos AJAX. Com o protocolo HTTP, buscar em torno de um formato de solicitação e resposta. O cliente faz uma solicitação e espera a resposta do servidor com os dados. Se nenhum estiver disponível, um valor será retornada. A sondagem adicional cria maior sobrecarga de HTTP.

  • Pesquisa longa (Suspensão de GET / COMET): se o servidor não tiver dados disponível, o servidor mantém a solicitação aberta até que novos dados sejam disponibilizados. Por isso, essa técnica é frequentemente chamada de "Suspensão GET". Quando fica disponível, o servidor responde, encerra a conexão, e o processo se repete. Assim, o servidor está constantemente respondendo com novos dados. Para configurar isso, os desenvolvedores costumam usar truques como anexar as tags de script para uma instância "infinita" iframe.

Os eventos enviados pelo servidor foram projetados do zero para serem eficientes. Ao se comunicar com SSEs, um servidor pode enviar dados para seu o app sempre que quiser, sem precisar fazer uma solicitação inicial. Em outras palavras, as atualizações podem ser transmitidas de servidor para cliente à medida que acontecem. SSEs abrem um único canal unidirecional entre servidor e cliente.

A principal diferença entre os eventos enviados pelo servidor e a pesquisa longa é que os SSEs são tratados diretamente pelo navegador, e o usuário só precisa ouvir as mensagens.

Eventos enviados pelo servidor versus WebSockets

Por que você escolheria eventos enviados pelo servidor em vez de WebSockets? Boa pergunta.

O WebSockets tem um protocolo avançado com bidirecional e full-duplex. Um canal bidirecional é melhor para jogos, aplicativos de mensagens e qualquer caso de uso em que você precisa de atualizações quase em tempo real em ambas as direções.

No entanto, às vezes você só precisa da comunicação unidirecional de um servidor. Por exemplo, quando um amigo atualiza o status, as ações da bolsa, os feeds de notícias ou outros mecanismos automatizados de push de dados. Em outras palavras, uma atualização em um banco de dados Web SQL ou um repositório de objetos IndexedDB do lado do cliente. Se você precisar enviar dados a um servidor, XMLHttpRequest será sempre um amigo.

Os SSEs são enviados por HTTP. Não há nenhum protocolo ou servidor especial para que a implementação funcione. Os WebSockets exigem full-duplex e novos servidores WebSocket para lidar com o protocolo.

Além disso, os eventos enviados pelo servidor têm uma variedade de recursos que os WebSockets não têm desde a concepção, incluindo reconexão automática, IDs de eventos e a capacidade de enviar eventos arbitrários.

Criar uma EventSource com JavaScript

Para se inscrever em um fluxo de eventos, crie um objeto EventSource e transmita a ele o URL da sua transmissão:

const source = new EventSource('stream.php');

Em seguida, configure um gerenciador para o evento message. Também é possível ouvir open e error:

source.addEventListener('message', (e) => {
  console.log(e.data);
});

source.addEventListener('open', (e) => {
  // Connection was opened.
});

source.addEventListener('error', (e) => {
  if (e.readyState == EventSource.CLOSED) {
    // Connection was closed.
  }
});

Quando as atualizações são enviadas pelo servidor, o gerenciador onmessage é acionado e novos dados são disponibilizados na propriedade e.data. A parte mágica é que, sempre que a conexão for encerrada, o navegador se reconecta automaticamente ao após cerca de 3 segundos. A implementação do seu servidor pode até mesmo ter controle sobre tempo limite de reconexão.

É isso. Agora seu cliente pode processar eventos de stream.php.

Formato de stream de eventos

O envio de um fluxo de eventos da origem é uma questão de construir um resposta de texto simples, veiculada com um Content-Type text/event-stream, que segue o formato SSE. Na forma básica, a resposta precisa conter uma linha data:, seguida pelo seguida de dois "\n" caracteres para encerrar a transmissão:

data: My message\n\n

Dados de várias linhas

Se a mensagem for mais longa, divida-a usando várias linhas data:. Duas ou mais linhas consecutivas que começam com data: são tratadas como uma uma única parte dos dados, o que significa que apenas um evento message é disparado.

Cada linha deve terminar em um único "\n" (exceto a última, que deve terminar com dois). O resultado transmitido ao gerenciador message é uma única string concatenados por caracteres de nova linha. Exemplo:

data: first line\n
data: second line\n\n</pre>

Isso produz "first line\nsecond line" em e.data. É possível usar e.data.split('\n').join('') para recriar a mensagem sem "\n" caracteres.

Enviar dados JSON

O uso de várias linhas ajuda você a enviar JSON sem quebrar a sintaxe:

data: {\n
data: "msg": "hello world",\n
data: "id": 12345\n
data: }\n\n

E possível código do lado do cliente para processar esse stream:

source.addEventListener('message', (e) => {
  const data = JSON.parse(e.data);
  console.log(data.id, data.msg);
});

Associar um código a um evento

É possível enviar um ID exclusivo com um evento de fluxo incluindo uma linha que começa com id::

id: 12345\n
data: GOOG\n
data: 556\n\n

Definir um ID permite que o navegador acompanhe o último evento disparado para que, se o conexão com o servidor for eliminada, um cabeçalho HTTP especial (Last-Event-ID) será com a nova solicitação. Isso permite que o navegador determine qual evento deve ser acionado. O evento message contém uma propriedade e.lastEventId.

Controlar o tempo limite de reconexão

O navegador tenta se reconectar à origem por aproximadamente três segundos após o encerramento de cada conexão. Para alterar esse tempo limite, inclua um linha que começa com retry:, seguida pelo número de milissegundos esperar antes de tentar se reconectar.

O exemplo a seguir tenta a reconexão após 10 segundos:

retry: 10000\n
data: hello world\n\n

Especifique um nome de evento

Uma única fonte de eventos pode gerar diferentes tipos de eventos incluindo um nome do evento. Se houver uma linha que começa com event:, seguido por um nome exclusivo, o evento é associado a esse nome. No cliente, um listener de eventos pode ser configurado para detectar esse evento específico.

Por exemplo, a saída do servidor a seguir envia três tipos de eventos: uma "mensagem" genérica event, "userlogon" e "update" evento:

data: {"msg": "First message"}\n\n
event: userlogon\n
data: {"username": "John123"}\n\n
event: update\n
data: {"username": "John123", "emotion": "happy"}\n\n

Com os listeners de eventos configurados no cliente:

source.addEventListener('message', (e) => {
  const data = JSON.parse(e.data);
  console.log(data.msg);
});

source.addEventListener('userlogon', (e) => {
  const data = JSON.parse(e.data);
  console.log(`User login: ${data.username}`);
});

source.addEventListener('update', (e) => {
  const data = JSON.parse(e.data);
  console.log(`${data.username} is now ${data.emotion}`);
};

Exemplos de servidor

Veja a seguir uma implementação básica do servidor em PHP:

<?php
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache'); // recommended to prevent caching of event data.

/**
* Constructs the SSE data format and flushes that data to the client.
*
* @param string $id Timestamp/id of this connection.
* @param string $msg Line of text that should be transmitted.
**/

function sendMsg($id, $msg) {
  echo "id: $id" . PHP_EOL;
  echo "data: $msg" . PHP_EOL;
  echo PHP_EOL;
  ob_flush();
  flush();
}

$serverTime = time();

sendMsg($serverTime, 'server time: ' . date("h:i:s", time()));
?>

Veja uma implementação semelhante no Node JS usando uma Gerenciador Express:

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

    // Sends an event to the client where the data is the current date,
    // then schedules the event to happen again after 5 seconds.
    const sendEvent = () => {
        const data = (new Date()).toLocaleTimeString();
        res.write("data: " + data + '\n\n');
        setTimeout(sendEvent, 5000);
    };

    // Send the initial event immediately.
    sendEvent();
});

sse-node.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
  </head>
  <body>
    <script>
    const source = new EventSource('/events');
    source.onmessage = (e) => {
        const content = document.createElement('div');
        content.textContent = e.data;
        document.body.append(content);
    };
    </script>
  </body>
</html>

Cancelar um stream de eventos

Normalmente, o navegador se reconecta automaticamente à origem do evento quando a conexão é fechado, mas esse comportamento pode ser cancelado pelo cliente ou pelo servidor.

Para cancelar um stream do cliente, chame:

source.close();

Para cancelar uma transmissão do servidor, responda com um que não seja text/event-stream Content-Type ou retornar um status HTTP diferente de 200 OK (por exemplo, 404 Not Found).

Os dois métodos impedem que o navegador restabeleça a conexão.

Sobre segurança

As solicitações geradas por EventSource estão sujeitas às políticas de mesma origem que outras APIs de rede, como Busca. Se você precisar que o endpoint SSE no servidor seja acessíveis de diferentes origens, leia como ativar Compartilhamento de recursos entre origens (CORS, na sigla em inglês):