25

I am currently working on a chat that uses Server-Sent Events to receive the messages. However, I am running into a problem. The server-sent event never connects and stays at pending because the page doesn't load.

For example:

<?php
    while(true) {
        echo "data: This is the message.";
        sleep(3);
        ob_flush();
        flush();
    }
?>

I expect that every 3 seconds, "data: This is the message." will be outputted. Instead, the page just doesn't load. However, I need this behavior for server-sent events. Is there a way to fix this?

Edit:

Full Code:

<?php
   session_start();

    require "connect.php";
    require "user.php";

    session_write_close();

    echo $data["number"];

    header("Content-Type: text/event-stream\n\n");
    header('Cache-Control: no-cache');

    set_time_limit(1200);

    $store = new StdClass(); // STORE LATEST MESSAGES TO COMPARE TO NEW ONES
    $ms = 200; // REFRESH TIMING (in ms)
    $go = true; // MESSAGE CHANGED

    function formateNumber ($n) {
            $areaCode = substr($n, 0, 3);
            $part1 = substr($n, 3, 3);
            $part2 = substr($n, 6, 4);
            return "($areaCode) $part1-$part2";
    }

    function shorten ($str, $mLen, $elp) {
        if (strlen($str) <= $mLen) { 
            return $str;
        } else {
            return rtrim(substr($str, 0, $mLen)) . $elp;
        }
    }

   do {
    $number = $data["number"];
        $sidebarQ = "
            SELECT * 
            FROM (
                SELECT * 
                FROM messages 
                WHERE deleted NOT LIKE '%$number%' 
                AND (
                    `from`='$number' 
                    OR 
                    `to`='$number'
                ) 
                ORDER BY `timestamp` DESC
            ) as mess 
            GROUP BY `id` 
            ORDER BY `timestamp` DESC";
        $query = $mysqli->query($sidebarQ);

        if ($query->num_rows == 0) {
            echo 'data: null' . $number;
            echo "\n\n";
        } else {

            $qr = array();
            while($row = $query->fetch_assoc()) {
                $qr[] = $row;
            }

            foreach ($qr as $c) {
                $id = $c["id"];
                if (!isset($store->{$id})) {
                    $store->{$id} = $c["messageId"];
                    $go = true;
                } else {
                    if ($store->{$id} != $c["messageId"]) {
                        $go = true;
                        $store->{$id} = $c["messageId"];
                    }
                }
            }

            if($go == true) {
                $el = $n = "";

                foreach ($qr as $rows) {
                    $to = $rows["to"];
                    $id = $rows["id"];
                    $choose = $to == $number ? $rows["from"] : $to;
                    $nameQuery = $mysqli->query("SELECT `savedname` FROM `contacts` WHERE `friend`='$choose' AND `number`='$number'");
                    $nameGet = $nameQuery->fetch_assoc();
                    $hasName = $nameQuery->num_rows == 0 ? formateNumber($choose) : $nameGet["savedname"];

                    $new = $mysqli->query("SELECT `id` FROM `messages` WHERE `to`='$number' AND `tostatus`='0' AND `id`='$id'")->num_rows;
                    if ($new > 0) {
                        $n = "<span class='new'>" . $new . "</span>";
                    }

                    $side = "<span style='color:#222'>" . ($to == $number ? "To you:" : "From you:") . "</span>";
                    $el .= "<div class='messageBox sBox" . ($nameQuery->num_rows == 0 ? " noname" : "") . "' onclick=\"GLOBAL.load($id, $choose)\" data-id='$id'><name>$hasName</name><div>$side " . shorten($rows["message"], 25, "...") . "</div>$n</div>";
                }
                echo 'data: '. $el;
                echo "\n\n";

                $go = false;
            }
        }

        echo " ";

        ob_flush();
        flush();
        sleep(2);
    } while(true);
?>

I would also like to note, that this infinite loop shouldn't be causing this to happen. This is just how SSE's are set up usually and it is even done so on the MDN website.

scrowler
  • 24,273
  • 9
  • 60
  • 92
