¿Cómo cierro una conexión antes de tiempo?

99

Estoy intentando hacer una llamada AJAX (a través de JQuery) que iniciará un proceso bastante largo. Me gustaría que el script simplemente envíe una respuesta indicando que el proceso ha comenzado, pero JQuery no devolverá la respuesta hasta que el script PHP termine de ejecutarse.

He intentado esto con un encabezado "cerrar" (abajo), y también con búfer de salida; ninguno parece funcionar. ¿Alguna conjetura? ¿O es esto algo que necesito hacer en JQuery?

<?php

echo( "We'll email you as soon as this is done." );

header( "Connection: Close" );

// do some stuff that will take a while

mail( '[email protected]', "okay I'm done", 'Yup, all done.' );

?>
Eric_WVGG
fuente
¿Vació su búfer de salida con ob_flush () y no funcionó?
Vinko Vrsalovic

Respuestas:

87

La siguiente página del manual de PHP (incluidas las notas del usuario) sugiere varias instrucciones sobre cómo cerrar la conexión TCP al navegador sin finalizar el script PHP:

Supuestamente requiere un poco más que enviar un encabezado cerrado.


OP luego confirma: sí , esto hizo el truco: apuntando a la nota de usuario # 71172 (noviembre de 2006) copiada aquí:

Cerrar la conexión del navegador de los usuarios mientras mantiene su script php en ejecución ha sido un problema desde [PHP] 4.1, cuando register_shutdown_function()se modificó el comportamiento de para que no cerrara automáticamente la conexión de los usuarios.

sts en mail dot xubion dot hu Publicado la solución original:

<?php
header("Connection: close");
ob_start();
phpinfo();
$size = ob_get_length();
header("Content-Length: $size");
ob_end_flush();
flush();
sleep(13);
error_log("do something in the background");
?>

Que funciona bien hasta que sustituir phpinfo()por echo('text I want user to see');en cuyo caso las cabeceras nunca se envían!

La solución es desactivar explícitamente el búfer de salida y borrar el búfer antes de enviar la información del encabezado. Ejemplo:

<?php
ob_end_clean();
header("Connection: close");
ignore_user_abort(true); // just to be safe
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 !
// Do processing here 
sleep(30);
echo('Text user will never see');
?>

Acabo de pasar 3 horas tratando de resolver esto, espero que ayude a alguien :)

Probado en:

  • IE 7.5730.11
  • Mozilla Firefox 1.81

Más tarde, en julio de 2010, en una respuesta relacionada, Arctic Fire luego vinculó dos notas de usuario adicionales que eran seguimientos a la anterior:

Joeri Sebrechts
fuente
7
sí, esto hizo el truco: php.net/manual/en/features.connection-handling.php#71172
Eric_WVGG
1
Autor y @Timbo White, ¿Es posible cerrar una conexión antes de tiempo sin conocer el tamaño del contenido? IE, sin tener que capturar contenido antes del cierre.
skibulk
3
Los piratas informáticos y los navegadores web de mala calidad aún pueden ignorar el encabezado HTTP de cierre de conexión y obtener el resto del resultado ... asegúrese de que lo que viene a continuación no sea confidencial. quizás un ob_start (); para suprimir todo: p
hanshenrik
3
Añadiendo fastcgi_finish_request (); Se ha dicho que cierra con éxito la conexión cuando lo anterior no funciona. Sin embargo, en mi caso impidió que mi script continuara ejecutándose, así que úselo con precaución.
Eric Dubé
@RichardSmith Porque el Connection: closeencabezado puede ser sobrescrito por el otro software en la pila, por ejemplo, el proxy inverso en el caso de un CGI (observé ese comportamiento con nginx). Vea la respuesta de @hanshenrik sobre eso. En general, Connection: closese ejecuta en el lado del cliente y no debe considerarse como una respuesta a esta pregunta. La conexión debe cerrarse desde el lado del servidor .
7heo.tk
56

