kontynuuj przetwarzanie php po wysłaniu odpowiedzi http

101

Mój skrypt jest wywoływany przez serwer. Z serwera otrzymam ID_OF_MESSAGEi TEXT_OF_MESSAGE.

W moim skrypcie obsłużę przychodzący tekst i wygeneruję odpowiedź z parametrami: ANSWER_TO_IDi RESPONSE_MESSAGE.

Problem w tym, że wysyłam odpowiedź na incomming "ID_OF_MESSAGE", ale serwer, który wyśle ​​mi wiadomość do obsługi, ustawi swoją wiadomość jako dostarczoną do mnie (to znaczy, że mogę wysłać mu odpowiedź na ten identyfikator), po otrzymaniu odpowiedzi http 200.

Jednym z rozwiązań jest zapisanie wiadomości w bazie danych i utworzenie crona, który będzie działał co minutę, ale muszę natychmiast wygenerować wiadomość z odpowiedzią.

Czy jest jakieś rozwiązanie, jak wysłać do serwera odpowiedź http 200 i kontynuować wykonywanie skryptu php?

Dziękuję bardzo

user1700214
źródło

Odpowiedzi:

191

Tak. Możesz to zrobić:

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
źródło
5
Czy można to zrobić z utrzymywanym połączeniem?
Congelli501
3
Doskonały!! To jedyna odpowiedź na to pytanie, która faktycznie działa !!! 10p +
Martin_Lakes
3
czy istnieje powód, dla którego używasz ob_flush po ob_end_flush? Rozumiem potrzebę użycia funkcji flush na końcu, ale nie jestem pewien, dlaczego miałbyś potrzebować ob_flush z wywołaniem ob_end_flush.
ars265
9
Należy pamiętać, że jeśli nagłówek kodowania treści jest ustawiony na cokolwiek innego niż „none”, może to uczynić ten przykład bezużytecznym, ponieważ nadal pozwoliłby użytkownikowi czekać przez cały czas wykonania (do przekroczenia limitu czasu?). Aby mieć absolutną pewność, że będzie działać lokalnie i w środowisku produkcyjnym, ustaw nagłówek „content-encoding” na „none”:header("Content-Encoding: none")
Brian
17
Wskazówka: zacząłem używać PHP-FPM, więc fastcgi_finish_request()na koniec musiałem dodać
vcampitelli
44

Widziałem tutaj wiele odpowiedzi, które sugerują użycie, ignore_user_abort(true);ale ten kod nie jest konieczny. Wszystko to polega na zapewnieniu, że skrypt będzie kontynuował wykonywanie, zanim zostanie wysłana odpowiedź w przypadku przerwania pracy przez użytkownika (poprzez zamknięcie przeglądarki lub naciśnięcie klawisza Escape, aby zatrzymać żądanie). Ale nie o to pytasz. Prosisz o kontynuowanie wykonywania PO wysłaniu odpowiedzi. Wszystko, czego potrzebujesz, to:

    // 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.
    ...

Jeśli obawiasz się, że praca w tle potrwa dłużej niż domyślny limit czasu wykonywania skryptu PHP, trzymaj set_time_limit(0);się góry.

Kosta Kontos
źródło
3
Wypróbowałem wiele różnych kombinacji, TO działa !!! Dzięki Kosta Kontos !!!
Martin_Lakes,
1
Działa doskonale na apache 2, php 7.0.32 i ubuntu 16.04! Dzięki!
KyleBunga
1
Wypróbowałem inne rozwiązania i tylko ten działał dla mnie. Kolejność linii jest również ważna.
Sinisa
32

Jeśli korzystasz z przetwarzania FastCGI lub PHP-FPM, możesz:

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);

Źródło: https://www.php.net/manual/en/function.fastcgi-finish-request.php

Wydanie PHP nr 68722: https://bugs.php.net/bug.php?id=68772

DarkNeuron
źródło
2
Dzięki za to, po spędzeniu kilku godzin, zadziałało to dla mnie w nginx
Ehsan
7
dat suma kosztowna kalkulacja tho: o wielkie wrażenie, takie drogie!
Friedrich Roell
Dzięki DarkNeuron! Świetna odpowiedź dla nas przy użyciu php-fpm, właśnie rozwiązałem mój problem!
Sinisa
21

Spędziłem kilka godzin nad tym problemem i otrzymałem tę funkcję, która działa na Apache i 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();
}

Możesz wywołać tę funkcję przed długim przetwarzaniem.

Ehsan
źródło
2
To jest cholernie cudowne! Po wypróbowaniu wszystkiego powyżej jest to jedyna rzecz, która działała z nginx.
przyprawa
Twój kod jest prawie taki sam jak ten tutaj, ale Twój post jest starszy :) +1
Księgowy م
uważaj na filter_inputfunkcję, która czasami zwraca NULL. zobaczyć ten wpis użytkownika o szczegółach
Księgowa م
4

