web-dev-qa-db-fra.com

fermer une connexion tôt

J'essaie de passer un appel AJAX (via JQuery) qui lancera un processus assez long. J'aimerais que le script envoie simplement une réponse indiquant que le processus a démarré, mais JQuery ne renverra pas la réponse tant que le script PHP n'est pas terminé.

J'ai essayé cela avec un en-tête "close" (ci-dessous), et aussi avec la mise en mémoire tampon de sortie; ni semble fonctionner. Des suppositions? ou est-ce quelque chose que je dois faire dans 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.' );

?>
88
Eric_WVGG

La page de manuel suivante PHP (y compris les notes de l'utilisateur) suggère plusieurs instructions pour fermer la connexion TCP au navigateur sans mettre fin au script PHP:

Soi-disant, cela nécessite un peu plus que d'envoyer un en-tête proche.


OP confirme ensuite: Oui, cela a été le cas: pointant vers la note utilisateur n ° 71172 (nov 2006) copié ici:

Fermer la connexion du navigateur des utilisateurs tout en maintenant votre script php en marche est un problème depuis [PHP] 4.1, lorsque le comportement de register_shutdown_function() a été modifié afin de ne pas fermer automatiquement la connexion des utilisateurs.

sts à la poste point xubion point hu Posté la solution 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");
?>

Ce qui fonctionne bien jusqu'à ce que vous remplaciez phpinfo() par echo('text I want user to see');, auquel cas les en-têtes ne sont jamais envoyés!

La solution consiste à désactiver explicitement la mise en tampon de sortie et à vider la mémoire tampon avant d'envoyer les informations de votre en-tête. Exemple:

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

Je viens de passer 3 heures à essayer de comprendre celui-ci, j'espère que cela aidera quelqu'un :)

Testé dans:

  • IE 7.5730.11
  • Mozilla Firefox 1.81

Plus tard en juillet 2010 dans un réponse liée Arctic Fire puis lié deux autres notes d'utilisateur ces suivis de celui ci-dessus:

83
Joeri Sebrechts

Il faut envoyer ces 2 entêtes:

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

Comme vous devez connaître la taille de votre sortie, vous devez la mettre en mémoire tampon, puis la vider dans le navigateur:

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