Es necesario enviar estos 2 encabezados:

Connection: close
Content-Length: n (n = size of output in bytes )

Como necesita saber el tamaño de su salida, deberá almacenar su salida en búfer y luego descargarla en el navegador:

// buffer all upcoming output
ob_start();
echo "We'll email you as soon as this is done.";

// get the size of the output
$size = ob_get_length();

// send headers to tell the browser to close the connection
header("Content-Length: $size");
header('Connection: close');

// flush all output
ob_end_flush();
ob_flush();
flush();

// if you're using sessions, this prevents subsequent requests
// from hanging while the background process executes
if (session_id()) session_write_close();

/******** background process starts here ********/

Además, si su servidor web utiliza la compresión gzip automática en la salida (es decir, Apache con mod_deflate), esto no funcionará porque el tamaño real de la salida cambia y la longitud del contenido ya no es precisa. Desactive la compresión gzip del script en particular.

Para obtener más detalles, visite http://www.zulius.com/how-to/close-browser-connection-continue-execution

Timbo Blanco
fuente
16
Si su servidor comprime la salida, puede deshabilitarla con De header("Content-Encoding: none\r\n");esa manera, apache no la comprimirá.
GDmac
1
@GDmac ¡gracias! No pude hacer que esto funcionara por un tiempo, pero deshabilitar la compresión funcionó.
Reactgular
No ob_flush()es necesario y en realidad provoca un aviso failed to flush buffer. Lo saqué y funcionó muy bien.
Levi
2
Descubrí que la ob_flush()línea era necesaria.
Deebster
21

Puede usar Fast-CGI con PHP-FPM para usar la fastcgi_end_request()función . De esta manera, puede continuar con el procesamiento mientras la respuesta ya ha sido enviada al cliente.

Puede encontrar esto en el manual de PHP aquí: FastCGI Process Manager (FPM) ; Pero esa función específicamente no se documenta más en el manual. Aquí el extracto de PHP-FPM: PHP FastCGI Process Manager Wiki :


fastcgi_finish_request ()

Alcance: función php

Categoría: Optimización

Esta característica le permite acelerar la implementación de algunas consultas php. La aceleración es posible cuando hay acciones en el proceso de ejecución del script que no afectan la respuesta del servidor. Por ejemplo, guardar la sesión en Memcached puede ocurrir después de que la página se haya formado y pasado a un servidor web. fastcgi_finish_request()es una función de php que detiene la salida de respuesta. El servidor web inmediatamente comienza a transferir la respuesta "lenta y tristemente" al cliente, y php al mismo tiempo puede hacer muchas cosas útiles en el contexto de una consulta, como guardar la sesión, convertir el video descargado, manejar todo tipo de estadísticas, etc.

fastcgi_finish_request() puede invocar la ejecución de la función de apagado.


Nota: fastcgi_finish_request() tiene una peculiaridad que las llamadas a flush, printo echodará por terminado el guión antes de tiempo.

Para evitar ese problema, puede llamar ignore_user_abort(true)justo antes o después de la fastcgi_finish_requestllamada:

ignore_user_abort(true);
fastcgi_finish_request();
Jorrit Schippers
fuente
3
ESTA ES UNA RESPUESTA REAL!
Kirill Titov
2
si está usando php-fpm, simplemente use esta función, olvídese de los encabezados y todo lo demás. ¡Me ahorraste tanto tiempo!
Ross
17

Versión completa:

ignore_user_abort(true);//avoid apache to kill the php running
ob_start();//start buffer output

echo "show something to user";
session_write_close();//close session file on server side to avoid blocking other requests

header("Content-Encoding: none");//send header to avoid the browser side to take content as gzip format
header("Content-Length: ".ob_get_length());//send length header
header("Connection: close");//or redirect to some url: header('Location: http://www.google.com');
ob_end_flush();flush();//really send content, can't change the order:1.ob buffer to normal buffer, 2.normal buffer to output

