Кэширование в PHP
Введение
В старые добрые времена, когда создание web-сайтов представляло из себя такое простое занятие, как набор нескольких HTML-страниц, отправка web-страниц в браузер была простой отправкой файла web-сервером. Посетители сайта могли видеть эти небольшие, исключительно текстовые странички, почти мгновенно (если не считать пользователей медленных модемов). Как только страница была загружена, браузер кэширует её где-нибудь на локальном компьютере, чтобы в случае повторного запроса страницы, можно было взять его локальную версию из кэша, послав лишь короткий запрос, чтобы убедиться, что страница на сервере не была изменена. Запросы обрабатывались быстро и как можно эффективней, и все были счастливы (кроме использующих модемы 9600 бод).
Появление динамических web-страниц изменило положение вещей в худшую сторону, эффективно «сломав» эту модель обслуживания web-страниц благодаря наличию двух проблем:
- Когда сервером получен запрос динамической web-странички, производится некоторая промежуточная обработка, например синтаксический анализ (парсинг) скрипта движком PHP, которая должна быть завершена. Благодаря этому получаем задержку перед тем, как web-сервер начнёт отправку вывода в браузер. Для простого PHP-скрипта это не существенно, но для более сложного приложения движок PHP может выполнить много действий прежде чем страница будет готова для отправки. Эти дополнительные действия приводят к заметной задержке между запросами пользователей и реальным отображением страниц в их браузерах.
- Типичный web-сервер, например Apache, использует время модификации файла чтобы правильно сообщить web-браузеру состояние кэша запрашиваемой странички. Для динамических web-страниц, фактически PHP-скрипт может изменяться только изредка, в то время как отображаемый им контент, возможно располагающийся в базе данных, изменяется часто. Web-сервер не имеет возможности знать о наличии изменений в базе данных, тем не менее он не отправляет дату последней модификации. Если клиент (браузер) не получает никакого признака того, как долго данные являются корректными, он предполагает, что в следующий раз необходимо запросить страничку по новой. Web-сервер всегда будет отвечать обновлённой версией странички, независимо от того, изменились ли данные. Чтобы избежать этого недостатка большинство web-разработчиков используют мета-тэги или HTTP-заголовки, чтобы сообщить браузеру никогда не использовать кэшированную версию странички. Однако это отрицает естественную способность web-браузера кэшировать web-страницы и обладает некоторыми существенными недостатками. Например, содержание динамической странички может изменяться раз в сутки, поэтому выгода, получаемая от наличия даже 24-часового кэширования странички браузером, очевидна.
Обычно для маленьких PHP-приложений вполне можно игнорировать существование этих проблем, однако с увеличением сложности и повышением трафика Вашего сайта Вы можете столкнуться с проблемами. Тем не менее, обе эти проблемы могут быть решены, первая путём кэширования на стороне сервера, вторая путём управления кэшированием на стороне клиента из вашего приложения. Подход, который вы будете использовать для решения проблем, будет зависеть от вашей области применения, но в этой главе мы увидим, как вы можете решить обе проблемы используя PHP и некоторые классы библиотеки PEAR.
Как я предотвращаю кэширование страницы браузерами?
Прежде чем мы рассмотрим методы клиентского и серверного кэширования, в первую очередь мы должны понять, как вообще предотвратить кэширование страниц web-браузером (и прокси-серверами). Основной способ достижения этого использует мета-тэги HTML:
<meta http-equiv="Expires" content="Mon, 26 Jul 1997 05:00:00 GMT" /> <meta http-equiv="Pragma" content="no-cache" />
Вставив прошедшую дату в мета-тэг Expires, вы сообщаете браузеру, что кэшированная копия странички всегда является устаревшей. Это значит, что браузер никогда не должен кэшировать страницу. Мета-тэг Pragma: no-cache довольно хорошо поддерживаемое соглашение, которому следует большинство web-браузеров. Обнаружив этот тэг, они обычно не кэшируют страницу (хотя никаких гарантий нет, это всего лишь соглашение).
Это хорошо звучит, но есть две проблемы, связанные с использованием мета-тэгов:
- Если тэг не существовал когда страница была запрошена браузером впервые, но появляется позже (например, вы модифицировали включаемый файл pageheader.php который является шапкой каждой web-страницы), браузер останется в блаженном неведении и воспользуется свей кэшированной копей оригинала.
- Прокси-серверы, кэширующие web-страницы, как например общий ISP, вообще не будет исследовать непосредственно содержимое HTML-документа. Вместо этого они полагаются только на web-сервер, с которого пришли документы, и протокол HTTP. Иными словами, web-браузер может считать, что не должен кэшировать страницу, но прокси-сервер между браузером и вашим web-сервером вероятно не знает этого – и продолжит отправлять клиенту ту же самую, уже устаревшую, страницу.
Лучший подход состоит в том, чтобы использовать непосредственно протокол HTTP с помощью функции PHP header(), эквивалентно приведённым выше двум мета-тэгам:
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Pragma: no-cache');
Мы можем пойти на один шаг вперёд, воспользовавшись заголовком Cache-Control совместимым с браузерами, поддерживающими HTTP 1.1:
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', FALSE);
header('Pragma: no-cache');
Это гарантирует, что никакой web-браузер или промежуточный прокси-сервер не будет кэшировать страницу, таким образом посетители всегда получат самую последнюю версию контента. Фактически, первый заголовок должен быть самодостаточным, это лучший способ гарантировать, что страница не кэшируется. Заголовки Cache-Control и Pragma добавлены с целью «подстраховаться». Хотя они не работают во всех браузерах или прокси, они отловят некоторые случаи, в которых Expires не работает должным образом (например, если дата на компьютере клиента установлена неправильно).
Конечно, полный отказ от кэширования обеспечивает нас проблемами, которые мы обсуждали в начале этой главы. Сейчас мы рассмотрим решение этих проблем.
Internet Explorer и кэширование загрузки файлов
Если при обслуживании загрузки файла PHP-скриптом используются такие заголовки, как например Content-Disposition: attachment, filename=myFile.pdf или Content-Disposition: inline, filename=myFile.pdf у вас будут проблемы с Internet Explorer’ом, если вы сообщите браузеру не кэшировать страницу.
Internet Explorer оперирует загрузкой довольно необычным образом, выполняя два запроса к web-сайту. Первый запрос загружает файл и сохраняет его в кэше, пока не будет создан второй запрос (без сохранения отклика). Этот запрос вызывает процесс передачи файла конечному пользователю в соответствии с типом файла (например, запускает Acrobat Reader, если файл является PDF-документом). Это значит, что если вы отправили заголовки, запрещающие браузеру кэшировать страницу, Internet Explorer удалит файл между первым и вторым запросом, в результате чего конечный пользователь ничего не получит. Если файл, который вы отдаёте PHP-скриптом, не изменяется, одним из простейших решений будет убрать «запрещающие кэширование» заголовки из скрипта.
Если загружаемый файл регулярно изменяется (т.е. вы хотите, чтобы браузер загружал новейшую версию), вы должны использовать заголовок Last-Modified, который будет рассмотрен в этой главе позднее, и гарантировать, что время модификации между двумя последовательными запросами не изменяется. Вы должны сделать это таким образом, чтобы не повлиять на пользователей браузеров, правильно оперирующих загрузкой. Одним из решений в этом случае будет сохранение файла на вашем web-сервере и предоставление простой ссылку к нему, предоставив web-серверу сообщать за вас заголовки кэширования. Конечно, это решение не может быть приемлемым, если предполагается авторизованный доступ к файлу, это решение допускает непосредственную загрузку сохранённого файла.
Как я могу захватить данные на стороне сервера для кэширования?
Пришло время взглянуть на то, как мы можем уменьшить задержку при помощи кэширования вывода на стороне сервера. Общий подход начинает предоставлять страницу как обычно, выполняя запросы к базе данных и так далее на PHP. Тем не менее, перед отправкой результата в браузер, мы захватываем его и сохраняем готовую страницу, например, в файле. При следующем запросе, PHP-скрипт сначала проверяет наличие кэшированной версии страницы. Если она существует, скрипт отправляет в браузер версию из кэша, исключая таким образом задержку на повторное создание страницы.
Несколько слов о кэшировании при помощи шаблонов
Шаблонные движки часто говорят о кэшировании шаблонов. Обычно эти движки предлагают встроенный механизм для сохранения откомпилированной версии шаблона (т.е. генерируют из шаблона PHP-исходник), что предохраняет нас от необходимости парсить шаблон каждый раз, когда запрашивается страница. Это не нужно путать с кэшированием вывода, которое имеет отношение к кэшированию предоставляемого HTML (или другого вывода), который посылает PHP в браузер. Вы можете успешно использовать оба типа кэширования одновременно на одном и том же сайте.
Сейчас мы рассмотрим встроенный механизм кэширования на PHP, использующий буферизацию вывода, который может использоваться вами независимо от способа создания контента (с шаблонами или без шаблонов). Рассмотрим ситуацию в которой ваш скрипт отображает результат использую, к примеру, echo или print, чтобы выдать данные непосредственно в браузер. В таком случае вы можете использовать функции управления выводом PHP для хранения данных в буферной памяти, над которой ваш PHP-скрипт имеет и доступ, и контроль.
Пример 1. Файл: 1.php
// Начинаем буферизацию вывода
ob_start();
// Выводим некоторый текст (который сохраняется в буфере);
echo '1. Выводим это в буфер<br />';
// Получаем содержимое буфера
$buffer = ob_get_contents();
// Останавливаем буферизацию и очищаем буфер вывода
ob_end_clean();
// Выводим некоторый текст обычным образом
echo '2. Нормальный вывод<br />';
// Вывод содержимого буфера
echo $buffer;
Сам буфер хранит вывод как строку. Так, в вышеприведённом скрипте мы начинаем буферизацию с ob_start() и используем echo, чтобы вывести что-либо. Затем мы используем ob_get_contents(), чтобы выбрать данные, помещённые в буфер оператором echo, и сохранить их в строке. Функция ob_end_clean() останавливает буферизацию вывода и уничтожает его содержимое, как альтернативу можно использовать ob_end_flush(), чтобы вывести содержимое буфера.
Другими словами, мы захватили вывод первого echo, затем послали его браузеру после второго echo. Как видно из этого простого примера, буферизация вывода является очень мощным инструментом для формирования вашего сайта, она обеспечивает решение для кэширования, как мы скоро увидим, и является отличным способом скрыть ошибки от посетителей вашего сайта (смотрите Обработка ошибок). Она также обеспечивает альтернативную возможность для переадресации браузера в ситуациях типа аутентификации пользователя.
Заголовки HTTP и буферизация вывода
Буферизация вывода может помочь решить наиболее общую проблему,
связанную с функцией header(), не говоря уже о
Использование буферизации вывода для кэширования на стороне сервера
Вы уже видели базовый пример буферизации вывода, теперь следующий шаг, в котором буфер сохраняется в файл:
Пример 2. Файл: 2.php
// Если существует кэшированная версия:
if (file_exists('./cache/2.cache')) {
// Читаем и выводим файл
readfile('./cache/2.cache');
exit();
}
// Начинаем буферизацию вывода
ob_start();
// Выводим остальной HTML
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Кэшированная страница</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>
<body>
Эта страница кэшируется средствами PHP
<a href="http://www.php.net/outcontrol">Функции управления выводом</a>
</body>
</html>
<?
// Получаем содержимое буфера
$buffer = ob_get_contents();
// Останов буферирования и вывод буфера
ob_end_flush();
// Сохранение кэш-файла с контентом
$fp = fopen('./cache/2.cache', 'w');
fwrite($fp, $buffer);
fclose($fp);
?>
Сначала вышеописанный скрипт проверяет наличие существования версии странички в кэше, и, если она имеется, скрипт читает и выводит её. В противном случае, он использует буферизацию вывода для создания версии страницы в кэше. Она сохраняется как файл, после использования ob_end_flush() для отображения страницы пользователю.
Файл 2.cache содержит точную копию HTML, которую предоставляет скрипт:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Кэшированная страница</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>
<body>
Эта страница кэшируется средствами PHP
<a href="http://www.php.net/outcontrol">Функции управления выводом</a>
</body>
</html>
Блочная буферизация
Упрощённый подход кэширует выводимый буфер как одну страницу. Однако этот подход лишает вас реальных возможностей, предоставляемых функциями управления выводом PHP, улучшающих производительность вашего сайта методом соответственно различающихся сроков жизни вашего контента.
Вне всякого сомнения, некоторые части отправляемой вами посетителю страницы изменяются очень редко, например, такие как шапку, меню и нижний колонтитул. Однако другие части, типа таблиц, содержащих обсуждения в форуме, могут изменяться довольно часто. Буферизация вывода может использоваться к кэшированию разделов страницы в отдельных файлах, затем создавать из них страницу – решение, устраняющее необходимость повторных запросов к базе данных, циклов while и т.д. Вы можете назначать каждому блоку страницы дату истечения срока, после которой пересоздаётся кэш-файл, или кроме того, вы можете включить в ваше приложение механизм, который будет удалять кэш-файл каждый раз, когда сохранённый в нём контент изменён.
Вот пример, демонстрирующий этот принцип:
Пример 3. Файл: 3.php
<?
/* Запись кэш-файла
* @param string contents - содержание буфера
* @param string filename - имя файла, используемое при создании кэш-файла
* @return void
*/
function writeCache($content, $filename) {
$fp = fopen('./cache/' . $filename, 'w');
fwrite($fp, $content);
fclose($fp);
}
/* Проверка кэш-файлов
* @param string filename - имя проверяемого кэш-файла
* @param int expiry - максимальный "возраст" файла в секундах
* @return mixed содержимое кэша или false
*/
function readCache($filename, $expiry) {
if (file_exists('./cache/' . $filename)) {
if ((time() - $expiry) > filemtime('./cache/' . $filename))
return FALSE;
$cache = file('./cache/' . $filename);
return implode('', $cache);
}
return FALSE;
}
?>
Первые две определённые нами функции, writeCache и readCache, используются соответственно для создания кэш-файлов и проверки их существования. Функция writeCache получает данные для кэширования в первом аргументе, и имя файла, используемое при создании кэш-файла. Функция readCache получает имя кэш-файла в первом параметре, вместе со временем в секундах, после которого кэш-файл должен считаться устаревшим. Если она сочтёт кэш-файл допустимым, скрипт вернёт его содержимое, в противном случае он вернёт FALSE, чтобы показать, что-либо кэш-файла не существует, либо он устарел.
В этом примере я использовал процедурный подход. Однако я не советую делать это на практике, поскольку это закончится очень грязным кодом (смотри последующие решения с лучшей альтернативой) и, вероятно, вызовет проблемы с блокировкой файла (например, что случится, когда кто-то обращается к кэшу в момент его обновления?).
Давайте продолжим этот пример. После того, как запущена буферизация вывода, начинается обработка. Сначала скрипт вызывает readCache, чтобы узнать, существует ли файл 3_header.cache, он содержит шапку страницы – заголовок HTML и начало тела. Мы используем функцию date() чтобы вывести время, когда страница фактически была сгенерирована, таким образом вы увидите различные кэш-файлы в работе, когда страница будет отображена.
Пример 4. Файл: 3.php
<?
// Начинаем буферизацию вывода
ob_start();
// Обработка шапки
if (!$header = readCache('3_header.cache', 604800)) {
// Вывод шапки
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Страница, кэшированная поблочно</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>
<body>
Время создания шапки: <?=date('H:i:s'); ?> <br />
<?
$header = ob_get_contents();
ob_clean();
writeCache($header,'3_header.cache');
}
?>
Что же случается когда кэш-файл не найден? Выводится некоторый контент и присваивается переменной при помощи ob_get_contents(), после чего буфер очищается функцией ob_clean(). Это позволяет нам перехватывать вывод по частям и сопоставлять их с индивидуальными кэш-файлами при помощи writeCache. Заголовок страницы теперь хранится как файл, который может быть использован без нашего вмешательства в пересборку страницы. Давайте вернёмся на секунду к началу условного оператора. Когда мы вызывали readCache, мы передали ей время жизни кэша в 604800 секунд (одна неделя), readCache использует время модификации кэш-файла, чтобы определить, является ли кэш-файл всё ещё допустимым.
Для содержимого (тела) страницы мы по прежнему будем использовать тот же процесс. Однако на сей раз при вызове readCache мы будем использовать время жизни кэша в пять секунд, кэш-файл будет модифицироваться каждый раз, когда он «старше» 5 секунд:
Пример 5. Файл: 3.php
<?
// Обработка тела страницы
if (!$body = readCache('3_body.cache', 5)) {
echo 'Время создания тела: ' . date('H:i:s') . '<br />';
$body = ob_get_contents();
ob_clean();
writeCache($body, '3_body.cache');
}
?>
Нижний колонтитул эффективно изменять так же, как заголовок. После этого буферизация вывода останавливается и отображается содержимое трёх переменных, содержащих данные страницы:
Пример 6. Файл: 3.php (окончание)
<?
// Обработка нижнего колонтитула страницы
if (!$footer = readCache('3_footer.cache', 604800)) {
?>
Время создания нижнего колонтитула: <?=date('H:i:s'); ?> <br />
</body>
</html>
<?
$footer = ob_get_contents();
ob_clean();
writeCache($footer, '3_footer.cache');
}
// останавливаем буферизацию
ob_end_clean();
// Выводим содержимое страницы
echo $header . $body . $footer;
?>
Заголовок и нижний колонтитул обновляются еженедельно, в время как тело модифицируется, когда оно старее 5 секунд.
Блок-схема на рисунке суммирует методологию блочной буферизации.
Блок-схема блочной буферизации вывода
Вложенные буферы
Вы можете вкладывать один буфер в другой фактически до бесконечности, просто вызвав ob_start() неоднократно. Это может быть полезным, если у вас имеется множество операций, использующих буфер вывода, например, одни перехватывают сообщения PHP об ошибках, другие имеют дело с кэшированием. Вы должны удостовериться, что ob_end_flush() или ob_end_clean() вызываются каждый раз, когда используется ob_start().
Как мне управлять кэшированием на стороне клиента средствами PHP?
Пришло время посмотреть на механизм, который позволит нам контролировать кеш на стороне клиента средствами PHP. Этот подход будет работать только если вы используете PHP в связке с сервером Apache, поскольку мы будем использовать функцию getallheaders(), чтобы получить заголовки, передаваемые браузером. Эта функция работает только в Apache.
Новые имена функций
Если вы используете PHP 4.3.0 с Apache,
HTTP-заголовки доступны функцией
Механизмом для работы с кэшем web-браузера вновь является HTTP. Множество заголовков вовлечёны в инструктирование web-браузеров и прокси-серверов независимо кэшировать страницу, ситуация осложняется тем фактом, что некоторые из них доступны только с HTTP 1.1.
Проверка HTTP-заголовков в вашем браузере
Простым но очень удобным инструментом для проверки заголовков запросов и откликов является LiveHttpHeaders – аддон к браузеру Mozilla. Необходимо точно знать, какие заголовки посылает ваш скрипт, особенно когда вы имеете дело с заголовками кэширования HTTP.
Для простоты мы рассмотрим только заголовки кэширования HTTP 1.0, а именно Expires, Last-Modified и If-Modified-Since, а также статус-код HTTP 304 (Not Modified).
Другие заголовки, доступные с HTTP 1.1, например Cache-Control и ETag, предназначены для обеспечения расширенного механизма, который может использоваться совместно с состоянием web-сессии, иными словами, версия данной страницы, отображаемой неавторизованному посетителю, может значительно отличаться от отображаемой авторизованному пользователю. Заголовки HTTP 1.1 изначально добавлялись для того, чтобы позволить кэшировать такие страницы.
Истечение срока жизни страницы
Самым простым в использовании заголовком является заголовок Expire, который устанавливает дату (возможно, будущую), когда страница устареет. До этого момента web-браузеру разрешается использовать кэшированную версию страницы.
Пример 7. 6.php
<?
/**
* Посылает заголовок Expires HTTP 1.0.
* @param int $expires - количество секунд до времени истечения срока жизни
*/
function setExpires($expires){
header('Expires: ' .
gmdate('D, d M Y H:i:s', time() + $expires) . 'GMT');
}
// Устанавливаем заголовок времени истечения срока жизни Expires
setExpires(10);
// Отображаем
echo 'Эта страница самоуничтожится через 10 секунд<br />';
echo 'Сейчас ' . gmdate('H:i:s') . ' GMT<br />';
echo '<a href="' . $_SERVER['PHP_SELF'] .
'">Посмотреть вновь </a><br />';
?>
Функция setExpires отправляет заголовок HTTP Expires с будущим временем, заданном в секундах. Вышеприведённый пример показывает текущее время по Гринвичу и выводит ссылку, которая вам позволяет перейти на страницу вновь. Используя кнопку Refresh вашего браузера, вы можете сообщить браузеру о желании обновить кэш. Используя ссылку, вы увидите, что время изменяется только раз в 10 секунд.
Даты и время в HTTP
Даты в HTTP всегда вычисляются относительного меридиана времени Гринвича (GMT). Функция PHP gmdate() точно такая же функция, как date(), за исключением того, что она автоматически компенсирует время по Гринвичу, основанное на системных часах и настройках региона вашего сервера.
Когда браузер сталкивается с заголовком Expires, он кэширует страницу. Все последующие запросы страницы, сделанные до указанного времени истечения срока жизни, используют версию страницы из кэша, никаких запросов к web-серверу при этом не происходит.
Заголовок Expires преимущественно прост в реализации, но в большинстве случаев, если вы не высокоорганизованный человек, вы не можете знать точно, когда данная страница вашего сайта обновлена. Поскольку браузер войдёт в контакт с сервером только после того, как страница устареет, нет ни одного способа сообщить браузеру, что страница, находящаяся в его кэше, устарела. Вы также теряете некоторую часть трафика к вашему web-сайту, поскольку браузер не обращается к серверу при запросе страницы из кэша.
Время изменения страницы
Более практично использовать заголовки Last-Modified и If-Modified-Since, доступные в HTTP 1.0. Технически он известно как выполнение условного GET-запроса, вы возвращаете любой контент, основываясь на условии пришедшего заголовка запроса If-Modified-Since.
При использовании этого метода вы должны отправлять заголовок Last-Modified каждый раз, когда обращаются к вашему PHP-скрипту. При следующем запросе страницы браузером, он отправит заголовок If-Modified-Since, содержащий время, по которому ваш скрипт может определить, обновлялась ли страница со времени последнего запроса. Если это не так, ваш скрипт посылает код статуса HTTP 304, чтобы указать, что страница не изменялась, не выводя при этом содержимого страницы.
Устанавливаем время модификации кэш-файла этой строкой:
$lastModified = filemtime($cache_file);
Затем, используя время модификации кэш-файла, мы посылаем заголовок Last-Modified. Нам нужно посылать её для каждой предоставляемой страницы, чтобы вынудить браузер посылать нам заголовок If-Modified-Since с каждым запросом.
// Выдаём заголовок HTTP Last-Modified
header('Last-Modified: ' .
gmdate('D, d M Y H:i:s', $lastModified) . ' GMT');
Использование функции getallheaders() обеспечивает нам получение от PHP всех входящих заголовков в виде массива. Затем мы должны проверить, что заголовок If-Modified-Since действительно существует, если он есть, мы должны обработать специальный случай старых версий Mozilla (ниже 6й версии), который добавлял в конец дополнительное поле к заголовку If-Modified-Since. Используя функцию PHP strtotime(), мы получаем таймштамп даты, переданной нам браузером. Если такого заголовка нет, мы присваиваем таймштампу ноль, вынуждая таким образом PHP отдать посетителю последнюю версию страницы.
<?
// Получаем заголовки запроса клиента - только для Apache
$request = getallheaders();
if (isset($request['If-Modified-Since'])) {
// Разделяем If-Modified-Since (Netscape < v6 отдаёт их неправильно)
$modifiedSince = explode(';', $request['If-Modified-Since']);
// Преобразуем запрос клиента If-Modified-Since в таймштамп
$modifiedSince = strtotime($modifiedSince[0]);
} else {
// Устанавливаем время модификации в ноль
$modifiedSince = 0;
}
?>
Наконец, мы проверяем, был ли модифицирован кэш с тех пор как посетитель получал эту страницу в последний раз. Если это не так, мы просто посылаем в заголовке отклик Not Modified и прекращаем выполнение скрипта, не нагружая канал передачи данных и экономя процессорное время, инструктируя браузер отобразить кэшированную версию страницы.
<?
// Сравниваем время последней модификации контента с кэшем клиента
if ($lastModified <= $modifiedSince) {
// Разгружаем канал передачи данных!
header('HTTP/1.1 304 Not Modified');
exit();
}
?>
И в заключение готовый кусок кода, позволяющий кешировать все статические ваши страницы.
index.php - через него загружаются все файлы с сервера. Настройка сделана с помощью .htaccess
<?
$url = $_SERVER['REQUEST_URI'];
if($url==""||$url=="/")
$url="index.htm" ;
else $url=substr($url, 1);
if ( !file_exists( $url ) )
{ header("HTTP/1.0 404 Not Found"); exit();}
// Получаем время последней модификации кэш-файла
$lastModified = max(filemtime($url),filemtime('index.php'));
$slastModified = gmdate('D, d M Y H:i:s', $lastModified) . ' GMT';
// Выдаём заголовок HTTP Last-Modified
header('Last-Modified: ' . $slastModified );
// Получаем заголовки запроса клиента - только для Apache
$headers = getallheaders();
if (isset($headers['If-Modified-Since'])) {
// Разделяем If-Modified-Since (Netscape < v6 отдаёт их неправильно)
$modifiedSince = explode(';', $headers['If-Modified-Since']);
// Преобразуем запрос клиента If-Modified-Since в таймштамп
$modifiedSince = strtotime($modifiedSince[0]);
// Сравниваем время последней модификации контента с кэшем клиента
if ($lastModified <= $modifiedSince)
// Разгружаем канал передачи данных!
{ header('HTTP/1.1 304 Not Modified'); exit(); }
}
echo "<DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\">\n<HTML>\n";
echo "<HEAD>\n";
echo "<META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=utf-8\">\n";
echo "<meta http-equiv=\"Last-Modified\" content=\"".$slastModified."\">\n";
?>
Если вы объедините подход времени последнего изменения со значением времени, являющимся уже доступным в вашем приложении (например, время самой последней новостной статьи, или время устаревания из системы серверного кэширования, которое мы видели в последнем решении), вы сможете воспользоваться преимуществами кэша web-браузера и разгрузите канал передачи данных, по возможности сэкономив информационный трафик с вашего сайта и улучшив его производительность.
Будьте осторожны при тестировании любого кэширования, выполненного в таком стиле, если вы сделаете это неправильно, вы можете заставить ваших посетителей всегда иметь устаревшие копии вашего сайта.
Кэширование ваших страниц в 5 шагов
Кэширование ваших страниц может оказаться красивым и полезным механизмом, особенно, если они генерируются средствами PHP и делают множество SQL запросов. Как только вы примените кэширование, ваш сервер тут же снизит нагрузку и перестанет съедать много памяти на генерацию страниц - он просто будет загружать их из КЭШа. Я покажу вам, как PHP может кэшировать страницы и, в дальнейшем, вы сможете тратить на это минут 5.
Расмотрим технологию кэширования пошагам:
- В домашней директории создаем файлы .htaccess,
start_cache.php, end_cache.php, а
также папку с названием cache_files.
- Папке cache_files необходимо проставить атрибуты 777.
- Внутри .htaccess файла пропишите следующие строки:
php_value auto_prepend_file /home/username/public_html/start_cache.php php_value auto_append_file /home/username/public_html/end_cache.php
Строку /home/username/public_html/ необходимо заменить на путь к вашей домашней директории.
- В скрипт start_cache.php помещаем следующий код:
<?php // раздел настроек, которые вы можете менять $settings_cachedir = '/home/username/public_html/cache_files/'; $settings_cachetime = 3600; //время жизни кэша (1 час) // код $thispage = 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; $cachelink = $settings_cachedir.md5($thispage).".html"; if (file_exists($cachelink)) { $cachelink_time = filemtime($cachelink); if ((time() - $settings_cachetime) < $cachelink_time) { readfile($cachelink);die(); } } ob_start(); ?>
Не забывайте исправлять путь /home/username/public_html/ на путь к вашей домашней директории.
- А следующий код поместите в скрипт end_cache.php:
<?php $fp = fopen($cachelink, 'w'); fwrite($fp, ob_get_contents()); fclose($fp); ob_end_flush(); ?>
Все ваши страницы будут кэшироваться на 3600 секунд = 1 час. Этот параметр вы легко можете поменять в скрипте start_cache.php. Кэш страниц будет сохранен в папке cache_files.
Совершенно очевидно, что в данном случае атрибуты 777 являются определенным нарушением безопасности. В связи с чем, рекомендую вынести папку cahce_files за пределы public_html, например, поместить ее на один уровень выше. Это закроет доступ к находящимся в ней файлам пользователей вашего сайта, но никак не повлияет на работоспособность системы.
Также, у данного метода есть еще один серьезный недостаток: автор статьи складывает весь кэш в одну папку, что, при достаточном количестве страниц на вашем сайте, вызовет проблему, например, в системах Unix наблюдается достаточное замедление работоспособности при наличие в папке более чем 1000 файлов. В связи с чем, в алгоритм необходимо внести ряд изменений и раскладывать файлы по отдельным подпапкам внутри папки cache_files. Например, используя для этого первые 3-4 символа md5 КЭШа.
Для динамических ресурсов вполне возможно выбрать время кэширования в несколько (5-10) секунд или 1-2 минуты, что уже значительно снизит нагрузку на сервер, но не нанесет вреда интерактивности сайта.
Для страниц, для которых особо важна интерактивность, можно ввести исключения в .htaccess, что позволит именно им постоянно изменяться, а для остальных страниц можно применять кэширование.
Регенерация содержания на лету
Динамически созданные, но статически обслуживаемые страницы, т.е. страницы которые должны передаваться как чисто статические (считываемые из файловой системы и затем передаваемые по запросу), однако они должны быть динамически сгенерированны веб-сервером если они отсутствуют в файловой системе. Таким образом вы можете иметь страницы сгенерированные PHP которые являются статически обслуживаемыми если только кто-либо (либо планировщик) не удалит статическое содержание. В таком случае содержание обновляется.
Это делается следующим набором директив:
RewriteCond %{REQUEST_FILENAME} !-s RewriteRule ^page\.html$ page.php [T=application/x-httpd-php,L]
Здесь, запрос к page.html приводит к внутреннему запуску соответствующего page.php, если page.html все-ещё отсутствует или имеет нулевой размер. Фокус здесь в том что page.php это обычный PHP скрипт который в дополнение к собственному выводу, записывает свой вывод в файл page.html. Запустив это один раз, сервер передает данные page.html. Когда вебмастер хочет обновить содержание, он просто удаляет page.html (обычно с помощью cronjob).
Проблема с кэшированием страниц у Internet Explorer.
У IE при работе с заголовком "Vary" встречается одна неприятная ошибочка, связанная с кэшированием страниц. Проблема решается добавлением в .htaccess следующих строк:
BrowserMatch "MSIE" brokenvary=1 BrowserMatch "Mozilla/4.[0-9]{2}" brokenvary=1 BrowserMatch "Opera" !brokenvary SetEnvIf brokenvary 1 force-no-vary