Search code examples
phpserver-sent-events

Data getting lost in server-sent event with PHP handler


I'm working on a one-way messaging system using server-sent events. I have a file (server.html) which sends the contents of a textarea to a PHP file (handler.php).

function sendSubtitle(val) {
    var xhr = new XMLHttpRequest();
    var url = "handler.php";
    var postdata = "s=" + val;
    xhr.open('POST', url, true);
    xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");  
    xhr.send(postdata);
    //alert(val);
}

This works (alert(val) displays the text in the textarea).

My handler.php code looks like this:

header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
$stringData = $_POST['s'];

echo "data: Data is {$stringData}\n\n";
flush();

And the relevant part of my SSE receiver file (client.html) is as follows:

if(typeof(EventSource) !== "undefined") {
    var source = new EventSource("handler.php");
    source.onmessage = function(event) {
        var textarea = document.getElementById('subtitles');
        textarea.value += event.data + "<br>";
        textarea.scrollTop = textarea.scrollHeight;

    };
} else {
    document.getElementById("subtitles").value = "Server-sent events not supported.";
}

The problem is that client.html only displays "data: Data is", so the text from server.html is getting lost somewhere along the way. I imagine it's the PHP code that's falling over, but I can't work out what's wrong. If anyone can help, I'd appreciate it.

EDIT

I chose to use SSE as opposed to websockets as I only need one-way communication: server.html should push the contents of its textarea to client.html whenever it changes. All the examples of SSE that I've looked at (and I've looked at a lot!) send "automatic" time-based data. I haven't seen any that use real-time user input. So perhaps I should clarify my original question and ask, "How can I use SSE to update a DIV (or whatever) in web page B whenever the user types in a textarea in web page A?"

UPDATE

I've narrowed the issue down to the while loop in the PHP file and have therefore asked a new question: Server-side PHP event page not loading when using while loop


Solution

  • Assuming you want to send a value from server.html and a value at client.html will be automatically updated...

    You will need to store the new value somewhere because multiple instances of a script do not share variables just like that. This new value can be stored in a file, database or as a session variable, etc.

    Steps:

    1. Send new value to phpScript1 with clientScript1.
    2. Store new value with phpScript1.
    3. Connect clientScript2 to phpScript2.
    4. Send stored value to clientScript2 if it is changed.

    Getting the new value 'on the fly' means phpScript2 must loop execution and send a message to clientScript2 whenever the value has been changed by clientScript1.

    Of course there are more and different approaches to achieve the same results.

    Below there's some code from a scratchpad I've used in previous project. Most parts come from a class (which is in development) so I had to adopt quite a lot of code. Also I've tried to fit it into your existing code. Hopefully I didn't introduce any errors.
    Do note I did not take any validation of your value into account! Also the code isn't debugged or optimized, so it's not ready for production.

    Client side (send new value, e.g. your code):

    function sendSubtitle(val) {
        var xhr = new XMLHttpRequest();
        var url = "handler.php";
        var postdata = "s=" + val;
        xhr.open('POST', url, true);
        xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");  
        xhr.send(postdata);
        //alert(val);
    }
    

    Server side (store new value):

    <?php
    session_start();
    $_SESSION['s'] = $_POST['s'];
    

    Client side (get new value):

    //Check for SSE support at client side.
    if (!!window.EventSource) {
        var es = new EventSource("SSE_server.php");
    } else {
        console.log("SSE is not supported by your client");
        //You could fallback on XHR requests.
    }
    
    //Define eventhandler for opening connection.
    es.addEventListener('open', function(e) {
      console.log("Connection opened!");
    }, false);
    
    //Define evenhandler for failing SSE request.
    es.addEventListener('error', function(event) {
        /*
         * readyState defines the connection status:
         * 0 = CONNECTING:  Connecting
         * 1 = OPEN:        Open
         * 2 = CLOSED:      Closed
         */
      if (es.readyState == EventSource.CLOSED) {
        // Connection was closed.
      } else {
          es.close(); //Close to prevent a reconnection.
          console.log("EventSource failed.");
      }
    });
    
    //Define evenhandler for any response recieved.
    es.addEventListener('message', function(event) {
        console.log('Response recieved: ' + event.data);
    }, false);
    
    // Or define a listener for named event: event1
    es.addEventListener('event1', function(event) {
        var response = JSON.parse(event.data);
        var textarea = document.getElementById("subtitles");
        textarea.value += response + "<br>";
        textarea.scrollTop = textarea.scrollHeight;
    });
    

    Server side (send new value):

    <?php
    $id = 0;
    $event = 'event1';
    $oldValue = null;
    session_start();
    
    //Validate the clients request headers.
    if (headers_sent($file, $line)) {
        header("HTTP/1.1 400 Bad Request");
        exit('Headers already sent in %s at line %d, cannot send data to client correctly.');
    }
    if (isset($_SERVER['HTTP_ACCEPT']) && $_SERVER['HTTP_ACCEPT'] != 'text/event-stream') {
        header("HTTP/1.1 400 Bad Request");
        exit('The client does not accept the correct response format.');
    }
    
    //Disable time limit
    @set_time_limit(0);
    
    //Initialize the output buffer
    if(function_exists('apache_setenv')){
        @apache_setenv('no-gzip', 1);
    }
    @ini_set('zlib.output_compression', 0);
    @ini_set('implicit_flush', 1);
    while (ob_get_level() != 0) {
        ob_end_flush();
    }
    ob_implicit_flush(1);
    ob_start();
    
    //Send the proper headers
    header('Content-Type: text/event-stream; charset=UTF-8');
    header('Cache-Control: no-cache');
    header('X-Accel-Buffering: no'); // Disables FastCGI Buffering on Nginx
    
    //Record start time
    $start = time();
    
    //Keep the script running
    while(true){
        if((time() - $start) % 300 == 0){
            //Send a random message every 300ms to keep the connection alive.
            echo ': ' . sha1( mt_rand() ) . "\n\n";
        }
    
        //If a new value hasn't been sent yet, set it to default.
        session_start();
        if (!array_key_exists('s', $_SESSION)) {
            $_SESSION['s'] = null;
        }
    
        //Check if value has been changed.
        if ($oldValue !== $_SESSION['s']) {
            //Value is changed
            $oldValue = $_SESSION['s'];
            echo 'id: '    . $id++  . PHP_EOL;  //Id of message
            echo 'event: ' . $event . PHP_EOL;  //Event Name to trigger the client side eventhandler
            echo 'retry: 5000'      . PHP_EOL;  //Define custom reconnection time. (Default to 3000ms when not specified)
            echo 'data: '  . json_encode($_SESSION['s']) . PHP_EOL; //Data to send to client side eventhandler
            //Note: When sending html, you might need to encode with flags: JSON_HEX_QUOT | JSON_HEX_TAG
            echo PHP_EOL;
            //Send Data in the output buffer buffer to client.
            @ob_flush();
            @flush();
        }
    
        //Close session to release the lock
        session_write_close();
    
        if ( connection_aborted() ) {
            //Connection is aborted at client side.
            break;
        }
        if((time() - $start) > 600) {
            //break if the time exceeds the limit of 600ms.
            //Client will retry to open the connection and start this script again.
            //The limit should be larger than the time needed by the script for a single loop.
            break;
        }
    
        //Sleep for reducing processor load.
        usleep(500000);
    }