//continue do something on server side
ob_start();
sleep(5);//the user won't wait for the 5 seconds
echo 'for diyism';//user can't see this
file_put_contents('/tmp/process.log', ob_get_contents());
ob_end_clean();
diyismo
fuente
completo en que sentido? ¿Qué problema requirió que completara el script de respuestas aceptadas (¿cuál?) Y cuál de sus diferencias de configuración lo hizo necesario?
hakre
4
esta línea: encabezado ("Codificación de contenido: ninguna"); -> muy importante.
Bobby Tables
2
Gracias, esta es la única solución que funciona en esta página. Esto debería aprobarse como la respuesta.
6

Una mejor solución es bifurcar un proceso en segundo plano. Es bastante sencillo en unix / linux:

<?php
echo "We'll email you as soon as this is done.";
system("php somestuff.php [email protected] >/dev/null &");
?>

Debería mirar esta pregunta para obtener mejores ejemplos:

PHP ejecuta un proceso en segundo plano

Salman A
fuente
4

Suponiendo que tiene un servidor Linux y acceso de root, intente esto. Es la solución más sencilla que he encontrado.

Cree un nuevo directorio para los siguientes archivos y otórguele todos los permisos. (Podemos hacerlo más seguro más adelante).

mkdir test
chmod -R 777 test
cd test

Pon esto en un archivo llamado bgping.

echo starting bgping
ping -c 15 www.google.com > dump.txt &
echo ending bgping

Tenga en cuenta el &. El comando ping se ejecutará en segundo plano mientras el proceso actual pasa al comando echo. Hará ping a www.google.com 15 veces, lo que tardará unos 15 segundos.

Hágalo ejecutable.

chmod 777 bgping

Pon esto en un archivo llamado bgtest.php.

<?php

echo "start bgtest.php\n";
exec('./bgping', $output, $result)."\n";
echo "output:".print_r($output,true)."\n";
echo "result:".print_r($result,true)."\n";
echo "end bgtest.php\n";

?>

Cuando solicite bgtest.php en su navegador, debería obtener la siguiente respuesta rápidamente, sin esperar unos 15 segundos para que se complete el comando ping.

start bgtest.php
output:Array
(
    [0] => starting bgping
    [1] => ending bgping
)

result:0
end bgtest.php

El comando ping ahora debería estar ejecutándose en el servidor. En lugar del comando ping, puede ejecutar un script PHP:

php -n -f largejob.php > dump.txt &

¡Espero que esto ayude!

Liam
fuente
4

Aquí hay una modificación al código de Timbo que funciona con compresión gzip.

// buffer all upcoming output
if(!ob_start("ob_gzhandler")){
    define('NO_GZ_BUFFER', true);
    ob_start();
}
echo "We'll email you as soon as this is done.";

//Flush here before getting content length if ob_gzhandler was used.
if(!defined('NO_GZ_BUFFER')){
    ob_end_flush();
}

// get the size of the output
$size = ob_get_length();

// send headers to tell the browser to close the connection
header("Content-Length: $size");
header('Connection: close');

// flush all output
ob_end_flush();
ob_flush();
flush();

// if you're using sessions, this prevents subsequent requests
// from hanging while the background process executes
if (session_id()) session_write_close();

/******** background process starts here ********/
Andrew Winter
fuente
USTED ES UN DIOS. He estado trabajando durante 2 días para intentar resolver esto. Funcionó en mi desarrollador local pero no en el host. Estaba manchado. ME SALVASTE. ¡¡¡¡GRACIAS!!!!
Chad Caldwell
3

Estoy en un host compartido y estoy fastcgi_finish_requestconfigurado para salir de los scripts por completo. Tampoco me gusta la connection: closesolución. Su uso fuerza una conexión separada para solicitudes posteriores, lo que cuesta recursos adicionales del servidor. Leí el Transfer-Encoding: cunked artículo de Wikipedia y supe que 0\r\n\r\ntermina una respuesta. No lo he probado a fondo en todas las versiones de navegadores y dispositivos, pero funciona en los 4 de mis navegadores actuales.

