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

    Мне было необходимо сделать показ интерактивного выполнения работы скрипта пользователю. Я реализовал многопоточного 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.4%Использую метод схожий с методом из статьи4
    • 48.2%Проверяю через периоды времени состояние, совершая отдельные запросы26
    • 33.3%Уведомляю пользователя лишь о завершении процесса используя Long-Pooling18
    • 27.8%Вообще не уведомляю пользователя, надо будет, сам обновит страничку и увидит.15

    Similar posts

    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 3

      0
      Способ с виду рабочий, но у него слишком много недостатков.

      Во-первых, держит соединение с сервером. Из-за этого на сервере может быстро исчерпаться пул рабочих потоков. Даже если каждые 2 секунды слать новый ajax-запрос («как там задача?») и получать ответ («пока пыхтит, сделано 30%)», серверу будет легче.

      Во-вторых, каждые 2 секунды шлет 4 килобайта несжатого мусора. Немного, конечно, но вдруг у пользователя трафик тарифицируемый? Опять же, короткий запрос-ответ в этом смысле лучше. А еще лучше посчитать по паре ответов примерный ETA и выводить клиенту прогресс-бар, вообще не обращаясь к серверу. Только когда задача, по нашему мнению, досчиталась, тогда и послать запрос «чо как?».

      В-третьих, долгоиграющие вещи лучше исполнять на сервере специальными задачами, запускаемыми в фоновом режиме, а не скриптами на PHP. Сервер может быть настроен на автоматическое убиение скриптов, исполняющихся слишком долго, так что пользователь рискует вообще никогда не дождаться результатов.
        0
        1. Сделано как вариант. Этот метод можно использовать еще и для обработки во время принятия. Например если у нас на JS построена какая нибудь игра или какие нибудь данные, которыми надо оперировать на клиенте, то их можно начать обрабатывать при приеме — лишь каждые 1-2 секунды проверять, что еще пришло.
        2. Да про мусор это минус. К слову мне кажется должны быть методы уменьшения этого порога в 4кб. Размер был установлен опытным путем, возможно при другом конфиге или веб сервере можно будет и меньше передавать.
        3. Так в моем случае у меня пользователь сначала делает запрос, потом демон обрабатывает этот запрос, а параллельно с демоном пользователь может смотреть через бд процесс выполнения.
        К слову вообще по сути это также удобно для каких нибудь чатов. Ведь по сути все равно long-pooling используется. Так ведь можно сделать вот так:
        1. Есть соединение через которое пользователь получает сообщения, JS каждую секунду проверяет пришли ли новые данные.
        2. Отправляются сообщения через паралелльный запрос AJAX.
        По сути эту статью можно брать как дополнение к Long-pooling. Например можно делать запросы с таймаутом по 1-2 минуты, но при этом коннект не прервется сразу при первой передаче. а можно будет просто делать соединение на 2 минуты, и за эти 2 минуты сделать несколько передач, хоть 10, хоть 20, да возможно она все равно одна будет.

        К слову лично перед написанием статьи, когда я собственно для задачи все это нашел и собрал вместе для меня стало открытием:
        1. То что PHP блокирует сессии, т.е. если есть какие то запросы больше 10-20 секунд, там лучше пытаться закрывать запись, иначе у пользователя это может растянуться в еще больше времени, а перейти он никуда не сможет. Что самое забавное в этой ситуации, я получал интересные казусы связанные с этим. Есть вкладка, где у нас выполняется задача, ну на 10-20 секунд например. Ты пытаешься там тыкнуть в AJAX меню — ничего не работает. Пытаешься открыть сайт в новой вкладке — тоже самое. Причем если в скрипте записано, чтобы он не заканчивал работу после сброса соединение, а скрипт зависнет из за какой либо ошибки, то пользователь вообще не сможет зайти на сайт, просто потому что его сессия будет заблокирована.
        2. Я вообще не думал, что можно в процессе передачи данных дергать уже полученные данные до окончания работы.

        Не буду спорить с тем, что способ специфичен, и мало где принимим, однако его составные части вполне могут послушить основой для последующих разработок.
          0
          Судя по всему вторая проблема должна решаться так:
          header('X-Accel-Buffering: no');
          

          Ну либо если не сработает в nginx
          proxy_buffering off;
          

          Но это если уж совсем радикально, и скорее всего сильно скажется на всем остальном. Но вроде как тот заголовок должен работать начиная с nginx 1.5.6

        Only users with full accounts can post comments. Log in, please.