Shawn31313
  • 5,978
  • 4
  • 38
  • 80
  • 6
    And why on earth would you use an infinite loop for this, that's why the page never loads, how could it ? – adeneo Apr 06 '15 at 22:36
  • 1
    There are some simple examples on [**MDN**](https://developer.mozilla.org/en-US/docs/Server-sent_events/Using_server-sent_events), follow those. – adeneo Apr 06 '15 at 22:37
  • 5
    And as you see, on the MDN site...they use an infinite loop. – Shawn31313 Apr 06 '15 at 22:38
  • 1
    The whole point of server-sent events is to keep the connection open. Thats why you do the infinite loop. As you can see, there is a sleep function used which is suppose to halt the page. But this is not happening. – Shawn31313 Apr 06 '15 at 22:39
  • Post more code. For example, from what you have, it looks like you never called `header("Content-Type: text/event-stream");` – developerwjk Apr 06 '15 at 22:45
  • @developerwjk I have added the full code that I am trying to get this to work on. Note that, this project used to be up and running in the past. I am just trying to set it back up. So this code shouldn't be faulty. But for some reason it isn't working the same anymore. – Shawn31313 Apr 06 '15 at 22:48
  • 3
    Take out `echo $data["number"];` before setting the Content-Type header. If you print something before setting your Content-Type header then you get a default content-type, probably text/html. – developerwjk Apr 06 '15 at 22:54
  • 1
    Good Suggestion. However, that didn't fix the issue. Could it be something with my host? I tried running the example code from the MDN website on mine, and it didn't work. I then tried to run it on writecodeonline.com and it at least gave an output. @developerwjk – Shawn31313 Apr 06 '15 at 23:01
  • Have you tried turning on error reporting to ensure you aren't getting some php error like a 500? `ini_set('display_errors', 1); error_reporting(E_ALL);` at the top of your script. – Darren Apr 06 '15 at 23:41
  • 1
    @sjagr browsers might not, but server sent events don't use a browser to load content. Just like the PHP CLI, output can be sent at any point through script execution – scrowler Apr 23 '15 at 23:55
  • Server-Sent Events. Look it up. In fact, here is the link: https://developer.mozilla.org/en-US/docs/Server-sent_events/Using_server-sent_events . The script doesn't need to finish executing. – Shawn31313 Apr 23 '15 at 23:56
  • 2
    In this case it should return data because it is using output buffering, BUT this at times can be a little tricky for each server.. Worth testing your output buffering code with a simpler page and then see if it still works in that case – Angry 84 Apr 23 '15 at 23:56
  • If it is a server issue then this makes total sense. Because the chat I was talking about in the question was already working at one point and I am now just in the process of resetting it up. But for some reason this particular part does not want to work as before. @Mayhem – Shawn31313 Apr 23 '15 at 23:58
  • The best i can suggest... Comment out as much code as you can.. Bring it back to basics... So comment every except for the loop itself, the ob_ functions and the have it produce a simple echo.. If this works.. Then slowly uncomment line by line or check your php error logs. Functions should be ok to leave as they are, they would only error as you call them.. BUT if the page does not load at all! and does not appear to be processing, i would say you have a syntax error.. Make a new page, enable error logging and include this page/script.. That will show you syntax errors ;) – Angry 84 Apr 24 '15 at 06:33
  • 1
    As someone else suggested - make sure no other headers are being kicked out which would cause any issues. I assume you are using an EventSource object to test this - have you tried simply loading it in a browser tab to see if it works there? I'm not sure if this affects set_time_limit, but certainly with max_execution_time, sleep does NOT affect this (except on Windows). Personally, to be on the safe side - I have an internal counter and will kill the script after X seconds, otherwise you can end up with a lot of zombie processes. – Andrew Spode Apr 27 '15 at 14:04
  • Also with output buffering, you are required to send so much data before the buffer reaches a limit where it can actually output.. Examples all over show a 5000x loop printing a single space or some other content to fill the buffer.. After this it will work as expected and is a common "why does this not work" issue – Angry 84 Apr 29 '15 at 07:25
  • BTW developing a chat application with PHP is not the coolest solution. There are better, easier and more scalable solutions like socket.io or ejabberd. – Cagatay Gurturk Apr 29 '15 at 08:29
  • Try outputting a much larger block of text each loop instead of just a number. You may be looking at a buffer at a different spot between your code and the browser. nginx? – creuzerm Apr 30 '15 at 22:13
  • Please select a correct answer from the posts below or update your post with your results – Angry 84 May 02 '15 at 04:20

