continuare a elaborare php dopo aver inviato la risposta http


101

Il mio script viene chiamato dal server. Dal server riceverò ID_OF_MESSAGEe TEXT_OF_MESSAGE.

Nel mio script gestirò il testo in arrivo e genererò la risposta con i parametri: ANSWER_TO_IDe RESPONSE_MESSAGE.

Il problema è che sto inviando la risposta a in entrata "ID_OF_MESSAGE", ma il server che mi invia il messaggio da gestire imposterà il suo messaggio come consegnato a me (significa che posso inviargli la risposta a quell'ID), dopo aver ricevuto la risposta http 200.

Una delle soluzioni è salvare il messaggio nel database e creare un cron che verrà eseguito ogni minuto, ma è necessario generare immediatamente un messaggio di risposta.

C'è qualche soluzione su come inviare al server la risposta http 200 e continuare a eseguire lo script php?

Grazie mille

Risposte:


191

Sì. Puoi farlo:

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
È possibile farlo con una connessione keep-alive?
Congelli501

3
Eccellente!! Questa è l'unica risposta a questa domanda che funziona davvero !!! 10p +
Martin_Lakes

3
c'è un motivo per cui usi ob_flush dopo ob_end_flush? Capisco la necessità della funzione flush alla fine, ma non sono sicuro del motivo per cui avresti bisogno di ob_flush con ob_end_flush chiamato.
ars265

9
Si noti che se un'intestazione di codifica del contenuto è impostata su qualcosa di diverso da "nessuno", potrebbe rendere questo esempio inutile in quanto consentirebbe comunque all'utente di attendere l'intero tempo di esecuzione (fino al timeout?). Quindi, per essere assolutamente sicuro che funzioni a livello locale e nell'ambiente di produzione, imposta l'intestazione 'content-encoding' su 'none':header("Content-Encoding: none")
Brian

17
Suggerimento: ho iniziato a utilizzare PHP-FPM, quindi ho dovuto aggiungere fastcgi_finish_request()alla fine
vcampitelli

44

Ho visto molte risposte qui che suggeriscono l'utilizzo, ignore_user_abort(true);ma questo codice non è necessario. Tutto ciò fa è assicurarsi che lo script continui a essere eseguito prima che venga inviata una risposta nel caso in cui l'utente interrompa (chiudendo il browser o premendo Esc per interrompere la richiesta). Ma non è quello che stai chiedendo. Stai chiedendo di continuare l'esecuzione DOPO che è stata inviata una risposta. Tutto ciò di cui hai bisogno è quanto segue:

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

Se sei preoccupato che il tuo lavoro in background richiederà più tempo del limite di tempo di esecuzione dello script predefinito di PHP, rimani set_time_limit(0);in alto.


3
Ho provato MOLTE combinazioni diverse, QUESTO è quello che funziona !!! Grazie Kosta Kontos !!!
Martin_Lakes

1
Funziona perfettamente su apache 2, php 7.0.32 e ubuntu 16.04! Grazie!
KyleBunga

1
Ho provato altre soluzioni e solo questa ha funzionato per me. Anche l'ordine delle righe è importante.
Sinisa

32

Se utilizzi l'elaborazione FastCGI o PHP-FPM, puoi:

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

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

Problema PHP n. 68722: https://bugs.php.net/bug.php?id=68772


2
Grazie per questo, dopo aver trascorso alcune ore questo ha funzionato per me in nginx
Ehsan

7
dat sum costoso calcolo tho: o molto impressionato, così costoso!
Friedrich Roell

Grazie DarkNeuron! Ottima risposta per noi che usiamo php-fpm, ho appena risolto il mio problema!
Sinisa

21

Ho trascorso alcune ore su questo problema e sono arrivato con questa funzione che funziona su Apache e 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();
}

È possibile chiamare questa funzione prima della lunga elaborazione.


2
Questo è dannatamente adorabile! Dopo aver provato tutto il resto sopra, questa è l'unica cosa che ha funzionato con nginx.
spezie

Il tuo codice è quasi esattamente lo stesso del codice qui, ma il tuo post è meno recente :) +1
Accountant م

fare attenzione con la filter_inputfunzione a volte restituisce NULL. vedere questo contributo dell'utente per i dettagli
Accountant م

