sonyps4.ru

Кэширование и PHP. Кэширование данных страниц в php

В предыдущем материале о веб-технологиях мы упомянули полезную статью Кэширование в HTTP (далее: «Статья с nomagic.ru»). По статье, однако, у нас возникли некоторые вопросы, а обсуждение там заглохло, поэтому пришлось искать все ответы самим. Вопросы, собственно, не именно по статье – они копились в течение нескольких лет. Надоело иметь их нерешёнными, а статья просто дала повод искать решения более активно.

Инструменты

Первый вопрос – как увидеть HTTP-заголовки запросов браузера и ответов сервера? Автор статьи с nomagic.ru рекомендует использовать для этой цели "Web Developer tools" в Firefox"е и какой-то мутный "DevToolbar" для ИЕ. Рука потянулась было кликнуть по ссылке, но зависла в воздухе:

1) Web Developer tools для FF у нас и так есть, и там нет инструмента для просмотра HTTP-заголовков, там даже DOM-инспектор в версии 3 зачем-то убрали!

3) И совсем мрачная мысль: ладно, допустим, для FF у нас есть-таки LiveHTTPHeaders; с ИЕ – вдруг да повезёт; ну, а Опера? А Google chrome?.. Нам что теперь, по всему огороду метаться?

Почему бы прямо на сайте, средствами PHP не отобразить все HTTP-заголовки? Там ведь есть переменные окружения , переменные для работы с сервером и всё такое. То есть точно известно, что там есть, например, $_SERVER["HTTP_HOST"] и HTTP_REFERER (у нас на каждом сайте используются). Надо добавить все остальные HTTP_ – вот и будут заголовки запроса. Тем более, что в PHP для этого есть специальная функция getallheaders(). Или apache_request_headers(). И apache_response_headers(). Да. Так можно вывести на экран все HTTP-заголовки. Казалось бы. Но нас ожидал тяжёлый удар ниже пояса и 15-минутные мучения, результатом которых стало открытие: на нашем хостинге PHP установлен как cgi (а не как модуль Апач) && в такой конфигурации все эти функции...headers() не работают!

Запустив скриптик с echo phpinfo() и бегло просмотрев результат, обнаруживаем, что искомые заголовки HTTP-запроса есть в массиве $_ENV (и больше нигде). Ладно, _env так _env. Но там много всякого хлама (в данный момент для нас лишнего), поэтому создаём новый массив $varrvis и аккуратно откромсываем туда из _env более-менее нужные куски:

Foreach($_ENV as $ke=>$va) { if (preg_match("/^HTTP\_/i",$ke) && !preg_match("/COOKIE/i",$ke)) $varrvis["$ke"]=$va; }

А вот получить заголовки ответа нашего сервера – ну ваще никак, кроме функции headers_list() . И только те заголовки, которые мы сами отправим в скрипте PHP с помощью функции header() . По идее функцию headers_list() следует запускать после написания всех заголовков. Мы так примерно и сделали, хотя, скорее всего, для данного сайта (сайт – на котором ставились опыты) это без разницы, потому что везде используется ob_start("ob_gzhandler") . В конец тестируемых скриптов добавляем конструкцию:

Foreach(headers_list() as $ke=>$va) { $varrvis[$ke]=$va; }

И дополняем наш массив заголовков ответами сервера. А между Запросом и Ответом для удобства чтения вставим строку:

$varrvis["Response"]="==============================";

Осталось в самом конце тестируемых скриптов написать print_r($varrvis) – и потом бодро листать страницы сайта во всех подручных браузерах, любуясь HTTP-заголовками.

HTTP-кэширование инструкциями Apache

В статье с nomagic.ru указывается два источника инструкций кэширования: конфигурационные файлы Апача (http.conf && .htacces) и непосредственно PHP-скрипт с командами вида header("Pragma: no-cache"). Но существует ещё третий источник – его можно обнаружить несложным опытом:

1) пишем (раскомментируем) в httpd.conf (Апач 1.3.39) cтроки:

LoadModule expires_module modules/mod_expires.so LoadModule headers_module modules/mod_headers.so AddModule mod_expires.c AddModule mod_headers.c

2) в папке нашего сайта в .htaccess добавляем инструкции:

Header append Cache-Control "public" ExpiresActive On ExpiresDefault "access plus 1 hours"

