Client-side Caching με ASP.NET και PHP

Πληροφορίες σχετικές με την ASP, ASP.NET και με τις εφαρμογές που είναι γραμμένες με αυτήν.

Συντονιστές: WebDev Moderators, Super-Moderators

Απάντηση
Άβαταρ μέλους
skeftomilos
Script Master
Δημοσιεύσεις: 2888
Εγγραφή: 07 Ιαν 2005 07:22
Τοποθεσία: Αθήνα

Client-side Caching με ASP.NET και PHP

Δημοσίευση από skeftomilos » 27 Ιαν 2007 22:48

Ένα πολυδιαφημισμένο χαρακτηριστικό της ASP.NET είναι οι δυνατότητες Caching που παρέχει. Υπάρχουν άφθονοι τρόποι να cachάρονται οι σελίδες και να σερβίρονται πιο γρήγορα, όπως για παράδειγμα η οδηγία OutputCache:

Κώδικας: Επιλογή όλων

<%@ OutputCache Duration="60" VaryByParam="none" %>
Μάλιστα κάθε νέα έκδοση της ASP.NET έχει να προσφέρει ακόμα περισσότερες σχετικές επιλογές. Μια τόσο εκτεταμένη υποδομή θα έπρεπε να αρκούσε για να καλύψει κάθε απαίτηση για κάθε είδους caching, όμως κάτι τέτοιο δε συμβαίνει. Ο λόγος είναι ότι η ASP.NET θέτει ως προτεραιότητα το caching στον server, και αφήνει σε δεύτερη μοίρα το caching στον browser. Μια πιθανή εξήγησή αυτής της μονομέρειας είναι ότι η συγκεκριμένη τεχνολογία προορίζεται κυρίως για μεσαίες και μεγάλες εφαρμογές, με μεγάλο αριθμό αναμενόμενων χρηστών. Ασφαλώς έχει μεγάλη σημασία σε τέτοιες συνθήκες να επιβαρύνεται ο server όσο το δυνατόν λιγότερο, ώστε να μπορέσει να τα βγάλει πέρα με την αυξημένη ζήτηση. Όμως ποιο είναι το όφελος για τον επισκέπτη;

Φυσικά είναι καλό για τον χρήστη να αποκρίνεται άμεσα ο server. Αλλά εξίσου καλό είναι να κατεβαίνουν γρήγορα οι σελίδες. Και ακόμα καλύτερο όταν ΔΕΝ κατεβαίνει δεύτερη φορά η ίδια σελίδα, αν δεν έχει αλλάξει στο μεταξύ. Ας εξετάσουμε τι ακριβώς συμβαίνει με την οδηγία OutputCache των εξήντα δευτερολέπτων που είδαμε παραπάνω. Στο πρώτο request που θα δεχτεί, ο server παράγει το output της σελίδας και το βάζει στην Cache για 60 δευτερόλεπτα. Ταυτόχρονα στέλνει τη σελίδα στον browser, ορίζοντας το Expiration σε 60 δευτερόλεπτα από την τρέχουσα ώρα. Αν ο ΙΔΙΟΣ χρήστης ξαναζητήσει την ίδια σελίδα μέσα σε αυτά τα 60 δευτερόλεπτα, ο browser θα χρησιμοποιήσει την τοπική cache χωρίς να ενοχλήσει καθόλου τον server. Αλλά αν τη ζητήσει μετά τα εξήντα δευτερόλεπτα, ο browser θα ξαναζητήσει τη σελίδα από το server και θα λάβει εκ νέου το πλήρες output. Τι πιθανότητες υπάρχουν να έχει αλλάξει το output στο μεταξύ; Για ένα πολυσύχναστο forum οι πιθανότητα δεν είναι ασήμαντη, αλλά για ένα εταιρικό site είναι πολύ μικρή.

Ο απλούστερος τρόπος να λυθεί το πρόβλημα είναι να δώσουμε στην παράμετρο Duration μια πιο κατάλληλη τιμή. Ποια είναι όμως η σωστή τιμή; Προκύπτει πως το ερώτημα δεν έχει ικανοποιητική απάντηση. Οποιαδήποτε τιμή και να επιλέξουμε θα έχουμε κάνει ένα συμβιβασμό ανάμεσα σε δύο εξίσου ανεπιθύμητες καταστάσεις. Μεγαλώνοντας την τιμή αυξάνουμε την πιθανότητα να σερβίρουμε στους επισκέπτες ξεπερασμένο περιεχόμενο, και ταυτόχρονα κάνουμε δύσκολη τη ζωή του content administrator, που θα πρέπει να περιμένει αρκετή ώρα για να ελέγξει τις αλλαγές που έκανε μέσω της διαχείρισης του site. Μειώνοντας την τιμή μικραίνουμε το χρόνο ζωής της cache του browser, και πρακτικά την αχρηστεύουμε.

