Я не претендую на полноту рассказа о кэшировании — для этого есть спецификация, но расскажу лишь то, что необходимо для реализации кэширования динамически создаваемых или изменяемых документов.
Когда клиент скачивает по протоколу HTTP какую-либо полезную информацию, он сохраняет её в памяти и затем отображает пользователю. При разработке стандарта HTTP Тим Бернерс Ли предложил способ сократить количество передаваемых данных за счёт системы кэширования, целью которой было предотвращение повторной загрузки ресурса, который уже был загружен клиентом и не изменился.
Кэширование всегда инициируется сервером, и поддерживается клиентом. Самый простой способ кэширования выглядит так: Клиент запрашивает какой-то ресурс, сервер включает в заголовки ответа заголовок Expires со значением какой-либо даты. Этот заголовок "говорит" клиенту о том, что данный ресурс будет актуальным до даты, указанной в нём. Если указать некоторую дату в будущем, клиент будет вправе до этой даты при запросе пользователя выдавать ресурс из своего кэша. Если указать 0, текущую дату или дату в прошлом, клиент будет при каждом запросе заново скачивать весь ресурс, так как будет полагать, что "срок годности" ресурса истёк и следует получить его свежую версию.
клиент запрашивает какой-либо ресурс.
GET http://somehost.ru/index.html HTTP/1.0
Сервер, "знающий", что данный ресурс не устареет до определённой даты, включает её в заголовок Expires.
HTTP/1.0 200 OK
Date: Tue, 08 Aug 2006 08:15:21 GMT
Expires: Wed, 09 Aug 2006 08:15:21 GMT
Content-Length: 4
Content-Type: text/html
test
В данном примере ресурс index.html устареет ровно через день.
Клиент высчитывает "срок годности" ресурса (просто вычитая Date из Expires) и сохраняет в кэше ресурс вместе со сроком годности.
В течение этого "срока годности" клиент не будет запрашивать ресурс с сервера вовсе, а будет отдавать его пользователю из кэша.
В стандарте HTTP/1.1 был добавлен заголовок Cache-control, контролирующий механизм кэширования (о нём я расскажу ниже). Одним из его возможных атрибутов является атрибут max-age, явно указывающий срок годности ресурса. В случае, если клиент поддерживает HTTP/1.1 и видит в заголовке Cache-control, принятом от сервера, атрибут max-age с каким-либо значением, клиент принимает срок годности, указанный в max-age, даже если есть заголок Expires с другим значением. Другими словами, max-age имеет бóльший приоритет, чем Expires, что позволяет, например, предоставлять разные возможности кэширования HTTP/1.0 и HTTP/1.1-клиентам.
Механизм устаревания ресурса удобен в некоторых случаях. Например, очень удобно отдавать различные служебные изображения на сайте с заведомо большим сроком устаревания, например, размером в год.
Таким образом можно предотвратить большинство самых частых запросов — клиенты будут скачивать такие изображения очень редко. Недостаток один — если картинка изменилась, нужно как-то заставить клиента заново её скачать. Обычно изображение просто переименовывают, клиент, соответственно, думает, что это новое изображение и скачивает его. Также, если сайт устоялся и на нём не планируется в ближайшее время изменений, можно выдать таблицы стилей и скрипты с таким заголовком.
Необходимо заметить, что большинство серверов для статических файлов автоматически реализуют кэширование, основанное на методе "GET-запроса с условием" (см. ниже), что даёт экономию в трафике, но всё равно заставляет клиента каждый раз выполнять множество запросов, что, в свою очередь, вынуждает сервер проверять все условия и отвечать клиенту либо 304 Not Modified, либо отдавать сам ресурс. Наиболее оптимально совмещать эти два способа, потому я и предлагаю заняться кэшированием вручную.
В течение срока, установленного сервером в заголовке Expires (или в значении атрибута max-age заголовка Cache-control), клиент отображает пользователю ресурс из кэша, вообще не запрашивая сервер.
Метод хорош для служебных данных — изображений, таблиц стилей, скриптов, — для действительно редко изменяемых данных.
Наиболее оптимально сочетать этот метод с методом "GET-запроса с условием".
Так как в большинстве браузеров по нажатию кнопки "Обновить" такие ресурсы запрашиваются заново, поэтому я и предлагаю сочетать этот метод с "GET-запросом с условием".
Не всегда авторам известна дата устаревания ресурса, потому был создан механизм "GET-запроса с условием".
Общий механизм прост — сервер каким-либо образом идентифицирует ресурс, и клиент в последующем запросе добавляет в заголовки присланный сервером идентификатор; сервер сверяет собственный и присланный идентификатор, если они равны, значит, ресурс не изменился, в таком случае сервер говорит клиенту, чтобы тот показывал ресурс из кэша. Если же идентификаторы различаются, сервер заново отдаёт ресурс с новым идентификатором. Образно говоря, клиент просит сервер отдать документ, если он изменился.
При запросе клиентом ресурса, определенного URI, сервер может добавить к стандартным заголовкам ответа заголовок Last-Modified со значением даты последней модификации ресурса. В случае запроса статического файла в качестве времени модификации удобно использовать время его последней модификации в файловой системе.
Клиент получает ответ, и если он умеет работать с кэшем, сохраняет скачанный документ вместе с полученной датой. При следующем запросе ресурса с тем же URI клиент смотрит в свой кэш, и если находит ресурс с сохранённой датой (полученной из заголовка Last-Modified), добавляет к заголовкам запроса заголовок If-Modified-Since, куда подставляет сохранённую дату.
Сервер, получая такой заголовок, сравнивает дату модификации ресурса у себя с датой, полученной в заголовке If-Modified-Since. Если даты совпали (или дата, присланная клиентом, "больше"), сервер отдаёт клиенту заголовок 304 Not Modified, таким образом оповещая клиента о том, что ресурс не изменился и клиент может отобразить его из своего кэша. Если у клиента дата старее, чем дата модификации ресурса, сервер отдаёт клиенту ресурс заново вместе с новой датой модификации.
клиент запрашивает какой-либо ресурс:
GET http://somehost.ru/index.html HTTP/1.0
Сервер, поддерживающий кэширование для данного ресурса, добавляет в ответ заголовок Last-Modified.
HTTP/1.0 200 OK
Date: Fri, 30 Dec 2003 13:20:12 GMT
Last-Modified: Fri, 29 Dec 2003 13:20:12 GMT
Content-Length: 4
Content-Type: text/html
test
Клиент сохраняет в кэше документ вместе с датой, полученной от сервера в заголовке Last-Modified.
В следующий раз клиент при запросе того же ресурса в заголовок запроса добавит сохранённую дату
GET http://somehost.ru/index.html HTTP/1.0
If-Modified-Since: Fri, 29 Dec 2003 13:20:12 GMT
Сервер сравнит полученную дату с датой последнего изменения ресурса. Если на сервере находится более "свежая" версия ресурса, он отдаст её вместе с новой датой модификации.
HTTP/1.0 200 OK
Date: Sat, 31 Dec 2003 11:20:22 GMT
Last-Modified: Sat, 31 Dec 2003 10:00:00 GMT
Content-Length: 7
Content-Type: text/html
newtest
Если же на сервере документ актуален (т.е. даты равны), то сервер отдаст заголовок HTTP/1.0 304 Not Modified, но уже не отдаст тело ответа
HTTP/1.0 304 Not Modified
Date: Sat, 31 Dec 2003 11:20:22 GMT
Клиент же, получив такой ответ, покажет пользователю документ из своего кэша.
Дата, указанная в Last-Modified, должна быть именно в таком виде и в формате GMT. Строго говоря, и HTTP/1.0 и HTTP/1.1 обязывают серверы понимать три формата времени:
Sun, 06 Nov 1994 08:49:37 GMT
Sunday, 06-Nov-94 08:49:37 GMT
Sun Nov 6 08:49:37 1994
Но HTTP/1.0 добавляет, что сами серверы не имеют права использовать последний формат, а HTTP/1.1 требует от серверов использовать только первый формат времени.
Таким образом реализуется замечательная возможность — передавать только те документы, которые действительно изменились. Но этот метод далёк от совершенства — бывает сложно однозначно идентифицировать изменённость документа только по дате модификации файла, а в случае динамически генерируемого содержимого и вовсе не всегда возможно. Потому в спецификации HTTP/1.1 механизм "GET-запроса с условием" был расширен и дополнен.
метод применим для любых типов ресурсов, если для них возможно определить время модификации
метод поддерживается всеми клиентами и всеми прокси-серверами (уж HTTP/1.0 сейчас реализуют все)
в сочетании с методом кэширования, основанном на устаревании ресурса (Expires) идеален для кэширования служебных изображений, таблиц стилей, скриптов. Клиент не будет запрашивать сервер до даты, определённой в Expires, а затем, когда запросит, если ресурс не изменился, получит 304 Not Modified с новым значением Expires и снова перестанет запрашивать сервер до даты из нового Expires.
В HTTP/1.1 в механизм "GET-запроса с условием" было добавлено ещё одно "условие" т.н. ETag — сущность, однозначно определяющая содержимое ресурса.
Это может быть хэш всего ресурса, его контрольная сумма или другая функция, однозначно идентифицирующая содержимое. В ситуации, когда документ генерируется шаблонной системой и CMS, бывает сложно определить время модификации ресурса, особенно если он "собирается" из нескольких частей. Вот в таких случаях удобнее определить ETag и сравнить его с присланным клиентом. Механизм сравнения похож на реализованный в HTTP/1.0 механизм сравнения даты модификации.
Только если в HTTP/1.0 использовалась пара заголовков Last-Modified(сервер)/If-Modified-Since(клиент), то в в HTTP/1.1 используется пара ETag(сервер)/If-None-Match(клиент), использующаяся таким же образом — сервер идентифицирует ресурс и шлёт клиенту в заголовке ETag этот идентификатор. Клиент в следующий раз в запрос того же ресурса добавляет заголовок If-None-Match с сохраненным значением присланного сервером заголовка ETag. Сервер сверяет идентификаторы и либо возвращает 304 Not Modified в случае, если ресурс не изменился, или же возвращает заново весь ресурс с новым значением ETag.
клиент запрашивает какой-либо ресурс.
GET /index.html HTTP/1.1
Host: somehost.ru
Сервер, поддерживающий кэширование для данного ресурса, добавляет в ответ заголовок ETag.
HTTP/1.1 200 OK
Date: Fri, 30 Dec 2003 13:20:12 GMT
ETag: "328e-1d-1b63fda6"
Content-Length: 4
Content-Type: text/html
test
Клиент сохраняет в кэше документ вместе со значением заголовка ETag, полученным с сервера.
В следующий раз клиент при запросе того же ресурса в заголовок запроса добавит сохранённое значение ETag.
GET /index.html HTTP/1.1
Host: somehost.ru
If-None-Match: "328e-1d-1b63fda6"
Сервер сравнит полученное значение идентификатора ETag с собственным. Если идентификаторы различаются, сервер отдаст документ вместе с новым значением идентификатора ETag.
HTTP/1.1 200 OK
Date: Sat, 31 Dec 2003 11:20:22 GMT
ETag: "435b-4t-jla890l9"
Content-Length: 7
Content-Type: text/html
newtest
Если же значение идентификатора ETag, присланное клиентом совпадает со значением серверным, то сервер отдаст заголовок HTTP/1.1 304 Not Modified, но уже не отдаст тело ответа.
HTTP/1.1 304 Not Modified
Date: Sat, 31 Dec 2003 11:20:22 GMT
Клиент же, получив такой ответ, покажет пользователю документ из своего кэша.
метод применим для любых типов ресурсов
метод может не поддерживаться некоторыми прокси-серверами
этот метод необходимо сочетать с методом, основанным на устаревании ресурса, и, если возможно, с методом, основанным на дате последней модификации файла.
Сжатие ответа было представлено в HTTP/1.0. Оно выполняется просто. Если клиент поддерживает какой-то механизм кодирования, он к каждому запросу может приложить заголовок Accept-Encoding с перечислением поддерживающихся механизмов кодирования. Сервер может закодировать ресурс одним из методов, поддерживающихся клиентом.
В HTTP/1.0 были введены два типа кодирования — x-gzip и x-compress (но было сказано, что gzip и compress им эквивалентны для совместимости с будущими реализациями протокола). В HTTP/1.1 уже просто используются gzip и compress (и говорится, что программы должны понимать x-gzip и x-compress для совместимости с предыдущими реализациями протокола:)). Также в HTTP/1.1 был введён метод deflate, но из-за кучи проблем с реализацией в разных браузерах этот метод используется гораздо реже. Чаще всего используется gzip. Его поддерживают все браузеры, начиная c IE4, Opera5.12, NN4.06 и ранних версий Firefox. Сжимать можно любой тип данных, но большинство браузеров понимает только text/plain, text/html, text/css, text/javascript. Необходимо заметить, что при сжатии в заголовке Content-Length указывается длина сжатого тела сообщения.
Клиент запрашивает какой-то ресурс, сообщая серверу о поддерживаемых типах кодирования:
GET /index.html HTTP/1.1
Host: somehost.ru
Accept-Encoding: gzip,deflate
Сервер выбирает тип кодирования, кодирует ресурс и шлёт пользователю
HTTP/1.1 200 OK
Date: Thu, 10 Aug 2006 17:08:26 GMT
Content-Encoding: gzip
Content-Length: 59
всякая_пожатая_непроизносимая_ерунда
Клиент, зная тип кодирования, декодирует ресурс.
Необходимо осветить также заголовок вариантности кэширования.
Vary: Accept-Encoding нужен для того, чтобы дать понять кэширующему механизму, что ответ сервера зависит oт принимаемого клиентом метода кодирования. Предположим ситуацию: есть два клиента, один поддерживает gzip, другой нет.
Поддерживающий gzip клиент шлёт в запросе Accept-Encoding: gzip, не поддерживающий не шлёт, соответственно :)
Всё замечательно работает — сервер шлёт одному клиенту пожатый контент, другому несжатый. Но если на пути к серверу попадётся прокси-сервер, поддерживающий кэширование, возникает проблема. Если первый клиент, поддерживающий gzip, получает от сервера пожатый контент, прокси эти данные кэширует. При обращении к тому же ресурсу второго клиента(не поддерживающего сжатие), прокси-сервер отдаёт этому клиенту пожатый контент! Клиент ничего не понимает :)
Эта проблема решается с помощью посылаемого сервером заголовка Vary. Его значением может быть любое HTTP-поле. Фактически этот заголовок говорит, что ответ на запрос будет разным в зависимости от некоторых заголовков.
В результате в кэше будут храниться все варианты ответов. Если скрипт отдаёт разные данные в зависимости от Cookie, нужно отдавать Vary: Cookie, таким образом кэш отдаст закэшированный вариант ответа только для конкретной куки.
Например, если два клиента обращались к ресурсу с Cookie: name=Vasya и Cookie: name=Petya, будут закэшированы отдельно ответы для обоих клиентов, соответственно Васе будет отдан его ответ, Пете — его.
Ещё раз акцентирую внимание на том, что все условия, относительно которых может быть получен разный ответ сервера, нужно прописывать в Vary, чтобы не допустить попадания кэша одного пользователя другому.
Нужно сказать, что существуют серьёзные проблемы при использовании сжатия. Во-первых, это нагрузка на процессор сервера.
Ни в коем случае не нужно на лету сжимать часто скачиваемые ресурсы. В таком случае правильнее будет сжимать их сразу при изменении, и потом отдавать уже сжатые данные. Сжатие оптимально использовать совместно со стратегиями кэширования (см. ниже). Также существует несколько проблем в реализации сжатия некоторыми браузерами.
IE5.5 и IE6 забывает об ETag при использовании заголовка вариантности кэширования Vary и просто при использовании кодирования gzip. В IE7 этой проблемы нет.
IE4.0 и IE5.0 вообще не умеют кэшировать сжатое содержимое.
Opera не умеет кэшировать сжатый text/css, text/javascript
IE5.5 и IE6 без сервис-пака из-за бага могут терять первые 2 килобайта сжатых данных (редкий случай, я не встречал).
Лучше всего себя в работе с кэшированием и сжатием показали Firefox и IE7b3.
Служебные изображения не нужно сжимать, это лишняя и бесполезная нагрузка на процессор, т.к. чаще всего такие изображения уже максимально сжаты в графическом редакторе.
Кэширование изображений нужно реализовывать на основе Expires, ETag, Last-Modified. Expires предлагает клиенту брать ответ на запрос из кэша в течение всего срока, Last-Modified и ETag обеспечивают "GET-запрос с условием", Last-Modified обеспечивает поддержку HTTP/1.0-клиентов, ETag — HTTP/1.1.
Таким образом, если клиент и приходит за изображением, то происходит "GET-запрос с условием" и клиент получает изображение только если оно изменилось.
Принудительно заставить всех клиентов получить изображение заново, несмотря на срок "годности" Expires можно только изменив путь к изображению или переименовав его.
Если сайт "устоялся" и не планируются частые изменения, то стратегия кэширования идентична кэшированию изображений, за исключением того, что к таблицам стилей и скриптам можно также применить сжатие. Правда, нужно решить, что для Вас важнее — неудобство пользователей Opera, теряющих из-за сжатия возможность кэширования или же удобство пользователей остальных браузеров, умеющих кэшировать сжатые таблицы стилей и скрипты. В принципе, можно совместить требования.
Получается следующее:
Добавляем в заголовок вариантности Vary вариант User-Agent для того, чтобы показать кэшу, что разным браузерам будет отдано разное содержимое.
Opera до 9-й версии включительно и ie4/5.0 нужно отдавать несжатое содержимое. Только так эти браузеры смогут кэшировать содержимое.
Для IE6 необходимо проверять только If-Modified-Since.
Иногда (x)html-код генерируется автоматически, потому использование времени последней модификации невозможно. Остаётся ETag. Cледовательно, учитывая неумение IE6 кэшировать сжатый контент на основе ETag, от сжатия, скорее всего, придётся отказаться; или же слать сжатое содержимое только тем браузерам, которые не забывают про Etag. Если же (x)html-код находится в статических файлах, можно и для IE6 использовать сжатие совместно с кэшированием на основе Last-Modified.
Также, как и в случае с кэшированием таблиц стилей и скриптов, статические файлы можно заранее сжать, чтобы потом не тратить ресурсы на сжатие на лету.