3) пишем простенький скрипт pi.php из двух строк:

4) открываем страницу pi.php в Firefox и видим в LiveHTTPHeaders (Наш PHP «инструмент» может показывать только заголовки, отправленные функцией header(), а пока мы ей не пользуемся). следующие строки, имеющие отношение к кэшированию:

Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0 Expires: Thu, 19 Nov 1981 08:52:00 GMT Pragma: no-cache

Вуаля. И не надо никакой Википедии – вот они заголовки, убивающие кэширование. Они исходят из третьего источника – файла php.ini. В нём по умолчанию, при установке PHP записана, в частности, следующая инструкция:

Session.cache_limiter = nocache

Именно она заставляет PHP посылать анти-кэширующие заголовки в определённых условиях (например, при использование функции session_register()). Мы, конечно, немного схитрили, подогнав ситуацию под эти условия. Но кто поручится, что никогда не будет использовать в своих скриптах функцию session_register() ? Да, в общем-то, и без неё дело обстоит достаточно хреново: уберите первую строку из скрипта pi.php (оставив только echo phpinfo();) – тоже ничего хорошего:

И это всё, что дают кэширующие инструкции Апача в сочетании с "session.cache_limiter=nocache" в php.ini. Отсутствует самый главный заголовок – Last-modified (дата последнего изменения страницы), без которого невозможно ни правильно установить, ни правильно уничтожить кэширование в браузере.

Самый забавный результат получается, если "дёрнуть попугая сразу за обе ноги" – написать в php.ini "session.cache_limiter=private" (нужна перезагрузка Апача) и оставить в скрипте строку session_register("var1"):

Cache-Control: private, max-age=300, pre-check=300 Expires: Thu, 19 Nov 1981 08:52:00 GMT Last-Modified: Mon, 06 Jul 2009 15:13:40 GMT

Появляется Last-Modified , который показывает время последнего изменения скрипта php, а Cache-Control противоречит Expires . Поведение браузеров будет непредсказуемым.

Правильное HTTP-кэширование

В последних полученных нами в ходе опыта заголовках противоречивость в общем-то совсем не фатальна: браузеры и не такое ещё видали, к таким вещам они вполне «помехоустойчивы». Наибольшую проблему представляет как раз Last-Modified , который нужен и пользователям (браузерам), и поисковикам. Понятно, что дату изменения файла PHP для него использовать нельзя – потому что реальное содержимое страницы может быть вообще никак не связано с этой датой: обычно содержимое страницы извлекается из базы данных, и дату его изменения тоже надо извлекать оттуда (из БД ).

Если это конкретная статья с нашего сайта, мы просто берём дату текущей записи из поля `datrec` таблицы `articles`. Если это список статей (на главной странице сайта), мы ищем наибольшую дату всех записей по формуле "select max(`datrec`) from `articles`" – именно она и будет датой последнего изменения страницы, которую мы передадим в заголовке Last-Modified .

Существуют ещё две «точки контроля» содержимого, реализуемые с помощью HTTP-заголовков:

1) Etag – хэш содержимого страницы, получаемый, например, с помощью функции md5 (текст_страницы);

2) Content-length – общая длина текста, отправленного браузеру в ответ на его запрос.

Мы не можем использовать Content-length , потому что этот параметр постоянно меняется: в правой колонке каждой страницы у нас висит напоминание о том, что это всё-таки сайт рекламной газеты «Деловая неделя», – список товаров последнего номера газеты. Список этот довольно большой, поэтому на странице выводится только небольшая часть списка, выбранная случайным образом .

Как же, спросите вы, мы используем Etag – он ведь тоже тогда постоянно случайным образом меняется? А очень просто: мы не включаем переменную часть страницы в хэш, а составляем хэш только «по материалам базы данных статей». Почему же нельзя так же поступить и с Content-length ? Да потому что Content-length браузер может легко проверить (ИЕ так и делает – отправляет на сервер обратно действительную длину полученного содержимого). А хэш можно написать какой попало (главное, чтобы он менялся при изменении отслеживаемой части страницы), браузер ведь не знает, какой мы используем алгоритм, и вынужден просто принимать наш Etag на веру.

Мы используем два способа хэширования:

1) в случае списка текстов, получаемых из многих строк таблицы, создаём Etag * по формуле $etag=md5($list) ;

