continuar procesando php después de enviar la respuesta http

101

Mi script es llamado por el servidor. Del servidor recibiré ID_OF_MESSAGEy TEXT_OF_MESSAGE.

En mi script, manejaré el texto entrante y generaré una respuesta con params: ANSWER_TO_IDy RESPONSE_MESSAGE.

El problema es que estoy enviando una respuesta a ining "ID_OF_MESSAGE", pero el servidor que me envía un mensaje para manejar establecerá su mensaje como entregado a mí (significa que puedo enviarle una respuesta a esa ID), después de recibir la respuesta http 200.

Una de las soluciones es guardar el mensaje en la base de datos y hacer un cron que se ejecutará cada minuto, pero necesito generar un mensaje de respuesta de inmediato.

¿Hay alguna solución para enviar al servidor la respuesta http 200 y continuar ejecutando el script php?

Muchas gracias

usuario1700214
fuente

Respuestas:

191

Si. Puedes hacerlo:

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
fuente
5
¿Es posible hacerlo con una conexión de mantener vivo?
Congelli501
3
¡¡Excelente!! ¡Esta es la única respuesta a esta pregunta que realmente funciona! 10p +
Martin_Lakes
3
¿Hay alguna razón por la que use ob_flush después de ob_end_flush? Entiendo la necesidad de la función flush allí al final, pero no estoy seguro de por qué necesitaría ob_flush con ob_end_flush que se llame.
ars265
9
Tenga en cuenta que si un encabezado de codificación de contenido se establece en algo más que 'ninguno', podría hacer que este ejemplo sea inútil, ya que aún permitiría al usuario esperar el tiempo de ejecución completo (¿hasta el tiempo de espera?). Entonces, para estar absolutamente seguro de que funcionará localmente y en el entorno de producción, establezca el encabezado 'codificación de contenido' en 'ninguno':header("Content-Encoding: none")
Brian
17
Consejo: comencé a usar PHP-FPM, así que tuve que agregar fastcgi_finish_request()al final
vcampitelli
44

He visto muchas respuestas aquí que sugieren usar, ignore_user_abort(true);pero este código no es necesario. Todo lo que hace es asegurarse de que su script continúe ejecutándose antes de que se envíe una respuesta en caso de que el usuario cancele (cerrando su navegador o presionando escape para detener la solicitud). Pero eso no es lo que estás preguntando. Estás pidiendo continuar con la ejecución DESPUÉS de que se envíe una respuesta. Todo lo que necesitas es lo siguiente:

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

Si le preocupa que su trabajo en segundo plano lleve más tiempo que el límite de tiempo de ejecución del script predeterminado de PHP, quédese set_time_limit(0);en la parte superior.

Kosta Kontos
fuente
3
Probé MUCHAS combinaciones diferentes, ¡ESTA es la que funciona! Gracias Kosta Kontos !!!
Martin_Lakes
1
¡Funciona perfectamente en apache 2, php 7.0.32 y ubuntu 16.04! ¡Gracias!
KyleBunga
1
Probé otras soluciones y solo esta funcionó para mí. El orden de las líneas también es importante.
Sinisa
32

Si está utilizando el procesamiento FastCGI o PHP-FPM, puede:

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

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

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

DarkNeuron
fuente
2
Gracias por esto, después de pasar unas horas, esto funcionó para mí en nginx
Ehsan
7
dat suma caro cálculo aunque: o muy impresionado, tan caro!
Friedrich Roell
¡Gracias DarkNeuron! Gran respuesta para nosotros usando php-fpm, ¡acabo de resolver mi problema!
Sinisa
21

Pasé algunas horas en este tema y he venido con esta función que funciona en Apache y 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();
}

Puede llamar a esta función antes de su largo procesamiento.

Ehsan
fuente
2
¡Esto es malditamente encantador! Después de probar todo lo anterior, esto es lo único que funcionó con nginx.
especia
Su código es casi exactamente el mismo que el código aquí, pero su publicación es anterior :) +1
Contador م
tenga cuidado con la filter_inputfunción que a veces devuelve NULL. vea esta contribución del usuario para más detalles
Contador م
4

