Недавно в Rails была запущена функция Streaming . Несмотря на то, что потоковая передача не является новой концепцией, встроить ее в среду, позволяющую разработчикам легко создавать быстро реагирующие приложения, — это, безусловно, огромный шаг. Как разработчику PHP, мне было интересно, можем ли мы иметь что-то похожее на нашем любимом языке? Ответ — да, мы можем легко создать потоковую передачу в приложениях PHP без особых усилий, но для того, чтобы сделать это правильно, мы должны ознакомиться с некоторыми основными концепциями.
В этой статье мы увидим, что такое потоковая передача, что такое output_buffering и как получить желаемый результат в различных конфигурациях веб-серверов (Apache, Nginx) и PHP (CGI, mod_php, FastCGI).
Потоковая и выходная буферизация
Потоковый
Потоковая передача в Интернете относится к механизму отправки ответов по частям, а не их отправки сразу. В традиционном цикле HTTP запрос / ответ ответ не передается в браузер, пока он не будет полностью подготовлен, что заставляет пользователей ждать. Это ограничение является остатком слабого программного и аппаратного обеспечения прошлого, но с тех пор все сильно изменилось. Сетевые системы и браузеры теперь достаточно мощные, чтобы быстро и эффективно обрабатывать передачу контента. Представьте себе, что вы пытаетесь посмотреть видео и не можете смотреть его, пока оно полностью не загрузится в буфер плеера. С помощью потоковой передачи мы можем достаточно эффективно смотреть видео или слушать нашу любимую музыку, поскольку контент загружается мгновенно, а остальная часть данных загружается за кулисами.
Потоковая передача кажется идеальной, чтобы дать пользователям представление о том, что веб-приложение загружается быстро. Однако потоковая передача ответов HTTP сильно отличается от потоковой передачи мультимедийного содержимого. Потоковая передача HTTP-ответов просто означает отправку ответа в виде фрагментов фиксированного или переменного размера в браузер, пока веб-сервер готовит остаток. Например, вы хотите отобразить список голливудских фильмов на одной странице. Если вы сначала подготовите полный ответ, а затем отправите его в браузер, конечный пользователь обязательно почувствует задержку. Но если вы отправите 100 фильмов одним блоком и отобразите их в браузере во время подготовки HTML-кода для следующих 100 фильмов, контент будет казаться загружающимся довольно быстро.
HTTP-ответы состоят не только из визуализируемых элементов — существует множество других вещей, таких как код состояния ответа, HTTP-заголовки, файлы cookie и т. Д., Которые являются важными частями ответа, но остаются незамеченными конечными пользователями, поскольку они никогда не отображаются , Без них наш контент вообще не имеет смысла. Вместо того, чтобы позволить пользователям ждать, более крупные сайты отправляют неперерисовываемую информацию в браузер, и как только браузер начинает ее получать, он начинает вращать индикатор загрузки, который вы почти наверняка видели при медленных соединениях. Файлы стилей и файлы JavaScript остаются неизменными в течение большей части времени, поэтому многие сайты также отправляют их вместе с фрагментами содержимого, которые не подлежат восстановлению, так что браузер начинает извлекать и выполнять их, пока готовится остальная часть ответа. Это довольно мощная техника для создания иллюзии скорости. Когда сгенерировано содержимое тега body
, оно отправляется в браузер, и этот контент может быть снова отправлен кусками, что еще больше распространяет иллюзию скорости.
Буферизация вывода
Буферизация вывода — это механизм, в котором вместо немедленной отправки ответа в браузер мы буферизируем его где-то, чтобы мы могли отправить его сразу, когда весь контент будет готов. Это стандартный способ, которым PHP отправляет ответы в браузеры. Мы все знаем, что для отправки ответа в браузер мы должны использовать такие выражения, как echo
или print
. Каждый раз, когда мы используем echo
мы в основном говорим PHP отправлять ответ браузеру. Но поскольку в PHP включена буферизация вывода по умолчанию, этот контент буферизуется, а не отправляется клиенту. output_buffering
вывода настраивается через output_buffering
в php.ini
и вы можете увидеть его текущее значение конфигурации, запустив phpinfo()
. Документация PHP содержит следующую информацию о буферизации вывода:
Вы можете включить буферизацию вывода для всех файлов, установив для этой директивы значение «Вкл». Если вы хотите ограничить размер буфера определенным размером — вы можете использовать максимальное количество байтов вместо «Вкл» в качестве значения для этой директивы (например, output_buffering = 4096). Начиная с PHP 4.3.5, эта директива всегда отключена в PHP-CLI.
Согласно приведенной выше информации, мы можем легко увидеть, что размер буфера PHP по умолчанию в большинстве конфигураций составляет 4096 байт (4 КБ), что означает, что PHP-буферы могут хранить данные до 4 КБ. Как только этот предел превышен или выполнение кода PHP завершено, буферизованное содержимое автоматически отправляется на любой используемый серверный конец PHP (CGI, mod_php, FastCGI). Выходная буферизация always Off
в PHP-CLI. Мы скоро увидим, что это значит.
Стоит также отметить, что даже на веб-серверах может быть включена буферизация, что означает, что данные, извлекаемые из серверной части PHP, могут буферизироваться веб-серверами и могут переопределять настройки PHP. Теперь, когда у нас есть достаточно четкое представление о потоковой и выходной буферизации, давайте рассмотрим небольшой пример.
Простой пример
Создайте файл output.php
в корневом каталоге вашего веб-сервера, чтобы он был доступен из браузера, например, http://localhost/output.php
. Поместите следующий код в output.php
.
<?php echo "Hello "; sleep(5); echo "World!"; ?>
Идите вперед и получите доступ к этому файлу в вашем браузере. Вы не увидите никакого контента, пока не пройдет пять секунд, после чего появится целая фраза «Hello World!». Это из-за буферизации вывода. Вместо отправки ответа в браузер при выполнении первого echo
его содержимое буферизуется. Поскольку буферизованный контент отправляется в браузер, если буферы заполнены или выполнение кода заканчивается, и с тех пор Hello World!
недостаточно, чтобы занимать более 4 КБ размера буфера, содержимое отправляется после завершения выполнения кода.
Теперь запустите тот же пример, но на этот раз из консоли (командной строки) с помощью следующего оператора:
php /path/to/directory/output.php
Как только вы нажмете Enter, вы увидите слово Hello
, а через пять секунд слово World!
появится тоже. Это то, что означает «всегда выключен в PHP-CLI». Поскольку выходная буферизация отключена, ответ отправляется, как только выполняется каждое echo
.
Теперь давайте рассмотрим буферизацию вывода более подробно.
Примеры
В последнем примере мы видели, что из-за буферизации вывода в PHP мы не получаем ответ, пока не завершится выполнение PHP. Это нежелательно, так как мы хотим отправить некоторое содержимое в браузер, пока мы готовим другие ответы. Как мы знаем, размер выходного буфера в PHP по умолчанию равен 4 КБ, поэтому, если мы хотим отправить ответ клиенту, мы должны сгенерировать ответ в виде фрагментов, и каждый блок должен иметь размер 4 КБ. Давайте посмотрим на пример:
Пример фрагмента 8 КБ
<?php $multiplier = 1; $size = 1024 * $multiplier; for($i = 1; $i <= $size; $i++) { echo "."; } sleep(5); echo "Hello World"; ?>
Сохраните приведенный выше код в файле в корневом каталоге вашего веб-сервера. Если вы запустите этот пример, вы увидите, что индикатор загрузки вашего браузера не показывал, что данные принимаются в течение пяти секунд. Теперь давайте изменим $multiplier
с 1
на 8
и обновим. Если конкретные параметры не настроены, вы заметите, что браузер сообщает нам, что он начал получать некоторые данные практически сразу. Отсутствие необходимости ждать пять секунд, чтобы понять, что страница начала загружаться, очень удобно для пользователя.
Вы можете быть удивлены, почему мы установили $multiplier
от 1
до 8
. Причина этого связана с буферами веб-сервера. Как мы уже говорили выше, на первом уровне находится буферизация PHP, которую мы можем проверить с помощью output_buffering
PHP output_buffering
. Затем может быть буферизация PHP-сервера (CGI, mod_php, FastCGI), а в конце — буферизация веб-сервера. Обычно содержимое буфера Nginx и Apache составляет до 4KB
8KB
или 8KB
зависимости от используемой операционной системы. Обычно в 64-разрядных операционных системах ограничение составляет 8KB
а в 32-разрядных операционных системах — 4KB
.
Последовательность приведенного выше кода выглядит следующим образом, предполагая, что output_buffering
в PHP имеет значение 4KB
: в цикле, когда в буфере PHP хранятся данные до 4KB
из-за оператора echo
, PHP автоматически отправляет эти данные на свой сервер ( CGI, mod_php, FastCGI). mod_php
не буферизует данные и отправляет их прямо в Apache. CGI
и FastCGI
обычно буферизуют данные до 4KB
по умолчанию (в зависимости от конфигурации), поэтому, когда они их получают, их буферы тоже заполняются, поэтому данные мгновенно отправляются на веб-сервер. Веб-сервер по очереди также буферизует данные, до 4KB
8KB
или 8KB
зависимости от операционной системы. Поскольку я использую 64-битную операционную систему, ограничение буферизации на моей стороне составляет 8KB
. Сервер получает данные размером 4KB
но его размер буфера составляет 8KB
поэтому это не приведет к переполнению буфера, и выходные данные не отправляются в браузер. Когда другой цикл 4KB
подготавливается циклом PHP, вышеупомянутая процедура повторяется, но на этот раз из-за уже сохраненных 4KB
в буфере сервера последующие 4KB
приведут к переполнению буфера, что приведет к его очистке и отправке в браузер.
Теперь идите вперед и разместите после кода $size = 1024 * $multiplier;
:
$size -= 1;
Обновите, и вы увидите, что на этот раз браузер не показывает, что начал получать контент, пока не прошло пять секунд. Это связано с тем, что мы повторяем цикл до 8KB - 1
раз, что не приведет к переполнению буфера, и данные не будут отправлены в браузер в течение пяти секунд. Теперь поместите следующий код перед sleep(5);
:
echo ".";
Обновите ваш браузер еще раз, и вы увидите, что на этот раз браузер показывает, что он начал получать контент без пятисекундной задержки. Мы повторяем цикл до 8KB - 1
раз, что означает, что первые 4096 байтов были переданы на сервер и помещены в буфер. Когда цикл завершится, в буферах PHP будет 4095 байт, но echo ".";
после того, как цикл помогает нам заполнить буфер 4096 байтами, в результате чего содержимое буфера отправляется на верхние уровни и, следовательно, в браузер.
Есть одна оговорка, о которой вы должны знать. output_buffering
PHP output_buffering
имеет два возможных значения. Один из них указывает, включен ли он, а второй — указывает максимальный размер буфера. Если для output_buffering
установлено значение 1
, возможно, вы не сможете видеть, как ваш контент или индикатор загрузки браузера вращаются, пока не завершится выполнение кода PHP. Это потому, что если output_buffering
1
означает, что мы его включили, но не указали максимальный размер, поэтому в этом случае PHP-буферы могут хранить данные вплоть до числа в параметре memory_limit
.
об_флеш и флеш
Теперь мы знакомы с концепцией буферизации и потоковой передачи в PHP, а также знаем, как отправлять ответы в виде блоков в браузер. Однако вам может быть интересно, есть ли лучший способ отправки контента по частям. Просто невозможно сгенерировать порции по 8KB
чтобы просто отправить данные клиенту заранее, потому что на обычных веб-страницах не так много контента, а 8KB
— это, конечно, приличный объем данных, который нужно отправить в виде блоков. Также не выгодно отправлять бесполезные данные, поскольку это только увеличит задержку. Оказывается, есть некоторые встроенные методы, которые мы можем использовать для решения этой проблемы.
ob_flush()
и flush
— это встроенные методы PHP, которые используются для отправки данных на верхние уровни. Буферизованные данные не отправляются на верхние уровни, если буферы не заполнены или выполнение кода PHP не завершено. Для отправки данных, даже если буферы не заполнены и выполнение кода PHP не завершено, мы можем использовать ob_flush
и flush
.
Теперь давайте посмотрим на пример:
<?php $multiplier = 1; $size = 1024 * $multiplier; for($i = 1; $i <= $size; $i++) { echo "."; } sleep(5); echo "Hello World"; ?>
В приведенном выше примере разместите следующие строки перед sleep(5);
ob_flush(); flush();
Сохраните файл и получите доступ к нему в браузере. Как только вы попросите браузер загрузить веб-страницу, вы увидите, что браузер показывает, что он начал получать контент. Это именно то, что мы хотим, потому что нам не нужно беспокоиться о генерации контента в 8KB
блоках, и мы можем легко транслировать контент в браузер, не дожидаясь генерации всего контента. Вы можете попробовать разные множители, чтобы получить более четкое представление об этих концепциях.
Есть, однако, некоторые предостережения, о которых вы должны знать. Приведенный выше код будет хорошо работать в Apache с mod_php
. Это даже будет работать без цикла for
. Как только ob_flush()
и flush()
будут выполнены, браузер начнет показывать, что приходит какой-то контент. Однако ob_flush()
и flush()
могут не работать с Nginx «из коробки» из-за того, как Nginx обрабатывает запросы. Чтобы ob_flush
и flush
работали без проблем в Nginx, вы можете использовать следующую конфигурацию:
fastcgi_buffer_size 1k; fastcgi_buffers 128 1k; # up to 1k + 128 * 1k fastcgi_max_temp_file_size 0; gzip off;
Вы можете узнать больше об этом в этом посте .
Потоковое с Ajax
Теперь, когда мы увидели, как отправлять содержимое порциями в стандартном цикле HTTP-запросов / ответов, давайте посмотрим, как сделать то же самое для Ajax-запросов. Ajax-запросы — это хороший и элегантный способ получения данных без перезагрузки полной страницы. Мы связываем обратный вызов с запросом Ajax, и этот обратный вызов выполняется после получения всего содержимого. Это означает, что мы не можем передавать содержимое в запросах Ajax. К счастью, у нас есть XMLHTTPRequest 2
, который является следующей версией Ajax API и поддерживается в последних браузерах. Эта новая версия имеет много интересных функций, таких как запросы из разных источников, загрузка событий и поддержка загрузки / загрузки двоичных данных. События прогресса используются для того, чтобы сообщить пользователю, сколько данных мы загрузили, и мы также можем получить загруженные данные порциями. Давайте посмотрим на пример:
Создайте файл HTML со следующим кодом:
<html> <head> <title>Ajax Streaming Test</title> </head> <body> <center><a href="#" id="test">Test Ajax Streaming</a></center> <script type="text/javascript"> document.getElementById('test').onclick = function() { xhr = new XMLHttpRequest(); xhr.open("GET", "response.php", true); xhr.onprogress = function(e) { alert(e.currentTarget.responseText); } xhr.onreadystatechange = function() { if (xhr.readyState == 4) { console.log("Complete = " + xhr.responseText); } } xhr.send(); }; </script> </body> </html>
Теперь загрузите этот файл в браузер и нажмите на ссылку. Ajax-запрос инициируется для получения данных из response.php
и мы прослушиваем событие onprogress
. Всякий раз, когда приходит новый кусок, мы выводим его в виде предупреждения.
Теперь поместите следующий код в response.php
и сохраните его в той же папке, относительно вышеуказанного HTML-файла.
<?php for ($i = 1; $i <= 10; $i++): ?> <?php sleep(1); ?> Count = <?php echo "$i\n"; ?> <?php ob_flush(); flush(); ?> <?php endfor; ?>
Как видите, мы запускаем цикл десять раз, делая паузу на одну секунду при каждом запуске, а затем выводим некоторый контент. Этот контент отправляется на верхние слои со сбросами. Теперь идите вперед и нажмите на Test Ajax Streaming
. Если все пойдет хорошо, вы заметите, что Count = 1
отображается в предупреждении. Когда вы отклоните предупреждение, вы увидите другое предупреждение с Count = 1 \n Count = 2
. Когда вы отклоните это, вы увидите Count = 1 \n Count = 2 \n Count = 3
в другом предупреждении и т. Д. До 10. Когда весь запрос Ajax будет успешно выполнен, вы увидите полный вывод в приставка. Мы только что реализовали потоковую передачу в Ajax-запросах, и мы можем легко обновить наш интерфейс соответствующим образом, предоставляя конечным пользователям выдающийся опыт.
Примечание: есть некоторые несовместимости браузера. Протестируйте приведенный выше код в Chrome и Firefox. Firefox будет вести себя точно так же, как продемонстрировано с точки зрения вывода, но Chrome сначала отобразит пустое предупреждение, а затем продолжит, как и ожидалось. Помните об этом крайнем случае при реализации потоковой передачи!
Вывод
Потоковая передача — это потрясающий способ отправки контента пользователю для имитации скорости. Но, как и все, потоковое вещание — не серебряная пуля, и у него есть свои недостатки. Ниже приведены некоторые ситуации, когда потоковая передача не является идеальным решением:
-
Обработка исключений. Код состояния необходим браузерам для определения успеха / неудачи каждого запроса. Поскольку мы заранее отправили код состояния вместе с заголовками, файлами cookie и т. Д., Если в какой-то момент произойдет исключение на стороне сервера, сервер не сможет передать его в браузер, поскольку код состояния уже отправлен.
-
Отправка небольших порций данных неэффективна, поскольку сети предпочитают небольшое количество порций с большими откликами вместо большого числа порций с меньшими откликами . Чтобы отправлять контент порциями, мы должны тщательно выбирать размер порций.
Я надеюсь, что эта статья помогла вам понять основы потоковой передачи и буферов. Пожалуйста, дайте нам знать о других экспериментах, которые вы придумали после прочтения этого, если таковые имеются, и, конечно, оставьте свой отзыв в комментариях ниже. Хотите увидеть больше примеров? Больше объяснений? Дайте нам знать!