Come si chiude una connessione in anticipo?


99

Sto tentando di eseguire una chiamata AJAX (tramite JQuery) che avvierà un processo abbastanza lungo. Vorrei che lo script inviasse semplicemente una risposta che indica che il processo è iniziato, ma JQuery non restituirà la risposta fino a quando lo script PHP non sarà terminato.

L'ho provato con un'intestazione "close" (sotto) e anche con il buffering dell'output; nessuno dei due sembra funzionare. Qualche ipotesi? o è qualcosa che devo fare in 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( 'dude@thatplace.com', "okay I'm done", 'Yup, all done.' );

?>

hai svuotato il buffer di output con ob_flush () e non ha funzionato?
Vinko Vrsalovic

Risposte:


87

La seguente pagina di manuale PHP (incl. Note per l'utente) suggerisce più istruzioni su come chiudere la connessione TCP al browser senza terminare lo script PHP:

Presumibilmente richiede un po 'di più dell'invio di un'intestazione chiusa.


OP quindi conferma: sì, questo ha funzionato: indicando la nota utente n. 71172 (novembre 2006) copiata qui:

Chiudere la connessione al browser degli utenti mantenendo in esecuzione lo script php è stato un problema sin da [PHP] 4.1, quando il comportamento di è register_shutdown_function()stato modificato in modo che non chiudesse automaticamente la connessione degli utenti.

sts at mail dot xubion dot hu Ha pubblicato la soluzione originale:

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

Che funziona bene fino a quando si sostituisce phpinfo()per echo('text I want user to see');, nel qual caso le intestazioni vengono mai inviati!

La soluzione è disattivare esplicitamente il buffer di output e svuotare il buffer prima di inviare le informazioni di intestazione. Esempio:

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

Ho appena trascorso 3 ore cercando di capire questo, spero che aiuti qualcuno :)

Testato in:

  • IE 7.5730.11
  • Mozilla Firefox 1.81

Più tardi, nel luglio 2010, in una risposta correlata, Arctic Fire ha quindi collegato due ulteriori note utente che erano un follow-up a quella sopra:



1
Autore e @Timbo White, è possibile chiudere una connessione in anticipo senza conoscere la dimensione del contenuto? IE, senza dover acquisire i contenuti prima della chiusura.
skibulk

3
Gli hacker ei browser Web scadenti possono ancora ignorare l'intestazione HTTP di chiusura della connessione e ottenere il resto dell'output .. assicurarsi che ciò che viene dopo non sia sensibile. forse un ob_start (); per sopprimere tutto: p
hanshenrik

3
Aggiunta di fastcgi_finish_request (); è stato detto che chiude con successo la connessione quando quanto sopra non funziona. Tuttavia, nel mio caso ha impedito al mio script di continuare l'esecuzione, quindi usalo con cautela.
Eric Dubé

@RichardSmith Perché l' Connection: closeintestazione può essere sovrascritta dall'altro software nello stack, ad esempio, il proxy inverso nel caso di un CGI (ho osservato quel comportamento con nginx). Vedi la risposta di @hanshenrik a riguardo. In generale, Connection: closeviene eseguito sul lato client e non dovrebbe essere considerato come una risposta a questa domanda. La connessione dovrebbe essere chiusa dal lato server .
7heo.tk

56

È necessario inviare queste 2 intestazioni:

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

Poiché è necessario conoscere la dimensione dell'output, è necessario eseguire il buffer dell'output, quindi scaricarlo nel browser:

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

Inoltre, se il tuo server web utilizza la compressione gzip automatica sull'output (es. Apache con mod_deflate), questo non funzionerà perché la dimensione effettiva dell'output viene modificata e Content-Length non è più accurato. Disabilita la compressione gzip del particolare script.

Per maggiori dettagli, visita http://www.zulius.com/how-to/close-browser-connection-continue-execution


16
Se il tuo server comprime l'output, puoi disabilitarlo con In header("Content-Encoding: none\r\n");questo modo apache non lo comprimerà.
GDmac