Modificó un poco la respuesta de @vcampitelli. No crea que necesita el closeencabezado. Veía encabezados cerrados duplicados en 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
fuente
3
Mencioné esto en la respuesta original, pero también lo diré aquí. No es necesario que haya cerrado la conexión, pero lo que sucederá es que el próximo activo solicitado en la misma conexión se verá obligado a esperar. Entonces, podría entregar el HTML rápidamente, pero luego uno de sus archivos JS o CSS podría cargarse lentamente, ya que la conexión debe terminar de obtener la respuesta de PHP antes de que pueda obtener el siguiente activo. Entonces, por esa razón, cerrar la conexión es una buena idea para que el navegador no tenga que esperar a que se libere.
Nate Lampton
3

Utilizo la función php register_shutdown_function para esto.

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

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

Editar : lo anterior no funciona. Parece que me engañó alguna documentación antigua. El comportamiento de register_shutdown_function ha cambiado desde PHP 4.1 link link

martti
fuente
Esto no es lo que se solicita: esta función básicamente solo extiende el evento de terminación del script y sigue siendo parte del búfer de salida.
fisk
1
Me pareció que valía la pena un voto a favor, porque muestra lo que no funciona.
Trendfischer
1
Lo mismo ocurre con la votación a favor porque esperaba ver esto como una respuesta, y es útil ver que no funciona.
HappyDog
2

No puedo instalar pthread y ninguna de las soluciones anteriores me funciona. Encontré solo la siguiente solución para funcionar (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
fuente
1

en caso de uso de php file_get_contents, el cierre de la conexión no es suficiente. php todavía espera a que eof lo envíe el servidor.

mi solución es leer 'Content-Length:'

aquí está la muestra:

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

Tenga en cuenta "\ n" en respuesta a la línea de cierre, si no, fget read mientras espera 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);

Como puede ver, este script no debe esperar unos segundos si se alcanza la longitud del contenido.

espero que ayude

Jérome S.
fuente
1

Le hice esta pregunta a Rasmus Lerdorf en abril de 2012, citando estos artículos:

Sugerí el desarrollo de una nueva función incorporada de PHP para notificar a la plataforma que no se generarán más resultados (¿en stdout?) (Tal función podría encargarse de cerrar la conexión). Rasmus Lerdorf respondió:

Ver Gearman . Realmente no quiere que sus servidores web frontend realicen un procesamiento backend como este.

¡Puedo ver su punto y apoyar su opinión para algunas aplicaciones / escenarios de carga! Sin embargo, en algunos otros escenarios, las soluciones de vcampitelli et al, son buenas.

Matthew Slyman
fuente
1

Tengo algo que puede comprimir y enviar la respuesta y dejar que se ejecute otro código 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
fuente
0

Hay otro enfoque y vale la pena considerarlo si no desea alterar los encabezados de respuesta. Si inicia un hilo en otro proceso, la función llamada no esperará su respuesta y volverá al navegador con un código http finalizado. Deberá configurar 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 vez que ejecutamos $ continue_processing-> start () PHP no esperará el resultado de retorno de este hilo y, por lo tanto, se considerará rest_endpoint. Se hace.

Algunos enlaces para ayudar con pthreads

Buena suerte.

Jonathan
fuente
0

Sé que es antiguo, pero posiblemente útil en este momento.

Con esta respuesta no apoyo la pregunta real sino cómo resolver este problema correctamente. Espero que ayude a otras personas a resolver problemas como este.

Sugeriría usar RabbitMQ o servicios similares y ejecutar la carga de trabajo en segundo plano utilizando instancias de trabajador . Hay un paquete llamado amqplib para php que hace todo el trabajo para que usted use RabbitMQ.

Pros:

  1. Es de alto rendimiento
  2. Bien estructurado y mantenible
  3. Es absolutamente escalable con instancias de trabajador

Neg's:

  1. RabbitMQ debe estar instalado en el servidor, esto puede ser un problema con algún proveedor de alojamiento web.
Samuel Breu
fuente