Comment fermer une connexion plus tôt?


99

J'essaye de faire 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 commencé, mais JQuery ne retournera pas la réponse tant que le script PHP n'est pas exécuté.

J'ai essayé cela avec un en-tête "close" (ci-dessous), et aussi avec la mise en mémoire tampon de sortie; ni l'un ni l'autre ne 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( 'dude@thatplace.com', "okay I'm done", 'Yup, all done.' );

?>

avez-vous vidé votre tampon de sortie avec ob_flush () et cela n'a pas fonctionné?
Vinko Vrsalovic le

Réponses:


87

La page de manuel PHP suivante (y compris les notes de l'utilisateur) suggère plusieurs instructions sur la façon de fermer la connexion TCP au navigateur sans terminer le script PHP:

Soi-disant, cela nécessite un peu plus que l'envoi d'un en-tête fermé.


OP confirme alors: oui, cela a fait l'affaire: pointant vers la note utilisateur # 71172 (novembre 2006) copiée ici:

Fermer la connexion du navigateur des utilisateurs tout en maintenant votre script php en cours d'exécution est un problème depuis [PHP] 4.1, lorsque le comportement de register_shutdown_function() été modifié afin qu'il ne ferme pas automatiquement la connexion des utilisateurs.

sts at mail dot xubion dot hu Publié 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 substituez phpinfo()pour echo('text I want user to see');dans ce cas , les en- têtes ne sont jamais envoyés!

La solution consiste à désactiver explicitement la mise en mémoire tampon de sortie et à effacer la mémoire tampon avant d'envoyer vos informations d'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é en:

  • IE 7.5730.11
  • Mozilla Firefox 1.81

Plus tard en juillet 2010, dans une réponse connexe, Arctic Fire a ensuite lié deux autres notes d'utilisateur qui faisaient suite à celle ci-dessus:



1
Auteur et @Timbo White, Est-il possible de fermer une connexion tôt sans connaître la taille du contenu? IE, sans avoir à capturer le contenu avant la fermeture.
skibulk

3
Les pirates informatiques et les navigateurs Web de merde peuvent toujours ignorer l'en-tête HTTP de fermeture de connexion et obtenir le reste de la sortie ... assurez-vous que ce qui vient ensuite n'est pas sensible. peut-être un ob_start (); pour tout supprimer: p
hanshenrik

3
Ajout de fastcgi_finish_request (); a été dit pour réussir à fermer la connexion lorsque ce qui précède ne fonctionne pas. Cependant, dans mon cas, cela a empêché mon script de continuer à s'exécuter, utilisez donc avec prudence.
Eric Dubé

@RichardSmith Parce que l'en- Connection: closetête peut être écrasé par les autres logiciels de la pile, par exemple, le proxy inverse dans le cas d'un CGI (j'ai observé ce comportement avec nginx). Voir la réponse de @hanshenrik à ce sujet. En général, Connection: closeest exécuté côté client et ne doit pas être considéré comme une réponse à cette question. La connexion doit être fermée du côté serveur .
7heo.tk

56

Il est nécessaire d'envoyer ces 2 en-têtes:

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

Puisque vous devez connaître la taille de votre sortie, vous devrez mettre votre sortie 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 automatique gzip 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 Content-Length 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


16
Si votre serveur compresse la sortie, vous pouvez la désactiver avec De header("Content-Encoding: none\r\n");cette façon, Apache ne la compressera pas.
GDmac

1
@GDmac merci! Je n'ai pas pu faire fonctionner cela pendant un certain temps, mais la désactivation de la compression a fait l'affaire.
Reactgular

Le ob_flush()n'est pas nécessaire et provoque en fait un avis failed to flush buffer. Je l'ai retiré et cela a très bien fonctionné.
Levi

2
J'ai trouvé que la ob_flush()ligne était nécessaire.
Deebster

21

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

Vous trouvez ceci dans le manuel PHP ici: FastCGI Process Manager (FPM) ; Mais cette fonction n'est pas spécifiquement documentée dans le manuel. Voici l'extrait du PHP-FPM: PHP FastCGI Process Manager Wiki :


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 en cours d'exécution du script n'affectent pas la réponse du serveur. Par exemple, l'enregistrement de la session dans Memcached peut se produire une fois que la page a été formée et transmise à un serveur Web.fastcgi_finish_request()est une fonctionnalité php, qui arrête la sortie de la réponse. Le serveur Web commence immédiatement à transférer la réponse "lentement et tristement" vers le client, et php peut en même temps 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 toutes sortes de choses des statistiques, etc.

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


Note: fastcgi_finish_request() a une bizarrerie où les appels à flush, printou echoprendra fin tôt le script.

Pour éviter ce problème, vous pouvez appeler ignore_user_abort(true)juste avant ou après l' fastcgi_finish_requestappel:

ignore_user_abort(true);
fastcgi_finish_request();

3
CECI EST UNE RÉPONSE RÉELLE!
Kirill Titov

2
si vous utilisez php-fpm - utilisez simplement cette fonction - oubliez les en-têtes et tout le reste. M'a fait gagner beaucoup de temps!
Ross

17

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

complet dans quel sens? Quel problème vous a obligé à compléter le script de réponses acceptées (lequel?) Et laquelle de vos différences de configuration l'a rendu nécessaire?
hakre

4
cette ligne: en-tête ("Content-Encoding: none"); -> très important.
Bobby Tables

2
Merci, c'est la seule solution qui fonctionne sur cette page. Cela devrait être approuvé comme réponse.

6

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

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

Vous devriez regarder cette question pour de meilleurs exemples:

PHP exécute un processus d'arrière-plan


4

En supposant que vous disposez d'un serveur Linux et d'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

Mettez ceci dans un fichier appelé bgping.

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

Notez le &. La commande ping s'exécutera en arrière-plan pendant que le processus en cours passera à la commande echo. Il enverra une requête ping à www.google.com 15 fois, ce qui prendra environ 15 secondes.

Rendez-le exécutable.

chmod 777 bgping

Mettez 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 bgtest.php dans votre navigateur, vous devriez obtenir la réponse suivante rapidement, sans attendre environ 15 secondes pour que la commande ping se termine.

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

result:0
end bgtest.php

La commande ping doit maintenant être exécutée 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

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

VOUS ÊTES UN DIEU. Je travaille depuis 2 jours pour essayer de comprendre cela. Cela a fonctionné sur mon développeur local mais pas sur l'hôte. J'ai été arrosé. TU M'AS SAUVÉ. MERCI!!!!
Chad Caldwell le

3

Je suis sur un hôte partagé et je suis fastcgi_finish_requestconfiguré pour quitter complètement les scripts. Je n'aime pas non plus la connection: closesolution. Son utilisation force une connexion distincte pour les demandes suivantes, ce qui coûte des ressources serveur supplémentaires. J'ai lu Transfer-Encoding: cunked l'article de Wikipédia et j'ai appris que cela 0\r\n\r\nmet fin à une réponse. Je n'ai pas testé cela de manière approfondie sur les versions de navigateurs et les appareils, 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);
}

Grâce à votre bonne réponse, j'ai réalisé à quel point il est stupide (et inutile) d'utiliser connection: close. Je suppose que certains ne sont pas familiers avec les écrous et les boulons de leur serveur.
Justin

@Justin J'ai écrit ceci il y a longtemps. En y regardant à nouveau, je dois noter qu'il peut être nécessaire de compléter les morceaux à 4 Ko. Il me semble que je me souviens que certains serveurs ne videront pas tant qu'ils n'atteindront pas ce minimum.
skibulk

2

Vous pouvez essayer de faire du multithreading.

vous pouvez créer un script qui effectue un appel système (en utilisant shell_exec ) qui appelle le binaire php avec le script pour faire votre travail comme paramètre. Mais je ne pense pas que ce soit le moyen le plus sûr. Peut-être que vous pouvez améliorer les choses en chrootant le processus php et d'autres choses

Alternativement, il existe une classe chez phpclasses qui fait cela http://www.phpclasses.org/browse/package/3953.html . Mais je ne connais pas les spécificités de l'implémentation


Et si vous ne voulez pas attendre la fin du processus, utilisez le &caractère pour exécuter le processus en arrière-plan.
Liam le

2

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

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 ici: Comment utiliser le multi threading dans les applications PHP

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


1

La réponse de Joeri Sebrechts est proche, mais elle détruit tout contenu existant qui pourrait être mis en mémoire tampon avant que vous ne souhaitiez vous déconnecter. Il n'appelle pas ignore_user_abortcorrectement, ce qui permet au script de se terminer prématurément. La réponse du diyisme est bonne mais n'est pas générique. Par exemple, une personne peut avoir plus ou moins de tampons de sortie que cette réponse ne gère pas, donc cela peut simplement ne pas fonctionner dans votre situation 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 mémoire supplémentaire, allouez-la avant d'appeler cette fonction.


1

Remarque pour les utilisateurs de mod_fcgid (veuillez l'utiliser à vos risques et périls).

Solution rapide

La réponse acceptée de Joeri Sebrechts est en effet fonctionnelle. Cependant, si vous utilisez mod_fcgid, vous constaterez peut-être que cette solution ne fonctionne pas d'elle-même. En d'autres termes, lorsque la fonction de vidage est appelée, la connexion au client ne se ferme pas.

Le FcgidOutputBufferSizeparamètre de configuration de mod_fcgid peut être à blâmer. J'ai trouvé cette astuce dans:

  1. cette réponse de Travers Carter et
  2. ce billet de blog de Seumas Mackinnon .

Après avoir lu ce qui précède, vous pouvez arriver à la conclusion qu'une solution rapide serait d'ajouter la ligne (voir «Exemple d'hôte virtuel» à la fin):

FcgidOutputBufferSize 0

soit dans votre fichier de configuration Apache (par exemple, httpd.conf), votre fichier de configuration FCGI (par exemple, fcgid.conf) ou dans votre fichier d'hôtes virtuels (par exemple, httpd-vhosts.conf).

Dans (1) ci-dessus, une variable nommée "OutputBufferSize" est mentionnée. C'est l'ancien nom du FcgidOutputBufferSizementionné dans (2) (voir les notes de mise à jour dans la page Web Apache pour mod_fcgid ).

Détails et une deuxième solution

La solution ci-dessus désactive la mise en mémoire tampon effectuée par mod_fcgid soit pour l'ensemble du serveur, soit pour un hôte virtuel spécifique. Cela peut entraîner une baisse des performances de votre site Web. D'un autre côté, cela peut ne pas être le cas puisque PHP effectue lui-même la mise en mémoire tampon.

Si vous ne souhaitez pas désactiver la mise en mémoire tampon de mod_fcgid , il existe une autre solution ... vous pouvez forcer ce tampon à se vider .

Le code ci-dessous fait exactement cela en s'appuyant sur la solution proposée par 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');
?>

Ce que la ligne de code ajoutée fait essentiellement est de remplir le tampon de mod_fcgi , le forçant ainsi à se vider. Le nombre "65537" a été choisi car la valeur par défaut de la FcgidOutputBufferSizevariable est "65536", comme mentionné dans la page Web Apache pour la directive correspondante . Par conséquent, vous devrez peut-être ajuster cette valeur en conséquence si une autre valeur est définie dans votre environnement.

Mon environnement

  • WampServer 2.5
  • Apache 2.4.9
  • PHP 5.5.19 VC11, x86, non compatible avec les threads
  • mod_fcgid / 2.3.9
  • Windows 7 Professionnel x64

Exemple d'hôte virtuel

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

    FcgidOutputBufferSize 0

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

J'ai essayé de nombreuses solutions. Et c'est la seule solution qui fonctionne pour moi avec mod_fcgid.
Tsounabe

1

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

0

Ok, donc fondamentalement la façon dont jQuery fait la requête XHR, même la méthode ob_flush ne fonctionnera pas car vous ne pouvez pas exécuter une fonction sur chaque onreadystatechange. jQuery vérifie l'état, puis choisit les actions appropriées à entreprendre (terminé, erreur, succès, délai d'expiration). Et bien que je n'ai pas pu trouver de référence, je me souviens avoir entendu dire que cela ne fonctionne pas avec toutes les implémentations XHR. Une méthode qui, je pense, devrait fonctionner pour vous est un croisement entre le sondage ob_flush et le sondage pour toujours.

<?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, lorsque les tampons sont vidés, vous obtenez une exécution. Pour rendre cela utile, remplacez le fichier console.log par une méthode de rappel définie dans la configuration de votre script principal pour recevoir les données et agir en conséquence. J'espère que cela t'aides. Salut, Morgan.


0

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.

J'ai dû le faire de cette façon récemment pour contourner les limites imposées par un hôte partagé - exec () et al était désactivé pour PHP exécuté par le serveur Web mais pouvait fonctionner dans un script shell.


0

Si la flush()fonction ne fonctionne pas. Vous devez définir les options suivantes dans php.ini comme:

output_buffering = Off  
zlib.output_compression = Off  

0

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

0

Après avoir essayé de nombreuses solutions différentes de ce fil (après qu'aucune d'elles n'a fonctionné 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();
    }
}
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.