I have the following code:
ignore_user_abort(true);
while(!connection_aborted()) {
// do stuff
}
and according to the PHP documentation, this should run until the connection is closed, but for some reason, it doesn't, instead it keeps running until the script times out. I've looked around online and some recommended adding
echo chr(0);
flush();
into the loop, but that doesn't seem to do anything either. Even worse, if I just leave it as
while(true) {
// do stuff
}
PHP still continues to run the script after the client disconnects. Does anyone know how to get this working? Is there a php.ini setting that I'm missing somewhere?
If it matters, I'm running PHP 5.3.5. Thanks in advance!
I'm a bit late to this party, but I just had this problem and got to the bottom of it. There are multiple things going on here -- a few of them mentioned here:
PHP doesn't detect connection abort at all
The gist of it: In order for connection_aborted() to work, PHP needs to attempt to send data to the client.
Output Buffers
As noted, PHP will not detect the connection is dead until it tries to actually send data to the client. This is not as simple as doing an echo, because echo sends the data to any output buffers that may exist, and PHP will not attempt a real send until those buffers are full enough. I will not go into the details of output buffering, but it's worth mentioning that there can be multiple nested buffers.
At any rate, if you'd like to test connection_abort(), you must first end all buffers:
while (ob_get_level()){ ob_end_clean(); }
Now, anytime you want to test if the connection is aborted, you must attempt to send data to the client:
echo "Something.";
flush();
// returns expected value...
// ... but only if ignore_user_abort is false!
connection_aborted();
Ignore User Abort
This is a very important setting that determines what PHP will do when the above flush() is called, and the user has aborted the connection (eg: hit the STOP button in their browser).
If true, the script will run along merrily. flush() will do essentially nothing.
If false, as is the default setting, execution will immediately stop in the following manner:
If PHP is not already shutting down, it will begin its shutdown
process.
If PHP is already shutting down, it will exit whatever shutdown
function it is in and move on to the next.
Destructors
If you'd like to do stuff when the user aborts the connection, you need to do three things:
Detect the user aborted the connection. This means you have to attempt to flush to the user periodically, as described further above. Clear all output buffers, echo, flush.
a. If ignore_connection_aborted is true, you need to manually test connection_aborted() after each flush.
b. If ignore_connection_aborted is false, a call to flush will cause the shutdown process to begin. You must then be especially careful not to cause flush from within your shutdown functions, or else PHP will immediate cease execution of that function and move on to the next shutdown function.
Putting it all together
Putting this all together, lets make an example that detects the user hitting "STOP" and does stuff.
class DestructTester {
private $fileHandle;
public function __construct($fileHandle){
// fileHandle that we log to
$this->fileHandle = $fileHandle;
// call $this->onShutdown() when PHP is shutting down.
register_shutdown_function(array($this, "onShutdown"));
}
public function onShutdown() {
$isAborted = connection_aborted();
fwrite($this->fileHandle, "PHP is shutting down. isAborted: $isAborted\n");
// NOTE
// If connection_aborted() AND ignore_user_abort = false, PHP will immediately terminate
// this function when it encounters flush. This means your shutdown functions can end
// prematurely if: connection is aborted, ignore_user_abort=false, and you try to flush().
echo "Test.";
flush();
fwrite($this->fileHandle, "This was written after a flush.\n");
}
public function __destruct() {
$isAborted = connection_aborted();
fwrite($this->fileHandle, "DestructTester is getting destructed. isAborted: $isAborted\n");
}
}
// Create a DestructTester
// It'll log to our file on PHP shutdown and __destruct().
$fileHandle = fopen("/path/to/destruct-tester-log.txt", "a+");
fwrite($fileHandle, "---BEGINNING TEST---\n");
$dt = new DestructTester($fileHandle);
// Set this value to see how the logs end up changing
// ignore_user_abort(true);
// Remove any buffers so that PHP attempts to send data on flush();
while (ob_get_level()){
ob_get_contents();
ob_end_clean();
}
// Let's loop for 10 seconds
// If ignore_user_abort=true:
// This will continue to run regardless.
// If ignore_user_abort=false:
// This will immediate terminate when the user disconnects and PHP tries to flush();
// PHP will begin its shutdown process.
// In either case, connection_aborted() should subsequently return "true" after the user
// has disconnected (hit STOP button in browser), AND after PHP has attempted to flush().
$numSleeps = 0;
while ($numSleeps++ < 10) {
$connAbortedStr = connection_aborted() ? "YES" : "NO";
$str = "Slept $numSleeps times. Connection aborted: $connAbortedStr";
echo "$str<br>";
// If ignore_user_abort = false, script will terminate right here.
// Shutdown functions will being.
// Otherwise, script will continue for all 10 loops and then shutdown.
flush();
$connAbortedStr = connection_aborted() ? "YES" : "NO";
fwrite($fileHandle, "flush()'d $numSleeps times. Connection aborted is now: $connAbortedStr\n");
sleep(1);
}
echo "DONE SLEEPING!<br>";
die;
The comments explain everything. You can fiddle with ignore_user_abort and look at the logs to see how this changes things.
I hope this helps anyone having trouble with connection_abort, register_shutdown_function, and __destruct.
Try using ob_flush(); just before flush(); and some browsers just won't update the page before some data is added.
Try doing something like
<? php
// preceding scripts
ignore_user_abort(true);
$i = 0;
while(!connection_aborted())
{ $i++;
echo $i;
echo str_pad('',4096); // yes i know this will increase the overhead but that can be reduced afterwords
ob_flush();
flush();
usleep(30000); // see what happens when u run this on my WAMP this runs perfectly
}
// Ending scripts
?>
Google Chrome has issues with this code, actually; it doesn't support streaming very nicely.
Try:
ignore_user_abort(true);
echo "Testing connection handling";
while (1) {
if (connection_status() != CONNECTION_NORMAL)
break;
sleep(1);
echo "test";
flush();
}
Buffering seems to cause issues depending on your server settings.
I tried disabling the buffer with ob_end_clean but that wasn't enough, I had to send some data to cause the buffer to fully flush out. Here is the final code that ended up working for me.
set_time_limit(0); // run the delay as long as the user stays connected
ignore_user_abort(false);
ob_end_clean();
echo "\n";
while ($delay-- > 0 && !connection_aborted())
{
echo str_repeat("\r", 1000) . "<!--sleep-->\n";
flush();
sleep(1);
}
ob_start();
Related
I'm trying to learn server side events on php.
At first almost every example had a loop in the server side and returning value with ob_flush() / flush(). So I tryed doing like this:
<?php
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
// Output a 'waiting message'
$progress = 0;
while ($progress < 100) {
echo 'data:';
echo $progress;
echo "\n\n";
flush();
ob_flush(); //I tried ob_implicit_flush(true) too
$progress++;
sleep(1);
}
?>
And javascript :
function start(btn) {
var source = new EventSource('/fix/streamingGet.php');
source.onmessage = function(e) {
console.log(e.data);
};
}
But nothing was received until the script was ended ...
so i ended up trying without an infinite loop and everything is working
<?php
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
// Output a 'waiting message'
echo 'data:';
echo rand(5, 15);
echo "\n\n";
?>
Edit: Nevermind, I probably did a mistake somewhere before because now it work
Edit : I meant a loop returning multiple result with ob_flush/flush (not infinite loop, my bad)
But do I need to return my data with multiple flush to gain performance with Server Side Envents ?
Update :
From my tests and researches, it seem that php doesn't allow multiple session which make the first example to block any other call to the server. So the only working php solution seem to return value everytimes. Which I think is the same of having multiple timed ajax call to the server.
I am guessing that if I really want to stream a value from the server then I should probably use nodejs on the side of my webapplication.
What you are doing is basically correct.
But put ob_flush() before flush() (because ob_flush() flushes PHP buffers to Apache, and then flush() tells Apache to send its buffered content to the client) and it is more robust to put # before ob_flush() (because if you have turned off PHP buffering, you might get an error message - the # suppresses that).
(The comments are wrong about using infinite loops in PHP: they are fine in this case. When the browser client disconnects, Apache will kill the PHP instance. There is no memory leak just from running an infinite/long-running loop.)
PHP sessions lock. So if the same user (the same session) might access some other part of your site while the SSE script is sending data, you need the SSE script to stop using sessions. So, grab any data you need from the session at the top of your script, then call session_write_close() before entering your main loop. See https://stackoverflow.com/a/19440378/841830
I'm working on speeding up the response time of some php code that generates html. One of the issues with the code, is that when determines a piece of information does not need to be displayed, it makes a sql call to delete the item from the database. This isn't visible to the user, and the won't be visible to the server until the next time the page is loaded, so that sql query does not need to be run as soon as the system knows that it should be run.
What I would like to do is return the response to the user, with the generated html, and then make the sql queries. I was trying this flush and ob_flush, but the page response is still not loaded until I make a call to die.
Is there anyway in PHP to run code after a call to die() so that the user gets their data and then I can run my database clean up code and the client is no long waiting on me to close the connection?
You can register shutdown functions using register_shutdown_function:
register_shutdown_function(function () {
// cleanup stuff
});
Or in older versions of PHP:
function myFunc() {
// cleanup stuff
}
register_shutdown_function("myFunc");
Thanks to #robbrit and #Luis Siquot. I was looking at register_shutdown_function and because of Luis' comment, I was reading the comments on that page, and I came across "When using php-fpm, fastcgi_finish_request() should be used instead of register_shutdown_function() and exit()"
which lead me to fastcgi_finish_request which says:
"This function flushes all response data to the client and finishes the request. This allows for time consuming tasks to be performed without leaving the connection to the client open."
So it looks like fastcgi_finish_request() is what I'm looking for, not register_shutdown_function()
Edit: It seems that fastcgi_finish_request() requires another library, so instead use:
ob_end_clean();
header("Connection: close");
ignore_user_abort(true); // just to be safe
ob_start();
echo "The client will see this!";
$size = ob_get_length();
header("Content-Length: $size");
//Both of these flush methods must be called, otherwise strange things happen.
ob_end_flush();
flush();
echo "The client will never see this";
I started using push in HTML5 using the JavaScript EventSource object. I was totally happy with a working solution in PHP:
$time = 0;
while(true) {
if(connection_status() != CONNECTION_NORMAL) {
mysql_close()
break;
}
$result = mysql_query("SELECT `id` FROM `table` WHERE UNIX_TIMESTAMP(`lastUpdate`) > '".$time."'");
while($row = mysql_fetch_array($result)) {
echo "data:".$row["id"].PHP_EOL;
echo PHP_EOL;
ob_flush();
flush();
}
$time = time();
sleep(1);
}
But suddenly my WebApp wasn't reachable anymore with an MySQL error "too many connections".
It turned out that the MySQL connection doesn't close after closing the event source in JavaScript:
window.onload = function() {
sse = new EventSource("push.php");
sse.onmessage = function(event) {
console.log(event.data.split(":")[1]);
}
}
window.onbeforeunload = function() {
sse.close();
}
So I guess that the PHP script does not stop to execute. Is there any way to call in function (like die();) before the clients connection disconnects? Why doesn't my script terminate after calling .close(); on the EventSource?!
Thanks for help! —
First off: when you wrap your code in a huge while(true) loop, your script will never terminate. The connection with the DB would have been closed when your script "ran out of code to execute", but since you've written a deadlock, that's not going to happen... ever.It's not an EventSource issue, that merely does what it's supposed to do. It honestly, truly, and sadly is your fault.
The thing is: a user connects to your site, and the EventSource object is instantiated. A connection to the server is established and a request for the return value of push.php is requested. Your server does as requested, and runs the script, that -again- is nothing but a deadlock. There are no errors, so it can just keep on running, for as long as the php.ini allows it to run. The .close() method does cancel the stream of output (or rather it should), but your script is so busy either performing its infinite loop, or sleeping. Assuming the script to be stopped because a client disconnects is like assuming that any client could interfere with what the server does. Security-wise this would be the worst that could happen. Now, to answer your actual question: What causes the issue, how to fix it?The answer: HEADERS
Look at this little PHP example: the script isn't kept alive server-side (by infinite loops), but by setting the correct headers.
Just as a side-note: Please, ditch mysql_* ASAP, it's being deprecated (finally), use PDO or mysqli_* istead. It's not that hard, honestly.
I had exactly the same issue and as far as I understood it the reason was that apache didn't terminate the php script until I tried to write data trough the closed socket connection again.
I didn't have any changes in my database so there was no output.
To fix this I simply echo a event source comment in my while loop like:
echo ": heartbeat\n\n";
ob_flush();
flush();
This causes the script to be terminated if the socket connection is closed.
you might want to try to add this to the loop:
if(connection_status() != CONNECTION_NORMAL)
{
break;
}
but php should stop when the client disconnected.
1.
window.onbeforeunload = function() {
sse.close();
}
this is not required, EventSource will be closed at page unloading time.
even if you can not find a way to detect disconnected client on PHP, you can just stop execution after N seconds, EventSource will reconnect automatically.
http://www.php.net/manual/ru/function.connection-aborted.php
comments on this page says, that u should use "flush" before connection_status
anyway, you should drop connection after N seconds, because u cant detect "bad" disconnects.
3.
echo "retry:1000\n"; // use this to tell EventSource the reconnection delay in ms
$time = 0;
while(true) {
if(connection_status() != CONNECTION_NORMAL) {
mysql_close()
break;
}
$result = mysql_query("SELECT id FROM table WHERE UNIX_TIMESTAMP(lastUpdate) > '".$time."'");
while($row = mysql_fetch_array($result)) {
$rect[] = $row;
}
for($i-0;$i echo "data:".implode('',$disp)."\n\n"; $time = time(); sleep(1);
}
As Elias Van Ootegem pointed out, your script never terminates and hence you have a bunch of MySQL connections active. More concerningly, you have a bunch of resources being used in the form of PHP loops running with no end in sight!
Unfortunately, the solution to keeping the is not "headers". In the example at this link referenced by Elias, the php script terminates, and the client javascript actually has to re-open a connection. You can test this by using the following code
source.addEventListener('open', function(e) {
// Connection was opened.
console.log("Opening new connection");
}, false);
If you follow his example on github, in implementation the author actually employs a loop that terminates after X seconds. See the do loop and comments at bottom of the script.
The solution is to give your loop a lifespan.
By defining a limit to the loop, IE X itterations or after X amount of time to trigger the end of the loop or die();, you will make it so that if the client disconnects there is a limit to how long a script will remain active. If the client is still there after X amount of time, they will simply re connect.
I have a ob_start() and a corresponding ob_flush(). I would like to flush a portion of data and continue executing the rest. Using ob_flush() didn't help. Also if possible rest needs to happen without showing loading in browser.
EDIT:
I don't want to use ajax
I have done this in the past and this is how I solved it:
ob_start();
/*
* Generate your output here
*/
// Ignore connection-closing by the client/user
ignore_user_abort(true);
// Set your timelimit to a length long enough for your script to run,
// but not so long it will bog down your server in case multiple versions run
// or this script get's in an endless loop.
if (
!ini_get('safe_mode')
&& strpos(ini_get('disable_functions'), 'set_time_limit') === FALSE
){
set_time_limit(60);
}
// Get your output and send it to the client
$content = ob_get_contents(); // Get the content of the output buffer
ob_end_clean(); // Close current output buffer
$len = strlen($content); // Get the length
header('Connection: close'); // Tell the client to close connection
header("Content-Length: $len"); // Close connection after $len characters
echo $content; // Output content
flush(); // Force php-output-cache to flush to browser.
// See caveats below.
// Optional: kill all other output buffering
while (ob_get_level() > 0) {
ob_end_clean();
}
As I said in a couple of comments before, you should watch out for gzipping your content, since that will alter the length of your content, but not change the header about it. It also can buffer your output, so it won't get send to the client instantly.
You could try letting apache know to not gzip your content by using apache_setenv('no-gzip', '1');. But this will not work if you use rewrite-rules to go to your page, since then it will also modify those environment variables. At least, it did so for me.
See more caveats about flushing your content to the user in the manual.
ob_flush writes the buffer. In other words, ob_flush tells PHP to give Apache (or nginx/lighttpd/whatever) the output and then for PHP to forget about it. Once Apache has the output, it does whatever it wants with it. (In other words, after ob_flush it's out of your control whether or not it gets immediately written to the browser).
So, short answer: There's no guaranteed way to do that.
Just a guess, you're likely looking for AJAX. Whenever people are trying to manipulate when page content loads as you're doing, AJAX is almost always the correct path.
If you want to continue a task in the background, you can use ignore_user_abort, as detailed here, however, that is often not the optimal approach. You essentially lose control over that thread, and in my opinion, a web server thread is not where heavy processing belongs.
I would try to extract it out of the web facing stuff. This could mean a cron entry or just spawning a background process from inside of PHP (a process that though started from inside of script execution will not die with the script, and the script will not wait for it to finish before dying).
If you do go that route, it will mean that you can even make some kind of status system if necessary. Then you could monitor the execution and give the user periodic updates on the progress. (Technically you could make a status system with a ignore_user_abort-ed script too, but it doesn't seem as clean to me.)
this is my function
function bg_process($fn, $arr) {
$call = function($fn, $arr){
header('Connection: close');
header('Content-length: '.ob_get_length());
ob_flush();
flush();
call_user_func_array($fn, $arr);
};
register_shutdown_function($call, $fn, $arr);
}
wrap the function to be executed in the end, after php close the connection. and of course the browser will stop buffering.
function test() {
while (true) {
echo 'this text will never seen by user';
}
}
this is how to call the function
bg_process('test');
first argument is callable,
second argument is an array to be passed to 'test' function with an indexed array
Note : I don't use ob_start() at the beginning of the script.
I have an article explaining how this can be achieved using apache/mod_php on my blog here: http://codehackit.blogspot.com/2011/07/how-to-kill-http-connection-and.html Hope this helps, cheers
If you are using PHP-FPM:
ignore_user_abort(true);
fastcgi_finish_request();
Above two functions are the key factors which ignore_user_abort prevents error and fastcgi_finish_request closes client connection.
fastcgi_finish_request
This function flushes all response data to the client and finishes the request. This allows for time consuming tasks to be performed without leaving the connection to the client open.
not working on Apache.(PHP 5 >= 5.3.3, PHP 7)
Use:
header("Content-Length: $len");
..where $len is the length of the data to be flushed to the client.
I don't have the background to know when and where this is going to work, but I tried on a few browsers, and all returned instantly with:
<?PHP
header("Content-length:5");
echo "this is more than 5";
sleep(5);
?>
edit: Chrome, IE, and Opera showed this, while FireFox showed this is more than 5. All of them closed the request after that though.
How does the server knows that i've closed the browser in a code like this?
<?php
$i = 0;
while (1) {
echo "a";
flush();
$fp = fopen("$i.txt", "w");
fclose($fp);
sleep(1);
$i++;
}
?>
If i close the browser, the script stops and no more files are created.
This is because when you try to output something, such as echo "a"; flush();", PHP sees that the request has been aborted, and therefore stops the request.
Just a quick note. This only happens when you output something. I'm guessing this is because PHP was primarily used for templating, and designed mainly for outputting content. Well, if the content is not going to go anywhere, why continue processing the script?
If you don't want it to stop. Do one of the following:
Option A: Don't output anything.
flush() and echo are both considered outputs, along with many other functions. PHP only checks to see if a user has aborted when it goes to send content, so not outputting anything will make sure it doesn't check. Although that is probably not as reliable as...
Option B: use ignore_user_abort(true)
This will make sure that the script continues to output even if the user leaves the page. You can then check with connection_aborted() to find out if the connection has been aborted.
You can read all of this on PHP's Connection Handling Documentation.