1. Дата изменения документа (Last-Modified)
При добавлении сайта или отдельных страниц для индексирования, некоторые
поисковые системы (в частности "Яндекс"), дают сообщение: "Внимание! Сервер не выдает дату документа, поэтому в
результатах поиска дата для него показываться не будет".
Но через пару недель, Вы видите, что сайт
проиндексировался и присутствует в "выдаче" поисковиков, и ощущение что "что-то
не так в Датском королевстве", постепенно проходит. А напрасно! Проблема
осталась, и, несмотря на отсутствие ярко выраженных внешних проявлений,
последствия её могут быть достаточно серьёзными, и потом будет очень трудно
сообразить откуда всё началось.
Вот что сам Яндекс пишет по этому
поводу:
Насколько критично, что мой сервер не выдает last-modified? Я пытался настроить,
но ничего не вышло.
Во-первых, в результатах поиска не будет
показываться дата рядом со страницами вашего сайта, а при сортировке по дате
сайт не будет виден большинству пользователей. Во-вторых, робот не сможет
получить информацию о том, обновилась ли страница сайта с момента последней
индексации, а так как число страниц, получаемых роботом с сайта за один заход,
ограничено, изменившиеся страницы будут переиндексироваться реже.
Зайдите, например, на эту страничку и введите
URL Вашего сайта. Если в результатах выдачи, среди показанных заголовков Вы не
обнаружите полей типа: " Last-Modified: Wed, 08 Feb 2006 09:34:29
GMT" и/или: "ETag: "1-0-1139391269" с возможным
дополнением в виде: "Cache-Control: max-age=600" "Expires: Fri, 10 Feb 2006 12:35:29 GMT"
- тогда эта статья - для Вас!!
2. Откуда возникло это Last-Modified
Раньше, когда сайты создавались в соновном на HTML и их страницы были
СТАТИЧЕСКИМИ, такой проблемы не возникало. Дело в том, что веб-сервер, например
Apache, сам обрабатывал присылаемые заголовки и выдавал в ответ правильный
заголовок с Last-Modified (время последнего изменения документа). Теперь
же, чаще создают ДИНАМИЧЕСКИЕ страницы: SSI, CGI-скрипты, PHP, Perl. И
веб-сервер не берёт на себя функции по выдаче Last-Modified для страниц,
которые он считает динамическими. Это достаточно логично с точки зрения
веб-сервера: контент, отдаваемый пользователю, фактически создаётся в момент
обращения к странице, поэтому дата модификации непосредственно самого
файла скрипта или SSI-страницы теряет практический смысл.
Можно, конечно, настроить веб-сервер, чтобы он обрабатывал отдавал дату
модификации собственно самого скрипта динамической страницы, так же как и для
статических страниц html, и в Интернете есть рекомендации как это реализовать на
практике. Например, для
SSI-документов("server-parsed"): В
зависимости от настроек хостинга, Веб-cервер Apache будет выдавать
Last-Modified в том случае, если указана директива "XBitHack
full" (например, в файле .htaccess), и для файла, к которому
происходит обращение, выставлен атрибут "исполняемый" для группы (например,
командой "chmod g+x имя_файла", выполненной в Unix-shell).
Или в файл .htaccess добавляется директива
"RemoveHandler .htm .html" - она убирает все ассоциации с этим
расширением у сервера (например обработку SSI команд). В результате сервер
начинает отдавать дату документов, но перестает выполнять SSI диррективы в этом
файле.
для скриптов на PHP:
<?php // Отдавать текущее время
header ("Last-Modified: "
. gmdate("D, d M Y H:i:s") ." GMT"); ?>
для скриптов на
PERL: #!/usr/local/bin/perl use POSIX qw(strftime); my
$LM = strftime "%a, %e %b %Y %H:%M:%S GMT",
gmtime(time()); print
"Last-Modified: $LM "; Но прежде, чем
делать подобные вещи, лучше сначала понять, что такое Last-Modified и
зачем оно надо?
3. Что такое КЭШ на стороне клиента?
Кэш на стороне клиента позволяет броузеру не запрашивать заново страницу при
рефреше или повторном её вызове, а запросить только дату её изменения, и если
веб-вервер подтвердит, что страница не менялась - показать её из своего КЭШа.
Это позволяет сильно экономить трафик при навигации по сайту и резко увеличить
скорость загрузки страниц - вместо повторной пересылки страницы, идет обмен
маленькими заголовками, в СОТНИ раз меньшими по размеру!
Спецификацией протоколов HTTP/1.0 и особенно HTTP/1.1,
предусмотрена гибкая возможность кэширования как броузером страниц на стороне
клиента так и на промежуточных прокси-серверах.
Кэш на стороне клиента реализуется 2-я способами:
1. Meta-тегами HTML. Например, следующие строки
запрещают кэширование броузером: <meta
http-equiv="Expires" content="Mon, 19 Jul 1997 15:11:03 GMT" /> <meta
http-equiv="Pragma" content="no-cache" /> Однако есть некоторые
недостатки и "опасности" этого метода: -
Если тэг не существовал когда страница была запрошена браузером впервые, но
появляется позже, например, при модифицкации файла, браузер может оставаться в
блаженном неведении и пользоваться свей кэшированной копей оригинала страницы.
- Прокси-серверы, кэширующие web-страницы, вообще не исследуют содержимое
HTML-документа. Вместо этого они полагаются только на web-сервер, с которого
пришли документы, и протокол HTTP. При неправильной выдаче заголовков сервером,
web-браузер может считать, что не должен кэшировать страницу, но прокси-сервер
между браузером и вашим web-сервером может не знать этого - и продолжит
отправлять клиенту ту же самую, уже устаревшую, страницу. ** Netscape версии
менее 6 не очень хорошо обрабатывает заголовок "Pragma: no-cache".
Поэтому, лучший подход состоит в том, чтобы использовать заголовки
протокола HTTP. Например, с помощью функции PHP header, можно реализовать
пример, эквивалентный приведённым выше двум мета-тэгам так:
<?php header('Expires: Mon, 19 Jul 1997 15:11:03 GMT');
header('Pragma: no-cache'); ?>
2. HTTP-заголовками, присыламыми как клиентом, так
и сервером. Протоколом HTTP/1.0 предусмотрена следующая возможность
управления КЭШем на стороне клиента: - Выдача <веб-сервером>
заголовка Last-Modified в ответ на заголовок клиента
If-Modified-Since Дополнительно, <веб-сервер> может
выдавать заголовок типа 'Expires: Fri, 10 Feb 2006 12:35:29 GMT', который
если дата, указанная в нём меньше текущей - запрещает кэширование, если больше -
разрешает.
Протокол HTTP/1.1 дополнительно ввёл ещё одну возможность
управления кэшем: - Выдача <веб-сервером>
заголовка 'ETag' в ответ на заголовок клиента
'If-None-Match' Дополнительно в HTTP/1.1 может
использоваться отправка "веб-сервером" заголовка 'Cache-Control:
max-age=600', которая говорит о том, что до 600 секудн броузер клиента может
считать страницу не изменившейся и даже на запрашивать о дате её модификации.
Так же может использоваться директива HTTP/1.0 с
полем заголовка 'Expires: Fri, 10 Feb 2006 12:35:29 GMT', которая при
значении даты, относящейся к будущему, означает, что страница может быть
занесена в кэш (если только не указано обратного в поле заголовка Cache-Control,
типа 'Cache-Control: no-store').
Замечание.
Если отклик содержит поле Cache-Control с директивой max-age, то эта
директива переписывает значение поля Expires. Под словом
"веб-сервер" надо понимать "Ваша веб-страница", поскольку в случае в случае
динамических страниц сервер Apache за Вас это делать уже не станет. Это
дополнительные "накладные расходы" за удобство PHP, SSI, CGI и прочих средств
создания динамических страниц, и ложатся они на Ваши плечи (кроме отдельно
подгружаемых JavaScript, стилей CSS и прочих статических элементов из ОТДЕЛЬНЫХ
файлов - их Apache по-прежнему "отслеживает" сам).
Если Вы проигнорируете эту возможность управления КЭШем на стороне клиента:
- Ваш
сайт будет грузиться намного дольше, что, естественно, оттолкнёт от сайта часть
Клиентов; - Клиенты получат дополнительные расходы на входящий трафик;
- Вы получите дополнительную нагрузку на веб-сервер, который будет все
страницы формировать и посылать заново; - Вы получите некоторый
дополнительный входящий трафик; - Вы получите <нелюбовь> поисковых
роботов, что проявится при большом количестве страниц на сайте, и будет
выражаться в их медленной индексации и переиндексации. Робот Яндекса,
например, сразу не индексирует весь сайт, и потом приходит повторно, за
"очередной порцией" и проверяет, что изменилось. А сайт ему отвечает - "а у меня
все 10 000 страниц, и все - новые". Даже если на загрузку страницы уходит 2
сек, загрузить 10 000 страниц - это 6 часов! А когда Робот Яндекса увидит, что
"новые" страницы - те же, что и хранятся в его индексе - быстро индексировать такой сайт он не
будет. А если у Вас стоит Web-камера, картинки
которой обновляются раз в несколько минут - какой смысл передавать клиенту одну
и ту же картинку, если она не поменялась?
Если Вы будете неправильно управлять КЭШем на стороне клиента
(выдавать некорректные данные) - возможна ситуация, когда содержимое сайта
действительно изменится, а Клиент будет ещё долго брать её из своего КЭШа.
Поэтому: или ничего не делать, или, если делать - то очень
тщательно!
4. Управление КЭШем на стороне клиента
If-Modified-Since - Last-Modified:
Для управления КЭШем на стороне клиента,
спецификацией протокола HTTP/1.0 предусмотрен следующий механизм: при обращении
к странице, Броузер клиента, если такая страница есть в его КЭШе, отправляет на
сервер заголовок: If-Modified-Since: Tue, 30 Mar 2004 13:57:13
GMT где "Tue, 30 Mar 2004 13:57:13 GMT" дата документа из КЭШа
броузера. Сервер должен сравнить эту дату с датой "последней модификации"
документа, и если документ не изменился - выдать в ответ заголовок: HTTP/1.0 304 Not
Modified не передавая саму страницу. Броузер "поднимает"
страницу их своего КЭШа и показывает Клиенту. Если документ изменился, то сервер
должен выдать заголовок:
HTTP/1.0 200 OK
Last-Modified: 30 Apr 2005 16:07:23
GMT" и следом передать саму изменённую страницу. Броузер обновляет
ской КЭШ для данной страницы и запоминает новую дату её "последней модификации".
Таким образом, экономится время и трафик на "передачу" страницы клиенту, если
она не изменилась и снижается нагрузка на сервер.
В дополнение к этому, согласно спецификации HTTP/1.0, вместе с полем
заголовка Last-Modified: 30 Apr 2005 16:07:23 GMT, сервер может
передавать заголовок: "Expires: 30 Apr 2005 20:07:23 GMT", который
информирует броузер клиента, что до 30 Apr 2005 20:07:23 GMT эта
страница не изменится и можно показывать её их КЭША, не выполняя запрос
"If-Modified-Since: ..." к серверу. Полный ответ сервера будет выглядеть
так: HTTP/1.0 200 OK
Last-Modified: 30 Apr 2005 16:07:23
GMT" Expires: 30 Apr 2005 20:07:23
GMT" и/или заголовок 'Cache-Control: max-age' (появился в
HTTP/1.1), где max-age - через сколько секунд устаревает сохранённая в кэше
броузера копия документа: Cache-Control: max-age=600" - копия
устаревает через 10 минут от времени запроса (см * - остальные директивы
Cache-Control). 1.
'Cache-Control: max-age' переопределяет 'Expires:', если
используются совместно. 2. Сервер Apache автоматически выдаёт и
перезаписывает директиву Expires: на 'Expires: 1 Jan 1970', если в
httpd.conf или .htaccess не включена директива 'CharsetOverrideExpires
Off', разрешающая такое поведение сервера ТОЛЬКО если страница сама не
выдаёт в заголовке поле Expires:.
If-None-Match - ETag:
Спецификацией протокола HTTP/1.1 предусмотрена
дополнительная, более гибкая возможность управления КЭШем на стороне клиента,
которая может осуществляться как ОДНОВРЕМЕННО, так и ОТДЕЛЬНО от
"If-Modified-Since - Last-Modified", это обмен заголовками ETag.
Если броузер клиента поддерживает обмен заголовками ETag, то в запросе к
серверу, он может прислать заголовок: If-None-Match: "1c9113-1be2-40697cb9 где
1c9113-1be2-40697cb9 и есть ETag - некий уникальный хеш документа,
хранимой в КЕШе борузера. Этот идентификатор генерируется сервером, и
присылается вместе со страницей. Как наиболее распространеный вариант - md5().
В ответ на заголовок "If-None-Match:
"1c9113-1be2-40697cb9", сервер проверяет изменение страницы с данным ETag, и
если она не изменилась, возвращает ответ:
HTTP/1.1 304 Not Modified ETag: "1c9113-1be2-40697cb9" то есть,
возвращает то же значение ETag-а с кодом 304 и не передаёт саму старницу. Если
страница изменилась, то ответ сервера: HTTP/1.1 200 OK ETag: "2b3412-3af4-67812ac6" то есть ETag
присылается новый и следом передаётся новая страница, которую вместе с ETag-ом
броузер запоминает в своём КЭШе.
Например, в
Apache значение ETag формируется из inode файла, размера файла, и времени его
последней модификации (mtime). ETag в Apache равен 16-тиричному представлению
Inode-FileSize-mtime, где mtime это Unix timestamp (количество секунд прошедших
с 1 января 1900 года). ETag, аналогичный выдаваемому сервером Apache, можно
сгенерировать php-функцией:
<?php function get_file_etag($filename) { return dechex(fileinode($filename)).'-'.dechex(filesize($filename)).'-'.dechex(filemtime($filename)); } ?>
ETag может быть "строгим" (два документа имеют одинаковые ETags только если
они совпадают побитово) или "нестрогим" (два документа имеют одинаковые ETags
если они совпадают по содержанию, но могут отличаться в незначительных деталях).
Для файла, "строгим" ETag-ом может быть, например, его md5-хэш, а для
динамической страницы "нестрогим" ETag-ом может быть md5-хэш её основного
содержимого (без учёта дизайна и баннеров). Строгий
ETag-заголовок имеет формат "Entity Tag", а нестрогий W/"Entity Tag": ETag:
"2b3412-3af4-67812ac6" Пример нестрогого ETag:
ETag:
W/"2b3412-3af4-67812ac6"
Так же надо отметить, что спецификация протокла HTTP/1.1 позволяет Клиенту
присылать запросы серверу на проверку ETag в форматах: If-None-Match: *
что означает "любой ETag", и в формате: If-None-Match: "строка1", "строка2",
"строкаN" что означает проверку всего списка ETag-ов.
Таким образом, с помощью ETag можно обеспечить более гибкий механизм
управления КЭШем на стороне клиента, поскольку можно разрешить броузеру
показывать страницу из КЭШа, если на странице произошли незначительные
изменения, например, изменились только баннеры.
5. Принятие сервером решения об обновлении страницы
При принятии сервером решения о выдаче ответа об изменении страницы, надо
учесть проверку If-Modified-Since на корректность - если
If-Modified-Since больше текущего времени, нужно отдать документ с
"200 OK". В общем виде, таблица принятия сервером решения какой заголовок
надо выдать в различных случаях и комбинациях If-Modified-Since и
If-None-Match, выглядит следующим образом:
Таблица принятия сервером решения выдачи КОДА
заголовка
If-Modified-Since: DATE |
If-None-Match: ETAG |
не прислан |
ETAG ==
ETag или ETAG =='*' |
ETAG !=
ETag |
не прислан |
200 |
304 |
200 |
DATE >=
Last-Modified |
304 |
304 |
200 |
DATE <
Last-Modified или DATE >
time() |
200 |
200 |
200 |
где: DATE - дата, присланная в
запросе "If-Modified-Since:"; ETAG - ETag, присланный в запросе
"If-None-Match:"; Last-Modified - дата последней модификации
страницы на сервере; ETag - текущий ETag страницы на сервере;
time() - текущая дата на сервере;
304 - означает, что сервер НЕ ПЕРЕДАЁТ
страницу, а только возвращает заголовок: HTTP/1.1 304 Not Modified ETag: "1c9113-1be2-40697cb9" Last-Modified: 30 Apr 2005 16:07:23 GMT"
где: "1c9113-1be2-40697cb9" - текущий ETag страницы (может
отсутствовать, если сервер не поддерживает ETag); 30 Apr 2005 16:07:23
GMT - дата последней модификации страницы;
200 - означает, что сервер возвращает
заголовок: HTTP/1.1 200
OK ETag: "1c9113-1be2-40697cb9"
Last-Modified: 30 Apr 2005 16:07:23
GMT" где: "1c9113-1be2-40697cb9" - новый ETag страницы
(может отсутствовать, если сервер не поддерживает ETag); 30 Apr 2005
16:07:23 GMT - новая дата последней модификации страницы; и следом ПЕРЕДАЁТ обновлённую страницу!
6. Практическая реализация
Ниже приведён пример PHP-функции для принятия решения о возвращении кода 304
или 200 в зависимости от присланных в запросе GET или HEAD заголовков
Last-Modified и/или If-None-Match.
<?php
.// $last_modified - дата последней модификации документа на сервере, Unix
Timestamp. Default -текущая .// $etag = текущий ETag документа на
серверре, default = "" .// return TRUE - обновить страницу (200 ОК),
FALSE - не обновлять (304 Not Modified) function
MakeHeaders($last_modified = "", $etag="") { global $_SERVER;
$refresh = TRUE;
if ($last_modified == "")
$last_modified = time();
$none_match =
(isset($_SERVER['HTTP_IF_NONE_MATCH'])) ?$_SERVER['HTTP_IF_NONE_MATCH'] :"";
$modified_since = (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']))
?$_SERVER['HTTP_IF_MODIFIED_SINCE'] :"";
if($modified_since) {
// BUG: NetScape sends ";lenght = xxx" after
the date $arraySince = explode(";", $modified_since);
$since = strtotime($arraySince[0]); }
switch (TRUE) { case (!$none_match &&
$modified_since): if ( ($since <= time())
&& is_int($since)
&& ($since >= $last_modified) ) {
return FALSE; }
break; case ($none_match): if
($modified_since) { //
Проверка и по If-None-Match, и по If-Modified-Since
if (($since > time()) || !is_int($since)
|| ($since < $last_modified)) break; // Файл в кеше клиента устарел по
If-Modified-Since }
.// Проверку If-Modified-Since, если
она была - прошли. Проверка по If-None-Match: if
($etag == "") break; $INM = split('[,][ ]?',
$none_match); foreach($INM as $enity) {
if ($enity=="\"$etag\"" || $enity=="\"*\"")
return FALSE; //
304 Not Modified }
break; default: ; // Conditional Get не задан - просто отдаем
страницу. } return TRUE; // Страница изменилась (200 ОК) }
?>
7.* Список директив HTTP/1.1 Cache-Control
Таблица директив Cache-Control, посылаемых сервером в
заголовках
Директива |
Описание |
public |
Ответ разрешается сохранять в любом кэше. |
private |
Ответ разрешается сохранять только в закрытом кэше
(т.е. только для этого пользователя). |
no-cache |
Кэшированный ответ не должен использоваться без
предварительной его валидации. |
no-store |
Ответ не разрешается сохранять в кэше. |
no-transform |
К передаваемому документу не должны применяться
преобразования. |
must-revalidate |
Если кэшированный ответ устарел, то к нему должна
применяться процедура валидации. |
max-age=delta-seconds |
Клиент использует кэшированный ответ, если его возраст
не превышает delta-seconds секунд; клиент не требует его валидации. |
s-maxage=delta-seconds |
То же, что max-age, но действует только на открытые
кэши. |
proxy-revalidate |
То же, что must-revalidate, но действует только на
прокси-сервера. | Если необходимо дать несколько директив
Cache-Control, они перечисляются в ОДНОМ заголовке через запятую:
"Cache-Control: max-age=$maxCache, public, must-revalidate".
Дополнительно возникает вопрос по поводу передачи сервером в заголовке поля
"Content-Length: ..." HTTP/1.0 - ответ при удаче всегда 200 ОК -
размер файла не передается; HTTP/1.1 - если в запросе Клиента не определен
"Accept-Ranges: ...", ответ сервера 200 OK + размер файла в
Content-Length, иначе 206 Partial Content - и в Content-Length - размер
контента; Кроме того, иногда используется блочная передача (chunked) , при
этом размер также не передается. Сообщения НЕ ДОЛЖНЫ одновременно включать и
поле заголовка Content-Length и применять кодирование передачи типа "chunked".
8. Ссылки по теме
* В связи с тем, что спецификация протокола HTTP/1.1 RFC-2068 несколько
устарела, с новой спецификацией RFC-2616 и обновленим к ней RFC-2817 можно
ознакомится на: RFC 2616 - описание и спецификация протокола
HTTP/1.1
- Простой скрипт просмотра заголовков, присылаемых веб-сервером. (Не показывает, но понимает Redirect и делает автоматический переход по нему)
- Другой скрипт просмотра заголовков, присылаемых веб-сервером. (Показывает Redirect но не делает автоматический переход по нему)
- SEO скрипт анализа страниц. (Показывает Redirect, если страница переадресует куда-либо, и массу дополнительных SEO-параметров для анализа сайта. Автоматический переход по Redirect-у не делает)
- Другой SEO скрипт анализа страниц. (Аналогичен предыдущему, но с несколько большей функциональностью)
- Скрипт просмотра того, что cервер посылает броузеру. (позволяет выбрать метод(GET/HEAD/TRACE), User-Agent, HTTP 1.0/1.1, Accept-Encoding, то есть может прикидываться любым броузером или Роботом)
- RFC-2068 - описание и спецификация протокола HTTP/1.1
- Библиотека i2r: RFC 2068 - HTTP/1.1
- RFC 1945 - описание и спецификация протокола HTTP/1.0
- Сервис по проверке возможности кэширования сайта
|