continuer à traiter php après l'envoi de la réponse http

101

Mon script est appelé par le serveur. Du serveur je recevrai ID_OF_MESSAGEet TEXT_OF_MESSAGE.

Dans mon script, je vais gérer le texte entrant et générer une réponse avec les paramètres: ANSWER_TO_IDet RESPONSE_MESSAGE.

Le problème est que j'envoie une réponse à incomming "ID_OF_MESSAGE", mais le serveur qui m'envoie un message à traiter définira son message comme livré (cela signifie que je peux lui envoyer une réponse à cet ID), après avoir reçu la réponse http 200.

L'une des solutions consiste à enregistrer le message dans la base de données et à créer un cron qui s'exécutera chaque minute, mais je dois générer un message de réponse immédiatement.

Existe-t-il une solution pour envoyer au serveur la réponse http 200 et continuer à exécuter le script php?

Merci beaucoup

user1700214
la source

Réponses:

191

Oui. Tu peux le faire:

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
la source
5
Est-il possible de le faire avec une connexion keep-alive?
Congelli501
3
Excellent!! C'est la seule réponse à cette question qui fonctionne réellement !!! 10p +
Martin_Lakes
3
y a-t-il une raison pour laquelle vous utilisez ob_flush après ob_end_flush? Je comprends la nécessité de la fonction flush à la fin, mais je ne sais pas pourquoi vous auriez besoin de ob_flush avec ob_end_flush appelé.
ars265
9
Veuillez noter que si un en-tête de codage de contenu est défini sur autre chose que «none», cela pourrait rendre cet exemple inutile car il laisserait toujours l'utilisateur attendre le temps d'exécution complet (jusqu'au délai d'expiration?). Donc, pour être absolument sûr que cela fonctionnera en local et dans l'environnement de production, définissez l'en-tête 'content-encoding' sur 'none':header("Content-Encoding: none")
Brian
17
Astuce: j'ai commencé à utiliser PHP-FPM, donc j'ai dû ajouter fastcgi_finish_request()à la fin
vcampitelli
44

J'ai vu beaucoup de réponses ici suggérant l'utilisation, ignore_user_abort(true);mais ce code n'est pas nécessaire. Tout cela fait est de s'assurer que votre script continue à s'exécuter avant qu'une réponse ne soit envoyée dans le cas où l'utilisateur abandonne (en fermant son navigateur ou en appuyant sur échapper pour arrêter la demande). Mais ce n'est pas ce que vous demandez. Vous demandez de continuer l'exécution APRÈS l'envoi d'une réponse. Tout ce dont vous avez besoin est ce qui suit:

    // 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 vous craignez que votre travail en arrière-plan prenne plus de temps que la limite de temps d'exécution du script par défaut de PHP, alors restez set_time_limit(0);en haut.

Kosta Kontos
la source
3
J'ai essayé BEAUCOUP de combinaisons différentes, c'est celle qui fonctionne !!! Merci Kosta Kontos !!!
Martin_Lakes
1
Fonctionne parfaitement sur apache 2, php 7.0.32 et ubuntu 16.04! Merci!
KyleBunga
1
J'ai essayé d'autres solutions, et seule celle-ci a fonctionné pour moi. L'ordre des lignes est également important.
Sinisa
32

Si vous utilisez le traitement FastCGI ou PHP-FPM, vous pouvez:

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

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

Problème PHP n ° 68722: https://bugs.php.net/bug.php?id=68772

DarkNeuron
la source
2
Merci pour cela, après avoir passé quelques heures, cela a fonctionné pour moi dans nginx
Ehsan
7
dat somme calcul cher tho: o beaucoup impressionné, tellement cher!
Friedrich Roell
Merci DarkNeuron! Excellente réponse pour nous en utilisant php-fpm, vient de résoudre mon problème!
Sinisa le
21

J'ai passé quelques heures sur ce problème et je suis venu avec cette fonction qui fonctionne sur Apache et 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();
}

Vous pouvez appeler cette fonction avant votre long traitement.

Ehsan
la source
2
C'est vraiment charmant! Après avoir essayé tout ce qui précède, c'est la seule chose qui a fonctionné avec nginx.
épice
Votre code est presque exactement le même que le code ici mais votre message est plus ancien :) +1
Accountant م
soyez prudent avec la filter_inputfonction, elle renvoie parfois NULL. voir la contribution de cet utilisateur pour plus de détails
Accountant م
4

