продолжить обработку php после отправки ответа http

Мой сценарий вызывается сервером. С сервера я получу ID_OF_MESSAGE и TEXT_OF_MESSAGE.

В моем скрипте я обрабатываю входящий текст и генерирую ответ с параметрами: ANSWER_TO_ID и RESPONSE_MESSAGE.

Проблема в том, что я отправляю ответ на входящий "ID_OF_MESSAGE", но сервер, который отправляет мне сообщение для обработки, установит его сообщение как доставленное мне (это означает, что я могу отправить ему ответ на этот идентификатор) после получения HTTP-ответа 200.

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

Есть ли какое-то решение, как отправить на сервер HTTP-ответ 200, а затем продолжить выполнение php-скрипта?

Большое спасибо


person user1700214    schedule 07.03.2013    source источник


Ответы (11)


да. Ты можешь сделать это:

ignore_user_abort(true);//not required
set_time_limit(0);

ob_start();
// do initial processing here
echo $response; // send the response
header('Connection: close');
header('Content-Length: '.ob_get_length());
ob_end_flush();
@ob_flush();
flush();
fastcgi_finish_request();//required for PHP-FPM (PHP > 5.3.3)

// now the request is sent to the browser, but the script is still running
// so, you can continue...

end;//a must especially if set_time_limit=0 is used and the task ends
person vcampitelli    schedule 07.03.2013
comment
Можно ли сделать это с помощью соединения keep-alive? - person Congelli501; 09.07.2014
comment
Превосходно!! Это единственный ответ на этот вопрос, который действительно работает !!! 10p + - person Martin_Lakes; 01.08.2014
comment
Отличный ответ! Единственное, что я изменил, это set_time_limit(0);. Вероятно, вы хотите, чтобы он работал дольше 30 секунд по умолчанию, но бесконечно долго может вызвать проблемы, если он войдет в бесконечный цикл! В моем php.ini файле установлено более длинное значение. - person CJ Dennis; 21.08.2014
comment
есть ли причина, по которой вы используете ob_flush после ob_end_flush? Я понимаю необходимость функции flush в конце, но я не уверен, зачем вам нужно ob_flush при вызове ob_end_flush. - person ars265; 20.05.2015
comment
Обратите внимание, что если заголовок кодирования содержимого установлен на любое другое значение, кроме «none», это может сделать этот пример бесполезным, поскольку он все равно позволит пользователю ждать полного времени выполнения (до тайм-аута?). Чтобы быть абсолютно уверенным, что он будет работать локально и в производственной среде, установите для заголовка content-encoding значение none: header("Content-Encoding: none") - person Brian; 25.06.2015
comment
Совет: я начал использовать PHP-FPM, поэтому мне пришлось добавить fastcgi_finish_request() в конце - person vcampitelli; 16.09.2015
comment
RE: Keep-alive соединение: вам не обязательно закрывать соединение, но тогда произойдет то, что следующий актив, запрошенный в том же соединении, будет вынужден ждать. Таким образом, вы можете доставить HTML быстро, но тогда один из ваших файлов JS или CSS может загружаться медленно, так как соединение должно завершить получение ответа от PHP, прежде чем оно сможет получить следующий ресурс. По этой причине закрытие соединения - хорошая идея, чтобы браузеру не приходилось ждать, пока оно освободится. - person Nate Lampton; 17.09.2015
comment
'ob_end_flush (); ob_flush (); ' выдает предупреждение, что промывать нечего. Документация PHP о 'ob_flush ();' состояния: Эта функция не уничтожает выходной буфер, как это делает ob_end_flush (). - person Delcon; 15.08.2016
comment
зачем нужны оба ob_end_flush и ob_flush. Это вызывает у меня проблемы, когда тот же клиент вызывает другой API. Во втором api он работает только в том случае, если я добавляю ob_start перед инициализацией моей инициализации переменной, которая отображается эхом - person FBP; 21.03.2018
comment
После ob_end_flush(); ob_flush(); больше не нужен. Фактически, это вызовет ошибку. Достаточно всего ob_end_flush();. - person lhermann; 18.05.2018
comment
Отличный ответ. Работает. - person David Okwii; 14.06.2018
comment
Это было так полезно для меня! Отлично работал с моей интеграцией команд Slack Bot, где я могу немедленно отправить ответ, чтобы сообщить им, что он был получен, а затем начать работу над вызовом других API и анализом данных. - person Sleepybear; 07.08.2018
comment
Этот код может вернуть пустой ответ. Я использовал это, и иногда появляется белый экран. Я переехал header('Connection: close'); после flush(), и теперь все в порядке. - person Meysam Valueian; 09.09.2018
comment
Примечание: это не сработает, если в вашем коде есть другие ob_start() вызовы, которые не были завершены с помощью ob_end_flush(). Чтобы гарантировать, что все буферы очищены и закончены, вы можете сделать: while (ob_get_level() > 0) { ob_end_flush(); }. - person Kevin Borders; 13.10.2018
comment
Это начало работать для меня только тогда, когда размер $response стал достаточно большим, поэтому я изменил echo $response; на echo str_pad($response, 4096);, чтобы установить минимальный размер. (Справочная информация: мне нужно вернуть HTTP 200 в веб-службу в течение 3 секунд, чтобы подтвердить, что запрос POST получен. Технически ответ не должен содержать никакого контента.) - person marcvangend; 15.04.2020
comment
ob_start() требуется только в том случае, если вы не знаете длину вывода. Если вы это сделаете, вы можете пропустить все вызовы ob_* и просто установить заголовки, распечатать вывод и вызвать flush(). После этого браузер отобразит страницу, и ваш скрипт сможет продолжить выполнение. set_time_limit() требуется только в том случае, если ваш скрипт будет работать дольше, чем max_execution_time в файле php.ini, то есть 30 секунд. по умолчанию. - person Russell G; 07.10.2020
comment
@marcvangend: Спасибо за подсказку о str_pad()! Я добавил вызов ini_get("output_buffering"), чтобы получить размер буфера, если он не установлен на значение по умолчанию 4096. - person Russell G; 07.10.2020
comment
@lhermann - я думаю, это зависит от сервера. Я пробовал каждую комбинацию этих трех вызовов, и единственное, что сработало для меня, - это наличие всех трех. Таким образом, я восстанавливаю исходный ответ, который сломался (для меня) после недавнего редактирования. - person billynoah; 23.07.2021

