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 establecida la conexión, los servidores pueden iniciar la transmisión de datos.
Te recomendamos que uses 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 del cliente.
Es posible que conozcas el concepto de SSE. Una app web se “suscribe” a un flujo de actualizaciones que genera un servidor y, cada vez que ocurre un evento nuevo, se envía una notificación al cliente. Sin embargo, para comprender realmente los eventos enviados por el servidor, debemos comprender las limitaciones de sus predecesores de AJAX. Esto incluye lo siguiente:
Consulta: La aplicación consulta un servidor de forma reiterada para obtener datos. La mayoría de las aplicaciones de AJAX usan esta técnica. Con el protocolo HTTP, la recuperación de datos gira en torno a un formato de solicitud y respuesta. El cliente realiza una solicitud y espera a que el servidor responda con datos. Si no hay ninguno disponible, se muestra una respuesta vacía. El sondeo adicional genera una mayor sobrecarga de HTTP.
Recuperación prolongada (GET colgante / COMET): Si el servidor no tiene datos disponibles, mantiene la solicitud abierta hasta que se ponen a disposición datos nuevos. Por lo tanto, esta técnica suele denominarse "GET colgante". Cuando la información esté disponible, el servidor responderá, cerrará la conexión y se repetirá el proceso. Por lo tanto, el servidor responde constantemente con datos nuevos. Para configurar esto, los desarrolladores suelen usar hacks, como agregar etiquetas de secuencia de comandos a un iframe "infinito".
Los eventos enviados por el servidor se diseñaron desde cero para ser eficientes. Cuando se comunica con 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 del servidor al cliente a medida que ocurren. Los SSEs abrieron un solo canal unidireccional entre el servidor y el cliente.
La principal diferencia entre los eventos enviados por el servidor y la solicitud de larga duración es que el navegador controla directamente los SSE y el usuario solo tiene que escuchar los mensajes.
Comparación entre los eventos enviados por el servidor y los 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 dúplex completa bidireccional. Un canal de dos vías 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, los indicadores bursátiles, los feeds de noticias o
otros mecanismos de envío automático de datos. En otras palabras, una actualización a una base de datos Web SQL del cliente o un almacén de objetos IndexedDB.
Si necesitas enviar datos a un servidor, XMLHttpRequest
siempre es una buena opción.
Los SSE se envían a través de HTTP. No hay un protocolo ni una implementación de servidor especial para que funcione. WebSockets requiere conexiones dúplex completas y nuevos servidores 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, como la reconexión automática, los IDs de evento y la capacidad de enviar eventos arbitrarios.
Crea un objeto EventSource con JavaScript
Para suscribirte a un flujo de eventos, crea un objeto EventSource
y pásale la URL de tu flujo:
const source = new EventSource('stream.php');
A continuación, configura un controlador para el evento message
. De manera opcional, puedes detectar 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 los datos nuevos estará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 controlar 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
Enviar un flujo de eventos desde la fuente es cuestión de construir una respuesta de texto sin formato, que se entrega con un Content-Type 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 y, luego, dos caracteres "n" para finalizar la transmisión:
data: My message\n\n
Datos de varias líneas
Si tu mensaje es más largo, puedes dividirlo con varias líneas data:
.
Dos o más líneas consecutivas que comienzan con data:
se tratan como un único dato, lo que significa que solo se activa un evento message
.
Cada línea debe terminar en una sola "n" (excepto la última, que debe terminar con dos). El resultado que se pasa a tu controlador message
es una sola cadena concatenada con caracteres de salto de línea. Por ejemplo:
data: first line\n
data: second line\n\n</pre>
Esto produce "first line\nsecond line" en e.data
. Luego, se podría usar e.data.split('\n').join('')
para reconstruir el mensaje sin los caracteres "n".
Cómo enviar datos JSON
El uso de varias líneas te ayuda a enviar JSON sin interrumpir la sintaxis:
data: {\n
data: "msg": "hello world",\n
data: "id": 12345\n
data: }\n\n
Y el 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);
});
Asocia un ID a un evento
Para enviar un ID único con un evento de flujo, 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 haga un seguimiento del último evento activado para que, si se pierde la conexión con el servidor, se configure un encabezado HTTP especial (Last-Event-ID
) con la solicitud nueva. Esto permite que el navegador determine qué evento es adecuado para activarse.
El evento message
contiene una propiedad e.lastEventId
.
Controla el tiempo de espera de la reconexión
El navegador intenta volver a conectarse a la fuente aproximadamente 3 segundos después de que se cierra cada conexión. Para cambiar ese tiempo de espera, incluye una línea que comience con retry:
, seguida de la cantidad de milisegundos que se deben esperar antes de intentar volver a conectarte.
En el siguiente ejemplo, se intenta restablecer la conexión después de 10 segundos:
retry: 10000\n
data: hello world\n\n
Especifica un nombre para el evento
Una sola fuente de eventos puede generar diferentes tipos de eventos si incluye 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 escuchar 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:
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 una transmisión de eventos
Por lo general, el navegador se reconecta automáticamente a la fuente de eventos cuando se cierra la conexión, 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 Content-Type
que no sea text/event-stream
o muestra un estado HTTP que no sea 200 OK
(como 404 Not Found
).
Ambos métodos evitan 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 se pueda acceder al extremo SSE de tu servidor desde diferentes orígenes, lee cómo habilitarlo con el uso compartido de recursos multiorigen (CORS).