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

102

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

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

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

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

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

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

user1700214
источник

Ответы:

192

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

ignore_user_abort(true);
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();

// now the request is sent to the browser, but the script is still running
// so, you can continue...
Vcampitelli
источник
5
Можно ли сделать это с помощью соединения keep-alive?
Congelli501 09
3
Превосходно!! Это единственный ответ на этот вопрос, который действительно работает !!! 10p +
Martin_Lakes 01
3
есть ли причина, по которой вы используете ob_flush после ob_end_flush? Я понимаю необходимость функции flush в конце, но я не уверен, зачем вам нужно ob_flush при вызове ob_end_flush.
ars265
9
Обратите внимание, что если для заголовка кодирования содержимого установлено значение, отличное от «none», это может сделать этот пример бесполезным, так как он все равно позволит пользователю ждать полного времени выполнения (до тайм-аута?). Чтобы быть абсолютно уверенным, что он будет работать локально и в производственной среде, установите для заголовка content-encoding значение «none»:header("Content-Encoding: none")
Брайан,
18
Совет: я начал использовать PHP-FPM, поэтому мне пришлось добавить fastcgi_finish_request()в конце
vcampitelli
44

Я видел здесь много ответов, в которых предлагается использовать, 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);наверху.

Коста Контос
источник
3
Перепробовал МНОГО разных комбинаций, ЭТО работает !!! Спасибо Коста Контос !!!
Martin_Lakes 05
1
Отлично работает на apache 2, php 7.0.32 и ubuntu 16.04! Спасибо!
KyleBunga
1
Я пробовал другие решения, и только это сработало для меня. Порядок строк также важен.
Синиса
32

Если вы используете обработку 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

ТемныйНейрон
источник
2
Спасибо за это, после того, как я провел несколько часов, это сработало для меня в nginx
Эхсан
7
эта сумма дорогостоящий расчет, хотя сильно впечатлила, такая дорогая!
Фридрих Роэлл
Спасибо DarkNeuron! Отличный ответ для нас, использующих php-fpm, просто решил мою проблему!
Синиса
21

Я потратил несколько часов на эту проблему, и у меня есть эта функция, которая работает на 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();
}

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

Эхсан
источник
2
Это чертовски мило! После того, как вы попробовали все остальное, это единственное, что сработало с nginx.
spice
Ваш код почти такой же, как здесь, но ваш пост старше :) +1
Бухгалтер م
будьте осторожны с filter_inputфункцией, она иногда возвращает NULL. увидеть этот вклад пользователей к деталям
бухгалтер م
4

Немного изменил ответ @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);
Джастин
источник
3
Я упомянул об этом в исходном ответе, но скажу и здесь. Вам не обязательно закрывать соединение, но тогда произойдет то, что следующий актив, запрошенный в том же соединении, будет вынужден ждать. Таким образом, вы можете доставить HTML быстро, но тогда один из ваших файлов JS или CSS может загружаться медленно, так как соединение должно завершить получение ответа от PHP, прежде чем оно сможет получить следующий ресурс. По этой причине рекомендуется закрыть соединение, чтобы браузеру не приходилось ждать, пока оно освободится.
Нейт Лэмптон
3

Для этого я использую 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 изменилось после PHP 4.1 ссылка ссылка

Мартти
источник
Это не то, о чем просят - эта функция в основном просто расширяет событие завершения скрипта и все еще является частью выходного буфера.
fisk
1
Нашел, что стоит проголосовать, потому что он показывает, что не работает.
Trendfischer
1
То же самое - голосование, потому что я ожидал увидеть это как ответ, и было полезно увидеть, что это не работает.
HappyDog
2

Я не могу установить 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');
?>
Fil
источник
1

в случае использования 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, если длина контента достигнута.

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

Жером С.
источник
1

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

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

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

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

Мэтью Слайман
источник
1

У меня есть что-то, что может сжимать и отправлять ответ, а также выполнять другой 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();
}
Маюр С
источник
0

Есть другой подход, и его стоит рассмотреть, если вы не хотите вмешиваться в заголовки ответов. Если вы запустите поток в другом процессе, вызываемая функция не будет ждать своего ответа и вернется в браузер с завершенным 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

Удачи.

Джонатан
источник
0

Я знаю, что это старый, но, возможно, полезный на данный момент.

С этим ответом я не поддерживаю актуальный вопрос, но как правильно решить эту проблему. Надеюсь, это поможет другим людям решить подобные проблемы.

Я бы предложил использовать RabbitMQ или аналогичные службы и запускать фоновую рабочую нагрузку с использованием рабочих экземпляров . Существует пакет amqplib для php, который выполняет всю работу по использованию RabbitMQ.

Плюсы:

  1. Это высокая производительность
  2. Красиво структурированный и удобный
  3. Абсолютно масштабируемость с помощью рабочих экземпляров

Нег:

  1. RabbitMQ должен быть установлен на сервере, это может быть проблемой для некоторых веб-хостеров.
Самуэль Бреу
источник