Últimos Cambios |
||
Blog personal: El hilo del laberinto |
Última Actualización: 4 de Febrero de 2.000 - Viernes
Los navegadores modernos incorporan una funcionalidad poco conocida pero tremendamente útil: la posibilidad de recibir los datos que le envía el servidor HTTP en formato comprimido, descomprimiéndolos automáticamente de forma local. Ni que decir tiene que esta funcionalidad beneficia a ambos extremos: el servidor necesita menos ancho de banda para atender a un usuario, y el usuario recibe la información solicitada considerablemente más rápido.
Aunque ya conocía esta posibilidad desde hace mucho tiempo, no fue hasta Noviembre de 1.999 que me decidí a explotarla en un sistema en producción. Como suele ocurrir, la necesidad es la madre del desarrollo.
Todo empezó a principios de Noviembre de 1.999, cuando uno de los clientes de Argo, la empresa en la que trabajo, acudió a nosotros para que le diésemos cobertura en un evento Internet en curso. Estaba teniendo problemas con su proveedor e intentaba migrar el desarrollo a nuestras infraestructuras. Existían dos problemas, no obstante:
Ello suponía que no podíamos permitirnos el invertir una semana en implementar un desarrollo realmente adptado a las necesidades y al funcionamiento de Internet.
El desarrollo heredado era pésimo desde una perspectiva Internet. Por ejemplo, suponía que decenas de usuarios simultaneos tendrían que descargarse una página HTML de más de 63Kbytes (decenas de miles de descargas al día). Obviamente ello nunca es práctico, pero mucho menos en un evento puntual con gran nivel de saturación.
La combinación de demasiados usuarios concurrentes y páginas demasiado grandes es un cóctel explosivo.
Los navegadores modernos tienen la capacidad de recibir los datos en formato comprimido y descomprimirlos en local, todo ello de manera absolutamente transparente para el usuario. ¿Cómo sabe el servidor si puede enviar al cliente los datos en forma comprimida o no?. Sencillamente empleando lo que se denomina negociación de contenidos.
Cuando un navegador web se conecta a un servidor HTTP, envía a éste una gran cantidad de información, que éste puede utilizar para personalizar la respuesta en función de sus características: versión del navegador, idiomas favoritos, resolución y número de colores, etc. Veamos un ejemplo: (los navegadores antiguos proporcionan todavía más información)
Netscape Communicator:GET /dds HTTP/1.0 Connection: Keep-Alive User-Agent: Mozilla/4.7 [en] (Win98; I) Host: www.argo.es Accept: image/gif,image/x-xbitmap, image/jpeg, image/pjpeg, image/png, */* Accept-Encoding: gzip Accept-Language: es,en,gl Accept-Charset: iso-8859-1,*,utf-8 Authorization: Basic ***********Microsoft Internet Explorer:GET /dds HTTP/1.1 Accept: application/vnd.ms-excel, application/msword, application/vnd.ms-powerpoint, image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, */* Accept-Language: es Accept-Encoding: gzip, deflate User-Agent: Mozilla/4.0 (compatible; MSIE 5.0; Windows 98; DigExt) Host: www.argo.es Connection: Keep-Alive
Como puede verse, los navegadores indican ostensiblemente qué formatos están más dispuestos a aceptar, los idiomas, etc.
Entre las cabeceras encontramos una especialmente interesante: "Accept-Encoding: gzip" y "Accept-Encoding: gzip, deflate". gzip y deflate son formatos de compresión de datos.
Si un servidor recibe una conexión desde un cliente que indica su disposición a utilizar compresión, puede responder de manera normal (lo más habitual) o bien puede enviarle el documento solicitado de forma comprimida, sin más que indicar en la cabecera de respuesta "Content-Encoding: gzip", por ejemplo.
A priori el asunto parece bastante sencillo: basta hacer pasar las peticiones por un CGI o una tecnología similar, y devolver al cliente los datos de forma normal o de forma comprimida sencillamente viendo si el cliente nos indica que es capaz de soportarlo.
Pero las cosas no son tan sencillas.
Probablemente el problema más grave y difícil de resolver en el tema de la negociación de contenidos es el hecho de que no siempre es posible, fiable o rentable asegurarse que todas las peticiones de los navegadores son servidos por un servidor, cuyas "versiones" de los documentos varían en función de las características del cliente. En muchos casos existen cachés intermedias que pueden resolver las peticiones de forma local, sin involucrar al servidor original. Y, obviamente, dichas cachés desconocen completamente la lógica tras la negociación de contenidos inicial.
Así, por ejemplo, es perfectamente posible que un cliente con capacidad de recibir datos de forma comprimida solicite un documento, y que éste quede almacenado en uno o más sistemas de caché intermedia a lo largo de la ruta de comunicaciones. Si otro cliente, éste sin capacidad de compresión, solicita el mismo documento, puede encontrarse con una versión comprimida del mismo, devuelto desde una caché.
El resultado es que el usuario visualiza una página con "basura" y, sencillamente, no puede acceder a la información deseada.
Existen varias formas de enfrentarse al problema, sin prescindir de la negociación de contenidos:
Esta solución es la más sencilla, y nos asegura que todos los clientes recibirán una copia directa desde el servidor. Como contrapartida, el tráfico en el servidor se incrementa, y el usuario tarda más en recibir la información, ya que no puede ser proporcionada por una caché local.
Esta opción es la más recomendable cuando se están sirviendo documentos generados de forma dinámica y que varían entre usuarios distintos, ya que los clientes -de todas maneras- tendrían que conectarse al servidor original para recibir sus versiones personalizadas.
Si podemos identificar las conexiones desde cachés, podemos enviarles una versión descomprimida, válida para todos sus clientes.
El problema de este esquema es que no siempre se puede saber a ciencia cierta si el cliente que se conecta al servidor es un cliente final o una caché. La mayoría de las cachés se identifican de manera más o menos clara con cabeceras tipo "Via", "X-Forwarded-For" o "Cache-Control" pero, ¿y si nos equivocamos?.
Podemos tener un documento de entrada para discriminar el tipo de cliente (¿admite compresión o no?) y redirigirlo con una cabecera "Location" hacia la versión que más le convenga. La redirección debe hacerse de tal forma que no se haga caché de ella.
Ésta sería la mejor opción de todas si no fuera porque no está implementada en la mayoría de las cachés actuales, aunque espero que esta situación cambie pronto.
Con las cabeceras "Vary" el servidor informa de qué cabeceras del cliente depende el contenido del documento que se está sirviendo. De esta forma se marca explícitamente qué versiones de documentos existen y de qué cabeceras depende su elección.
Lamentablemente, muchas cachés consideran que un documento con cabeceras "Vary" sencillamente no es apto para ser almacenado, lo que a efectos prácticos implica un "no-cache" implícito.
Es de esperar que la situación cambie poco a poco.
En algunas ocasiones un cliente es capaz de seleccionar una URI automáticamente y de forma transparente, en función de una selección planteada por el servidor. Lamentablemente la mayoría de los navegadores no parecen contemplar esta posibilidad.
En el momento de escribir este artículo no conozco ninguna caché con esta funcionalidad.
Lo que sigue es nuestra implementación para el proyecto del cliente referido al principio de este documento. Consta de dos programas, uno escrito en PHP y otro escrito en SHELL. El primero de ellos es invocado por el servidor HTTP, y realiza todo el trabajo duro. El segundo programa sencillamente se encarga de hacer limpieza de los ficheros temporales que se hayan quedado huérfanos.
<? function salida($idioma) { global $HTTP_REFERER; global $HTTP_ACCEPT_ENCODING; set_time_limit(0); $HORA=date("Y-m-d H:i:s", time()); $cadena="<input type=\"hidden\" name=\"referer\" value=\"".$HTTP_REFERER."\">\n"; $cadena=$cadena."<input type=\"hidden\" name=\"entrada\" value=\"".$HORA."\">\n"; $HTTP_ACCEPT_ENCODING=" ".$HTTP_ACCEPT_ENCODING; if(!strpos(strtolower($HTTP_ACCEPT_ENCODING),"gzip")) { readfile($idioma."-pre"); echo $cadena; readfile($idioma."-post"); } else { // GZIP header("Content-Encoding: gzip"); $fich="/export/home/usuarios/b/CLIENTE/enc/tmp/encuesta99-".md5(microtime().getmypid()); $err=error_reporting(0); // ?BUG PHP? $id=gzopen($fich,"wb9"); error_reporting($err); $fich2=$idioma."-pre"; $id2=fopen($fich2,"rb"); gzwrite($id,fread($id2,999999)); fclose($id2); gzwrite($id,$cadena); $fich2=$idioma."-post"; $id2=fopen($fich2,"rb"); gzwrite($id,fread($id2,999999)); gzclose($id); readfile($fich); unlink($fich); } } ?>
En este caso, necesitábamos recomprimir el documento a enviar en cada caso, porque incluye campos personalizados para cada cliente en concreto. Si la página fuera completamente estática, bastaría con tener en disco dos versiones de la página, una comprimida y otra normal, y enviar el cliente la versión que más le convenga.
Debido también al hecho de que la página se genera de forma dinámica, no incluye fecha de modificación, por lo que implícitamente se le dá un "no-cache", tal y como se describió en la sección anterior.
Básicamente, la página a servir se divide en tres secciones. La sección intermedia es la sección que se genera de forma dinámica. La sección anterior y posterior son estáticas.
Cuando se recibe una conexión, se construye la parte variable (la variable "$variable" en el código anterior), y se verifica si el cliente acepta o no documentos comprimidos.
Si no es así (navegador antiguo), sencillamente se le envía el documento en las tres partes que lo forma: prefijo, parte variable y sufijo.
Si, por el contrario, el cliente acepta datos comprimidos, se le envía un "Content-Encoding: gzip" y se genera un fichero temporal que contendrá el documento en formato comprimido. El nombre de ese fichero se calcula de forma pseudoaleatoria, ya que cada petición concurrente debe tener su propio fichero de datos. Una vez generado dicho fichero, se envía al cliente y se borra del disco.
Algunos detalles a tener en cuenta:
Se trata de un bug del navegador, pero no hay mucho que nosotros podamos hacer al respecto.
#!/bin/sh # # Borra los ficheros con mas de una hora cd /export/home/usuarios/b/CLIENTE/enc/tmp fecha=`/usr/local/bin/awk 'BEGIN {a=strftime("%b +%e +%H:"); print a}'` /usr/bin/rm -f `/usr/bin/ls -lA| /usr/bin/grep " encuesta99-" | \ /usr/bin/egrep -v "$fecha"|/usr/local/bin/awk '{print $NF}'`
Este sencillo script se ejecuta vía CRON en el minuto 55 de cada hora, y elimina los ficheros temporales que se hayan creado (y no borrado) durante la hora anterior. Es decir, elimina los ficheros que tengan más de 55 minutos de antigüedad, bajo el supuesto de que cualquier transferencia de más de 55 minutos ha terminado ya hace tiempo.
No debería ocurrir nunca, pero es bastante frecuente que el script PHP deje ficheros temporales de vez en cuando en disco, debido a que Apache termina su ejecución de forma prematura.
Obviamente, los datos de la experiencia de Noviembre de 1.999 son privados y están protegidos por la política de confidencialidad que tenemos con nuestros clientes. No obstante podemos mostrar alguna conclusiones, avaladas por números, de los beneficios que nos reportó esta técnica:
Evidentemente, esta cifra aumentará a medida que los usuarios vayan actualizando sus navegadores.
Más información sobre los OpenBadges
Donación BitCoin: 19niBN42ac2pqDQFx6GJZxry2JQSFvwAfS