Le but de cette page est d'évaluer les solutions pour gérer le cache des navigateurs au niveau applicatif, à l'aide des en-têtes HTTP.
Le principe fonctionnel
1. Activation du cache pour une période déterminée
- L'utilisateur demande une page donnée au client HTTP.
- Le client demande la page au serveur.
- L'application, côté serveur, sert la page demandé et informe le client que cette page ne doit pas être redemandée avant une date d'expiration qu'il précise ("Expires: Thu, 05 Dec 2002 20:45:37 GMT")
- Le client mémorise la date d'expiration de la page.
- Le client affiche la page demandée pour l'utilisateur.
- L'utilisateur redemande la page concernée :
- si la demande est effectuée avant la date d'expiration, le navigateur va rechercher la page dans son cache.
- si la demande est effectuée après la date d'expiration, le navigateur redemande la dite page au serveur.
2. Comparaison de date
- Le client demande au serveur une page donnée : si le client a déjà lu la page, il envoie une requête contenant la date de dernière modification de sa page en cache (par exemple "If-Modified-Since: Thu, 05 Dec 2002 20:13:47 GMT").
- L'application, côté serveur, compare la date donnée par le client avec à la date de dernière modification de la page qui lui est demandée :
- Si la page sur le serveur a la même date, alors le serveur informe le client qu'il peut utiliser celle de son cache ("HTTP/1.1 304 Not Modified"). L'échange entre client et serveur en reste là.
- Si la page sur le serveur est plus récente, alors le serveur informe le client de ce changement ("Last-modified: Thu, 05 Dec 2002 20:45:37 GMT") et lui sert la page. Le navigateur mémorise la date du dernier changement de la page.
Affichage des en-têtes HTTP : outillage préalable
Il peut-être très fastidueux d'observer ou déboguer l'échange des en-têtes HTTP entre clients et serveurs. Nous détaillons ici des possibilités non limitatives.
1. Solution côté serveur
- En PhP, le code suivant permet de voir les en-têtes HTTP renvoyés par le navigateur. A priori, il ne permet cependant pas de suivre correctement le comportement du client en matière de cache : [expliquer]
- <?php $headers = getallheaders(); foreach ($headers as $header => $value) { echo "<p>$header: $value</p>\n"; } ?> Remarque :getallheaders() ne fonctionne que si php est installé en module Appache
2. Solution côté client
- Telnet : très long à l'usage mais permet de faire des tests chirurgicaux pour évaluer les comportements côté serveur ;
- SamSpade : SamSpade? est un outil en ligne, mais aussi un client sous Windows permettant, entre autre, de tracer les en-têtes HTTP. Par exemple, on peut observer , concernant la page d'accueil de ce site. L'inconvénient de cette solution est qu'on ne peut pas observer le comportement de l'application comme si on était un navigateur.
- Lynx : L'option -head (demande headers seulement) combiné avec l'option -dump retourne un résultat proche de SamSpade :
- Exemple :
- lynx -head -dump http://www.wikini.net/wakka.php?wiki=PagePrincipale
- MoZilla : un additif à Mozilla, très pratique, permet de visualiser les en-tête HTTP échangés entre le client et le serveur (ce qui évite au développeur de consulter les logs Apache et de placer des traces dans son code Php). Cette solution présente notamment l'énorme avantage de ne pas modifier le comportement du navigateur (ce qui est très utile dans le cas où il y a des identifications, des cookies, des JavaScript au cours du processus d'échange).
La réalisation
Désactiver les caches
Exemple de code qui
désactive tous les caches entre un client et un serveur :
<?php
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); // Date du passé
header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT"); // toujours modifié
header("Cache-Control: no-cache, must-revalidate"); // HTTP/1.1
header("Pragma: no-cache"); // HTTP/1.0
?>
Activation du cache pour une période déterminée en PhP
Exemple de code qui
active le cache local du navigateur :
<?php
$offset = 60 * 60 * 24 * 3;
$ExpireString = "Expires: " . gmdate("D, d M Y H:i:s", time() + $offset) . " GMT";
Header($ExpireString);
?>
Avec ce code, le serveur indique au navigateur que la page peut être demandée au cache jusque pendant 3 jours (60 * 60 * 24 * 3 secondes).
Au delà de cette date le navigateur va voir la version qui est sur le serveur. Ces directives devraient bien fonctionner avec la plupart des navigateurs (
MoZilla 1.1 est OK).
Un clic sur "recharger la page" ou "reload" provoque en revanche, sur
MoZilla, une demande au serveur : le cache ne fonctionne qu'avec le bouton "Back" ou lorsque l'on clique sur un lien qui mène à la page qui intègre ce code. --
CharlesNepote
- Le code de Charles indique au navigateur qu'il peut utiliser la page de son cache pendant 3 jours, sans avoir à vérifier si cette page a changé sur le serveur. C'est parfait si elle ne change pas... Mais sinon, l'utilisateur est obligé de faire un Shift-Reload (ou Ctrl-Reload avec InternetExplorer) pour charger la nouvelle version. Dans une version précédente d'un Wiki je positionnais l'entête "Expires: " à +10 min si le temps de traitement de la page dépasse 2 sec. Cela permet au Wiki de s'adapter automatiquement à la charge. C'est très facile à faire, pour peut que l'on puisse positionner les entête http aprés le traitement.
- Actuellement j'utilise une autre technique, qui permet au navigateur de demander l'autorisation au serveur d'utiliser son cache au serveur, sans que le serveur n'ait à recalculer toute la page; c'est en cours de test. -- EricSegui
Cacher en fonction de la date de modification de la page
Ce qu'il faut arriver à faire, et qui est, par exemple,
mis en oeuvre sur FPWiki, c'est de faire dépendre le rafraichissement d'une page en fonction de la dernière date de modification d'une page.
Je pense avoir réalisé ce que nous souhaitons. Il va falloir tout de même tester quelque temps et voir encore si l'on ne peut pas optimiser.
Le code suivant détecte dans
WikiNi la date de dernière modification de la page et demande au navigateur d'aller la chercher dans son cache si elle n'a pas été modifée.
<?php
// CACHE CONTROL : Control the last update date send by browser, and decide if the page need to be resend
function CacheControl()
{
// Get the page's date (2002-12-04 20:13:47)
list($year, $month, $day, $page_time) = split("[ -]", $this->GetPageTime());
// and transform it to HTTP date format : Thu, 05 Dec 2002 20:13:47 GMT
$begin_date = date("D, d M Y", mktime (0,0,0,$month,$day,$year));
$last_modified = $begin_date." ".$page_time." GMT";
$last_modified = $begin_date." ".$page_time." GMT";
// Read the headers sent by the browser
//$headers = getallheaders(); // Does not work on every Php installation
//$if_modified_since = $headers['If-Modified-Since']; // Does not work on every Php installation
$if_modified_since = $_SERVER['HTTP_IF_MODIFIED_SINCE']; // Seems to work on every Php installation [to be confirmed]
// Purify the $if_modified_since so that it can be well compared with the page's last modification
// (The browser usually give this type of string "Thu, 05 Dec 2002 20:13:47 GMT", but some, like Netscape 4.7
// give other strings such as ""Fri, 05 Sep 1997 01:03:46 GMT; length=2291") -- Thanks to Eric Segui.
$if_modified_since = preg_replace ("/^(.*)(Mon|Tue|Wed|Thu|Fri|Sat|Sun)(.*)(GMT)(.*)/", "$2$3 GMT", $if_modified_since);
// Transform strings into dates
$date_last_modified = strtotime($last_modified);
$date_if_modified_since = strtotime($if_modified_since);
// If the server's page hasn't been modified since last visit
if ($date_if_modified_since === $date_last_modified)
{
// Tells the browser page hasn't been modified ; the browser will then look for the page in his cache
header("HTTP/1.1 304 Not Modified");
header("Last-modified: ".$last_modified);
header("Cache-Control: Public"); // Tells HTTP 1.1 clients to cache
header("Pragma:"); // Tells HTTP 1.0 clients to cache
exit();
}
// else If the server's page has been modified since last visit
else
{
header("Last-modified: ".$last_modified);
header("Cache-Control: Public"); // Tells HTTP 1.1 clients to cache
header("Pragma:"); // Tells HTTP 1.0 clients to cache
}
}
?>
J'ai ensuite placé l'appel de fonction "
CacheControl?" dans la fonction "Run" de la manière suivante :
- $this->SetPage?($this->LoadPage?($tag, $_REQUEST["time"]));
- $this->CacheControl?();
Les résultats en local semblent plutôt concluants. Je vous donne en exemple ci-dessous, le journal des accès d'Apache (quand il y a 304 suivit d'un 0 c'est que le serveur n'a rien renvoyé) :
localhost - - [06/Dec/2002:10:35:50 +0100] "GET /wakka/wakka.php?wakka=
BacASable HTTP/1.1" 200 96204
localhost - - [06/Dec/2002:10:36:05 +0100] "GET /wakka/wakka.php?wakka=
BacASable HTTP/1.1" 304 0
localhost - - [06/Dec/2002:10:36:21 +0100] "GET /wakka/wakka.php?wakka=
PagePrincipale HTTP/1.1" 200 5024
localhost - - [06/Dec/2002:10:36:23 +0100] "GET /wakka/wakka.php?wakka=
DerniersChangements HTTP/1.1" 200 16156
localhost - - [06/Dec/2002:10:36:27 +0100] "GET /wakka/wakka.php?wakka=
DerniersChangements HTTP/1.1" 304 0
localhost - - [06/Dec/2002:10:36:29 +0100] "GET /wakka/wakka.php?wakka=
PagePrincipale HTTP/1.1" 304 0
localhost - - [06/Dec/2002:10:36:33 +0100] "GET /wakka/wakka.php?wakka=
BacASable HTTP/1.1" 304 0
localhost - - [06/Dec/2002:10:36:36 +0100] "GET /wakka/wakka.php?wakka=
PagePrincipale HTTP/1.1" 304 0
localhost - - [06/Dec/2002:10:36:38 +0100] "GET /wakka/wakka.php?wakka=
BacASable HTTP/1.1" 304 0
- Attention la fonction "getallheaders() ne fonctionne que si PHP est installé comme module Apache", ce qui n'est pas le cas de free ou de tuxfamily. Il faut donc utiliser la variable $_SERVER['HTTP_IF_MODIFIED_SINCE'] pour tester la date de dernière modification envoyée par le client.
- Il semble que certains serveurs n'acceptent pas de renvoyer un en-tête "HTTP 1.x 304 Not modified" : par exemple : free.fr ou tuxfamily.org. Après avoir lu et rerelu les docs et avoir expérimenté, il semble que c'est la syntaxe header("HTTP 1.x 304 Not modified"); qui pose bien problème : il existe alors une syntaxe alternative header("Status: 304 Not modified"); qui fonctionne tant free.fr et sur Tuxfamily.
For requests which come from a HTTP/1.0 compliant client (either a browser or a cache), the directive
CacheNegotiatedDocs? can be used to allow caching of responses which were subject to negotiation. This directive can be given in the server config or virtual host, and takes no arguments. It has no effect on requests from HTTP/1.1 clients.
Problème du rafraichissement des pages contenant une action
Actuellement le cache fonctionne tellement bien (en local), que la page
DerniersChagnements? n'est pas rafraichie, même si une page a été modifiée... Techniquement, c'est normal : cela est du au fait que les actions sont le résultat d'un calcul : les pages contenant des actions peuvent ne pas "changer" en terme d'édition alors que le résultat des actions change. Il faudrait donc détecter, à la suite d'une modification de page, que telle page demandée a été impactée et doit être raffraichie.
- Solution 1 : adopter pour la comparaison la date de dernière modification de la dernière page modifiée (le plus simple mais pas le plus efficace)
- Solution 2 : placer la comparaison après le chargement des données de la page et effectuer les traitements de cache après avoir déterminé si la page contient une action .
- Solution 3 : ajouter un paramètre (type "&refresh=1") pour les pages qui en ont besoin (je ne vois pas encore bien comment)
--
CharlesNepote
[ J'ai l'impression, qu'au final on se retrouve face aux mêmes difficultés que la gestion d'un cache dans l'application aurait pu apporter. La gestion d'un cache n'est pas si triviale que cela dans un wiki dès que l'on veut traiter les actions. Une enquête rapide sur les autres wiki ne m'a pas permis de trouver une solution élégante à ce problème, d'ailleurs peu de wikis fonctionnent avec des caches.
Quelques solutions ou propositions de gestion de cache dans d'autres wikis :
- PhpWiki:CacheWiki (interwiki) [en] : propose d'appeler une fonction particulière gérant le cache pour chaque actions (si j'ai bien compris), signale l'existence d'outils de cache de requête SQL.
- PhpWiki:PerformanceHacks (interwiki) [en] : quelqu'un a mis en place la logique if-modified-since dans son phpwiki, mais rencontre des difficultés avec son proxy : voir http://phpwiki.sourceforge.net/phpwiki/AcadWiki.
- Ontosys semble avoir réalisé ce cache avec succès, au moins sur son serveur. -- CharlesNepote [ Pas si sûr, voir en fin de page : This scheme assumes that the last-modified date of the PHP3 file itself determines the modification date of the generated page. That makes sense in templating schemes, but might not be appropriate if the page's code accesses other data that could change such as from a database. -- DavidDelon ]
--
DavidDelon]
Je viens de découvrir la fonction
session_cache_limiter qu'il faut utiliser dans le cas où on gère des sessions.
Je vais refaire des tests. La
solution proposée sur Ontosys semble fonctionner partiellement sur Tuxfamily : cf :
http://development.wikini.net/charles/wikini/tests/cacheable.php
--
CharlesNepote
Sinon, je viens d'imaginer une solution qui pourrait fonctionner : gérer la date de dernière consultation d'une page via un cookie, au lieu de le faire gérer par un en-tête HTTP.
Le principe :
- l'utilisateur consulte le site sans envoyer de cookie (cas d'une première visite ou d'un cookie périmé)
- le site sert la page et envoie un cookie qui contient la date de consultation et la page consultée
- l'utilisateur redemande au serveur cette même page
- le serveur vérifie que la page a déjà et consulté par l'utilisateur (cookie) compare la date dans le cookie et la dernière date de modification de toute page
- si aucune page n'a changée, le serveur renvoie un 304
- si une page à changée, le serveur sert la page et envoie un cookie qui contient la date de consultation et la page consultée
Je vais tester ça.
<?php
// CACHE CONTROL : Control the last update date send by browser, and decide if the page need to be resend
function CacheControl()
{
// Get the page's date (2002-12-04 20:13:47)
list($year, $month, $day, $page_time) = split("[ -]", $this->GetPageTime());
// and transform it to HTTP date format : Thu, 05 Dec 2002 20:13:47 GMT
$begin_date = date("D, d M Y", mktime (0,0,0,$month,$day,$year));
$last_modified = $begin_date." ".$page_time." GMT";
// Transform strings into dates
$date_last_modified = strtotime($last_modified);
$page_actuelle = $this->GetPageTag();
// If the server's page hasn't been modified since last visit and visitor has already been saw this page
if (($_SESSION["last-visit"] > $date_last_modified) && (isset ($_SESSION[$page_actuelle])))
{
// Tells the browser page hasn't been modified ; the browser will then look for the page in his cache
header("HTTP/1.x 304 Not Modified");
header("Date:" . $last_modified);
//header("Last-modified: ".$last_modified);
//header("Cache-Control: Public"); // Tells HTTP 1.1 clients to cache
//header("Pragma:"); // Tells HTTP 1.0 clients to cache
//echo "304"; // debug
exit();
}
// else If the server's page has been modified since last visit
else
{
// Save in session's user his last visit and the page's name
$_SESSION["last-visit"] = time();
$_SESSION[$page_actuelle] = "1";
}
}
?>
Voilà donc un premier code qui fonctionne parfaitement chez moi mais qui me donne un superbe "The server made a boo boo." sur Tuxfamily... manifestement à cause de l'instruction :
header("HTTP/1.x 304 Not Modified");
--
CharlesNepote (qui commence à baisser un peu les bras... d'autant que je n'ai toujours aucune réponse des admins de TF)
Je me permet d'intervenir. Si votre PHP est en 4.3.0 ou supérieur une solution probablement fonctionnelle est header('Not Modified', TRUE, 304) à la place de header("HTTP/1.x 304 Not Modified");
--
EricDaspet? (blog : article sur les caches HTTP) [fr]
Au cours du débug de
DotClear?, j'ai pu constaté que header('HTTP/1.x 304 Not Modified') était pas apprécié du tout chez Free. La solution header(''Not Modified',true,304) est bien mais demande un PHP 4.3.0+ (pas le cas chez free) (Jarod: Free fournit php 4.3.4 - 3 mai 2004). J'ai finalement trouvé que header('Status: 304 Not Modified') marchait très bien dans ce cas.
Concernant les erreurs chez TF, elles sont peut-être dues à leur Apache 2, voir à ce propos le bug sur php.net :
http://bugs.php.net/bug.php?id=17098
--
OlivierMeunier?
Côté serveur
[à compléter]
Extrait de la documentation d'Apache :
Note on Caching
When a cache stores a representation, it associates it with the request URL. The next time that URL is requested, the cache can use the stored representation. But, if the resource is negotiable at the server, this might result in only the first requested variant being cached and subsequent cache hits might return the wrong response. To prevent this, Apache normally marks all responses that are returned after content negotiation as non-cacheable by HTTP/1.0 clients. Apache also supports the HTTP/1.1 protocol features to allow caching of negotiated responses.
Sur
CacheNegociatedDocs [en].
Voir aussi le module
mod_headers [en].
Références :