1
@GDmac grazie! Non sono riuscito a farlo funzionare per un po ', ma disabilitare la compressione ha funzionato.
Reactgular

Il ob_flush()non è necessario e provoca effettivamente un avviso failed to flush buffer. L'ho tolto e ha funzionato benissimo.
Levi

2
Ho scoperto che la ob_flush()linea era necessaria.
Deebster

21

È possibile utilizzare Fast-CGI con PHP-FPM per utilizzare la fastcgi_end_request()funzione . In questo modo, puoi continuare a eseguire alcune elaborazioni mentre la risposta è già stata inviata al client.

Lo trovi nel manuale PHP qui: FastCGI Process Manager (FPM) ; Ma quella funzione specificamente non è ulteriormente documentata nel manuale. Ecco l'estratto dal PHP-FPM: PHP FastCGI Process Manager Wiki :


fastcgi_finish_request ()

Ambito: funzione php

Categoria: ottimizzazione

Questa funzionalità ti consente di velocizzare l'implementazione di alcune query php. L'accelerazione è possibile quando sono presenti azioni nel processo di esecuzione dello script che non influiscono sulla risposta del server. Ad esempio, il salvataggio della sessione in memcached può avvenire dopo che la pagina è stata creata e passata a un server web. fastcgi_finish_request()è una funzionalità php, che arresta l'output di risposta. Il server Web inizia immediatamente a trasferire la risposta "lentamente e tristemente" al client e php allo stesso tempo può fare molte cose utili nel contesto di una query, come salvare la sessione, convertire il video scaricato, gestire tutti i tipi di statistiche, ecc.

fastcgi_finish_request() può richiamare l'esecuzione della funzione di arresto.


Nota: fastcgi_finish_request() ha una stranezza in cui le chiamate a flush, printo echoterminerà la sceneggiatura in anticipo.

Per evitare questo problema, puoi chiamare ignore_user_abort(true)subito prima o dopo la fastcgi_finish_requestchiamata:

ignore_user_abort(true);
fastcgi_finish_request();

3
QUESTA È LA RISPOSTA EFFETTIVA!
Kirill Titov

2
se stai usando php-fpm - usa questa funzione - dimentica le intestazioni e tutto il resto. Mi ha fatto risparmiare così tanto tempo!
Ross

17

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

completo in che senso? Quale problema richiedeva di completare lo script delle risposte accettate (quale?) E quali differenze di configurazione lo rendevano necessario?
hakre

4
questa riga: header ("Content-Encoding: none"); -> molto importante.
Bobby Tables,

2
Grazie, questa è l'unica soluzione funzionante in questa pagina. Questo dovrebbe essere approvato come risposta.

6

Una soluzione migliore è eseguire il fork di un processo in background. È abbastanza semplice su unix / linux:

<?php
echo "We'll email you as soon as this is done.";
system("php somestuff.php dude@thatplace.com >/dev/null &");
?>

Dovresti guardare questa domanda per esempi migliori:

PHP esegue un processo in background


4

Supponendo che tu abbia un server Linux e accesso root, prova questo. È la soluzione più semplice che ho trovato.

Crea una nuova directory per i seguenti file e dagli le autorizzazioni complete. (Possiamo renderlo più sicuro in seguito.)

mkdir test
chmod -R 777 test
cd test

Mettilo in un file chiamato bgping.

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

Nota il &. Il comando ping verrà eseguito in background mentre il processo corrente passa al comando echo. Eseguirà un ping su www.google.com 15 volte, operazione che richiederà circa 15 secondi.

Rendilo eseguibile.

chmod 777 bgping

Mettilo in un file chiamato 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";

?>

Quando richiedi bgtest.php nel tuo browser, dovresti ottenere rapidamente la seguente risposta, senza attendere circa 15 secondi per il completamento del comando ping.

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

result:0
end bgtest.php

Il comando ping dovrebbe ora essere in esecuzione sul server. Invece del comando ping, potresti eseguire uno script PHP:

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