Αναζητώντας έναν καλύτερο μηχανισμό από τον Expires HTTP header, συναντάμε τον Last-Modified που λειτουργεί ως εξής: Στο πρώτο request ο Server στέλνει μαζί με τη σελίδα και τον Last-Modified header, που δηλώνει την ημερομηνία τελευταίας μεταβολής της σελίδας. Στο δεύτερο request προς την ΙΔΙΑ σελίδα, ο browser συνοδεύει το αίτημα με τον header If-Modified-Since, με τιμή την ημερομηνία που έλαβε στο πρώτο request. Ο Server υπολογίζει την τρέχουσα ημερομηνία τελευταίας μεταβολής της σελίδας και τη συγκρίνει με αυτή του header. Αν ταυτίζονται τότε αντί να στείλει το output απαντά με τον κωδικό Status: 304, που σημαίνει not modified, και ο browser χρησιμοποιεί την τοπική cache. Αν η ημερομηνία έχει αλλάξει τότε ο server στέλνει τη ανανεωμένη σελίδα, και μαζί της τη νέα ημερομηνία στον Last-Modified header.

Το πλεονέκτημα του Last-Modified συγκριτικά με τον Expires είναι ότι ο πρώτος ΔΕΝ έχει προκαθορισμένη διάρκεια ζωής. Ακόμα και αν περάσουν μέρες και εβδομάδες, η cache του browser θα εξακολουθεί να παραμένει δυνητικά έγκυρη, έτοιμη για χρήση αν ο server επιστρέψει Status: 304. Το μειονέκτημα είναι ότι γίνεται πάντα request προς τον Server, έστω και minimal, αντίθετα με τον Expires όπου χρησιμοποιείται άμεσα η client case. Αλλά το μεγαλύτερο πρόβλημα του Last-Modified είναι προγραμματιστικό. Είναι ιδιαίτερα δύσκολο να προσδιοριστεί η ημερομηνία τελευταίας μεταβολής μίας δυναμικής σελίδας aspx. Δεν είναι μόνο η ημερομηνία των δεδομένων της βάσης που πρέπει να φυλαχτεί και να παραμείνει επίκαιρη, αλλά και τα ίδια τα αρχεία .aspx .ascx .asax .config .dll που αποτελούν τους δομικούς λίθους μιας ASP.NET σελίδας πρέπει να βρίσκονται σε συνεχή επιτήρηση για ενδεχόμενες αλλαγές. Και απ' όσο έψαξα δε φαίνεται να υπάρχει κάποιος καλός και γενικός τρόπος να μάθουμε ποια είναι αυτά τα αρχεία. Για παράδειγμα η undocumented μεταβλητή __fileDependencies (προστίθεται αυτόματα κατά το compilation των aspx) έχει αρκετά από τα στοιχεία που θέλουμε, αλλά είναι ασυμβατή μεταξύ των ASP.NET 1.1 και 2.0.

Η τρίτη και τελευταία εναλλακτική δυνατότητα είναι ο ETag header. Λειτουργεί παρόμοια με τον Last-Modified, με τη διαφορά πως αντί για ημερομηνία μπαίνει μια αυθαίρετη αλφαριθμητική τιμή. Η τιμή αυτή προσδιορίζει την version της σελίδας, δηλαδή πρέπει να παραμένει σταθερή όσο η σελίδα παραμένει αμετάβλητη, και πρέπει να αλλάζει κάθε φορά που η σελίδα αλλάζει. Αυτός ο header αποδεικνύεται πολύ πιο βολικός από τον Last-Modified για να επιτύχουμε το επιδιωκόμενο αποτέλεσμα. Ας δούμε πώς μπορούμε να τον χρησιμοποιήσουμε στην πράξη, κατ' αρχήν με PHP:

Κώδικας: Επιλογή όλων

<?php
  function etag_caching&#40;$buffer&#41;
  &#123;
    $hash = md5&#40;$buffer&#41;;
    if &#40;isset&#40;$_SERVER&#91;'HTTP_IF_NONE_MATCH'&#93;&#41; && $_SERVER&#91;'HTTP_IF_NONE_MATCH'&#93; == $hash&#41; &#123;
      header&#40;'Status&#58; 304'&#41;;
      return '';
    &#125; else &#123;
      header&#40;'Cache-Control&#58; public'&#41;;
      header&#40;'ETag&#58; ' . $hash&#41;;
      return $buffer;
    &#125;
  &#125;
  ob_start&#40;"etag_caching"&#41;;
?>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
  <head>
    <title>...