De plus, si votre serveur Web utilise la compression gzip automatique sur la sortie (c'est-à-dire Apache avec mod_deflate), cela ne fonctionnera pas car la taille réelle de la sortie est modifiée et la longueur de contenu n'est plus précise. Désactivez la compression gzip du script particulier.

Pour plus de détails, visitez http://www.zulius.com/how-to/close-browser-connection-continue-execution

53
Timbo White

Version complète:

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();
14
diyism

Vous pouvez utiliser Fast-CGI avec PHP-FPM pour utiliser la fonction fastcgi_end_request() . De cette manière, vous pouvez continuer à effectuer certains traitements alors que la réponse a déjà été envoyée au client.

Vous le trouverez dans le manuel PHP ici: FastCGI Process Manager (FPM) ; Mais cette fonction n’est pas explicitement décrite dans le manuel. Voici l'extrait du PHP-FPM: PHP Wiki de FastCGI Process Manager :


fastcgi_finish_request ()

Portée: Fonction php Catégorie: Optimisation

Cette fonctionnalité vous permet d'accélérer la mise en œuvre de certaines requêtes php. L'accélération est possible lorsque des actions du processus d'exécution du script n'affectent pas la réponse du serveur. Par exemple, l'enregistrement de la session dans memcached peut avoir lieu une fois la page formée et transmise à un serveur Web. fastcgi_finish_request() est une fonctionnalité php qui arrête la réponse. Le serveur Web commence immédiatement à transférer la réponse «lentement et tristement» au client, et php en même temps peut faire beaucoup de choses utiles dans le contexte d'une requête, comme enregistrer la session, convertir la vidéo téléchargée, gérer tous de statistiques, etc.

fastcgi_finish_request() peut appeler l'exécution de la fonction d'arrêt.

13
Jorrit Schippers

En supposant que vous ayez un serveur Linux et un accès root, essayez ceci. C'est la solution la plus simple que j'ai trouvée.

Créez un nouveau répertoire pour les fichiers suivants et accordez-lui toutes les autorisations. (Nous pouvons le rendre plus sûr plus tard.)

mkdir test
chmod -R 777 test
cd test

Placez ceci dans un fichier nommé bgping.

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

Notez le &. La commande ping sera exécutée en arrière-plan, tandis que le processus en cours passera à la commande echo . Il lancera un ping sur www.google.com 15 fois, ce qui prendra environ 15 secondes.

Rendez-le exécutable.

chmod 777 bgping

Placez ceci dans un fichier appelé 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";

?>

Lorsque vous demandez le fichier bgtest.php dans votre navigateur, vous devez obtenir rapidement la réponse suivante sans attendre environ 15 secondes la fin de la commande ping.

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

result:0
end bgtest.php

La commande ping devrait maintenant être en cours d'exécution sur le serveur. Au lieu de la commande ping, vous pouvez exécuter un script PHP:

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

J'espère que cela t'aides!

4
Liam

Voici une modification du code de Timbo qui fonctionne avec la compression 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 ********/
3
AWinter

Une meilleure solution consiste à créer un processus d'arrière-plan. C'est assez simple sous unix/linux:

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

Vous devriez regarder cette question pour de meilleurs exemples: 

PHP exécuter un processus en arrière-plan

3
Salman A

Vous pouvez essayer de faire du multithreading.

vous pouvez créer un script qui effectue un appel système (à l'aide de Shell_exec ) qui appelle le binaire php avec le script pour effectuer votre travail en tant que paramètre. Mais je ne pense pas que ce soit le moyen le plus sûr. Peut-être que vous pouvez resserrer les choses en chrootant le processus php et autres

Sinon, il existe une classe chez phpclasses qui fait cela http://www.phpclasses.org/browse/package/3953.html . Mais je ne connais pas les détails de la mise en œuvre

2
paan

Je suis sur un hôte partagé et fastcgi_finish_request est configuré pour quitter complètement les scripts. Je n'aime pas non plus la solution connection: close. Son utilisation force une connexion distincte pour les demandes suivantes, entraînant des coûts supplémentaires pour les ressources du serveur. J'ai lu le Transfer-Encoding: cunkedArticle Wikipedia et appris que 0\r\n\r\n met fin à une réponse. Je n'ai pas testé cela à fond sur les versions et les appareils des navigateurs, mais cela fonctionne sur les 4 de mes navigateurs actuels.

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

La réponse de Joeri Sebrechts est proche, mais elle détruit tout contenu existant pouvant être mis en mémoire tampon avant que vous ne souhaitiez vous déconnecter. Il n'appelle pas ignore_user_abort correctement, ce qui permet au script de se terminer prématurément. La réponse du diyisme est bonne mais n'est pas génériquement applicable. Par exemple. une personne peut avoir plus ou moins de tampons de sortie que cette réponse ne gère pas, elle risque donc de ne pas fonctionner dans votre cas et vous ne saurez pas pourquoi.

Cette fonction vous permet de vous déconnecter à tout moment (tant que les en-têtes n'ont pas encore été envoyés) et conserve le contenu que vous avez généré jusqu'à présent. Le temps de traitement supplémentaire est illimité par défaut.

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 vous avez également besoin de plus de mémoire, allouez-la avant d'appeler cette fonction.

1
Walf

TL; DR Réponse:

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.

Réponse de fonction:

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
FluorescentGreen5

cela a fonctionné pour moi

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

Votre problème peut être résolu en faisant de la programmation parallèle en php. J'ai posé une question à ce sujet il y a quelques semaines: Comment utiliser le multi-threading dans les applications PHP

Et obtenu d'excellentes réponses. J'ai beaucoup aimé un en particulier. L’écrivain a fait référence au tutoriel Easy Parallel Processing dans PHP (septembre 2008; par johnlim) qui peut réellement résoudre votre problème très bien, car je l’avais déjà utilisé problème similaire qui est apparu il y a quelques jours.

0
Steve Obbayi

Si flush() la fonction ne fonctionne pas. Vous devez définir les options suivantes dansphp.inilike:

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

Ok, donc, de manière générale, jQuery traite la demande XHR. Même la méthode ob_flush ne fonctionnera pas car vous ne pourrez pas exécuter de fonction à chaque changement de lecture. jQuery vérifie l'état, puis choisit les actions appropriées à effectuer (complète, erreur, succès, délai d'attente). Et bien que je ne sois pas en mesure de trouver une référence, je me souviens avoir entendu dire que cela ne fonctionnait pas avec toutes les implémentations XHR ... Une méthode qui, à mon avis, devrait fonctionner pour vous est un croisement entre le poll ob_flush et le scrutation à perpétuité.

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

Et comme les scripts sont exécutés en ligne, les tampons étant vidés, vous obtenez l'exécution. Pour que cela soit utile, changez le fichier console.log avec une méthode de rappel définie dans la configuration de votre script principal pour recevoir des données et agir en conséquence. J'espère que cela t'aides. Cordialement, Morgan.

0
Morgan ARR Allen

Une autre solution consiste à ajouter le travail à une file d'attente et à créer un script cron qui vérifie les nouveaux travaux et les exécute.

Je devais le faire de cette façon récemment pour contourner les limites imposées par un hôte partagé - exec () et al ont été désactivés pour PHP exécuté par le serveur Web, mais pouvaient être exécutés dans un script Shell.

0
Ole Helgesen

Dernière solution de travail

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

Après avoir essayé différentes solutions de ce fil (aucune d'entre elles ne fonctionnant pour moi), j'ai trouvé une solution sur la page officielle 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();
    }
}
0
WhiteAngel