Server Sent Events (SSE)

TEORÍA

SSE es una sistema de comunicación entre cliente y servidor, cuyo flujo de comunicación sólo va desde el servidor hacia el cliente (no es bidireccional como los WebSockets). La idea consiste en que el cliente crea una conexión con el servidor una sola vez y este le va enviando información al cliente cuando hay nuevos datos (por ejemplo, ha detectado un cambio en una BD y le manda los nuevos datos). Ya que está orientado a cambios que detecte el servidor para informar al cliente, a este sistema se le ha denominado Server Sent Events (Eventos Enviados por el Servidor).

La comunicación se realiza vía HTTP.

El «avance» obtenido con este método es que se obtienen los datos del servidor sin tener que realizar una petición mediante AJAX cada varios segundos, ni tener que realizar long polling ni ningún otro «truco» (o como se dice en el habla anglosajona, «hacky»). Este tipo de conexión se considera como «contenido en streaming»: el servidor no tiene que esperar a tener todo el buffer de salida preparado para mandárselo todo al cliente tras terminar su ejecución; en lugar de ello, el servidor va escribiendo datos en streaming que le llegan al cliente sin que, para ello, tenga que terminar la ejecución del script de la parte servidora.

Así que, la idea es similar a tener un socket en el cual solo puede escribir el servidor y el cliente solo puede leer.

La ventaja de utilizar este sistema en lugar de «hackies» (como el long polling), es que tanto JavaScript como PHP están ya preparados para manejar esta información en streaming (menos IE y Edge que no han implementado aún a fecha del 2018 el código para usar SSE) y que la conexión no se tiene que reiniciar contínuamente para recibir nueva información del servidor.

PRÁCTICA

Vamos a centrarnos en PHP para la parte servidora y en JS para la parte cliente.

PARTE SERVIDORA

La primera duda que se viene a la mente es ¿Cómo voy a escribir datos de salida para el cliente sin terminar la ejecución del script PHP y sin usar sockets? Aquí entran en juego 3 sentencias del lenguaje PHP que va a cambiar completamente la forma en la que se escribe el buffer de salida:

1.- La primera es indicar en la cabecera el tipo de información que se va a manejar en el script PHP (Content-Type), al indicar que es de tipo «text/event-stream» estamos preparándolo para que escriba la salida en un buffer por streaming (orientado a eventos):

header('Content-Type: text/event-stream');

2.- La segunda es indicarle al script de PHP que limpie y deshabilite el buffer de salida para que seamos nosotros quienes tengamos control sobre lo que se escribe y cuándo se escribe en el buffer de salida:

ob_end_clean();

3.- Flush (ro dah!). Escribir en el buffer de salida será tan sencillo como escribir uno o más «echo» y luego ejecutar el método «flush()» para enviar (y vaciar) el contenido de dicho buffer, el cual, además, está preparado ahora para streaming gracias a la cabecera que le hemos puesto.

echo "foo";
flush();

Pero no queremos que el script termine después del flush, así que habrá que meterlo en un bucle while infinito (y para ahorrar recursos le diremos que se duerma unos segundos antes de volver a realizar el siguiente bucle).

while( true ) {
$time = date('d/m/Y H:i:s');
echo "data: {$time}\n\n";
flush();
sleep(1);
}

Este es el ejemplo que aparece en casi todas las páginas webs que hablan del SSE para tener la hora actualizada del servidor de manera constante en la parte cliente; pero se pueden hacer cosas mucho más interesantes que esta. Como por ejemplo leer los últimos cambios de una tabla de una BD y, al detectar un cambio, mandar la tabla actualizada al cliente.

NOTA: Es interesante destacar que el bucle «while» no funcionará si no se usa previamente la sentencia «ob_end_clean()«, ya que, sin ello, el servidor nunca devolverá ningún dato y la parte cliente interpretará que la conexión ha sido rechazada por el servidor.

Voy a usar XAMPP para realizar el ejemplo. Almaceno el archivo PHP en «src/time_sse.php».

PARTE CLIENTE

La parte cliente la voy a desarrollar con JavaScript.

En HTML 5 existe un objeto del tipo EventSource (menos en IE y Edge) que va a trabajar directamente con el tipo de comunicación por streaming propia del SSE. Se ha denominado «fuente de eventos» ya que, como hemos mencionado, este método está orientado a eventos lanzados por el servidor.

La idea es sencilla, el objeto EventSource se conecta al script PHP (usando una URL) que va a devolver eventos mediante streaming, y a dicho objeto se le añaden los EventListener «onopen«, «onmessage» y «onerror» (Habiendo comprobado previamente que existe el objeto de tipo EventSource en esta implementación de JavaScript):

