web-dev-qa-db-fra.com

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

Mon script est appelé par serveur. Du serveur, je vais recevoir ID_OF_MESSAGE et TEXT_OF_MESSAGE

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

Le problème, c’est que j’envoie une réponse à "ID_OF_MESSAGE", mais le serveur qui m’envoie un message à gérer va définir son message comme remis à moi (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 fichier 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

80
user1700214

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...
165
vcampitelli

J'ai vu beaucoup de réponses ici suggérant d'utiliser ignore_user_abort(true); mais ce code n'est pas nécessaire. Tout ce que cela signifie, c'est que votre script continue de s'exécuter avant l'envoi d'une réponse au cas où l'utilisateur abandonnerait (en fermant son navigateur ou en appuyant sur échap 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 le suivant:

    // 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 pensez que votre travail de fond prendra plus de temps que le délai d’exécution du script par défaut, collez alors set_time_limit(0); en haut.

31
Kosta Kontos

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

session_write_close(); //close the session
fastcgi_finish_request(); //this returns 200 to the user, and processing continues

// do desired processing ...
$expensiveCalulation = 1+1;
error_log($expensiveCalculation);

Source: http://fi2.php.net/manual/en/function.fastcgi-finish-request.php

23
DarkNeuron

J'ai passé quelques heures sur cette question 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 traitement long. 

12
Ehsan

Modifie un peu la réponse de @vcampitelli. Ne pensez pas que vous avez besoin de l'en-tête close. Je voyais des en-têtes proches 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);
3
Justin

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 PHP 4.1 linklink

1
martti

Pour des raisons obscures, aucune de ces solutions ne fonctionne avec MAMP PRO (Apache, MySQL, PHP).

1
asiby

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

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

Voir Gearman . Vous ne voulez vraiment pas que vos serveurs Web frontaux effectuent un traitement d’arrière-plan comme celui-ci.

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

1
Matthew Slyman

J'ai quelque chose qui peut compresser et envoyer la réponse et laisser l'autre code php à 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();
}
1
Mayur Shedage

Je ne peux pas installer pthread et les solutions précédentes ne fonctionnent pas pour moi. J'ai trouvé que la solution suivante qui fonctionnait (réf: 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');
?>
0
Fil

en cas d'utilisation de php avec file_get_contents, la fermeture de la connexion ne suffit pas .

ma solution est de lire 'Content-Length:'

voici un échantillon:

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 est 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 constater, le contenu du script attend une heure si la longueur du contenu est atteinte.

j'espère que ça va aider

0
Jérome S.

Il existe une autre approche qui mérite d’être envisagée si vous ne souhaitez 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 avons exécuté, $ continue_processing-> start () PHP n'attendra pas le résultat renvoyé par ce thread et donc tant que rest_endpoint sera considéré. C'est fait.

Quelques liens pour vous aider avec les pthreads

Bonne chance.

0
Jonathan

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

Avec cette réponse, je ne supporte pas la question mais comment résoudre ce problème correctement. J'espère que cela aide 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 de instances de travail . Il y a un paquet appelé amqplib pour php qui fait tout le travail pour que vous utilisiez 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, ce qui peut poser problème avec un hébergeur Web.
0
Sam