4

Modificata un po 'la risposta di @vcampitelli. Non pensare di aver bisogno closedell'intestazione. Stavo vedendo intestazioni chiuse duplicate in 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
L'ho menzionato nella risposta originale, ma lo dirò anche qui. Non è necessario che tu abbia chiuso la connessione, ma ciò che accadrà è che l'asset successivo richiesto sulla stessa connessione sarà costretto ad aspettare. Quindi potresti fornire l'HTML velocemente ma poi uno dei tuoi file JS o CSS potrebbe caricarsi lentamente, poiché la connessione deve terminare di ottenere la risposta da PHP prima di poter ottenere la risorsa successiva. Quindi, per questo motivo, chiudere la connessione è una buona idea in modo che il browser non debba attendere che venga liberato.
Nate Lampton

3

Per questo utilizzo la funzione php register_shutdown_function.

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

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

Modifica : quanto sopra non funziona. Sembra che io sia stato fuorviato da una vecchia documentazione. Il comportamento di register_shutdown_function è cambiato dal collegamento al collegamento PHP 4.1


Questo non è ciò che viene richiesto: questa funzione fondamentalmente estende solo l'evento di terminazione dello script ed è ancora parte del buffer di output.
fisk

1
L'ho trovato degno di un voto positivo, perché mostra cosa non funziona.
Trendfischer

1
Idem - voto positivo perché mi aspettavo di vederlo come una risposta e utile per vedere che non funziona.
HappyDog

2

Non riesco a installare pthread e nemmeno le soluzioni precedenti funzionano per me. Ho trovato solo la seguente soluzione per funzionare (rif: 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

in caso di utilizzo di php file_get_contents, la chiusura della connessione non è sufficiente. php aspetta ancora l'invio di eof witch dal server.

la mia soluzione è leggere "Content-Length:"

ecco un esempio:

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

Notare "\ n" in risposta alla riga di chiusura, se non il fget letto durante l'attesa 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);

Come puoi vedere, questo script attende circa eof se la lunghezza del contenuto è raggiunta.

spero che possa aiutare


1

Ho posto questa domanda a Rasmus Lerdorf nell'aprile 2012, citando questi articoli:

Ho suggerito lo sviluppo di una nuova funzione incorporata in PHP per notificare alla piattaforma che non verrà generato alcun ulteriore output (su stdout?) (Tale funzione potrebbe occuparsi della chiusura della connessione). Rasmus Lerdorf ha risposto:

Vedi Gearman . Davvero non vuoi che i tuoi server Web front-end eseguano l'elaborazione back-end in questo modo.

Posso capire il suo punto di vista e supportare la sua opinione per alcune applicazioni / scenari di caricamento! Tuttavia, in alcuni altri scenari, le soluzioni di vcampitelli et al. Sono buone.


1

Ho qualcosa che può comprimere e inviare la risposta e lasciare che altro codice php venga eseguito.

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

C'è un altro approccio e vale la pena considerare se non si desidera manomettere le intestazioni di risposta. Se avvii un thread su un altro processo, la funzione chiamata non aspetterà la sua risposta e tornerà al browser con un codice http finalizzato. Dovrai configurare 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)
}

Una volta eseguito $ continue_processing-> start (), PHP non aspetterà il risultato di ritorno di questo thread e quindi per quanto riguarda rest_endpoint viene considerato. È fatta.

Alcuni link per aiutare con i pthread

In bocca al lupo.


0

So che è vecchio, ma forse utile a questo punto.

Con questa risposta non supporto la domanda reale ma come risolvere correttamente questo problema. Spero che aiuti altre persone a risolvere problemi come questo.

Suggerirei di utilizzare RabbitMQ o servizi simili ed eseguire il carico di lavoro in background utilizzando le istanze di lavoro . C'è un pacchetto chiamato amqplib per php che fa tutto il lavoro per farti usare RabbitMQ.

Professionisti:

  1. È ad alte prestazioni
  2. Ben strutturato e manutenibile
  3. È assolutamente scalabile con le istanze di lavoro

Neg:

  1. RabbitMQ deve essere installato sul server, questo può essere un problema con alcuni web hoster.
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.