Мне было необходимо сделать показ интерактивного выполнения работы скрипта пользователю. Я реализовал многопоточного PHP-бота, выполняющего фоновую задачу получая запросы на выполнение. Результаты своей деятельности он записывает в базу. Дальше мне нужно было каким-то образом информировать пользователя о процессе выполнения.
Обычно в данном случае делают к примеру долгий запрос, по окончанию которого с сервера идет ответ о текущем состоянии, после чего идет снова повтор, либо проверять состояние каждые пару секунд. Но мне хотелось реализовать это в более живом виде, используя одно соединение.
Полученный метод можно использовать для непрерывного получения ответа от сервера и параллельной его обработки без прерывания. То есть можно не только использовать его для получения состояния готовности процесса, но и также можно например обрабатывать огромные данные в процессе их получения, не дожидаясь их полной загрузки.
Метод заключается в том, чтобы заставить PHP-скрипт отдавать данные клиенту заранее заданными порциями. И в приеме этих порций на стороне клиента.
К сожалению, многие браузеры не умеют правильно обрабатывать состояние передачи данных в AJAX, поэтому сделаем условную синхронизацию данных в секундах. Будем через каждые 2 секунды сбрасывать данные с сервера, и каждые 2 секунды проверять приход новых данных на стороне клиента. В данном случае часто происходят нестыковки, какие то данные пришли уже более новые чем нужны, либо новых данных еще нет. Я буду учитывать лишь первый случай, т.к. предполагается что ваш пинг достаточно высок, чтобы обеспечить приход данных в избытке.
Для этого нам надо запретить все методы сжатия, сообщить браузеру, что мы передаем поток, и заставить сбрасывать все выводимые данные пользователю.
Также одним из самых данных пунктов — необходимо закрыть запись открытой сессии. Дело в том, что PHP не может открыть одну и ту же сессию в двух параллельных потоках, поэтому в случае если ваш процесс будет работать, а пользовать сделает параллельный запрос — он будет висеть в очереди. В итоге все функции AJAX у вас будут парализированы.
Выше я использую разделитель |, чтобы выводить только свежие данные и разделить переданные данные на логические части.
Заметьте — мы забиваем лишние 4096 байт, чтобы заставить отправить пакет пользователю. Иначе веб-сервер будет кэшировать данные, пока они не соберутся в необходимый для отправки пакет. Возможно, существует метод отправлять и без этого, буду готов выслушать решения в комментариях.
Скрипт выше будет каждые 2 секунды передавать числа от 0 до 19. Браузер будет выводить только наиболее свежие данные.
Вызов функции live() сначала прервет предыдущий «живой» канал, а потом инициализирует новое соединение с сервером. Мы будем записывать старые данные, чтобы знать точку отсчета для новых данных. Также используя разделитель | мы будем отделать только самые свежие данные из полученных. Вообще, можно просто отделять только самые свежие данные, однако вы можете обработать все новые данные, если у вас к примеру генерируется онлайн-лог.
Ниже предусмотрена функция для прерывания соедиенения, её можно встроить в ваши AJAX скрипты, чтобы при переходе на другую страницу у вас закрывалось соединение.
Обычно в данном случае делают к примеру долгий запрос, по окончанию которого с сервера идет ответ о текущем состоянии, после чего идет снова повтор, либо проверять состояние каждые пару секунд. Но мне хотелось реализовать это в более живом виде, используя одно соединение.
Полученный метод можно использовать для непрерывного получения ответа от сервера и параллельной его обработки без прерывания. То есть можно не только использовать его для получения состояния готовности процесса, но и также можно например обрабатывать огромные данные в процессе их получения, не дожидаясь их полной загрузки.
Как будем действовать?
Метод заключается в том, чтобы заставить PHP-скрипт отдавать данные клиенту заранее заданными порциями. И в приеме этих порций на стороне клиента.
К сожалению, многие браузеры не умеют правильно обрабатывать состояние передачи данных в AJAX, поэтому сделаем условную синхронизацию данных в секундах. Будем через каждые 2 секунды сбрасывать данные с сервера, и каждые 2 секунды проверять приход новых данных на стороне клиента. В данном случае часто происходят нестыковки, какие то данные пришли уже более новые чем нужны, либо новых данных еще нет. Я буду учитывать лишь первый случай, т.к. предполагается что ваш пинг достаточно высок, чтобы обеспечить приход данных в избытке.
Заставляем сервер отдавать данные
Для этого нам надо запретить все методы сжатия, сообщить браузеру, что мы передаем поток, и заставить сбрасывать все выводимые данные пользователю.
Также одним из самых данных пунктов — необходимо закрыть запись открытой сессии. Дело в том, что PHP не может открыть одну и ту же сессию в двух параллельных потоках, поэтому в случае если ваш процесс будет работать, а пользовать сделает параллельный запрос — он будет висеть в очереди. В итоге все функции AJAX у вас будут парализированы.
header('Content-Encoding: none;'); //вырубаем сжатие
header('Content-type: application/octet-stream');
// выключаем буферизацию
ini_set('output_buffering', 'off');
// выключаем сжатие со стороны php
ini_set('zlib.output_compression', false);
// включаем сброс данных в браузер после каждого вывода
ini_set('implicit_flush', true);
ob_implicit_flush(true);
// закрываем сессию на запись, после этой команды нельзя будет записывать данные в сессию (!)
session_write_close();
for ($i = 0;$i < 20; $i++) {
echo '|'.$i.str_repeat(' ', 4096).PHP_EOL;
flush();
sleep(2);
}
Выше я использую разделитель |, чтобы выводить только свежие данные и разделить переданные данные на логические части.
Заметьте — мы забиваем лишние 4096 байт, чтобы заставить отправить пакет пользователю. Иначе веб-сервер будет кэшировать данные, пока они не соберутся в необходимый для отправки пакет. Возможно, существует метод отправлять и без этого, буду готов выслушать решения в комментариях.
Скрипт выше будет каждые 2 секунды передавать числа от 0 до 19. Браузер будет выводить только наиболее свежие данные.
Клиентская часть
var xhr;
var timerlive;
function live(cont){
try{
xhr.abort();
}catch(e) {
}
var temp;
clearTimeout(timerlive);
var prevtext = '';
xhr = new XMLHttpRequest();
xhr.open('POST', 'ajax.php', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send("type=live");
timerlive = setInterval(function() {
if (xhr.readyState == 4) {
clearTimeout(timerlive);
}
temp=xhr.responseText.substring(prevtext.length);
$('#'+cont).html(temp.substr(temp.lastIndexOf("|")+1));
prevtext = xhr.responseText;
}, 2000);
}
function breaklive(){
try{
xhr.abort();
}catch(e) {
}
clearTimeout(timerlive);
}
Вызов функции live() сначала прервет предыдущий «живой» канал, а потом инициализирует новое соединение с сервером. Мы будем записывать старые данные, чтобы знать точку отсчета для новых данных. Также используя разделитель | мы будем отделать только самые свежие данные из полученных. Вообще, можно просто отделять только самые свежие данные, однако вы можете обработать все новые данные, если у вас к примеру генерируется онлайн-лог.
Ниже предусмотрена функция для прерывания соедиенения, её можно встроить в ваши AJAX скрипты, чтобы при переходе на другую страницу у вас закрывалось соединение.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Как вы уведомляете пользователя о состоянии выполнения задачи?
7.41% Использую метод схожий с методом из статьи4
48.15% Проверяю через периоды времени состояние, совершая отдельные запросы26
33.33% Уведомляю пользователя лишь о завершении процесса используя Long-Pooling18
27.78% Вообще не уведомляю пользователя, надо будет, сам обновит страничку и увидит.15
Проголосовали 54 пользователя. Воздержались 49 пользователей.