In questo articolo ci occuperemo di introdurre i lettori un argomento molto interessante: il caching delle pagine web. Quando un browser richiede ad un webserver una pagina contenente del codice lato server (PHP nel nostro caso), il webserver si deve occupare di eseguirla e restituire il risultato dell'operazione al client. Durante il processo di esecuzione solitamente avvengono diversi accessi a disco o a database, vengono eseguiti più o meno complessi algoritmi. Il tempo richiesto per l'esecuzione della pagina viene sommato a quello richiesto per il download ed aumenta l'attesa dell'utente. Oltretutto il processo di esecuzione di codice lato server comporta un "notevole" dispendio di risorse.
Tramite le operazioni di caching, che si occupano di salvare il risultato del processo di esecuzione sul server, possiamo minimizzare i tempi di attesa e diminuire notevolmente il carico di lavoro della nostra macchina remota. Esistono svariate soluzioni per eseguire il caching di una pagina, da soluzioni molto semplici ad altre molto complesse. In questo articolo il mio unico intento è quello di introdurre l'argomento proponendovi del codice semplice da comprendere e riprodurre. Starà a voi adattare quello che avete imparato a casi reali.
Il caching in pratica
Come accennato precedentemente le operazioni di caching possono essere implementate in diversi modi. Qualunque sia la nostra implementazione dobbiamo tenere conto di alcune informazioni che ci saranno molto utili per sviluppare il sistema di caching:
- Conviene eseguire il caching di pagine che cambiano il loro contenuto sporadicamente o con bassa frequenza.
- Le pagine salvate dovranno avere un "periodo di vita", dopo il quale dovranno essere sostituite con una versione più recente della stessa. Questo per evitare il mancato aggiornamento di alcune pagine. Nessuno vieta comunque di utilizzare periodi di scadenza molto lunghi in casi particolari.
- Il contenuto delle pagine varia spesso in base ad alcune condizione dovute a dati ricevuti in input dalla pagina. Questi dati possono provenire da variabili superglobali, file o molto altro. Per questo motivo è necessario fornire un sistema che permetta di eseguire il caching di pagina più di una volta, associando ad ogni salvataggio le informazioni relative ai dati provenienti dall'esterno.
- Spesso non è necessario o conveniente eseguire il caching completo della nostra pagina. Ci possono essere situazioni in cui dei blocchi di html rimangono invariati per molto tempo, mentre altri vengono aggiornati continuamente. Per questo motivo sarebbe opportuno che il nostro sistema permettesse il caching di porzioni di pagina;
- Il caching non è limitato solamente a codice HTML: è possibile (e molto conveniente) eseguire il caching di immagini generate con librerie GD, di file PDF e molto altro ancora.
- I primi define
- La funzione CacheError
- Il costruttore della classe CacheContent
- Il metodo SetNameModifier
- Il metodo SetTimeout
- Il metodo Create
- Il metodo Stop
- Il costruttore della classe HtmlCache
- Il metodo Create USE_CUSTOM
Tenendo presente queste considerazioni iniziamo con una semplicissima implementazioni di caching che chiarisca a tutti le idee; in questa implementazione useremo le funzioni di output buffering di PHP per salvare l'output generato dal nostro codice e le funzioni di I/O per salvare e leggere i dati da disco.
$page_id = "miapagina.php";
$timeout = 60;
$path = "./chached/".$page_id;
if(!file_exists("./chached /"))
mkdir("./chached /");
if(file_exists($path) and filemtime($path) + $timeout > time()) {
$result = readfile($path);
if($result)
exit();
}
set_time_limit(0);
ob_start();
for($i = 0; $i < 50; ++$i){
sleep(1);
echo date("h : i : s")."
";
}
$output = ob_get_flush();
$fp = fopen($path, "w");
fwrite($fp, $output, strlen($output));
fclose($fp);
?>
Il codice è molto semplice: le prime tre righe definiscono tre variabili contenenti rispettivamente un ID univoco per la pagina, i millisecondi di vita della pagina ed il path in cui sarà salvato l'output. Dalla riga 8 alla riga 11 ci occupiamo di controllare che esista la cache e che questa non sia scaduta. In caso affermativo visualizziamo il contenuto della cache ed in caso di visualizzazione avvenuta terminiamo lo script. Nel caso in cui la cache sia scaduta oppure nel caso in cui la lettura della cache non abbia restituito esito positivo, inizializziamo l'output buffering di PHP ed eseguiamo del codice volutamente lento (la prima esecuzione durerà circa 55 secondi). Terminato il blocco di codice centrale, recuperiamo l'output e lo salviamo all'interno della cache.
Come è possibile notare, il caching non è un concetto molto complesso da implementare, e richiede poco lavoro per poter essere reso operativo e funzionante.
Un'implementazione alternativa
Dopo aver presentato un'implementazione introduttiva del caching di una pagina, vi mostrerò come è possibile, con poco e semplice codice, implementare un sistema leggermente più avanzato, che permetta di rispondere a tutti i punti elencati nel paragrafo precedente.
Lo script seguente fornirà una semplice libreria composta da due classi che si occuperanno di gestire il caching delle vostre pagine ed alcune funzioni di utilità. La prima classe, CacheContent, servirà per la gestione dell'output di una sezione di codice PHP: si occuperà di creare la cache, visualizzarla, settare eventuali modificatori che tengano traccia dell'input ricevuto dalla pagina e settare il timeout della cache. La seconda classe, HtmlCache, si occuperà di gestire i dati salvati in cache, generando istanze di CacheContent quando richiesto.
define("INVALID_DIR", 501);
define("FLAGS_NONE", 0x0);
define("USE_GET", 2 << 0);
define("USE_POST", 2 << 1);
define("USE_CUSTOM", 2 << 2);
define("CACHE_CREATED", 2);
define("CACHE_READ", 1);
function CacheError($code){
switch($code){
case INVALID_DIR:
$error = "Ivalid cache directory specified.";
break;
default:
$error = "Unknown Error";
break;
}
die("Cache error: ".$error);
exit();
}
class CacheContent {
var $working_directory = "";
var $name_modifiers = array();
var $timeout = 0;
var $file_extension = "";
var $fp = null;
var $type = -1;
function CacheContent($dir, $ext, $n){
$this->working_directory = $dir."/".basename($_SERVER['PHP_SELF'])."/";
if(!file_exists($this->working_directory))
mkdir($this->working_directory);
array_push($this->name_modifiers, $n);
$this->file_extension = $ext;
}
function SetNameModifier($mod){
$new = array();
foreach($mod as $key=>$val){
if(is_array($val)){
$this->SetNameModifier(array_merge(array($key), $val));
}else array_push($new, $key.$val);
}
array_merge($this->name_modifiers, $new);
}
function SetTimeout($timeout){
$this->timeout = $timeout;
}
function Create(){
$expired = false;
$file_id = md5(implode("", $this->name_modifiers));
$file_name = $file_id.".".$this->timeout.$this->file_extension;
if(file_exists($this->working_directory.$file_name)){
if(time() > filemtime($this->working_directory.$file_name)+$this->timeout){
$expired = true;
}else echo file_get_contents($this->working_directory.$file_name);
}else $expired = true;
if($expired){
$this->fp = fopen($this->working_directory.$file_name, "w");
ob_start();
$this->type = CACHE_CREATED;
return;
}
$this->type = CACHE_READ;
}
function GetType(){
return $this->type;
}
function Stop(){
if(!is_null($this->fp)){
fwrite($this->fp, ob_get_contents());
fclose($this->fp);
ob_end_flush();
}
}
}
class HtmlCache {
var $cached_dir = "";
var $cached_files_ext = "";
var $cached_global_timeout = "";
var $cached_counter = 0;
function HtmlCache($dir, $ext, $timeout){
$dir = $_SERVER['DOCUMENT_ROOT'].dirname($_SERVER['PHP_SELF'])."/".$dir;
if(!file_exists($dir)){
mkdir($dir."/".basename($_SERVER['PHP_SELF'])."/", 0700);
}
$this->cached_dir = $dir;
$this->cached_files_ext = $ext;
$this->cached_global_timeout = intval($timeout, 10);
}
function Create($flags = 0x0, $timeout = null, $custom_data = null){
$cache = new CacheContent($this->cached_dir, $this->cached_files_ext, $this->cached_counter);
$this->cached_counter++;
if($flags & USE_GET){
global $_GET;
$cache->SetNameModifier($_GET);
}
if($flags & USE_POST){
global $_POST;
$cache->SetNameModifier($_POST);
}
if($flags & USE_CUSTOM){
$custom_data = (is_array($custom_data) && $custom_data != null) ? $custom_data : array($custom_data);
$cache->SetNameModifier($custom_data);
}
$timeout = is_numeric($timeout) ? $timeout : $this->cached_global_timeout;
$cache->SetTimeout($timeout);
$cache->Create();
return $cache;
}
}
?>
Descriviamo brevemente il codice presentato qui sopra:
Per testare la libreria utilizziamo questo semplice script:
function getmicrotime(){
list($usec, $sec) = explode(" ",microtime()) ;
return ((float)$usec + (float)$sec) ;
}
include_once("HtmlCache.php");
$time_start = getmicrotime();
$c = new HtmlCache("cached",".html", 60);
$section = $c->Create();
if($section->GetType() == CACHE_CREATED){
for($i =0 ; $i < 10000; $i++){
echo "ciao<br>";
}
}
$section->Stop();
echo $section->GetType()."<br>";
$time_end = getmicrotime();
$time = $time_end - $time_start;
echo ("
Eseguito in $time secondi
");
?>
Come potete notare viene eseguito il caching solamente del codice restituito dal ciclo for; per eseguire il caching di altre porzioni di codice, basta generare una nuova sezione utilizzando $c->Create() e gestirla come nel seguente esempio, in cui vengono generate due porzioni di cache di durata 60 e 30 secondi:
include_once("HtmlCache.php");
set_time_limit(0);
$c = new HtmlCache("cached",".html", 60);
$section = $c->Create(FLAGS_NONE, 60);
if($section->GetType() == CACHE_CREATED){
echo date("h:i:s")."<br>";
for($i =0 ; $i < 10; $i++){
sleep(1);
echo "ciao<br>";
}
}
$section->Stop();
$section2 = $c->Create(FLAGS_NONE, 30);
if($section2->GetType() == CACHE_CREATED){
echo date("h:i:s")."<br>";
for($i =0 ; $i < 10; $i++){
sleep(1);
echo "ciao 2<br>";
}
}
$section2->Stop();
?>
Conclusioni
Concludo qui la mia introduzione al caching. Dopo la lettura di questo articolo dovreste aver compreso come sviluppare ed utilizzare un semplice sistema di caching da affiancare al vostro sito web o alla vostra applicazione. Ovviamente l'argomento non si ferma qui, e starà a voi implementare nuove soluzioni per gestire i problemi che via via si poranno sulla vostra strada.