// Disable automatic compression
// @ini_set('zlib.output_compression', 'Off');
// @ini_set('output_buffering', 'Off');
// @ini_set('output_handler', '');
// @apache_setenv('no-gzip', 1);

// Chunked Transfer-Encoding & Gzip Content-Encoding
function ob_chunked_gzhandler($buffer, $phase) {
    if (!headers_sent()) header('Transfer-Encoding: chunked');
    $buffer = ob_gzhandler($buffer, $phase);
    return dechex(strlen($buffer))."\r\n$buffer\r\n";
}

ob_start('ob_chunked_gzhandler');

// First Chunk
echo "Hello World";
ob_flush();

// Second Chunk
echo ", Grand World";
ob_flush();

ob_end_clean();

// Terminating Chunk
echo "\x30\r\n\r\n";
ob_flush();
flush();

// Post Processing should not be displayed
for($i=0; $i<10; $i++) {
    print("Post-Processing");
    sleep(1);
}
skibulk
fuente
Gracias a tu buena respuesta, me di cuenta de lo estúpido (e innecesario) que es usar connection: close. Supongo que algunos no están familiarizados con las tuercas y tornillos de su servidor.
Justin
@Justin Escribí esto hace mucho tiempo. Mirándolo de nuevo, debo tener en cuenta que puede ser necesario rellenar los trozos a 4 KB. Me parece recordar que algunos servidores no se descargarán hasta que alcancen ese mínimo.
skibulk
2

Podría intentar hacer varios subprocesos.

podría crear un script que haga una llamada al sistema (usando shell_exec ) que llame al binario php con el script para hacer su trabajo como parámetro. Pero no creo que esa sea la forma más segura. Tal vez puedas mejorar las cosas haciendo chroot en el proceso php y otras cosas

Alternativamente, hay una clase en phpclasses que hace eso http://www.phpclasses.org/browse/package/3953.html . Pero no conozco los detalles de la implementación.

paan
fuente
Y si no desea esperar a que finalice el proceso, utilice el &personaje para ejecutar el proceso en segundo plano.
Liam
2

TL; DR Respuesta:

ignore_user_abort(true); //Safety measure so that the user doesn't stop the script too early.

$content = 'Hello World!'; //The content that will be sent to the browser.

header('Content-Length: ' . strlen($content)); //The browser will close the connection when the size of the content reaches "Content-Length", in this case, immediately.

ob_start(); //Content past this point...

echo $content;

//...will be sent to the browser (the output buffer gets flushed) when this code executes.
ob_end_flush();
ob_flush();
flush();

if(session_id())
{
    session_write_close(); //Closes writing to the output buffer.
}

//Anything past this point will be ran without involving the browser.

Respuesta de función:

ignore_user_abort(true);

function sendAndAbort($content)
{
    header('Content-Length: ' . strlen($content));

    ob_start();

    echo $content;

    ob_end_flush();
    ob_flush();
    flush();
}

sendAndAbort('Hello World!');

//Anything past this point will be ran without involving the browser.
FluorescenteVerde5
fuente
1

Su problema puede resolverse realizando una programación paralela en php. Hice una pregunta al respecto hace unas semanas aquí: ¿Cómo se pueden usar subprocesos múltiples en aplicaciones PHP?

Y obtuve excelentes respuestas. Me gustó mucho uno en particular. El escritor hizo una referencia al tutorial Easy Parallel Processing en PHP (septiembre de 2008; por johnlim) que realmente puede resolver su problema muy bien, ya que ya lo he usado para tratar un problema similar que surgió hace un par de días.

Steve Obbayi
fuente
1

La respuesta de Joeri Sebrechts es cercana, pero destruye cualquier contenido existente que pueda almacenarse en búfer antes de que desee desconectarse. No llama ignore_user_abortcorrectamente, lo que permite que el script finalice prematuramente. La respuesta del diyismo es buena pero no es aplicable genéricamente. Por ejemplo, una persona puede tener más o menos búferes de salida que esa respuesta no maneja, por lo que es posible que simplemente no funcione en su situación y no sepa por qué.

