Actualizaciones de transmisión con eventos enviados por el servidor

Los eventos enviados por el servidor (SSE) envían actualizaciones automáticas a un cliente desde un servidor, con una conexión HTTP. Una vez que se establece la conexión, los servidores pueden iniciar la transmisión de datos.

Te recomendamos que los uses para enviar notificaciones push desde tu app web. Los SSE envían información en una dirección, por lo que no recibirás actualizaciones del cliente.

El concepto de SSE puede resultarte familiar. Una app web se "suscribe" a una transmisión de actualizaciones que genera un servidor y, cada vez que ocurre un evento nuevo, se envía una notificación al cliente. Pero para entender realmente los eventos enviados por el servidor, debemos entender las limitaciones de sus predecesores de AJAX. Esto incluye:

  • Sondeo: La aplicación sondea repetidamente un servidor en busca de datos. La mayoría de las aplicaciones AJAX utilizan esta técnica. Con el protocolo HTTP, la recuperación de datos gira en torno a un formato de solicitud y respuesta. El cliente hace una solicitud y espera a que el servidor responda con los datos. Si no hay ninguna disponible, se muestra una respuesta vacía. El sondeo adicional crea una mayor sobrecarga de HTTP.

  • Sondeo largo (GET / COMET colgante): Si el servidor no tiene datos disponibles, mantiene la solicitud abierta hasta que haya datos nuevos disponibles. Por lo tanto, esta técnica a menudo se conoce como “Hanging GET”. Cuando la información está disponible, el servidor responde, cierra la conexión y se repite el proceso. Por lo tanto, el servidor responde de forma constante con datos nuevos. Para configurarlo, los desarrolladores suelen usar trucos como agregar etiquetas de secuencia de comandos a un iframe "infinito".

Los eventos enviados por el servidor se diseñaron desde cero para que sean eficientes. Cuando se comunica con los SSE, un servidor puede enviar datos a tu app cuando quiera, sin necesidad de realizar una solicitud inicial. En otras palabras, las actualizaciones se pueden transmitir de un servidor a otro a medida que ocurren. Los SSE abren un único canal unidireccional entre el servidor y el cliente.

La principal diferencia entre los eventos enviados por el servidor y los sondeos largos es que el navegador controla directamente los SSE, y el usuario solo tiene que escuchar los mensajes.

Eventos enviados por el servidor frente a WebSockets

¿Por qué elegirías los eventos enviados por el servidor en lugar de WebSockets? Buena pregunta.

WebSockets tiene un protocolo enriquecido con comunicación bidireccional y dúplex completo. Un canal bidireccional es mejor para juegos, apps de mensajería y cualquier caso de uso en el que necesites actualizaciones casi en tiempo real en ambas direcciones.

Sin embargo, a veces solo necesitas una comunicación unidireccional desde un servidor. Por ejemplo, cuando un amigo actualiza su estado, la cotización de acciones, los feeds de noticias o cualquier otro mecanismo automatizado de envío de datos. En otras palabras, una actualización de una base de datos web de SQL del cliente o un almacén de objetos de IndexedDB. Si necesitas enviar datos a un servidor, XMLHttpRequest siempre es un amigo.

Los SSE se envían a través de HTTP. No hay una implementación de servidor o protocolo especial para funcionar. Los WebSockets requieren conexiones dúplex completos y servidores WebSocket nuevos para controlar el protocolo.

Además, los eventos enviados por el servidor tienen una variedad de características que los WebSockets no tienen por diseño, como la reconexión automática, los IDs de eventos y la capacidad de enviar eventos arbitrarios.

Crea una EventSource con JavaScript

Para suscribirte a una transmisión de eventos, crea un objeto EventSource y pásale la URL de tu transmisión:

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

A continuación, configura un controlador para el evento message. De manera opcional, puedes escuchar open y 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.
  }
});

