- Streaming HTTP response in PHP — turn long-running process into realtime UI
- I was working on something
- Concept Overview
- Streaming
- Output Buffering
- A simple experiment
- Let’s try some streaming
- Explanation
- How to get and process the response in javascript
- Что такое потоки
- Схема формирования потока
- Поддерживаемые схемы
- Потоки PHP
- php://stdin, php://stdout, php://stderr
- php://input
- php://output
- Дополнительные обертки
- Модуль CLI SAPI
Streaming HTTP response in PHP — turn long-running process into realtime UI
For a “ not too long ” single server-side request what if you could let the user know how the request is handled at the server’s end, in real-time without them waiting for the entire process being finished !
without any package or WebSocket or anything “ on trend ”
I was working on something
Recently I was working on an multi-vendor large application where the requirement was to make a multi currency payment processing to facilitate transaction from different source to target currency.
The entire flow to make a transaction with parameters like source & target currency, amount, recipient etc is quite lengthy. It consists of different stages like quote creation, recipient handling, transfer creation followed by funding. There are total 4 API requests that are dependent on each other’s response so the entire process takes ⏱ approx. 10 seconds at the server’s end.
I couldn’t bear just a dumb loader holding my attention for that long 🙄, so I started thinking if I could go for Laravel Echo or anything equivalent that uses WebSocket which could help me out pushing events at different stages of processing, then I could inform the user accordingly and it feels more responsive.
Suddenly this idea of streaming came to the mind. Though this was not the first time I used this technique for such kind of situation, it was last year when I was working on an E-commerce application where the app needed a functionality to sync. it’s products database via API.
Concept Overview
Streaming
Streaming is not a new concept, it is a data transfer technique which allows a web server to continuously send data to a client over a single HTTP connection that remains open indefinitely. In streaming response comes in chunk rather than sending them at once. In the traditional HTTP request / response cycle, a response is not transferred to the browser until it is fully prepared which makes users wait.
Output Buffering
Output buffering allows to have output of PHP stored into an memory (i.e. buffer) instead of immediately transmitted, it is a mechanism in which instead of sending a response immediately we buffer it in memory so that we can send it at once when whole content is ready.
Each time using echo we are basically telling PHP to send a response to the browser, but since PHP has output buffering enabled by default that content gets buffered and not sent to the client.
A simple experiment
echo "Hi"; sleep(2); echo "There !";
running this code will execute Hi There ! together after a wait of 2 seconds in total. This is because of output buffering which is on by default in PHP.
instead of sending the response to the browser when the first echo is executed, its contents are buffered.
💡 Since buffered content is sent to the browser if either the buffers get full or code execution ends we’ll get the two echoed response merged & responded by server all together at once.
Since Hi There ! is not enough to occupy more than 4KB (default size of output buffer in PHP) of buffer size, the content is sent when code execution ends.
Let’s try some streaming
Route::get('/mock', function() < // defining a route in Laravel set_time_limit(0); // making maximum execution time unlimited ob_implicit_flush(1); // Send content immediately to the browser on every statement which produces output ob_end_flush(); // deletes the topmost output buffer and outputs all of its contents sleep(1); echo json_encode(['data' => 'test 1']); sleep(2); echo json_encode(['data' => 'test 2']); sleep(1); echo json_encode(['data' => 'test 3']); die(1); >);
Run the example in browser you’ll see response in parts one after another according to the sleep we sprinkled which in real world would be our time consuming data processing like API call or multiple heavy sql execution etc.
Explanation
Output buffers catch output given by the program. Each new output buffer is placed on the top of a stack of output buffers, and any output it provides will be caught by the buffer below it. The output control functions handle only the topmost buffer, so the topmost buffer must be removed in order to control the buffers below it.
✔ The ob_implicit_flush(1) enables implicit flushing which sends output directly to the browser as soon as it is produced.
✔ If you need more fine grained control then use flush() function. To send data even when buffers are not full and PHP code execution is not finished we can use ob_flush and flush . The flush() function requests the server to send it’s currently buffered output to the browser
How to get and process the response in javascript
I found a way to do so with traditional xhr ( XMLHTTPRequest ) request
function testXHR() let lastResponseLength = false; xhr = new XMLHttpRequest(); xhr.open("GET", "/mock", true); xhr.setRequestHeader("Content-Type", "application/json"); xhr.setRequestHeader("Accept", "application/json"); xhr.setRequestHeader('X-CSRF-Token', document.querySelector('meta[name="csrf-token"]').content); xhr.onprogress = function(e) let progressResponse; let response = e.currentTarget.response; progressResponse = lastResponseLength ? response.substring(lastResponseLength) : response; lastResponseLength = response.length; let parsedResponse = JSON.parse(progressResponse); console.log(parsedResponse); if(Object.prototype.hasOwnProperty.call(parsedResponse, 'success')) // handle process success > > xhr.onreadystatechange = function() if (xhr.readyState == 4 && this.status == 200) console.log("Complete = " + xhr.responseText); > > xhr.send(); >;
xhr.onprogress : is the function called periodically with information until the XMLHttpRequest completely finishes
Что такое потоки
В программировании постоянно приходиться работать с различными ресурсами: файлами , сокетами , http-соединениями . У всех есть некий интерфейс доступа, часто несовместимый друг с другом, чтобы устранить данные несоответствия и унифицировать работу с различными источниками данных, начиная с PHP 4.3 были придуманы PHP Streams потоки .
Поток, это передача данных между местами. В данном контексте, местом может быть — файл, ZIP-архив, соединение и даже процесс через командную строку.
Вы должны понимать разницу между открытием файла и открытием веб-страницы, у них обоих есть контент, но это два совершенно разных типа потоковых данных, поэтому для них требуются разные протоколы. Например, мы можем открыть веб-страницу, подключившись к удаленным веб-серверам с помощью HTTP , HTTPS , SSL . Эти протоколы называются потоковыми оболочками, они предоставляют уникальный интерфейс.
Поток stream , это ресурс resource который ведет себя, как источник непрерывной последовательности данных. То есть из потока можно последовательно читать данные, равно как и записывать в него. Также возможно перемещаться в разные позиции внутри потока.
Обертка wrapper , дополнительный код который объясняет потоку особенности работы со специфичными протоколами или кодировками. Например, обертка http знает, как преобразовать URL в HTTP/1.0-запрос для файла на удаленном сервере. Существует множество оберток, как встроенных в PHP изначально, так и дополнительных.
Схема формирования потока
Каждый поток формируется из схемы и цели , в приведенном ниже формате:
Схема это название обертки. Например — file , http , https , ftp , ftps , compress.zlib , compress.bz2 , php . На случай, если название обертки не указать, каждая функция, работающая с потоком, имеет свои умолчания, обычно это file:// .
Цель зависит от того, какая обертка используется. Для потоков связанных с файловой системой это обычно путь и имя файла. Для сетевых потоков это, как правило, имя хоста с добавлением к нему пути.
Поддерживаемые схемы
- file:// доступ к локальной файловой системе
- http:// доступ к URL-адресам по протоколу HTTP(s)
- ftp:// доступ к URL-адресам по протоколу FTP(s)
- php:// доступ к различным потокам ввода-вывода
- zlib:// сжатые потоки
- data:// схема Data (RFC 2397)
- glob:// нахождение путей, соответствующих шаблону
- phar:// PHP архив
- ssh2:// Secure Shell 2
- rar:// RAR
- ogg:// аудио потоки
- expect:// потоки для взаимодействия с процессами
Потоки PHP
PHP предоставляет несколько разнообразных потоков ввода-вывода, которые позволяют получить доступ к собственным потокам ввода-вывода PHP,к дескрипторам стандартного ввода, вывода и потока ошибок, к временным файловым потокам в памяти и на диске, и фильтрам, которые могут манипулировать другими файловыми ресурсами по мере их считывания или записи.
Потоки PHP имеют функции, которые помогают разработчикам управлять различными ресурсами:
php://stdin, php://stdout, php://stderr
php://stdin, php://stdout и php://stderr позволяют получить прямой доступ к соответствующим потокам ввода или вывода процесса PHP. Поток указывает на копию файлового дескриптора, таким образом, если вы откроете php://stdin и потом закроете его, вы закроете только вашу копию дескриптора. Актуальный поток, на который ссылается STDIN остается неизменным. Обратите внимание, что PHP демонстрировал ошибочное поведение в этом отношении до версии PHP 5.2.1. Рекомендуется просто использовать константы STDIN , STDOUT и STDERR вместо ручного открытия потоков, используя эти обертки.
Поток php://stdin предназначен только для чтения, тогда как php://stdout и php://stderr предназначены только для записи.
php://input
php://input является потоком только для чтения, который позволяет вам читать необработанные данные из тела запроса. В случае POST-запросов предпочтительней использовать php://input вместо $HTTP_RAW_POST_DATA , так как этот метод не зависит от специальных php.ini директив. Кроме того, в тех случаях, где $HTTP_RAW_POST_DATA не заполняется по умолчанию, это потенциально менее затратно для памяти, чем активация директивы always_populate_raw_post_data . php://input не доступен с типом содержимого enctype=»multipart/form-data» .
До версии PHP 5.6, поток, открытый с php://input может быть прочтен только один раз. Поток не поддерживает операции поиска. Тем не менее, в зависимости от реализации SAPI интерфейса, может быть возможно открыть другой поток php://input и повторить чтение. Это возможно только если тело запроса заранее сохраняется. Это типично для случая с POST -запросом, но не для других методов запросов, таких как PUT или PROPFIND .
php://output
php://output является потоком только для записи, который позволяет вам записать данные в выходной буфер аналогично как это делают функции print и echo .
Дополнительные обертки
Дополнительные обертки можно добавлять либо отдельным скриптом с помощью функции stream_wrapper_register() , либо напрямую из расширения, используя API Working with streams . Добавлять можно произвольное количество оберток, что делает возможности работы с потоками практически безграничными. Посмотреть список зарегистрированных на данный момент оберток можно с помощью функции stream_get_wrappers() .
stream_wrapper_register() регистрирует обёртку URL, реализованную в виде PHP-класса, bool stream_wrapper_register ( string $protocol , string $classname [, int $flags = 0 ] ) .
Позволяет реализовать собственные обработчики протоколов и потоков для использования со всеми другими функциями файловой системы, такими как fopen() , fread() .
stream_wrapper_register("var", "VariableStream"); $myvar = ""; $fp = fopen("var://myvar", "r+"); fwrite($fp, "line1\n"); fwrite($fp, "line2\n"); fwrite($fp, "line3\n"); rewind($fp); while (!feof($fp)) < echo fgets($fp); >fclose($fp); var_dump($myvar); if ($existed) < stream_wrapper_restore("var"); >/* line1 line2 line3 string(18) "line1 line2 line3 " */
Модуль CLI SAPI
Модуль CLI SAPI определяет несколько констант для потоков ввода/вывода для упрощения работы с командной строкой.
Константы, специфичные для модуля CLI SAPI :
- STDIN уже открытый поток ввода stdin . Он предотвращает необходимость его открывать через fopen(‘php://stdin’, ‘r’);
- STDOUT уже открытый поток вывода stdout . Он предотвращает необходимость его открывать через $stdout = fopen(‘php://stdout’, ‘w’);
- STDERR уже открытый поток ошибок stderr . Он предотвращает необходимость его открывать через $stderr = fopen(‘php://stderr’, ‘w’);