10 Answers10

12

No doubt by now you have figured this out but on the offchance you have not I used code like the following on a couple of sse scripts and it worked like a charm. The code below is generic and does not feature your sql or recordset processing but the idea is sound(!?)

<?php
    set_time_limit( 0 );
    ini_set('auto_detect_line_endings', 1);
    ini_set('mysql.connect_timeout','7200');
    ini_set('max_execution_time', '0');

    date_default_timezone_set( 'Europe/London' );
    ob_end_clean();
    gc_enable();



    header('Content-Type: text/event-stream');
    header('Cache-Control: no-cache');
    header('Access-Control-Allow-Credentials: true');
    header('Access-Control-Allow-Methods: GET');
    header('Access-Control-Expose-Headers: X-Events');  




    if( !function_exists('sse_message') ){
        function sse_message( $evtname='chat', $data=null, $retry=1000 ){
            if( !is_null( $data ) ){
                echo "event:".$evtname."\r\n";
                echo "retry:".$retry."\r\n";
                echo "data:" . json_encode( $data, JSON_FORCE_OBJECT|JSON_HEX_QUOT|JSON_HEX_TAG|JSON_HEX_AMP|JSON_HEX_APOS );
                echo "\r\n\r\n";    
            }
        }
    }

    $sleep=1;
    $c=1;

   $pdo=new dbpdo();/* wrapper class for PDO that simplifies using PDO */

    while( true ){
        if( connection_status() != CONNECTION_NORMAL or connection_aborted() ) {
            break;
        }
        /* Infinite loop is running - perform actions you need */

        /* Query database */
        /*
            $sql='select * from `table`';
            $res=$pdo->query($sql);
        */

        /* Process recordset from db */
        /*
        $payload=array();
        foreach( $res as $rs ){
            $payload[]=array('message'=>$rs->message);  
        }
        */

        /* prepare sse message */
        sse_message( 'chat', array('field'=>'blah blah blah','id'=>'XYZ','payload'=>$payload ) );

        /* Send output */
        if( @ob_get_level() > 0 ) for( $i=0; $i < @ob_get_level(); $i++ ) @ob_flush();
        @flush();

        /* wait */
        sleep( $sleep );
        $c++;

        if( $c % 1000 == 0 ){/* I used this whilst streaming twitter data to try to reduce memory leaks */
            gc_collect_cycles();
            $c=1;   
        }
    }



    if( @ob_get_level() > 0 ) {
        for( $i=0; $i < @ob_get_level(); $i++ ) @ob_flush();
        @ob_end_clean();
    }
?>
Professor Abronsius
  • 33,063
  • 5
  • 32
  • 46
  • 1
    You will be using VPS or dedicated server, where set_time_limit and other ini_set is enabled by default, on some hosting these function will not work because they have disabled by system administrator due to shared environment. – Touqeer Shafi Apr 30 '15 at 07:02
6

While this is not a direct answer as to the problem, try using this method to find the error.. Your not getting errors, but this should help you find them maybe?

Basically you want to have a simple PHP script which includes your main script, but this page enables errors... Example below..

index.php / Simple Error Includer

<?php
    ini_set('display_errors',1);
    ini_set('display_startup_errors',1);
    error_reporting(-1);
    require "other.php";
?> 

other.php / You Main Script

<?php
    ini_set('display_errors',1);
    ini_set('display_startup_errors',1);
    error_reporting(-1);
 weqwe qweqeq
 qweqweqweqwe

?> 

If you create a setup like this, if you view index.php you will see the following error Parse error: syntax error, unexpected 'qweqeq' (T_STRING) in /var/www/html/syntax_errors/other.php on line 5 because it does not have an invalid syntax on the main page and allows any includes to be error checked..

But if you where to view other.php, you would simply get a white / blank page because its unable to validate the whole page/script.

I use this method in my projects, that way regardless of what i do in other.php or any linked php pages, i will see an error report for them.

Please understand the code before commenting to say this disables error control means you did not bother to RTM

Fill the buffer

Another issue in the past that i remember was filling the buffer before it would output to the browser. So try something like this before your loop.

echo str_repeat("\n",4096);  // Exceed the required browser threshold 
for($i=0;$i<70;$i++)     {
    echo "something as normal";
    flush();
    sleep(1);
}