Zmodyfikował nieco odpowiedź przez @vcampitelli. Nie myśl, że potrzebujesz closenagłówka. Widziałem zduplikowane zamknięte nagłówki w 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);
Justin
źródło
3
Wspomniałem o tym w oryginalnej odpowiedzi, ale powiem to również tutaj. Nie musisz koniecznie zamykać połączenia, ale wtedy, co się stanie, następny zasób żądany na tym samym połączeniu będzie zmuszony czekać. Możesz więc szybko dostarczyć kod HTML, ale jeden z plików JS lub CSS może ładować się powoli, ponieważ połączenie musi zakończyć otrzymywanie odpowiedzi z PHP, zanim będzie można uzyskać następny zasób. Z tego powodu zamknięcie połączenia jest dobrym pomysłem, aby przeglądarka nie musiała czekać na jego zwolnienie.
Nate Lampton,
3

Używam do tego funkcji register_shutdown_function.

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

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

Edycja : powyższe nie działa. Wygląda na to, że wprowadziła mnie w błąd stara dokumentacja. Zachowanie register_shutdown_function zmieniła się od PHP 4.1 łącza łącza

martti
źródło
Nie o to się prosi - ta funkcja po prostu rozszerza zdarzenie kończenia skryptu i nadal jest częścią bufora wyjściowego.
fisk
1
Okazało się, że warto go pochwalić, ponieważ pokazuje, co nie działa.
Trendfischer
1
Jak wyżej - podbijanie głosów, ponieważ spodziewałem się, że potraktuję to jako odpowiedź i przydatne, aby zobaczyć, że to nie działa.
HappyDog
2

Nie mogę zainstalować pthread i żadne poprzednie rozwiązania nie działają. Znalazłem tylko następujące rozwiązanie do pracy (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
źródło
1

w przypadku użycia php file_get_contents, zamknięcie połączenia nie wystarczy. php nadal czeka na wysłanie eof wiedźmy przez serwer.

moim rozwiązaniem jest przeczytanie „Content-Length:”

tutaj jest próbka:

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);

Zwróć uwagę na "\ n" w odpowiedzi na zamkniętą linię, jeśli nie na fget read while wait 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);

Jak widać, ten skrypt nie może czekać około eof, jeśli długość treści jest osiągnięta.

mam nadzieję, że to pomoże

Jérome S.
źródło
1

Zadałem to pytanie Rasmusowi Lerdorfowi w kwietniu 2012 roku, cytując następujące artykuły:

Zasugerowałem opracowanie nowej funkcji wbudowanej w PHP, aby powiadomić platformę, że nie będą generowane żadne dalsze dane wyjściowe (na stdout?) (Taka funkcja może zająć się zamknięciem połączenia). Rasmus Lerdorf odpowiedział:

Zobacz Gearman . Naprawdę nie chcesz, aby Twoje frontendowe serwery WWW wykonywały wewnętrzne przetwarzanie w ten sposób.

Rozumiem jego punkt widzenia i popieram jego opinię w przypadku niektórych aplikacji / scenariuszy ładowania! Jednak w niektórych innych scenariuszach rozwiązania z vcampitelli i wsp. Są dobre.

Matthew Slyman
źródło
1

Mam coś, co może skompresować i wysłać odpowiedź i pozwolić na wykonanie innego kodu 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();
}
Mayur S
źródło
0

Istnieje inne podejście i warto je rozważyć, jeśli nie chcesz manipulować nagłówkami odpowiedzi. Jeśli uruchomisz wątek w innym procesie, wywołana funkcja nie będzie czekać na odpowiedź i wróci do przeglądarki ze sfinalizowanym kodem http. Będziesz musiał skonfigurować 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)
}

Gdy wykonamy $ continue_processing-> start (), PHP nie będzie czekać na wynik powrotu tego wątku, a zatem o ile rozważany jest punkt końcowy. Zrobione.

Kilka linków do pomocy w pthreads

Powodzenia.

Jonathan
źródło
0

Wiem, że to stary, ale prawdopodobnie przydatny w tym momencie.

Tą odpowiedzią nie popieram rzeczywistego pytania, ale jak poprawnie rozwiązać ten problem. Mam nadzieję, że pomoże to innym ludziom rozwiązać takie problemy.

Sugerowałbym użycie RabbitMQ lub podobnych usług i uruchomienie obciążenia w tle przy użyciu instancji roboczych . Istnieje pakiet o nazwie amqplib dla php, który wykonuje całą pracę za Ciebie, aby użyć RabbitMQ.

Profesjonaliści:

  1. Jest bardzo wydajny
  2. Ładnie skonstruowany i łatwy w utrzymaniu
  3. Jest absolutnie skalowalny z instancjami roboczymi

Neg:

  1. RabbitMQ musi być zainstalowany na serwerze, może to być problem z niektórymi usługami hostingowymi.
Samuel Breu
źródło