Verarbeite PHP weiter, nachdem du eine http-Antwort gesendet hast


101

Mein Skript wird vom Server aufgerufen. Vom Server werde ich ID_OF_MESSAGEund erhalten TEXT_OF_MESSAGE.

In meinem Skript werde ich eingehenden Text verarbeiten und eine Antwort mit params generieren: ANSWER_TO_IDund RESPONSE_MESSAGE.

Das Problem ist, dass ich eine Antwort auf eingehende "ID_OF_MESSAGE"Nachrichten sende , aber der Server, der mir die zu behandelnde Nachricht sendet, setzt seine Nachricht als an mich zugestellt (dies bedeutet, dass ich ihm eine Antwort auf diese ID senden kann), nachdem er die http-Antwort 200 erhalten hat.

Eine der Lösungen besteht darin, die Nachricht in der Datenbank zu speichern und ein Cron zu erstellen, das jede Minute ausgeführt wird. Ich muss jedoch sofort eine Antwortnachricht generieren.

Gibt es eine Lösung, wie man eine HTTP-Antwort 200 an den Server sendet und dann das PHP-Skript weiter ausführt?

Vielen Dank

Antworten:


191

Ja. Du kannst das:

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

5
Ist es möglich, dies mit einer Keep-Alive-Verbindung zu tun?
Congelli501

3
Ausgezeichnet!! Dies ist die einzige Antwort auf diese Frage, die tatsächlich funktioniert !!! 10p +
Martin_Lakes

3
Gibt es einen Grund, warum Sie ob_flush nach ob_end_flush verwenden? Ich verstehe die Notwendigkeit der Flush-Funktion dort am Ende, bin mir aber nicht sicher, warum Sie ob_flush benötigen würden, wenn ob_end_flush aufgerufen wird.
Ars265

9
Bitte beachten Sie, dass ein Inhaltscodierungs-Header, der auf etwas anderes als "Keine" gesetzt ist, dieses Beispiel unbrauchbar machen kann, da der Benutzer dennoch die volle Ausführungszeit warten muss (bis zum Timeout?). Um absolut sicher zu sein, dass es lokal und in der Produktionsumgebung funktioniert, setzen Sie den Header 'content-encoding' auf 'none':header("Content-Encoding: none")
Brian

17
Tipp: Ich habe angefangen, PHP-FPM zu verwenden, also musste ich fastcgi_finish_request()am Ende hinzufügen
vcampitelli

44

Ich habe hier viele Antworten gesehen, die die Verwendung vorschlagen, ignore_user_abort(true);aber dieser Code ist nicht erforderlich. Dies stellt lediglich sicher, dass Ihr Skript weiterhin ausgeführt wird, bevor eine Antwort gesendet wird, falls der Benutzer den Vorgang abbricht (indem er seinen Browser schließt oder die Esc-Taste drückt, um die Anforderung zu stoppen). Aber das fragst du nicht. Sie möchten die Ausführung fortsetzen, nachdem eine Antwort gesendet wurde. Sie benötigen lediglich Folgendes:

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

Wenn Sie befürchten, dass Ihre Hintergrundarbeit länger dauert als das Standard-Zeitlimit für die Skriptausführung von PHP, bleiben Sie set_time_limit(0);oben.


3
Versuchte viele verschiedene Kombinationen, DIESES ist das, was funktioniert !!! Danke Kosta Kontos !!!
Martin_Lakes

1
Funktioniert perfekt auf Apache 2, PHP 7.0.32 und Ubuntu 16.04! Vielen Dank!
KyleBunga

1
Ich habe andere Lösungen ausprobiert und nur diese hat für mich funktioniert. Die Reihenfolge der Zeilen ist ebenfalls wichtig.
Sinisa

32

Wenn Sie FastCGI-Verarbeitung oder PHP-FPM verwenden, können Sie:

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

Quelle: https://www.php.net/manual/en/function.fastcgi-finish-request.php

PHP-Problem Nr. 68722: https://bugs.php.net/bug.php?id=68772


2
Vielen Dank dafür, nachdem ich ein paar Stunden verbracht hatte, funktionierte dies für mich in Nginx
Ehsan

7
dat sum teure Berechnung tho: o sehr beeindruckt, so teuer!
Friedrich Roell

Danke DarkNeuron! Tolle Antwort für uns mit php-fpm, habe gerade mein Problem gelöst!
Sinisa

21

Ich habe ein paar Stunden mit diesem Problem verbracht und bin mit dieser Funktion gekommen, die auf Apache und Nginx funktioniert:

/**
 * 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();
}

Sie können diese Funktion vor Ihrer langen Verarbeitung aufrufen.


2
Das ist verdammt schön! Nachdem Sie alles andere ausprobiert haben, ist dies das einzige, was mit Nginx funktioniert hat.
Gewürz

Ihr Code ist fast genau der gleiche wie der Code hier, aber Ihr Beitrag ist älter :) +1
Buchhalter م

Seien Sie vorsichtig mit der filter_inputFunktion, die manchmal NULL zurückgibt. siehe diesen Benutzerbeitrag für Details
Buchhalter

4

Die Antwort von @vcampitelli wurde ein wenig geändert. Glaube nicht, dass du den closeHeader brauchst . Ich habe in Chrome doppelte geschlossene Header gesehen.

<?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
Ich habe dies in der ursprünglichen Antwort erwähnt, aber ich werde es auch hier sagen. Sie müssen die Verbindung nicht unbedingt schließen, aber dann wird das nächste Asset, das für dieselbe Verbindung angefordert wird, warten müssen. Sie könnten also den HTML-Code schnell bereitstellen, aber dann wird möglicherweise eine Ihrer JS- oder CSS-Dateien langsam geladen, da die Verbindung die Antwort von PHP erhalten muss, bevor sie das nächste Asset erhalten kann. Aus diesem Grund ist das Schließen der Verbindung eine gute Idee, damit der Browser nicht warten muss, bis sie freigegeben wird.
Nate Lampton

3

Ich benutze dafür die PHP-Funktion register_shutdown_function.

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

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

Bearbeiten : Das obige funktioniert nicht. Es scheint, dass ich durch eine alte Dokumentation in die Irre geführt wurde. Das Verhalten von register_shutdown_function ist seit PHP 4.1 geändert Link Link


Dies ist nicht das, wonach gefragt wird - diese Funktion erweitert im Grunde nur das Skriptbeendigungsereignis und ist immer noch Teil des Ausgabepuffers.
Fisk

1
Fand es eine positive Bewertung wert, weil es zeigt, was nicht funktioniert.
Trendfischer

1
Dito - Upvoting, weil ich erwartet hatte, dies als Antwort zu sehen, und nützlich zu sehen, dass es nicht funktioniert.
HappyDog

2

Ich kann pthread nicht installieren und die vorherigen Lösungen funktionieren auch nicht für mich. Ich habe nur die folgende Lösung gefunden (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');
?>

1

Bei Verwendung von PHP-Datei_get_contents reicht das Schließen der Verbindung nicht aus. PHP warten noch auf Eof Hexe vom Server gesendet.

Meine Lösung besteht darin, "Inhaltslänge:" zu lesen.

Hier ist ein Beispiel:

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

Beachten Sie das "\ n" als Antwort auf das Schließen der Zeile, wenn nicht das fget gelesen wird, während Sie warten.

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

Wie Sie sehen können, wartet dieses Skript nicht auf eof, wenn die Länge des Inhalts erreicht ist.

hoffe es wird helfen


1

Diese Frage habe ich Rasmus Lerdorf im April 2012 unter Berufung auf folgende Artikel gestellt:

Ich schlug die Entwicklung einer neuen integrierten PHP-Funktion vor, um die Plattform darüber zu informieren, dass keine weitere Ausgabe (auf stdout?) Erzeugt wird (eine solche Funktion könnte das Schließen der Verbindung übernehmen). Rasmus Lerdorf antwortete:

Siehe Gearman . Sie möchten wirklich nicht, dass Ihre Frontend-Webserver eine solche Backend-Verarbeitung durchführen.

Ich kann seinen Standpunkt sehen und seine Meinung für einige Anwendungen / Ladeszenarien unterstützen! In einigen anderen Szenarien sind die Lösungen von vcampitelli et al.


1

Ich habe etwas, das komprimiert und die Antwort senden und anderen PHP-Code ausführen lassen kann.

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

Es gibt einen anderen Ansatz, und es lohnt sich zu überlegen, ob Sie die Antwortheader nicht manipulieren möchten. Wenn Sie einen Thread in einem anderen Prozess starten, wartet die aufgerufene Funktion nicht auf ihre Antwort und kehrt mit einem endgültigen http-Code zum Browser zurück. Sie müssen pthread konfigurieren .

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

Sobald wir $ continue_processing-> start () ausführen, wartet PHP nicht auf das Rückgabeergebnis dieses Threads und daher auf rest_endpoint. Es ist vollbracht.

Einige Links zur Unterstützung von Pthreads

Viel Glück.


0

Ich weiß, dass es alt ist, aber möglicherweise an dieser Stelle nützlich.

Mit dieser Antwort unterstütze ich nicht die eigentliche Frage, sondern wie man dieses Problem richtig löst. Ich hoffe, es hilft anderen Menschen, solche Probleme zu lösen.

Ich würde vorschlagen, RabbitMQ oder ähnliche Dienste zu verwenden und die Hintergrund-Workload mithilfe von Worker-Instanzen auszuführen . Es gibt ein Paket namens amqplib für PHP, das die ganze Arbeit für Sie erledigt, um RabbitMQ zu verwenden.

Profis:

  1. Es ist leistungsstark
  2. Schön strukturiert und wartbar
  3. Es ist absolut skalierbar mit Worker-Instanzen

Neg's:

  1. RabbitMQ muss auf dem Server installiert sein. Dies kann bei einigen Webhostern ein Problem sein.
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.