Examples at http://www.sitepoint.com/php-streaming-output-buffering-explained/

Funk Forty Niner
  • 74,450
  • 15
  • 68
  • 141
Angry 84
  • 2,935
  • 1
  • 25
  • 24
  • I'm not sure that's what the OP wanted. Also, that is completely wrong. You are temporarily setting the errors to not show, so you don't see them. The same errors are there. This has nothing to do with two files, nor is it a "technique." This is a very very bad idea to turn all errors off. – Evan Carslake Apr 29 '15 at 14:33
  • The first sentence states its not a direct answer and this is the most common method of showing errors on a PHP script.. It does not disable error control.. error_reporting -1 means show all errors... refer to http://php.net/manual/en/function.error-reporting.php ... As for this "technique" it actually is very handy. Sorry you disagree ... I'll let the majority decide on this one ;) – Angry 84 Apr 29 '15 at 15:30
  • And the "Fill the [browser's!] buffer" advice is actually a life-saver! It's totally non-obvious, it's rarely mentioned in examples, and SO is full of voodoo trying to avoid the first message delay... So, thanks for that, at least! – Sz. Aug 29 '19 at 00:31
5

It seems like the sleep function is interfering with the output. Putting the sleep function AFTERWARDS did work:

<?php
while(true) {
    echo "data: This is the message.";
    ob_flush();
    flush();
    sleep(3);
    }

As other people suggest, I would encourage to use AJAX instead of an infinite loop, but that was not your question.

galfrano
  • 59
  • 1
  • Do take into account that the script will most likely not show any output until after the 30th iteration (37th in my environment). If you reduce the sleep time to 1, it may work putting sleep before the flush. – galfrano Apr 28 '15 at 22:30
  • This simply sounds like your not accounting for a 30 second max script execution.. – Angry 84 Apr 29 '15 at 15:36
  • 30 seconds is only the default. Even without changing the php.ini, the directive can be overridden with ini_set or a .htaccess file. For this question I think it is implied that such limitation is not the issue. – galfrano Apr 29 '15 at 15:45
  • well yes of course, but this was directed at your first comment.. Though thinking about it now.. This would simply be the buffer is not full.. When using output buffering,... Its best to send a few 100 or 1000 blank spaces to fill the initial buffer.. This is common practice with OB.. This is browser dependant as well – Angry 84 Apr 29 '15 at 15:48
5

The following code works fine here, also using Mayhem his str_repeat function to add 4k of data (that is usually the minimum for a tcp packet to be flushed by php)

echo str_repeat(' ', 4096);
while(true)
{
    echo "data: This is the message.";
    flush();
    sleep(3);
}
twicejr
  • 1,319
  • 3
  • 13
  • 21
  • 2
    Generally you do not need the 4096 characters in the loop, just only once initially.. After that you can flush as you need with small content/output.. I would remove the str_repeat function to above the while /loop.. – Angry 84 May 01 '15 at 07:47
  • 1
    Yes, there's a common confusion here: there are (at least...) _two_ buffers: PHP's OB, and the browser's own input buffer! To tame the PHP side is relatively easy. The _initial_ 4 (or even 8, e.g. for Firefox here, apparently) KB is for the _browser_, to get it actually rendering the "unfinished" page. – Sz. Aug 29 '19 at 00:36
4

One thing I have noticed here is sleep() function in combination with ob_start() and - THERE IS NO - ob_start() anywhere in the full code example, yet there is flush() and ob_flush() ..

What are you flushing anyway? And why not simply ob_end_flush() ?

The thing is that sleep() than echo(), than sleep() again, than echo() again, etc, etc.. has no effect when output buffering is turned on. Sleep function works as expected when output buffering is not in play - in between. In fact, it might *(and it will) produce quite unexpected results, and those results won't be the one we want to see.