Esta función le permite desconectarse en cualquier momento (siempre que los encabezados no se hayan enviado todavía) y retiene el contenido que ha generado hasta el momento. El tiempo de procesamiento adicional es ilimitado por defecto.

function disconnect_continue_processing($time_limit = null) {
    ignore_user_abort(true);
    session_write_close();
    set_time_limit((int) $time_limit);//defaults to no limit
    while (ob_get_level() > 1) {//only keep the last buffer if nested
        ob_end_flush();
    }
    $last_buffer = ob_get_level();
    $length = $last_buffer ? ob_get_length() : 0;
    header("Content-Length: $length");
    header('Connection: close');
    if ($last_buffer) {
        ob_end_flush();
    }
    flush();
}

Si también necesita memoria adicional, asígnela antes de llamar a esta función.

Walf
fuente
1

Nota para los usuarios de mod_fcgid (utilice bajo su propio riesgo).

Solución rápida

La respuesta aceptada de Joeri Sebrechts es de hecho funcional. Sin embargo, si usa mod_fcgid, es posible que esta solución no funcione por sí sola. En otras palabras, cuando se llama a la función de descarga , la conexión con el cliente no se cierra.

El FcgidOutputBufferSizeparámetro de configuración de mod_fcgid puede ser el culpable. He encontrado este consejo en:

  1. esta respuesta de Travers Carter y
  2. esta publicación de blog de Seumas Mackinnon .

Después de leer lo anterior, puede llegar a la conclusión de que una solución rápida sería agregar la línea (consulte "Ejemplo de host virtual" al final):

FcgidOutputBufferSize 0

en su archivo de configuración de Apache (por ejemplo, httpd.conf), su archivo de configuración de FCGI (por ejemplo, fcgid.conf) o en su archivo de hosts virtuales (por ejemplo, httpd-vhosts.conf).

En (1) arriba, se menciona una variable llamada "OutputBufferSize". Este es el nombre antiguo del FcgidOutputBufferSizemencionado en (2) (consulte las notas de actualización en la página web de Apache para mod_fcgid ).

Detalles y una segunda solución

La solución anterior deshabilita el almacenamiento en búfer realizado por mod_fcgid para todo el servidor o para un host virtual específico. Esto puede provocar una penalización del rendimiento de su sitio web. Por otro lado, este puede no ser el caso, ya que PHP realiza el almacenamiento en búfer por sí solo.

En caso de que no desee deshabilitar el almacenamiento en búfer de mod_fcgid , existe otra solución ... puede forzar el vaciado de este búfer .

El siguiente código hace precisamente eso basándose en la solución propuesta por Joeri Sebrechts:

<?php
    ob_end_clean();
    header("Connection: close");
    ignore_user_abort(true); // just to be safe
    ob_start();
    echo('Text the user will see');

    echo(str_repeat(' ', 65537)); // [+] Line added: Fill up mod_fcgi's buffer.

    $size = ob_get_length();
    header("Content-Length: $size");
    ob_end_flush(); // Strange behaviour, will not work
    flush(); // Unless both are called !
    // Do processing here 
    sleep(30);
    echo('Text user will never see');
?>

Lo que la línea de código agregada esencialmente hace es llenar el búfer de mod_fcgi , forzándolo así a vaciarse. Se eligió el número "65537" porque el valor predeterminado de la FcgidOutputBufferSizevariable es "65536", como se menciona en la página web de Apache para la directiva correspondiente . Por lo tanto, es posible que deba ajustar este valor en consecuencia si se establece otro valor en su entorno.

Mi entorno

  • WampServer 2.5
  • Apache 2.4.9
  • PHP 5.5.19 VC11, x86, no seguro para subprocesos
  • mod_fcgid / 2.3.9
  • Windows 7 Professional x64