var source;
//Primero comprobar que exista compatibilidad del navegador
if(typeof(EventSource) === "undefined") {
alert('Tu navegador no tiene soporte para el objeto EventSource');
return;
}

//Apunta al script PHP que le devuelve la hora por streaming
source = new EventSource("src/time_sse.php");

source.onopen = function (event) {
console.log('streaming SSE abierto');
};

source.onmessage = function(event) {
console.log('recibido mensaje de streaming');
document.getElementById("hour").innerHTML = event.data;
};
/*
"source.onmessage" es igual que escribir el siguiente event listener:
source.addEventListener('message', function(event) {
console.log(event.data);
}, false);
*/

source.onerror = function (event) {
console.log('ERROR en EventSource');
//Los códigos de estado de "readyState" son los mismos que un recurso XMLHttpRequest
/*if(event.target.readyState == 0){
//Se cerró la conexión
}*/
}

Almaceno este código en un documento HTML llamado «get_time.html».

Nótese que, al recibir un mensaje, tenemos el EventListener «onmessage» pero que también es equivalente a poner un EventListener de JS que responda al tipo de evento llamado «message«. Esto es importante ya que se pueden establcecer mecanismos que respondan ante distintos tipos de eventos recibidos desde el servidor. Para conseguir crear diferentes tipos de eventos entra en juego el formato del string enviado por el servidor.

FORMATO DEL STRING DEL SERVIDOR

El objeto EventSource va a recibir un String y lo primero que hará será parsearlo para trabajar con sus datos. Para ello, el string debe venir en un formato concreto:

event: <nombreEvento>\n
data: <datos>\n
data: <más datos>\n
data: \n\n

<nombreEvento> es el nombre del evento que debería ejecutarse en la parte cliente. Por defecto, si no indicamos el tipo de evento, tenemos que el valor de «event» es «message«. Esto es muy útil para poder tener varios eventos lanzados desde el mismo script SSE, y distinguir qué debemos hacer en la parte cliente con los datos recibidos en base al tipo de evento recibido.

Los datos se pueden mandar en más de una fila; lo que hará que el objeto EventSource lo parsee concatenándolos en un solo string con un salto de línea entre ellos. Siguiendo el ejemplo, el contenido que llegaría a la parte cliente sería «<datos>\n<más datos>\n». Para indicar que el string de datos ha terminado, se escriben dos saltos de línea seguidos «\n\n».

Lo más lógico sería serializar un objeto como JSON y mandarlo en una sola línea terminada en dos saltos de línea:

event: <nombreEvento>\n
data: <Json>\n\n

PROBLEMAS TÉCNICOS

LA CONEXIÓN NO ES PERMANENTE

Si abrimos la consola del navegador, con el ejemplo que se ha planteando al principio (el de mandar el tiempo del servidor actualizado al segundo), obtenemos algo como esto:

Cada 30 mensajes recibidos del servidor (a razón de 1 segundo por mensaje, esto es, cada 30 segundos), vemos que se produce un error y que se vuelve a abrir la conexión automáticamente y sigue recibiendo mensajes. Este error se debe a que la conexión con el servidor se ha cerrado por límite de tiempo de ejecución.

Como es natural, en el archivo de configuración de PHP (esto es, php.ini) tenemos un tiempo límite de ejecución de los scripts de PHP (que por defecto son 30 segundos) para evitar que haya procesos «atascados» o que algún script esté consumiendo demasiados recursos. Cada vez que la conexión con el servidor se pierde (porque la cierra PHP o el propio servidor HTTP) el objeto EventSource vuelve a realizar la conexión por su cuenta de forma automática.

De hecho, si nos fijamos en la consola de red, podemos ver lo siguiente:

Realiza varias conexiones GET que duran aproximadamente 30 segundos, todas apuntando a «time_sse.php«… Suena a que están realizando peticiones AJAX una y otra vez ¿verdad? Es uno de los métodos que al principio denominé como «hacky». La diferencia es que con este sistema no hay que realizar conexiones al servidor hasta pasados los 30 segundos (mejorando así un poco los tiempos de respuesta); pero a fin de cuentas estamos en las mismas.

La solución a este problema no es siempre aplicable, ya que el propio servidor HTTP (Apache, Nginx, etc) puede estar configurado de tal forma que tenga un límite de tiempo de conexión con el cliente, o bien que la configuración del intérprete de PHP no te permita cambiar el tiempo de ejecución dentro del script PHP (y que no podamos acceder a dichas configuraciones, como ocurriría, por ejemplo, en servidores alquilados). En este aspecto SSE se queda muy limitado solo a aquellos desarrolladores que tengan control total sobre su intérprete de PHP y su servidor HTTP (de la misma manera que ocurre con los WebSockets).