Spero che questo ti aiuti!


4

Ecco una modifica al codice di Timbo che funziona con la compressione 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 ********/

SEI UN DIO. Ho lavorato per 2 giorni per cercare di capirlo. Ha funzionato sul mio dev locale ma non sull'host. Sono stato lavato. MI HAI SALVATO. GRAZIE!!!!
Chad Caldwell

3

Sono su un host condiviso ed fastcgi_finish_requestè configurato per uscire completamente dagli script. Neanche a me piace la connection: closesoluzione. Il suo utilizzo impone una connessione separata per le richieste successive, con un costo aggiuntivo delle risorse del server. Ho letto l' Transfer-Encoding: cunked articolo di Wikipedia e ho scoperto che 0\r\n\r\ntermina una risposta. Non l'ho testato a fondo su versioni e dispositivi dei browser, ma funziona su tutti e 4 i miei browser attuali.

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

Grazie alla tua buona risposta, mi sono reso conto di quanto sia stupido (e non necessario) usare connection: close. Immagino che alcuni non abbiano familiarità con i dadi e i bulloni del loro server.
Justin

@Justin L'ho scritto molto tempo fa. Guardandolo di nuovo, dovrei notare che potrebbe essere necessario riempire i blocchi a 4KB. Mi sembra di ricordare che alcuni server non scaricheranno finché non raggiungeranno quel minimo.
skibulk

2

Potresti provare a fare il multithreading.

potresti creare uno script che effettua una chiamata di sistema (usando shell_exec ) che chiama il binario php con lo script per fare il tuo lavoro come parametro. Ma non credo che sia il modo più sicuro. Forse puoi rafforzare le cose eseguendo il chroot del processo php e altre cose

In alternativa, c'è una classe in phpclasses che lo fa http://www.phpclasses.org/browse/package/3953.html . Ma non conosco le specifiche dell'implementazione


E se non vuoi aspettare che il processo finisca, usa il &carattere per eseguire il processo in background.
Liam

2

TL; Risposta DR:

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.

Risposta di funzione:

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.

1

Il tuo problema può essere risolto eseguendo un po 'di programmazione parallela in php. Ho posto una domanda al riguardo alcune settimane fa qui: Come si può usare il multi threading nelle applicazioni PHP

E ho ottenuto ottime risposte. Uno in particolare mi è piaciuto molto. Lo scrittore ha fatto riferimento al tutorial Easy Parallel Processing in PHP (settembre 2008; di johnlim) che può effettivamente risolvere il tuo problema molto bene poiché l'ho già usato per affrontare un problema simile che è emerso un paio di giorni fa.


1

La risposta di Joeri Sebrechts è vicina, ma distrugge qualsiasi contenuto esistente che potrebbe essere memorizzato nel buffer prima che tu desideri disconnetterti. Non chiama ignore_user_abortcorrettamente, consentendo allo script di terminare prematuramente. La risposta del fai da te è buona ma non è genericamente applicabile. Ad esempio, una persona potrebbe avere buffer di output maggiori o minori che quella risposta non gestisce, quindi potrebbe semplicemente non funzionare nella tua situazione e non saprai perché.

Questa funzione ti consente di disconnetterti in qualsiasi momento (purché le intestazioni non siano state ancora inviate) e conserva il contenuto che hai generato finora. Il tempo di elaborazione aggiuntivo è illimitato per impostazione predefinita.

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

Se hai bisogno anche di memoria aggiuntiva, allocala prima di chiamare questa funzione.


1

Nota per gli utenti mod_fcgid (per favore, usa a tuo rischio).

Soluzione rapida

La risposta accettata di Joeri Sebrechts è davvero funzionale. Tuttavia, se usi mod_fcgid potresti scoprire che questa soluzione non funziona da sola. In altre parole, quando viene chiamata la funzione flush , la connessione al client non viene chiusa.

La colpa potrebbe essere FcgidOutputBufferSizedel parametro di configurazione di mod_fcgid . Ho trovato questo suggerimento in:

  1. questa risposta di Travers Carter e
  2. questo post sul blog di Seumas Mackinnon .

