1

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

2 Answers2

1

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);
}
DigiLive
  • 1,093
  • 1
  • 11
  • 28
  • I understand now. Thanks so much for your detailed answer! – Mark Raishbrook Oct 03 '18 at 08:02
  • @MarkRaishbrook No problem. Good luck with your development. – DigiLive Oct 03 '18 at 10:36
  • I've corrected a couple of errors in the server side send code: $_SESION, for example, a missing ) at the end of the exit('Headers already sent... line, and I moved the curly brace above echo 'retry: 5000' to the end of the file. So now I have the client side send code as above but sending to store.php, store.php (as per your code), the client side get code (where EventSource = handler.php) and finally the edited server side send code (handler.php). It worked once, but then errors with "The client does not accept the correct response format". Can you see where I'm sending the wrong format? – Mark Raishbrook Oct 03 '18 at 10:57
  • Sorry about the errors, I've corrected them in my answer as well. The client should call parser.php only once en keep the connection open. So it shouldn't work once and then give that error because the script is stuck in the while loop. Perhaps the curly brace you moved to the end is now at the wrong place. It should have been removed. The curly brace below @flush() is the one which is needed. Please try it again with new code. FYI: When your client script does an SSE request, one of the request headers should be 'text/event-stream'. If it isn't, you'll get the error about the response format. – DigiLive Oct 03 '18 at 11:27
  • OK, will try again now! – Mark Raishbrook Oct 03 '18 at 11:29
  • I'm still getting the incorrect response format error. Where exactly in the client code above should I set the header format to 'text/event-stream'? – Mark Raishbrook Oct 03 '18 at 12:13
  • Assuming you're using a browser, the browser would do it by itself. You can check it in the network tab of the browsers development tools. You can also choose to skip this validation and see what happens. – DigiLive Oct 03 '18 at 13:32
  • I think it's a session locking issue. Thanks for all your help - I'll post a new question if needed. – Mark Raishbrook Oct 03 '18 at 15:19
  • There is a session locking issue, but that shouldn't trigger that error message. I'll update the code about the session locking after dinner. – DigiLive Oct 03 '18 at 15:24
0

You called handler.php first time in the server.html and again in client.html. Both are different processes. The variable state won't be retained in the web server. You need to store it somewhere if you want that value in another PHP process. May be you can use sessions or database.

While using sessions you can store the values in two files like:

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

And in client.php

<?php
//client.php
session_start();
echo "data: Data is ".$_SESSION['s']."\n\n";
Prakash S
  • 632
  • 6
  • 12
  • Thanks for the answer, Prakash, but I'm not sure that's the issue. I've simply adapted the example here https://www.w3schools.com/html/html5_serversentevents.asp. Client.html is using handler.php as the event source so when handler.php is updated, it pushes the data to client.html. – Mark Raishbrook Oct 02 '18 at 21:04
  • I've edited my question to clarify what I am trying to do, @Prakash S. – Mark Raishbrook Oct 02 '18 at 21:37
  • The issue still applies... the php file of the example defines the data which is sent back itself. I will look if I've stored a gist about sse where the server loops and sent data on change of a stored variable. – DigiLive Oct 03 '18 at 04:25
  • 1
    @PrakashS You were actually on the right track, so thank you very much. Unfortunately, I don't have enough reputation to upvote you, so please accept a virtual +1! – Mark Raishbrook Oct 03 '18 at 22:04