En cualquier caso, la solución es simple: utilizar el método «set_time_limit()» de php para evitar que se termine la ejecución. La primera idea que se nos viene a la mente sería poner 0 segundos para que no exista límite de tiempo, pero eso es peligroso. Si provocamos algún bucle infinito o algún bloqueo inesperado en el proceso se quedaría atascado para siempre. Por ello, es mejor indicar cuántos segundos debería durar como máximo el bucle while en cada iteración, poniendo «set_time_limit()» dentro del mismo:

while( true ) {
set_time_limit(30);
//Tu código...
}

CONFIGURACIÓN DEL TIEMPO MÁXIMO DE EJECUCIÓN

El método «set_time_limit()» solo funcionará si:

1.- El intérprete de PHP tiene desactivado el modo seguro (safe mode). Aunque este valor desaparece de php.ini a partir de la versión 5.4, para desactivarlo en versiones anteriores hay que buscar en php.ini el valor «safe_mode» y darle el valor «Off«:

safe_mode = Off

2.- El servidor HTTP es Apache. Por defecto debería funcionar, pero también se puede configurar el valor «TimeOut» desde un archivo de configuración «.httaccess» o de forma global en «httpd.conf«.

3.- El servidor HTTP es Windows IIS y se modifica el valor del atributo «executionTimeout» del nodo «httpRuntime«. Se puede configurar desde un archivo de configuración (web.config) de forma global:

<system.web>
<httpRuntime executionTimeout="0"/>
</system.web>

También se puede configurar para un archivo concreto:

<location path="somefile.php">
<system.web>
<httpRuntime executionTimeout="0"/>
</system.web>
</location>

También se puede configurar globalmente desde el administrador del servidor -> Administrador de IIS -> Botón derecho sobre el sitio web que se quiere configurar -> Administrar sitio web -> Configuración avanzada. Expandir la subsección «Límites» y modificar el valor de «tiempo de espera de la conexión».

LIMITACIÓN DEL NÚMERO DE SSE ABIERTOS

Hay una limitación muy grande del número de recursos EventSource abiertos a la vez para el mismo dominio (difiere entre los navegadores de internet, pero el límite suele estar en torno a 6). Cada navegador impone un número máximo de conexiones HTTP simultáneas por dominio que usan para descargar archivos CSS, JS, imágenes, etc. SSE es una conexión permanente por HTTP al dominio, así que tendremos menos conexiones libres para descargar recursos del dominio.

De hecho, si abres varias pestañas del archivo «get_time.html» en el mismo navegador, verás que llega un momento en el que la página no carga (En firefox concretamente ocurre al intentar abrir la décima pestaña); pero sí puedes abrir pestañas de otros dominios.

Las posibles soluciones a este problema serían:

1.- Utilizar un nombre de dominio diferente para los SSE, es decir, poner la lógica de los SSE en otro dominio que esté dentro de la misma máquina para poder realizar una comunicación inter-proceso (mediante sockets por ejemplo).

2.- Controlar el número de conexiones al mismo dominio desde JavaScript mediante los Web Storage (que es algo similar a una cookie) o bien limitar el número de SSE abiertos en la parte servidora usando un contador almacenado en la sesión PHP del usuario logeado en el sistema.

UN SSE POR CLIENTE

Si por ejemplo quisiéramos leer una BD que contenga las últimas modificaciones de otra tabla para que, cuando se produzca un cambio, se le manden los datos a los clientes, tendríamos varios scripts PHP leyendo dicha BD, uno por cada cliente conectado (no se conectan todos los clientes al mismo hilo de proceso PHP, ni comparten memoria, ni nada similar).

Lo normal sería tener un sistema de comunicación inter-proceso (por ejemplo por sockets) en la que solo exista un proceso leyendo y enviando datos nuevos de la BD a los diferentes scripts SSE y que estos se encarguen a su vez de enviárselo a sus respectivos clientes.

DESCARGA EL EJEMPLO

Puedes descargar el ejemplo que se encuentra en ESTE archivo comprimido.

¡SSE achieved!


Créditos de fuentes externas:

Iconos:

  • PC by art shop from the Noun Project
  • Server by Chanut is Industries from the Noun Project
  • Cloud by AlePio from the Noun Project
  • Gears by Gregor Cresnar from the Noun Project
  • Database by IcoMoon from the Noun Project

Deja un comentario