2) в более простом случае (извлекается только одна запись из таблицы) заставляем работать mysql, добавляя в запрос лишнее значение: "select `id`, `title`, `text`, `author`, `datrec`, old_password(concat(`title`,`text`,`author`)) as `etag` from `articles`...".

При отправке заголовков функцией header() нужно следить, чтобы эти действия производились раньше какой-либо отправки содержимого браузеру (через echo, print PHP или просто обычным HTML-кодом). То есть сначала вся проверяемая часть помещается в переменную, вычисляется Etag *, отправляются все заголовки, и только потом можно выводить содержимое. Если вы, конечно, не написали в начале страницы ob_start("ob_gzhandler"). Мы-то как раз написали, поэтому отправляем заголовки как попало и когда попало. Вот этот ob_gzhandler ещё позволяет получить всё содержимое, отправляемое браузеру, сразу – функцией ob_get_contents() , а также истинную длину содержимого (для заголовка Content-length ) – функцией ob_get_length() . Мы, как уже говорили, не можем на данном сайте использовать всё содержимое страницы для формирования этих заголовков. Но на других сайтах – вполне.

304 Not Modified

Итак, мы отправляем клиентам правильную дату изменения страницы и Etag . Клиенты относятся с пониманием – посылают в следующих обращениях к этой странице заголовки If-Modified-Since и If-None-Match , что вы можете увидеть сами в самом низу любой нашей статьи (после нажатия клавиши F5, разумеется). Но желанный результат не достигнут: сервер в ответ на все запросы браузера исправно посылает заголовок HTTP/1.x 200 OK , и никаких 304 . Наш «инструмент» не отображает заголовки "200 OK", потому что мы их не формируем функцией header().

Заголовок 304 можно увидеть в большом количестве через LiveHTTPHeaders – у файлов картинок, Javascript, css и простых HTML страниц. Этот заголовок отправляет сам Апач, и он делает это без всяких наших ухищрений с модулем headers.so и без дополнительных инструкций типа "ExpiresActive On". Но не для страниц, формируемых PHP.

Мы сами вписали в PHP-скрипт отправку заголовков браузеру, и сами должны проверять на наличие-отсутствие валидации последующие запросы браузера, и сами же потом сличать контрольные параметры и, в зависимости о результата, отправлять браузеру заголовок 200 или 304. Точнее, заголовок 200 PHP отправляет всегда сам, нам нужно только вычислять ситуацию необходимости 304. Мы делаем это в главном конфигурационном файле всех сайтов configbase.php.

Сложность получения информации о заголовках в том, что на одном хостинге PHP работает как cgi, а на другом как модуль Апач, поэтому сначала приходится проверять наличие переменных в «суперглобальных» массивах Env и Server , и в зависимости от результат создавать ссылку на подходящий массив:

$h304="HTTP/1.x 304 Not Modified"; $match=""; $since=""; $varr=array(); $varrvis=array(); if (array_key_exists("HTTP_HOST",$_ENV)) $varr =& $_ENV; if (array_key_exists("HTTP_HOST",$_SERVER)) $varr =& $_SERVER; if (isset($varr["HTTP_IF_NONE_MATCH"])) $match=$varr["HTTP_IF_NONE_MATCH"]; $match=trim(strval($match)); if (isset($varr["HTTP_IF_MODIFIED_SINCE"])) $since=$varr["HTTP_IF_MODIFIED_SINCE"]; $since=explode(";",$since); $since=strtotime(trim($since));

Предпоследняя строчка нужна из-за ИЕ, который в заголовке IF_MODIFIED_SINCE отправляет ещё и длину страницы: "Fri, 03 Jul 2009 15:42:30 GMT; length=20994" – мы отрезаем от данного заголовка всё, что может быть после точки с запятой. Затем создаём независимый от конкретного хостинга массив HTTP-заголовков:

Foreach($varr as $ke=>$va) { if (preg_match("/^HTTP\_/i",$ke) && !preg_match("/COOKIE/i",$ke)) $varrvis["$ke"]=$va; } $varrvis["Response"]="=============================";

Ну, и главный фрагмент кэширования, ядро всей нашей системы, находящееся внутри страниц PHP (где $dat – время из таблицы mysql, переведённое в секунды функцией strtotime ):