Για να προσδιοριστεί η version της σελίδας χρησιμοποιούμε εδώ τον πιο ευθύ τρόπο, δηλαδή το MD5 hash του πλήρους output. Έπειτα ελέγχουμε αν ο browser έχει στείλει τον If-None-Match header, και αν είναι ίδιος με το τρέχον hash της σελίδας. Αν ναι τότε επιστρέφουμε μόνο Status: 304, και ο browser θα χρησιμοποιήσει την τοπική cache. Αν όχι τότε στέλνουμε όλο το output, μαζί με τον ETag header.

Αν προσπαθήσουμε τώρα να κάνουμε το ίδιο με την ASP.NET, θα δούμε ότι είναι πιο περίπλοκα τα πράγματα. Για να αποκτηθεί πρόσβαση στο πλήρες output δεν υπάρχει άλλη επιλογή παρά η χρήση filter. Έχουμε ήδη χρησιμοποιήσει filter για να αφαιρέσουμε τα περιττά κενά από τις σελίδες, και τώρα θα το ξανακάνουμε για να υπολογίσουμε το hash. Ο filter που θα φτιάξουμε θα έχει όνομα HashFilter και μια έξτρα ιδιότητα Hash. Ας δούμε κατ' αρχήν τον τρόπο χρήσης του μέσα στο Global.asax.cs:

Κώδικας: Επιλογή όλων

protected void Application_PostRequestHandlerExecute&#40;Object sender, EventArgs e&#41; &#123;
  HashFilter hashFilter = new HashFilter&#40;Response.Filter&#41;;
  Response.Filter = hashFilter;
  Context.Items&#91;"HashFilter"&#93; = hashFilter;
&#125;

protected void Application_EndRequest&#40;Object sender, EventArgs e&#41; &#123;
  HashFilter hashFilter = &#40;HashFilter&#41;Context.Items&#91;"HashFilter"&#93;;
  string requestETag = Request.ServerVariables&#91;"HTTP_IF_NONE_MATCH"&#93;;
  string responseHash = hashFilter.Hash;
  if &#40;requestETag == responseHash&#41; &#123;
    Response.ClearContent&#40;&#41;;
    Response.StatusCode = 304;
    Response.End&#40;&#41;;
  &#125; else &#123;
    Response.AddHeader&#40;"Cache-Control", "public"&#41;;
    Response.AddHeader&#40;"ETag", responseHash&#41;;
  &#125;
&#125;
Μια δυσκολία που προκύπτει είναι ότι χρησιμοποιούμε αναγκαστικά τον HashFilter σε δύο διαφορετικά συμβάντα του Application. Δε μπορούμε να αναφερθούμε σε αυτόν με την ιδιότητα Response.Filter, γιατί μπορεί να έχουν προστεθεί και άλλα φίλτρα στο μεταξύ. Για να αποκτήσουμε λοιπόν μια reference με τρόπο αξιόπιστο, η καλύτερη λύση είναι η χρήση της συλλογής Context.Items. Βέβαια αυτή είναι κατά κάποιο τρόπο μια συλλογή από global μεταβλητές, προσβάσιμες με string keys, οπότε δεν είναι απόλυτα safe. Για το λόγο αυτό μπορεί να φτιαχτεί ένας strongly-typed wrapper αυτής της συλλογής, δηλαδή κάτι σαν αυτό:

Κώδικας: Επιλογή όλων

public class CX &#123;

  public static HashFilter HashFilter &#123;
    get &#123;
      return &#40;HashFilter&#41;HttpContext.Current.Items&#91;"HashFilter"&#93;;
    &#125;
    set &#123;
      HttpContext.Current.Items&#91;"HashFilter"&#93; = value;
    &#125;
  &#125;

&#125;
Αλλά ας μη βγούμε άλλο εκτός θέματος, ας δούμε μια υλοποίηση του HashFilter:

Κώδικας: Επιλογή όλων

using System;
using System.IO;