Spooky
  • 1,235
  • 15
  • 17
  • I know I would also be using flush() here to try to get the data sent to the browser without needing to fill the PHP and Apache buffers. These would be different buffers than the code-space output buffer that you mentioned. – creuzerm Apr 28 '15 at 14:01
  • Where is `ob_start()` .. in Your code? I asked 'what do you flush, anyway?' because of that. Where do you start output_buffering ? You need to start it somewhere before using other buffer functions. All I saw with 'start' is `session_start()` which has nothing to do with output_buffering.. Two different worlds. – Spooky Apr 28 '15 at 15:57
  • ob_flush() would do nothing here is there is no output buffer to flush. Sloppy coding, but also potentially addressing a library that could open an output buffer elsewhere (not in the provided example). – creuzerm Apr 30 '15 at 22:10
  • Regarding coding it self, `set_time_limit()` after calling `header()` function is also a bad idea. Also, `ob_flush()` and `ob_end_flush()` are not the same thing. `flush()` than `ob_flush()` is the same as `ob_end_flush()` .. Just like `$var = ob_get_clean();` is the same as `$var = ob_get_contents(); ob_end_clean();` From Your perspective, I can tell that You still don't have a habit to see what the most credible source for php *(http://php.net - manual) holds for us, explained briefly. Including very valuable comments for each and every function in php arsenal. – Spooky Apr 30 '15 at 22:54
4

Instead of using loop try this code given below which is working(tested myself) fine as per your requirement

echo "data: This is the message.";
$url1="<your-page-name>.php";
header("Refresh: 5; URL=$url1");

what this will do is it will call itself every 5 seconds (in your case set it to 3 instead of 5) and echo the output.

ruleboy21
  • 5,510
  • 4
  • 17
  • 34
4

I had the same issue and finally found the easy and quick solution on kevin choppin's blog:

Session Locks
First and foremost, if you're using sessions for whatever reason you will need to make them read-only on the stream. If they're writable, this will lock them everywhere else, so any page loads will hang while the server waits for them to become writable again. This is easily fixed by calling; session_write_close();

pato
  • 335
  • 1
  • 12
3

I am going to take a chance and state the obvious,

you could query the server every 3 seconds, and let the client do the waiting...

This could be done easily with javascript

for example, try this code and name if file.php

<?php
$action='';
if (array_key_exists('action',$_GET))
{$action=$_GET['action'];}
if ($action=='poll')
{
 echo "this message will be sent every 3 sec";
}
else
{
?><HTML><HEAD>
<SCRIPT SRC="http://code.jquery.com/jquery-2.1.3.min.js"></SCRIPT>
<SCRIPT>
function doPoll()
{
    $('#response').append($.get("file.php?action=poll"));
    setTimeout(doPoll, 3000);
}
doPoll();
</SCRIPT>
</HEAD><BODY><DIV id="response"></DIV></BODY></HTML><?php
}
Uri Goren
  • 13,386
  • 6
  • 58
  • 110
  • 3
    this is a completely incorrect answer. What you have done above is long polling, which defeats the purpose of server sent events - and is atleast 3 to 4 orders of magnitude away from what the OP is trying to achieve (websockets, server sent event, forever frame and then this long polling technique) in terms of strain on both server and client Please look at this tutorial http://www.howopensource.com/2014/12/introduction-to-server-sent-events/ – BMac Apr 30 '15 at 08:47
3

Could it be as simple as the script timing out?

Eventually PHP scripts self terminate if they run for too long. The solution for when you don't want this to happen is to keep resetting the time out.

So a simple addition might be all you need:

<?php
    while(true) {
        echo "data: This is the message.";
        set_time_limit(30);
        sleep(3);
        ob_flush();
        flush();
    }
?>

Of course, that might not be it but my gut instinct is that this is the problem.

UPDATE: I noticed in the comments that you are using some free hosting. If they are running PHP in safe mode then you cannot reset your timeout.

  • 1
    This can't be done with pure PHP. Updating would require page reloads or AJAX. – Evan Carslake Apr 27 '15 at 22:27
  • 1
    @EvanCarslake this can be done with pure PHP, thats the cool part about output buffering.. If you have a iframe that loads a PHP with output buffering,,, you can trigger an update with PHP code... While its not always a clean solution it does work and is a very well known method. – Angry 84 Apr 29 '15 at 07:22
0

I suggest using if() statement instead of using while. And in your case your condition is always true, hence it is in infinite loop.

Thyagi
  • 190
  • 1
  • 6
  • 1
    The point of his script is... To have the loop, replacing a DO/WHILE with IF will simply execute once and then end.. A loop/while true means it will run always without needing a fake condition.. Its the simplest means of always returning a true condition check. – Angry 84 May 01 '15 at 03:55