Estoy muy contento con los cambios realizados al RSS de esta bitácora. No solo porque funciona ;), sino porque implementar el soporte de If-Modified-Since se me antojaba mucho más complicado de lo que al final ha resultado.
La técnica es bastante sencilla: en las peticiones HTTP 1.1 puede aparecer una cabecera que nos indica que el recurso pedido debe servirse sólo si se ha modificado desde una fecha dada.
Esta bitácora tiene un periodo de actualización medio de una vez al día (o cada varios días, depende :P), pero los agregadores suelen configurarse para consultar las fuentes una vez cada hora. Así que devolver siempre el mismo fichero RSS, que no se ha modificado, es un desperdicio evidente si podemos indicar en su lugar que el fichero no ha cambiado.
¿Qué diferencia supone en cifras? Bueno, antes mostrar un poco de código, voy a repasar un poco los logs de esta bitácora, del día 11 de Junio concretamente, para ver qué tal va funcionando mi implementación.
Para ir más rápido, primero guardo en un fichero aparte la actividad del día en cuestión:
$ grep "11/Jun/2006" < /var/www/logs/blog-access_log > jun11.log
Las respuestas normales devuelven el código HTTP 200, y las del tipo contenido no modificado se indican con el código 304. Mientras que en el primer caso se registra la cantidad de bytes enviada, sin cabeceras (solo el tamaño del recurso), en el segundo no se envía contenido, con lo que no hay cantidad :).
Si despreciamos el tamaño de las cabeceras (son pocos bytes), podemos emplear el siguiente script AWK para analizar el log:
BEGIN { cm=0; bm=0; cnm=0; }
/GET\ \/(blackshell.rss|rss.php)\ HTTP/ {
if($9=="200") { cm++; bm=bm + $10 }
if($9=="304") { cnm++; }
}
END {
print "Total de peticiones: " (cm + cnm);
print "Respuesta 200: " cm " (" bm " bytes, " (bm/cm) " bytes/petición)";
print "Respuesta 304: " cnm;
}
Es muy sencillo. Empleamos tres variables: cm (cuenta respuestas 200), bm (bytes enviados en respuestas 200) y cnm (cuenta respuestas 304). Al ejecutarlo contra el registro del 11 de Julio, tenemos:
$ awk -f procesa.awk < jun11.log Total de peticiones: 808 Respuesta 200: 385 (4953410 bytes, 12866 bytes/petición) Respuesta 304: 423
En efecto, ese día no se modificó el RSS, así que su tamaño fue siempre el que aparece de media por petición (12866 bytes). Si hay 423 resuestas del tipo no modificado, podemos decir que el ahorro ha sido de 5442318 bytes que no fue necesario enviar. Esto, en mi caso, con un número de visitas modesto y sirviendo las páginas desde mi conexión de casa, no es tampoco muy importante (algo sí, que tengo el ancho de banda de subida muy limitado :P), pero para casos en los que pagamos un hospedaje y nuestra bitácora tiene más éxito, puede suponer una diferencia al mes.
La implementación es sencilla, aunque yo tuve que hacer algunos cambios en el CMS. Por ejemplo: no guardaba la fecha de las modificaciones en las anotaciones, que resulta necesaria para saber cuándo se debe dar una nueva versión del RSS.
Aún con todo, fue fácil :). La estrategia es la siguiente:
Si existe la cabecera If-Modified-Since
Si la fecha de la última modificación = If-Modified-Since
Devuelve respuesta 302 y termina
Respuesta 200 con contenido y Last-Modified a fecha última modificación
El único problema es que necesitamos una fecha, y además que esté en formato GMT idéntico al que emplea HTTP (por ejemplo: Sat, 10 Jun 2006 16:42:27 GMT). En mi caso tuve que hacer una corrección de 2 horas porque almaceno la fecha en el curso horario local, no en GMT, pero para eso MySQL tiene DATE_FORMAT y DATE_SUB (lástima que no hayan anclas a las funciones). Así podemos obtener la fecha en formato adecuado desde el mismo query, si suponemos en actualizado la fecha de la actualización de cada anotación:
-- en mi caso, que guardo las fechas en CEST -- SELECT date_format(date_sub(actualizado, interval 2 hour), '%a, %d %b %Y %T GMT') FROM tblNoticia ORDER BY actualizado DESC LIMIT 1; -- si guardamos las fechas en GMT -- SELECT date_format(actualizado, '%a, %d %b %Y %T GMT') FROM tblNoticia ORDER BY actualizado DESC LIMIT 1;
Esto claramente es un hack, y no de los buenos. Con ese query obtenemos la fecha de la última actualización de la bitácora, y a nosotros solo nos interesan las últimas actualizaciones de las 10 anotaciones más recientes (que en mi caso son las que aparecen en el RSS). Es un mal menor porque no ando editando anotaciones viejas ;), pero si podemos hacer una subconsulta, podemos solucionarlo fácil:
-- en mi caso, que guardo las fechas en CEST -- SELECT date_format(date_sub(actualizado, interval 2 hour), '%a, %d %b %Y %T GMT') FROM tblNoticia WHERE id IN (SELECT id FROM tblNoticia ORDER BY id DESC LIMIT 10) ORDER BY actualizado DESC LIMIT 1;
No he probado esa posibilidad porque en blackshell trabajo con MySQL 4.0.24, y no dispongo de subconsultas (recordemos lo de ir siempre hacia adelante). Tampoco es tan terrible, pero que conste ;).
Ahora la comprobación de la cabecera contra la fecha obtenida:
/* uso mi encapsulación OO de las llamadas a MySQL */ $sql=new sql(); /* obtenemos la fecha -supongo GMT-, además perfectamente formateada */ $lm_res=$sql->query( "select date_format(actualizado, '%a, %d %b %Y %T GMT')" ." from tblNoticia order by actualizado desc limit 1" ); /* si tenemos que mandar el RSS, necesitamos la fecha de la última modificación para la cabecera de Last-Modified */ $lm=$sql->result($lm_res, 0); /* hay que ser educado y liberar recursos ;) */ $sql->free($lm_res); /* necesitamos todas las cabeceras para inspeccionar If-Modified-Since */ $headers=getallheaders(); /* ¿el cliente usa If-Modified-Since? */ if(isset($headers["If-Modified-Since"])) /* bien, ¿es la fecha que tenemos? */ if($lm==$headers["If-Modified-Since"]) { /* bien! contenido no modificado, tenga un buen día */ header("HTTP/1.1 304 Not Modified"); $sql->close(); exit; } /* ...continua... */
Ahora generaríamos el RSS normalmente, enviando en primer lugar un par de cabeceras, una con la fecha de la última modificación y la otra indicando el tipo MIME (eso ya lo hacía, ojo :D):
/* empleamos la fecha almacenada antes */ header("Last-Modified: " .$lm); header("Content-Type: application/rss+xml"); header("Content-Disposition: attachment; filename=blackshell.rss"); /*...generamos el RSS...*/
Como se puede apreciar no es nada complicado realizar una respuesta HTTP condicional, y estoy seguro que la mayoría de los CMS del mercado lo implementan. No obstante saber cómo funciona puede sernos de utilidad, ya sea porque hemos programado nuestro propio gestor de contenidos, o porque devolvemos contenido dinámico al que se accede con más frecuencia de la que cambia y no queremos desperdiciar ancho de banda ;).

![[xml]](/images/xml.gif)
