neunzehn83.de

Ein Mann, ein Blog, kein Plan.

PHP-Skript im Hintergrund ausführen

Achtung: Paint-Skills

Wenn gleichzeitig mit einem Seitenaufruf eine rechen- oder zeitintensive Aufgabe ausgeführt werden soll, hat das den Nachteil, dass der Benutzer im Browser so lange einen Ladebalken sieht, bis die gesamte Rechenoperation beendet ist. Auch wenn die Berechnung an das Skriptende mit vorherigem ob_flush() gesetzt wird, bleibt der Ladebalken im Browser sichtbar. Das sieht nicht nur unschön aus - auch JavaScript-Events wie onload oder domready werden verzögert gefeuert.

Idealerweise startet man rechenintensive Aufgaben nicht zusammen mit einem Webseitenaufruf, sondern per cron direkt über das PHP CLI ohne Webserver-Overhead. Cron steht aber nicht immer zur Verfügung. Außerdem macht es die Anwendung weniger portabel.

Die Tatsache, dass jeder Browser die Verbindung beendet, sobald er alle mittels HTTP-Header "content-length" angekündigten Bytes empfangen hat, lässt sich ausnutzen. Den Rest regelt PHPs Ausgabepuffer.

Beispiel:

#!php@1
<?php
ignore_user_abort(true);
ob_start();
 
// Webseiten-Content
echo '<html>...</html>;';
 
header('HTTP/1.1 200 OK');
header('Content-Length: ' . ob_get_length());
 
ob_end_flush();
flush();
 
// Background-Prozess ab hier
sleep(10);
?>

Ein ignore_user_abort(true) verhindert den Scriptabbruch durch den Benutzer während der Ladezeit. Nach dem flush() kann das Skript in keinem Fall mehr abgebrochen werden (abgesehen von Runtime-Fehlern, time- oder memory_limit). Sämtlicher Output vom Background-Prozess landet im Nirvana.

Wird das obige Skript aufgerufen, erscheint nur während der Ladezeit des ge-flush-ten Buffers ein Ladebalken ("Webseiten-Content"). Der Background-Prozess (beispielhaftes Sleep(10)) findet statt, nachdem die Verbindung zum Browser bereits beendet wurde.

Die Implementierung ist je nach Browser/Webserver-Umgebung etwas hakelig und bedarf in jedem Einzelfall der genauen Überprüfung. Der Beispielcode oben hat in meinen Tests gut funktioniert.

Wider Erwarten funktioniert das auch ohne die explizite Angabe von header('Connection: close'). Im IE6 führt die Angabe sogar dazu, dass der Hintergrundprozess überhaupt nicht mehr funktioniert. Wenn der IE darüber hinaus immer noch Zicken macht, so hilft es vielleicht mindestens 256 Byte zu flushen. Code:

echo '...';
 
if (($diff = 256 - ob_get_length()) > 0) echo str_repeat(' ', $diff);
 
header('HTTP/1.1 200 OK');
header('Content-Length: ' . ob_get_length());

Ade, 's war schee!

Geschrieben am Montag, 27. Dezember 2010 und abgelegt unter Webtechnik.