Modifié un peu la réponse par @vcampitelli. Ne pensez pas que vous avez besoin de l'en- closetête. Je voyais des en-têtes de fermeture en double dans 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
la source
3
J'en ai parlé dans la réponse originale, mais je vais le dire ici aussi. Vous n'avez pas nécessairement besoin de fermer la connexion, mais ce qui se passera, c'est que le prochain actif demandé sur la même connexion sera obligé d'attendre. Vous pouvez donc livrer le HTML rapidement, mais l'un de vos fichiers JS ou CSS peut se charger lentement, car la connexion doit finir d'obtenir la réponse de PHP avant de pouvoir obtenir le prochain élément. Donc, pour cette raison, la fermeture de la connexion est une bonne idée afin que le navigateur n'ait pas à attendre qu'elle soit libérée.
Nate Lampton
3

J'utilise la fonction php register_shutdown_function pour cela.

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

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

Edit : ce qui précède ne fonctionne pas. Il semble que j'ai été induit en erreur par une vieille documentation. Le comportement de register_shutdown_function a changé depuis le lien de lien PHP 4.1

Martti
la source
Ce n'est pas ce qui est demandé - cette fonction étend simplement l'événement de fin de script et fait toujours partie du tampon de sortie.
fisk
1
J'ai trouvé que cela valait un vote positif, car cela montre ce qui ne fonctionne pas.
Trendfischer
1
Idem - upvoting parce que je m'attendais à voir cela comme une réponse, et utile de voir que cela ne fonctionne pas.
HappyDog
2

Je ne peux pas installer pthread et aucune des solutions précédentes ne fonctionne pour moi. Je n'ai trouvé que la solution suivante pour fonctionner (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
la source
1

en cas d'utilisation de php file_get_contents, la fermeture de la connexion ne suffit pas. php attend toujours l'envoi de eof witch par le serveur.

ma solution est de lire "Content-Length:"

voici un exemple:

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

Notez le "\ n" en réponse à la ligne de fermeture, sinon le fget lu en attendant 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);

Comme vous pouvez le voir, ce script attend environ eof si la longueur du contenu est atteinte.

j'espère que ça aidera

Jérome S.
la source
1

J'ai posé cette question à Rasmus Lerdorf en avril 2012, en citant ces articles:

J'ai suggéré le développement d'une nouvelle fonction PHP intégrée pour notifier à la plate-forme qu'aucune autre sortie (sur stdout?) Ne sera générée (une telle fonction pourrait prendre soin de fermer la connexion). Rasmus Lerdorf a répondu:

Voir Gearman . Vous ne voulez vraiment vraiment pas que vos serveurs Web frontaux effectuent un traitement backend comme celui-ci.

Je peux voir son point, et soutenir son opinion pour certaines applications / scénarios de chargement! Cependant, dans certains autres scénarios, les solutions de vcampitelli et al, sont bonnes.

Matthew Slyman
la source
1

J'ai quelque chose qui peut compresser et envoyer la réponse et laisser un autre code php s'exécuter.

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
la source
0

Il existe une autre approche et il vaut la peine de considérer si vous ne voulez pas altérer les en-têtes de réponse. Si vous démarrez un thread sur un autre processus, la fonction appelée n'attendra pas sa réponse et retournera au navigateur avec un code http finalisé. Vous devrez configurer 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)
}

Une fois que nous exécutons $ continue_processing-> start (), PHP n'attendra pas le résultat de retour de ce thread et donc aussi loin que rest_endpoint est considéré. C'est fait.

Quelques liens pour vous aider avec les pthreads

Bonne chance.

Jonathan
la source
0

Je sais que c'est un ancien, mais peut-être utile à ce stade.

Avec cette réponse, je ne soutiens pas la question réelle, mais comment résoudre ce problème correctement. J'espère que cela aidera d'autres personnes à résoudre des problèmes comme celui-ci.

Je suggérerais d'utiliser RabbitMQ ou des services similaires et d'exécuter la charge de travail en arrière-plan à l'aide d' instances de travail . Il existe un package appelé amqplib pour php qui fait tout le travail pour que vous puissiez utiliser RabbitMQ.

Avantages:

  1. C'est très performant
  2. Bien structuré et maintenable
  3. Il est absolument évolutif avec les instances de travail

Neg's:

  1. RabbitMQ doit être installé sur le serveur, cela peut être un problème avec certains hébergeurs Web.
Samuel Breu
la source