Header("Etag: $etag"); header("Cache-Control: private, max-age=0"); header("Expires: ".gmdate("r")." GMT"); header("Connection: Keep-Alive"); header("Keep-Alive: timeout=5, max=100"); if ($since==$dat) { if (!$match || $match==$etag){ $varrvis=$h304; include "bottom.php"; header($h304); header("Connection: Close"); exit; } } else { header("Last-Modified: ".gmdate("r", $dat)." GMT"); }

Система работает корректно во всех упомянутых в данной статье браузерах: кэширует, когда это нужно, и отправляет браузеру новую информацию, если она есть. Например, если после открытия главной страницы сайта (со списком статей) нажать F5 (не в Опере!:-), внизу страницы можно увидеть долгожданный заголовок 304 (в Опере его тоже можно увидеть, если попасть на данную страницу, щёлкнув по ссылке на другой странице сайта). Если в заголовок какой-нибудь статьи были внесены изменения или, например, добавилась новая статья, скрипт, получив от браузера запрос валидации, обнаружит изменение данных и отправит браузеру новое содержимое страницы, а не заголовок 304 .

Человеческими словами то, что мы делаем с помощью этих заголовков, можно пересказать так:

1) мы посылаем браузеру (вообще любому клиенту) две метки идентификации: время последнего изменения содержимого старницы и хэш страницы (контрольную сумму); мы посылаем также инструкцию, разрешающую кэширование только конечному клиенту (Cache-Control: private); в этом же заголовке (max-age=0) мы говорим о том, что клиент не должен запрашивать новое содержимое в течение 0 секунд (то есть должен запрашивать вего всегда); в следующем заголовке (Expires) мы говорим клиенту то же самое: срок «сгорания» актуальности страницы истекает немедленно, прямо сейчас;

2) браузер послушно складывает страницу в свой кэш, вместе с картинками и файлами css; при последующих обращениях к странице браузер спрашивает у сервера, изменилась ли дата (IF_MODIFIED_SINCE) и, иногда, контрольная сумма (IF_NONE_MATCH) – про контрольную сумму ИЕ, например, не спрашивает;

3) если дата изменилась, мы проверяем, был ли от браузера запрос контрольной суммы, и если был, проверяем также её изменение; если ничего не поменялось, отправляем браузеру заголовок 304 ; если поменялось – не отправляем 304 (и PHP сам отправляет 200 OK);

Да, и ещё одна деталь для нашего «инструмента»: первый заголовок (HTTP-статуса) почему-то никак не извлекается функцией headers_list() . Когда он 200 , это не очень принципиально, но 304 хотелось бы видеть (чтобы убедиться в работоспособности нашей системы кэширования). Поэтому приходится «подрисовывать» этот заголовок в массив заголовков руками в строке

$varrvis=$h304; ,

а потом для всех остальных полученных функцией headers_list() заголовков увеличить индекс на единицу ($ke+1):

Foreach(headers_list() as $ke=>$va) { $varrvis[$ke+1]=$va; }

Последний нюанс. Как увидеть заголовок 304 в браузере, если браузер получил этот заголовок от сервера, и не получил никакого содержимого страницы (страница не должна меняться на экране)? Пусть это останется нашей маленькой тайной.

© 2009, «Деловая неделя», Михаил Гутентог

501 . SlipkeR

Спасибо) все понятно и доходчиво написано) автору спс)

Современные браузеры достаточно часто используют в своей работе локальный кэш. Что это означает? Это означает что браузер, получив от сервера html-документ, картинку или другой ресурс, размещает его в своем локальном кэше (проще говоря, записывает полученный ресурс на жесткий диск машины пользователя) и при последующих запросах к такому ресурсу не обращается на сервер, а получает ресурс из локального кеша.

Данная алгоритм работы браузеров резко повышает скорость загрузки html-документов. Так как если ресурс уже загружался, и как следствие расположен в локальном кэше, то время доступа определяется не пропускной способностью канала связи (например, модемного подключения) а скоростью работы жесткого диска.

Однако наряду с достоинствами данный метод так же порождает ряд проблем. В частности большинство начинающих web-программистов, при разработке динамических сайтов, сталкивается с одной и той же проблемой. Суть этой проблемы заключается в том, что вместо повторного обращения на сервер за страницей, запускающей на сервере скрипт, модифицирующий некую информацию, браузер обращается в локальный кэш. И в результате, например трех обращений, происходит не три модификации информации, расположенной на сервере, а только одна.