Dopo aver letto quanto sopra, potresti giungere alla conclusione che una soluzione rapida sarebbe aggiungere la riga (vedi "Esempio di host virtuale" alla fine):

FcgidOutputBufferSize 0

nel file di configurazione di Apache (ad esempio, httpd.conf), nel file di configurazione FCGI (ad esempio fcgid.conf) o nel file degli host virtuali (ad esempio, httpd-vhosts.conf).

In (1) sopra, viene menzionata una variabile denominata "OutputBufferSize". Questo è il vecchio nome del FcgidOutputBufferSizemenzionato in (2) (vedere le note di aggiornamento nella pagina web di Apache per mod_fcgid ).

Dettagli e una seconda soluzione

La soluzione precedente disabilita il buffering eseguito da mod_fcgid per l'intero server o per uno specifico host virtuale. Ciò potrebbe comportare una riduzione delle prestazioni del tuo sito web. D'altra parte, questo potrebbe non essere il caso poiché PHP esegue il buffering da solo.

Nel caso in cui non desideri disabilitare il buffering di mod_fcgid , c'è un'altra soluzione ... puoi forzare lo scaricamento di questo buffer .

Il codice seguente fa proprio questo basandosi sulla soluzione proposta da 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');
?>

Ciò che essenzialmente fa la riga di codice aggiunta è riempire il buffer di mod_fcgi , costringendolo così a svuotarlo . Il numero "65537" è stato scelto perché il valore predefinito della FcgidOutputBufferSizevariabile è "65536", come menzionato nella pagina web di Apache per la direttiva corrispondente . Pertanto, potrebbe essere necessario regolare questo valore di conseguenza se è impostato un altro valore nel proprio ambiente.

Il mio ambiente

  • WampServer 2.5
  • Apache 2.4.9
  • PHP 5.5.19 VC11, x86, non thread-safe
  • mod_fcgid / 2.3.9
  • Windows 7 Professional x64

Host virtuale di esempio

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

    FcgidOutputBufferSize 0

    <Directory "d:/wamp/www/example">
        Require all granted
    </Directory>
</VirtualHost>

Ho provato molte soluzioni. E questa è l'unica soluzione che funziona per me con mod_fcgid.
Tsounabe

1

questo ha funzionato per me

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

0

Ok, quindi fondamentalmente il modo in cui jQuery esegue la richiesta XHR, anche il metodo ob_flush non funzionerà perché non sei in grado di eseguire una funzione su ogni onreadystatechange. jQuery controlla lo stato, quindi sceglie le azioni corrette da intraprendere (completo, errore, riuscito, timeout). E anche se non sono stato in grado di trovare un riferimento, ricordo di aver sentito che questo non funziona con tutte le implementazioni XHR. Un metodo che credo dovrebbe funzionare per te è un incrocio tra ob_flush e il polling del frame per sempre.

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

E poiché gli script vengono eseguiti in linea, quando i buffer vengono svuotati, si ottiene l'esecuzione. Per renderlo utile, cambia console.log in un metodo di callback definito nella configurazione dello script principale per ricevere i dati e agire su di essi. Spero che questo ti aiuti. Salute, Morgan.


0

Una soluzione alternativa è aggiungere il lavoro a una coda e creare uno script cron che controlli i nuovi lavori e li esegua.

Ho dovuto farlo in questo modo di recente per aggirare i limiti imposti da un host condiviso: exec () e altri erano disabilitati per PHP eseguito dal server web ma potevano essere eseguiti in uno script di shell.


0

Se la flush()funzione non funziona. È necessario impostare le opzioni successive in php.ini come:

output_buffering = Off  
zlib.output_compression = Off  

0

Ultima soluzione funzionante

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

0

Dopo aver provato molte soluzioni diverse da questo thread (dopo che nessuna di esse ha funzionato per me), ho trovato la soluzione sulla pagina ufficiale di 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();
    }
}
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.