public class HashFilter &#58; System.IO.Stream &#123;

  private Stream stream;
  private long hash;
  private long multiplier;
  
  public HashFilter&#40;Stream stream&#41; &#123;
    this.stream = stream;
    this.hash = 0;
    this.multiplier = 1;
  &#125;

  public string Hash &#123;
    get &#123;
      return Math.Abs&#40;this.hash&#41;.ToString&#40;&#41;;
    &#125;
  &#125;

  public override bool CanRead &#123;
    get &#123;
      return false;
    &#125;
  &#125;
  
  public override bool CanSeek &#123;
    get &#123;
      return false;
    &#125;
  &#125;
  
  public override bool CanWrite &#123;
    get &#123;
      return true;
    &#125;
  &#125;
  
  public override long Length &#123;
    get &#123;
      throw new NotImplementedException&#40;&#41;;
    &#125;
  &#125;
  
  public override long Position &#123;
    get &#123;
      throw new NotImplementedException&#40;&#41;;
    &#125;
    set &#123;
      throw new NotImplementedException&#40;&#41;;
    &#125;
  &#125;
  
  public override long Seek&#40;long offset, SeekOrigin direction&#41; &#123;
    throw new NotImplementedException&#40;&#41;;
  &#125;
  
  public override void SetLength&#40;long length&#41; &#123;
  &#125;
  
  public override void Flush&#40;&#41; &#123;
  &#125;
  
  public override int Read&#40;byte&#91;&#93; buffer, int offset, int count&#41; &#123;
    throw new NotImplementedException&#40;&#41;;
  &#125;
  
  public override void Write&#40;byte&#91;&#93; buffer, int offset, int count&#41; &#123;
    this.stream.Write&#40;buffer, offset, count&#41;;
    unchecked &#123;
      for &#40;int i = offset; i < count; i++&#41; &#123;
        this.multiplier *= 113;
        this.hash += buffer&#91;i&#93; * this.multiplier;
      &#125;
    &#125;
  &#125;

  public override void Close&#40;&#41; &#123;
    this.stream.Close&#40;&#41;;
  &#125;

&#125;
Δεν γίνεται χρήση MD5 hashing γιατί αντίθετη με την PHP η μέθοδος Write μπορεί να κληθεί περισσότερες από μία φορές, με ημιτελές output. Βέβαια θα μπορούσε να γίνει χρήση ενός εσωτερικού buffer (όπως κάναμε στην αφαίρεση των κενών), αλλά εδώ μοιάζει με σπατάλη πόρων χωρίς ιδιαίτερο λόγο. Το hash που προκύπτει μοιάζει να είναι αρκετά καλό, τουλάχιστον για τη χρήση για την οποία προορίζεται.

Ας κάνουμε μια αξιολόγηση των συν και πλην αυτής της τεχνικής. Κατ' αρχήν πρέπει να τονιστεί ότι δεν ελαφραίνει καθόλου το φόρτο του server. Σε κάθε request γίνεται πλήρες processing, με όλα τα database queries και ό,τι άλλο χρειάζεται. Αντίθετα ο server επιβαρύνεται με το επιπλέον καθήκον υπολογισμού του ETag. Βέβαια τίποτα δε μας εμποδίζει να συνδυάσουμε αυτή την τεχνική με server-side caching, αν η επισκεψιμότητα του site δικαιολογεί την επακόλουθη αύξηση χρήσης RAM. Όσον αφορά το φόρτο του δικτύου, μπορούμε να περιμένουμε κάποια μείωση. Το όφελος θα είναι ανάλογο της συχνότητας με την οποία οι χρήστες επισκέπτονται για δεύτερη, τρίτη κ.λπ. φορά την ίδια σελίδα. Για κάποια sites μπορεί να είναι σημαντικό. Αλλά αυτός που κερδίζει τα περισσότερα είναι ο επισκέπτης. Το να περιμένει κανείς δέκα δευτερόλεπτα για να κατέβει μια νέα σελίδα δεν είναι τραγικό. Το να περιμένει όμως τον ίδιο χρόνο για να ξανακατέβει μια σελίδα που είδε πριν λίγα μόλις λεπτά, είναι κάπως δυσάρεστο.

Ένα ζωντανό παράδειγμα χρήσης αυτής της τεχνικής είναι το site της ECDL. Δεν είναι το καλύτερο παράδειγμα γιατί οι σελίδες αυτού του site έχουν γενικά μικρό μέγεθος (<20KB) οπότε είναι δύσκολο να παρατηρήσει κανείς τη διαφορά στο χρόνο απόκρισης, ειδικά αν έχει γρήγορη σύνδεση.
The pure and simple truth is rarely pure and never simple. Ο μη νους δε σκέπτεται μη σκέψεις για το τίποτα.

Άβαταρ μέλους
mrpc
WebDev Moderator
Δημοσιεύσεις: 3393
Εγγραφή: 03 Μάιος 2000 03:00
Τοποθεσία: Εξάρχεια
Επικοινωνία:

Client-side Caching με ASP.NET και PHP

Δημοσίευση από mrpc » 27 Ιαν 2007 23:43

Έγινε βοήθημα ;)

Απάντηση

Επιστροφή στο “ASP, ASP.NET”

Μέλη σε σύνδεση

Μέλη σε αυτήν τη Δ. Συζήτηση: Δεν υπάρχουν εγγεγραμμένα μέλη και 0 επισκέπτες