Для того, что бы заставить браузер каждый раз обращаться за страницей на сервер необходимо запретить браузеру заносить данный ресурс в кэш. Ниже приведены наиболее распространенные методы, запрещающие кэширование или позволяющие его обойти.

Генерация нового URL

Допустим что запрашиваемый ресурс имеет следующий url: test.html?id=7. Как видно из url’а ему передается один параметр. Добавим, например, при помощи JavaScript, в url еще один параметр, а его значением сделаем случайное число. В результате url будет выглядеть следующим образом: test.html?id=7&rnd=0.6700820127538827. Случайный параметр будет каждый раз генерироваться заново. Ниже приводится листинг, демонстрирующий этот подход:

Генерация нового URL document.write (""); тестовая ссылка

Каждый раз результат такого запроса будет кэшироваться, но так как кэширование производится по всему url, то каждый раз будет получаться новый url и браузер будет вынужден запрашивать с сервера ресурс, так как url двух запросов не будут совпадать в точности.
Поля заголовков

Управлять кэшированием можно так же со стороны сервера. Для этого ресурс, отправляемый браузеру, сопровождается полями заголовка. Детальное описание полей заголовка может быть найдено в стандарте Rfc 2068, который описывает протокол HTTP 1.1.

Поле заголовка Expires

Значением данного заголовка является дата, после которой содержимое ресурса устареет. Если пользователь после этой даты обратиться к ресурсу, браузер должен запросить ресурс у сервера, а не из локального кэша.

Если поле >Expires< содержит дату, прошедшую, по отношению к текущей, то при следующем обращении к ресурсу браузер будет вынужден снова обратиться к серверу. Это произойдет вследствие того, что либо документ не будет занесен в кэш — как уже устаревший, либо при обращении к кэшу браузер определит, что документ уже устарел. Следующий листинг на PHP демонстрирует использование заголовка Expires:

Поле заголовка Last-Modified

Значением данного заголовка является дата последнего обновления ресурса. Большинство современных браузеров используют следующий алгоритм, если ресурс уже находится в локальном кэше:

* запрашивает с сервера дату последнего обновления ресурса
* сравнивает полученную дату и дату ресурса в локальном кэше
* если ресурс на сервере новее ресурса в кэше — запрашивается ресурс с сервера

Если ресурс, расположенный на сервере, содержит в данном поле текущую дату, то браузер будет каждый раз запрашивать ресурс с сервера, а не из локального кэша. Следующий листинг демонстрирует использование поля заголовка Last-Modified:

header ("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");

Поля заголовка Cache-Control и Pragma

И, наконец, поля заголовка, непосредственно отвечающие за кэширование ресурса. Поле Было определено в стандарте Rfc 1945, описывающим протокол HTTP 1.0. Данное поле считается устаревшим, но в некоторых случаях приходится использовать именно его. В частности некоторые proxy-сервера неправильно обрабатывают запросы к постоянно изменяющимся ресурсам, если вместе с ресурсом не передается данное поле заголовка.

Второе поле определено в стандарте Rfc 2068, который описывает протокол HTTP 1.1. Данное поле заголовка позволяет запретить кэширование, и каждый раз запрашивать ресурс с сервера. Следующий листинг демонстрирует использование полей заголовка Cache-Control и Pragma для запрета кэширования:

header("Cache-Control: no-cache, must-revalidate"); header("Pragma: no-cache");

Хорошо Плохо

Кэш - это специальный промежуточный буфер с очень быстрым доступом, содержащий данные, которые могут быть запрошены с наибольшей вероятностью.

Что такое кэширование?

Для оптимизации работы с сетью применяется механизм сохранения полученных однажды по HTTP документов в кеше для их повторного использовании, при этом без обращения к серверу-источнику. Документ, который сохранен в кеше будет доступен при последующем обращении к нему, при этом без выгрузки с сервера-источника. Это призвано увеличить скорость доступа клиента к нему, а также снизить расход трафика сети.

На сегодняшний день кэши бываю двух типов - локальные и общие.

  • Локальный - кеш, хранимый на диске у клиента, создаваемый, а также управляемый его интернет-браузером.
  • Общий - это кэш прокси-сервера провайдера, он может состоять из одного или ряда прокси-серверов.