Я видел здесь много ответов, в которых предлагается использовать ignore_user_abort(true);, но этот код не нужен. Все это гарантирует, что ваш скрипт продолжит выполнение до того, как будет отправлен ответ в случае, если пользователь прервет работу (закрыв свой браузер или нажав escape, чтобы остановить запрос). Но вы спрашиваете не об этом. Вы просите продолжить выполнение ПОСЛЕ отправки ответа. Все, что вам нужно, это следующее:

    // Buffer all upcoming output...
    ob_start();

    // Send your response.
    echo "Here be response";
     
    // Get the size of the output.
    $size = ob_get_length();
     
    // Disable compression (in case content length is compressed).
    header("Content-Encoding: none");

    // Set the content length of the response.
    header("Content-Length: {$size}");

    // Close the connection.
    header("Connection: close");
     
    // Flush all output.
    ob_end_flush();
    @ob_flush();
    flush();
     
    // Close current session (if it exists).
    if(session_id()) session_write_close();
     
    // Start your background work here.
    ...

Если вас беспокоит, что ваша фоновая работа займет больше времени, чем ограничение по времени выполнения скрипта PHP по умолчанию, тогда вставьте set_time_limit(0); вверху.

person Kosta Kontos    schedule 26.02.2015
comment
Пробовал МНОГО разных комбинаций, ЭТО работает !!! Спасибо Коста Контос !!! - person Martin_Lakes; 05.08.2016
comment
Отлично работает на apache 2, php 7.0.32 и ubuntu 16.04! Спасибо! - person KyleBunga; 26.01.2019
comment
Я пробовал другие решения, и только это сработало для меня. Порядок строк также важен. - person Sinisa; 01.10.2019
comment
Я не могу понять, почему здесь требуется ob_flush(), поскольку это вызывает уведомление PHP Notice: ob_flush(): failed to flush buffer. No buffer to flush in php shell code on line 1. Как ни странно, без него это не работает. Я думаю, он все-таки что-то делает ... - person billynoah; 23.07.2021

Если вы используете обработку FastCGI или PHP-FPM, вы можете:

session_write_close(); //close the session
ignore_user_abort(true); //Prevent echo, print, and flush from killing the script
fastcgi_finish_request(); //this returns 200 to the user, and processing continues

// do desired processing ...
$expensiveCalulation = 1+1;
error_log($expensiveCalculation);

Источник: https://www.php.net/manual/en/function.fastcgi-finish-request.php

Проблема PHP № 68722: https://bugs.php.net/bug.php?id=68772

person DarkNeuron    schedule 12.08.2016
comment
Спасибо за это, потратив несколько часов, это сработало для меня в nginx - person Ehsan; 14.02.2017
comment
Спасибо DarkNeuron! Отличный ответ для нас, использующих php-fpm, просто решил мою проблему! - person Sinisa; 01.01.2020
comment
Блестяще! Я просто часами пытался понять, почему наше приложение перестало работать на странице, где мы использовали буферизацию вывода, после переноса его на новый сервер, и это было ключевым моментом. Спасибо! - person D Durham; 12.12.2020

