Actualizaciones de transmisión con eventos enviados por el servidor

Eric Bidelman

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

Te recomendamos usar los SSE 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 cliente.

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

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

  • Sondeo prolongado (Hanging GET / COMET): Si el servidor no tiene datos. disponible, el servidor mantiene la solicitud abierta hasta que haya nuevos datos disponibles. Por lo tanto, a menudo se la denomina “GET pendiente”. Cuándo de que la información esté disponible, el servidor responde, cierra la conexión y el proceso se repite. Por lo tanto, el servidor responde constantemente con con datos nuevos. Para configurar esto, los desarrolladores suelen usar hackeos, como agregar contenido etiquetas de secuencia de comandos a “infinite”. iframe.

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 cuando quiera, sin necesidad de realizar una solicitud inicial. En otras palabras, las actualizaciones se pueden transmitir del servidor al cliente a medida que ocurren. SSE abrir un único canal unidireccional entre servidor y cliente.

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

Eventos enviados por el servidor versus WebSockets

¿Por qué elegirías 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 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, cotizaciones de bolsa, feeds de noticias o y otros mecanismos automatizados de envío de datos. En otras palabras, una actualización a una base de datos web de SQL del cliente o a un almacén de objetos IndexedDB. Si necesitas enviar datos a un servidor, XMLHttpRequest siempre será una buena opción.

Los SSE se envían a través de HTTP. No hay protocolos ni servidores especiales la implementación para comenzar a trabajar. Los WebSockets requieren dúplex completo conexiones de red y nuevos servidores de WebSocket para controlar el protocolo.

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

Cómo crear un EventSource con JavaScript

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

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

A continuación, configura un controlador para el evento message. Opcionalmente, 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, se activa el controlador onmessage y hay nuevos datos disponibles en su propiedad e.data. La parte mágica es que, cada vez que se cierra la conexión, el navegador se reconecta automáticamente a la 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 la transmisión de eventos

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

data: My message\n\n

Datos de varias líneas

Si tu 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 un solo dato, lo que significa que solo se activa un evento message.

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

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

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

Enviar datos JSON

Si usas varias líneas, podrás enviar JSON sin afectar la sintaxis:

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

Y un posible código 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

Puedes enviar un ID único con un evento de transmisión si incluyes una línea que comience con id::

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

Configurar un ID permite que el navegador lleve un registro del último evento activado, de modo que, si el se interrumpe una conexión con el servidor, se agrega un encabezado HTTP especial (Last-Event-ID) que se establecen con la nueva solicitud. Esto permite que el navegador determine qué evento es apropiado para activarse. El evento message contiene una propiedad e.lastEventId.

Controla el tiempo de espera de reconexión

El navegador intenta volver a conectarse a la fuente aproximadamente 3 segundos después de cerrar cada conexión. Para cambiar ese tiempo de espera, incluye un Línea que comienza con retry:, seguida de la cantidad de milisegundos que esperar antes de intentar reconectarse.

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 única fuente de eventos puede generar diferentes tipos de eventos al incluir un el nombre del evento. Si hay una línea que comienza con event:, seguido 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 escuchar ese evento en particular.

Por ejemplo, la siguiente salida del servidor envía tres tipos de eventos: un "mensaje" genérico event, "userlogon" y "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

Con los objetos de escucha de eventos configurados en el 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}`);
};

Ejemplos de servidores

Esta es 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>

Cancela la transmisión de un evento

Normalmente, el navegador se vuelve a conectar automáticamente a la fuente del evento cuando la conexión está cerrado, pero ese comportamiento se puede cancelar desde el cliente o el servidor.

Para cancelar una transmisión desde el cliente, llama a:

source.close();

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

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

Información sobre la seguridad

Las solicitudes que genera EventSource están sujetas a las políticas del mismo origen que otras APIs de red, como Fetch. Si necesitas que el extremo de SSE de tu servidor esté accesibles desde diferentes orígenes, lee cómo habilitar Uso compartido de recursos multiorigen (CORS).