Ejemplo de host virtual

<VirtualHost *:80>
    DocumentRoot "d:/wamp/www/example"
    ServerName example.local

    FcgidOutputBufferSize 0

    <Directory "d:/wamp/www/example">
        Require all granted
    </Directory>
</VirtualHost>
Tasos
fuente
Probé muchas soluciones. Y esta es la única solución que me funciona con mod_fcgid.
Tsounabe
1

esto funcionó para mí

//avoid apache to kill the php running
ignore_user_abort(true);
//start buffer output
ob_start();

echo "show something to user1";
//close session file on server side to avoid blocking other requests
session_write_close();

//send length header
header("Content-Length: ".ob_get_length());
header("Connection: close");
//really send content, can't change the order:
//1.ob buffer to normal buffer,
//2.normal buffer to output
ob_end_flush();
flush();
//continue do something on server side
ob_start();
//replace it with the background task
sleep(20);
Williem
fuente
0

Ok, básicamente de la forma en que jQuery hace la solicitud XHR, incluso el método ob_flush no funcionará porque no puede ejecutar una función en cada cambio de estado onready. jQuery verifica el estado y luego elige las acciones adecuadas a tomar (completa, error, éxito, tiempo de espera). Y aunque no pude encontrar una referencia, recuerdo haber escuchado que esto no funciona con todas las implementaciones de XHR. Un método que creo que debería funcionar para usted es un cruce entre el sondeo ob_flush y forever-frame.

<?php
 function wrap($str)
 {
  return "<script>{$str}</script>";
 };

 ob_start(); // begin buffering output
 echo wrap("console.log('test1');");
 ob_flush(); // push current buffer
 flush(); // this flush actually pushed to the browser
 $t = time();
 while($t > (time() - 3)) {} // wait 3 seconds
 echo wrap("console.log('test2');");
?>

<html>
 <body>
  <iframe src="ob.php"></iframe>
 </body>
</html>

Y debido a que los scripts se ejecutan en línea, a medida que se vacían los búferes, se obtiene la ejecución. Para que esto sea útil, cambie el archivo console.log a un método de devolución de llamada definido en la configuración de su script principal para recibir datos y actuar sobre ellos. Espero que esto ayude. Salud, Morgan.

Morgan ARR Allen
fuente
0

Una solución alternativa es agregar el trabajo a una cola y crear un script cron que verifique si hay nuevos trabajos y los ejecutará.

Tuve que hacerlo de esa manera recientemente para eludir los límites impuestos por un host compartido: exec () et al estaba deshabilitado para PHP ejecutado por el servidor web, pero podía ejecutarse en un script de shell.

Ole Helgesen
fuente
0

Si la flush()función no funciona. Debe configurar las siguientes opciones en php.ini como:

output_buffering = Off  
zlib.output_compression = Off  
Nir O.
fuente
0

Última solución de trabajo

    // client can see outputs if any
    ignore_user_abort(true);
    ob_start();
    echo "success";
    $buffer_size = ob_get_length();
    session_write_close();
    header("Content-Encoding: none");
    header("Content-Length: $buffer_size");
    header("Connection: close");
    ob_end_flush();
    ob_flush();
    flush();

    sleep(2);
    ob_start();
    // client cannot see the result of code below
Sudharshana SL Srihari
fuente
0

Después de probar muchas soluciones diferentes de este hilo (después de que ninguna de ellas funcionó para mí), encontré una solución en la página oficial de PHP.net:

function sendResponse($response) {
    ob_end_clean();
    header("Connection: close\r\n");
    header("Content-Encoding: none\r\n");
    ignore_user_abort(true);
    ob_start();

    echo $response; // Actual response that will be sent to the user

    $size = ob_get_length();
    header("Content-Length: $size");
    ob_end_flush();
    flush();
    if (ob_get_contents()) {
        ob_end_clean();
    }
}
Ángel blanco
fuente