Я потратил несколько часов на эту проблему, и у меня есть эта функция, которая работает на Apache и Nginx:

/**
 * respondOK.
 */
protected function respondOK()
{
    // check if fastcgi_finish_request is callable
    if (is_callable('fastcgi_finish_request')) {
        /*
         * This works in Nginx but the next approach not
         */
        session_write_close();
        fastcgi_finish_request();

        return;
    }

    ignore_user_abort(true);

    ob_start();
    $serverProtocole = filter_input(INPUT_SERVER, 'SERVER_PROTOCOL', FILTER_SANITIZE_STRING);
    header($serverProtocole.' 200 OK');
    header('Content-Encoding: none');
    header('Content-Length: '.ob_get_length());
    header('Connection: close');

    ob_end_flush();
    ob_flush();
    flush();
}

Вы можете вызвать эту функцию перед длительной обработкой.

person Ehsan    schedule 15.02.2017
comment
Это чертовски мило! После того, как вы попробовали все остальное, это единственное, что сработало с nginx. - person spice; 07.01.2019
comment
Ваш код почти полностью совпадает с кодом, который здесь но ваш пост старше :) +1 - person Accountant م; 16.02.2019
comment
будьте осторожны с функцией filter_input, она иногда возвращает NULL. подробности см. в вкладе пользователя - person Accountant م; 16.02.2019

Немного изменил ответ @vcampitelli. Не думайте, что вам нужен заголовок close. Я видел повторяющиеся закрытые заголовки в Chrome.

<?php

ignore_user_abort(true);

ob_start();
echo '{}';
header($_SERVER["SERVER_PROTOCOL"] . " 202 Accepted");
header("Status: 202 Accepted");
header("Content-Type: application/json");
header('Content-Length: '.ob_get_length());
ob_end_flush();
ob_flush();
flush();

sleep(10);
person Justin    schedule 03.06.2015
comment
Я упомянул об этом в исходном ответе, но скажу и здесь. Вам не обязательно закрывать соединение, но тогда произойдет то, что следующий актив, запрошенный в том же соединении, будет вынужден ждать. Таким образом, вы можете доставить HTML быстро, но тогда один из ваших файлов JS или CSS может загружаться медленно, так как соединение должно завершить получение ответа от PHP, прежде чем оно сможет получить следующий ресурс. По этой причине рекомендуется закрыть соединение, чтобы браузеру не приходилось ждать, пока оно освободится. - person Nate Lampton; 17.09.2015

Для этого я использую php-функцию register_shutdown_function.

void register_shutdown_function ( callable $callback [, mixed $parameter [, mixed $... ]] )

http://php.net/manual/en/function.register-shutdown-function.php

Изменить: указанное выше не работает. Кажется, меня ввела в заблуждение какая-то старая документация. Поведение функции register_shutdown_function изменилось после link ссылка

person martti    schedule 18.05.2015
comment
Это не то, о чем просят - эта функция в основном просто расширяет событие завершения скрипта и по-прежнему является частью выходного буфера. - person fisk; 03.01.2016
comment
Нашел, что стоит проголосовать, потому что он показывает, что не работает. - person Trendfischer; 09.09.2016
comment
То же самое - голосование за, потому что я ожидал увидеть это как ответ, и было полезно увидеть, что это не работает. - person HappyDog; 15.11.2019