Локальный кеш есть, наверное в каждом интернет-браузере, общими пользуется большая часть людей, которые используют интернет. И если малую часть веб-сайтов сегодня оценивают по расходу трафика, то скорость загрузки - очень важный критерий, который должен обязательно учитываться при разработке вашего веб-проекта.

Для динамических страничек, которые создаются в результате работы PHP-приложения, казалось бы, кэширование весьма вредно. Содержание странички формируются по запросу пользователя на основе определенного источника информации. Тем не менее, кэширование бывает полезным. Управляя им можно сделать работу с вашим сервером гораздо удобнее для пользователя, разрешая загрузку из кэш конкретных страниц, предотвращая таким образом их повторную выгрузку с сервера и экономя пользователю трафик и время.

Ключевые принципы сохранения страниц в кэш

PHP-приложение может управлять кэшированием результатов его работы формируя дополнительные поля в заголовке HTTP ответа вызовом специальной функции Header().

Несколько общих утверждений, которые характерны не только для PHP-приложений:

  • Странички, которые передаются по POST не сохраняются в кэш никогда.
  • Странички, которые запрашиваются по GET и содержат параметры (в URL есть "?") не сохраняются в кэш, в случае если не указано обратное.

В большинстве ситуаций каких-то дополнительных инструкций в приложение добавлять не нужно. Основные нюансы, на которые надо обратить внимание:

  • запрет кэширования документов, которые кэшируются по умолчанию;
  • кэширование документов, которые не подлежат кэшированию по умолчанию.
Запрет на кэширования документов (которые кэшируются по умолчанию)

Эта задача возникает для PHP-скриптов, которые вызываются без параметров или являются индексами директорий, но формируют информацию персонально под пользователя (к примеру на основе user agent или же cookies) или работают на основе быстро изменяющихся сведений. Мы по спецификации HTTP/1.1 можем управлять такими полями:

  • Expires - задает дату истечения срока годности определенного документа. Задание ее в прошлом определяет запрет кэш для этой странички.
  • Cache-control: no-cache - управление кэшем. Значение no-cache определяет запрет кэш этой странички. Для версии протокола HTTP/1.0 здесь действует "Pragma: no-cache".
  • Last-Modified - это дата последнего изменения определенного содержимого. Поле применяется исключительно для статических страничек. Apache заменяет данное поле значением поля Date для динамически генерируемых страничек, в частности для страниц, которые содержат SSI.
  • Чтобы запретить кэширование, достаточно прописать:

    Для того, чтобы документ пометить как "устаревший", необходимо установить Expires равным полю Date.

    Header("Expires: " . gmdate("D, d M Y H:i:s") . " GMT");

    Не следует также забывать о том, что формы, которые запрошены по POST кэшированию не подлежат.

    Кэширование с прогнозируемым обновлением

    Рассмотрим пример - прайс лист обновляемый именно по понедельникам. Вы знаете заранее о том, что содержание странички можно хранить в кэш до наступления следующей недели, что и нужно указать в заголовке ответа, обеспечивая необходимое поведение странички в кэш.

    Главной задачей здесь является получить дату следующего понедельника в виде RFC-1123.

    $dt_tmp=getdate(date("U")); header("Expires: " . gmdate("D, d M Y H:i:s", date("U")-(86400*($dt_tmp["wday"]-8))) . " GMT"); header("Cache-control: public");

    Данным способом можно очень эффективно управлять поведением странички в кэш. Можно выделить особые временные интервалы в течении которых содержание определенной странички остается постоянным.

    Другой подход, который применяется при более оперативном обновлении данных и одновременной большой посещаемости сервера (в другом случае кэширование эффективным не будет) состоит в использовании специального заголовка Cache-control: max-age=секунды, который определяет время, по истечении которого документ уже считается устаревшим и имеющий гораздо больший приоритет при вычислении свежести конкретного документа.

    Если Вы публикуете новости с интервалом в 60 минут:

    Header("Cache-control: public"); header("Cache-control: max-age=3600");

    Реализация кэширования на PHP

    Вам необходимо создать два специальных файла. Первый будет делать вывод кэша, а второй - создавать кэш.

    Начнем с первого файла, который назовем read_cache.php .



    Загрузка...