Cuando se envían actualizaciones desde el servidor, el controlador onmessage se activa y los datos nuevos están disponibles en su propiedad e.data. La parte mágica es que cada vez que se cierra la conexión, el navegador se vuelve a conectar automáticamente a la fuente después de unos 3 segundos. La implementación de tu servidor incluso puede tener control sobre este tiempo de espera de reconexión.

Eso es todo. Tu cliente ahora puede procesar eventos de stream.php.

Formato de transmisión del evento

Para enviar una transmisión de eventos desde la fuente, se debe construir una respuesta de texto sin formato, que se entregue con un tipo de contenido text/event-stream, que siga el formato SSE. En su forma básica, la respuesta debe contener una línea data:, seguida de tu mensaje y dos caracteres "\n" para finalizar la transmisión:

data: My message\n\n

Datos de varias líneas

Si el mensaje es más largo, puedes dividirlo usando varias líneas data:. Dos o más líneas consecutivas que comienzan con data: se tratan como un solo dato, lo que significa que solo se activa un evento message.

Cada línea debe terminar en un solo "\n" (excepto la última, que debe terminar con dos). El resultado que se pasa a tu controlador message es una sola string concatenada por caracteres de línea nueva. Por ejemplo:

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

Esto produce una "primera línea\nsegunda línea" en e.data. Luego, se puede usar e.data.split('\n').join('') para reconstruir el mensaje sin caracteres "\n".

Envía datos JSON

Usar varias líneas te ayuda a enviar JSON sin romper la sintaxis:

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

Y posible código del lado del cliente para controlar esa transmisión:

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

Cómo asociar un ID a un evento

Para enviar un ID único con un evento de transmisión, incluye una línea que comience con id::

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

Configurar un ID permite que el navegador realice un seguimiento del último evento activado, de modo que, si se interrumpe la conexión con el servidor, se establezca un encabezado HTTP especial (Last-Event-ID) con la solicitud nueva. Esto le permite al navegador determinar qué evento es apropiado activar. El evento message contiene una propiedad e.lastEventId.

Controlar el tiempo de espera de reconexión

El navegador intenta volver a conectarse a la fuente aproximadamente 3 segundos después de que se cierra cada conexión. Puedes cambiar ese tiempo de espera si incluyes una línea que comience con retry:, seguida de la cantidad de milisegundos que hay que esperar antes de intentar volver a establecer la conexión.

En el siguiente ejemplo, se intenta volver a establecer la conexión después de 10 segundos:

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

Especifica un nombre de evento

Una sola fuente de eventos puede generar diferentes tipos de eventos mediante la inclusión de un nombre de evento. Si hay una línea que comienza con event: seguida de un nombre único para el evento, el evento se asocia con ese nombre. En el cliente, se puede configurar un objeto de escucha de eventos para que escuche ese evento en particular.

Por ejemplo, el siguiente resultado del servidor envía tres tipos de eventos: un evento genérico “message”, “userlogon” y “update”:

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

Con los objetos de escucha de eventos configurados en el cliente, ocurre lo siguiente:

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}`);
};

Ejemplos de servidores

A continuación, se muestra una implementación básica de servidor en 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()));
?>

Esta es una implementación similar en Node JS con un controlador 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>

Cómo cancelar la transmisión de un evento

Por lo general, el navegador se vuelve a conectar de forma automática a la fuente del evento cuando se cierra la conexión, pero se puede cancelar ese comportamiento desde el cliente o el servidor.

Para cancelar una transmisión desde el cliente, llama al siguiente comando:

source.close();

Para cancelar una transmisión del servidor, responde con un Content-Type que no sea text/event-stream o muestra un estado HTTP diferente de 200 OK (como 404 Not Found).

Ambos métodos evitan que el navegador restablezca la conexión.

Información sobre seguridad

Las solicitudes que genera EventSource están sujetas a las políticas de mismo origen que otras APIs de red, como la recuperación. Si necesitas que el extremo de SSE de tu servidor sea accesible desde diferentes orígenes, consulta cómo habilitar el uso compartido de recursos entre dominios (CORS).