Я не могу установить pthread, и ни одно из предыдущих решений у меня не работает. Я нашел только следующее решение для работы (ref: https://stackoverflow.com/a/14469376/1315873):

<?php
ob_end_clean();
header("Connection: close");
ignore_user_abort(); // optional
ob_start();
echo ('Text the user will see');
$size = ob_get_length();
header("Content-Length: $size");
ob_end_flush(); // Strange behaviour, will not work
flush();            // Unless both are called !
session_write_close(); // Added a line suggested in the comment
// Do processing here 
sleep(30);
echo('Text user will never see');
?>
person Fil    schedule 01.02.2019
comment
Это вообще не работает. - person George Garchagudashvili; 01.04.2021
comment
Какую версию php и какую операционную систему вы используете? Возможно, что-то изменилось в последних версиях php ... - person Fil; 05.04.2021

Я задал этот вопрос Расмусу Лердорфу в апреле 2012 года, цитируя эти статьи:

Я предложил разработать новую встроенную функцию PHP, чтобы уведомить платформу о том, что дальнейший вывод (на stdout?) Генерироваться не будет (такая функция может позаботиться о закрытии соединения). Расмус Лердорф ответил:

См. Gearman. Вы действительно не хотите, чтобы ваши внешние веб-серверы выполняли такую ​​внутреннюю обработку.

Я понимаю его точку зрения и поддерживаю его мнение о некоторых сценариях загрузки и приложений! Однако при некоторых других сценариях решения от vcampitelli et al. Являются хорошими.

person Matthew Slyman    schedule 17.06.2017

У меня есть что-то, что может сжимать и отправлять ответ, а также выполнять другой PHP-код.

function sendResponse($response){
    $contentencoding = 'none';
    if(ob_get_contents()){
        ob_end_clean();
        if(ob_get_contents()){
            ob_clean();
        }
    }
    header('Connection: close');
    header("cache-control: must-revalidate");
    header('Vary: Accept-Encoding');
    header('content-type: application/json; charset=utf-8');
    ob_start();
    if(phpversion()>='4.0.4pl1' && extension_loaded('zlib') && GZIP_ENABLED==1 && !empty($_SERVER["HTTP_ACCEPT_ENCODING"]) && (strpos($_SERVER["HTTP_ACCEPT_ENCODING"], 'gzip') !== false) && (strstr($GLOBALS['useragent'],'compatible') || strstr($GLOBALS['useragent'],'Gecko'))){
        $contentencoding = 'gzip';
        ob_start('ob_gzhandler');
    }
    header('Content-Encoding: '.$contentencoding);
    if (!empty($_GET['callback'])){
        echo $_GET['callback'].'('.$response.')';
    } else {
        echo $response;
    }
    if($contentencoding == 'gzip') {
        if(ob_get_contents()){
            ob_end_flush(); // Flush the output from ob_gzhandler
        }
    }
    header('Content-Length: '.ob_get_length());
    // flush all output
    if (ob_get_contents()){
        ob_end_flush(); // Flush the outer ob_start()
        if(ob_get_contents()){
            ob_flush();
        }
        flush();
    }
    if (session_id()) session_write_close();
}
person Mayur S    schedule 11.08.2018

в случае использования php file_get_contents закрытия соединения недостаточно. php все еще ждет отправки eof witch сервером.

мое решение - прочитать Content-Length:

вот образец:

response.php:

 <?php

ignore_user_abort(true);
set_time_limit(500);

ob_start();
echo 'ok'."\n";
header('Connection: close');
header('Content-Length: '.ob_get_length());
ob_end_flush();
ob_flush();
flush();
sleep(30);

Обратите внимание на "\ n" в ответ на закрытие строки, если fget не читается во время ожидания eof.

read.php:

<?php
$vars = array(
    'hello' => 'world'
);
$content = http_build_query($vars);

fwrite($fp, "POST /response.php HTTP/1.1\r\n");
fwrite($fp, "Content-Type: application/x-www-form-urlencoded\r\n");
fwrite($fp, "Content-Length: " . strlen($content) . "\r\n");
fwrite($fp, "Connection: close\r\n");
fwrite($fp, "\r\n");

fwrite($fp, $content);

$iSize = null;
$bHeaderEnd = false;
$sResponse = '';
do {
    $sTmp = fgets($fp, 1024);
    $iPos = strpos($sTmp, 'Content-Length: ');
    if ($iPos !== false) {
        $iSize = (int) substr($sTmp, strlen('Content-Length: '));
    }
    if ($bHeaderEnd) {
        $sResponse.= $sTmp;
    }
    if (strlen(trim($sTmp)) == 0) {
        $bHeaderEnd = true;
    }
} while (!feof($fp) && (is_null($iSize) || !is_null($iSize) && strlen($sResponse) < $iSize));
$result = trim($sResponse);

Как вы можете видеть, этот скрипт dosent ждет около eof, если длина контента достигнута.

надеюсь, это поможет

person Jérome S.    schedule 26.08.2016

Есть другой подход, и его стоит рассмотреть, если вы не хотите вмешиваться в заголовки ответов. Если вы запустите поток в другом процессе, вызываемая функция не будет ждать своего ответа и вернется в браузер с завершенным http-кодом. Вам потребуется настроить pthread.

class continue_processing_thread extends Thread 
{
     public function __construct($param1) 
     {
         $this->param1 = $param1
     }

     public function run() 
     {
        //Do your long running process here
     }
}

//This is your function called via an HTTP GET/POST etc
function rest_endpoint()
{
  //do whatever stuff needed by the response.

  //Create and start your thread. 
  //rest_endpoint wont wait for this to complete.
  $continue_processing = new continue_processing_thread($some_value);
  $continue_processing->start();

  echo json_encode($response)
}

Как только мы выполним $ continue_processing-> start (), PHP не будет ждать результата возврата этого потока и, следовательно, пока учитывается rest_endpoint. Сделано.

Некоторые ссылки в помощь с pthreads

Удачи.

person Jonathan    schedule 19.07.2016