Pull to refresh

Показываем процесс работы непрерывной задачи на сервере, используя одно соединение

Reading time3 min
Views8.6K
Мне было необходимо сделать показ интерактивного выполнения работы скрипта пользователю. Я реализовал многопоточного PHP-бота, выполняющего фоновую задачу получая запросы на выполнение. Результаты своей деятельности он записывает в базу. Дальше мне нужно было каким-то образом информировать пользователя о процессе выполнения.

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

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

Как будем действовать?


Метод заключается в том, чтобы заставить 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 скрипты, чтобы при переходе на другую страницу у вас закрывалось соединение.
Only registered users can participate in poll. Log in, please.
Как вы уведомляете пользователя о состоянии выполнения задачи?
7.41% Использую метод схожий с методом из статьи4
48.15% Проверяю через периоды времени состояние, совершая отдельные запросы26
33.33% Уведомляю пользователя лишь о завершении процесса используя Long-Pooling18
27.78% Вообще не уведомляю пользователя, надо будет, сам обновит страничку и увидит.15
54 users voted. 49 users abstained.
Tags:
Hubs:
0
Comments3

Articles

Change theme settings