PHP Server Sent Events locking up every other request - php

I implemented a simple endpoint on my PHP server, I am subscribing to it in my Angular application using Azure Event Source to be able to send headers with the request.
Connection it self works and I get the stream back, but once the connection is established every other call locks up. They stay in "Pending" state until I close the connection to the SSE endpoint. I've tried session_write_close(); but no help. Is there something I'm missing? Ideally once the connection is established I would not want it to effect any other calls. Any suggestions are much appreciated.
PHP: Server Side Implementation
public function newSSEEvent()
{
session_write_close();
header("Content-Type: text/event-stream");
header("Cache-Control: no-store");
$counter = rand(1, 10);
while (true) {
echo "event: Date\n";
$curDate = date(DATE_ISO8601);
echo 'data: {"time": "' . $curDate . '"}';
echo "\n\n";
$counter--;
if (!$counter) {
echo "event: Ping\n";
echo 'data: Last Ping' . $curDate . "\n\n";
$counter = rand(1, 10);
}
ob_flush();
flush();
sleep(1);
}
}
Angular: Client Side Implementation
async getEvent(url) {
await fetchEventSource(
url,
{
method: "GET",
headers: {
CustomHeaders
},
onmessage(ev) {
console.log(ev);
},
}
);
}

Related

SSE not logging disconnect

I currently have a simple SSE page for testing. I can currently get the page to log when a user connects and while they are connected; however, when the page closes it seems (eg user disconnects) it doesnt log anything. I am using the function error_log for logging. Also, using NGINX and PHP 7.2. Here is my code:
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('X-Accel-Buffering: no');
ignore_user_abort(true);
$aid = 1;
function sendMsg($id , $msg) {
echo "id: $id" . PHP_EOL;
echo "data: " . json_encode(array('x'=> $msg)) . PHP_EOL;
echo "retry: 0" . PHP_EOL;
echo PHP_EOL;
#ob_end_flush();
flush();
}//END FUNCTION sendMsg
function endPacket() {
echo "0\r\n\r\n";
#ob_end_flush();
flush();
}//END FUNCTION endPacket
$startedAt = time();
error_log("Starting SSE to User: " . $aid);
while (true) {
set_time_limit(15);
if ( connection_status() != 0 ) {
error_log("Ending SSE to User: " . $aid);
endPacket();
die();
}//END IF
error_log("Connected SSE as User: " . $aid);
sendMsg( $startedAt, $aid . ' | ' . connection_status() );
sleep(10);
}//END WHILE
Your code is failing to detect a disconnect because you need to write to the user before you can detect a disconnect
While it looks like your demo code does this, it actually doesn't. This is what your code does now:
detect_connection();
sendMsg();
sleep();
detect_connection();
sendMsg();
<timeout>
The only way in your example to trigger the connection disconnect message is if the connection is closed the first time you call "sendMsg()", what probably is not going to happen as you are not that quick

Server-side PHP event page not loading when using while loop

I have a file named handler.php which reads data from a text file and pushes it to a client page.
Relevant client code:
<script>
if(typeof(EventSource) !== "undefined") {
var source = new EventSource("handler.php");
source.onmessage = function(event) {
var textarea = document.getElementById("subtitles");
textarea.value += event.data;
textarea.scrollTop = textarea.scrollHeight;
};
} else {
document.getElementById("subtitles").value = "Server-sent events not supported.";
}
</script>
Handler.php code:
$id = 0;
$event = 'event1';
$oldValue = null;
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('X-Accel-Buffering: no');
while(true){
try {
$data = file_get_contents('liveData.txt');
} catch(Exception $e) {
$data = $e->getMessage();
}
if ($oldValue !== $data) {
$oldValue = $data;
echo 'id: ' . $id++ . PHP_EOL;
echo 'event: ' . $event . PHP_EOL;
echo 'retry: 2000' . PHP_EOL;
echo 'data: ' . json_encode($data) . PHP_EOL;
echo PHP_EOL;
#ob_flush();
#flush();
sleep(1);
}
}
When using the loop, handler.php is never loaded so the client doesn't get sent any data. In the Chrome developer network tab, handler.php is shown as "Pending" and then "Cancelled". The file itself stays locked for around 30 seconds.
However, if I remove the while loop (as shown below), handler.php is loaded and the client does receive data (only once, even though the liveData.txt file is constantly updated).
Handler.php without loop:
$id = 0;
$event = 'event1';
$oldValue = null;
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('X-Accel-Buffering: no');
try {
$data = file_get_contents('liveData.txt');
} catch(Exception $e) {
$data = $e->getMessage();
}
if ($oldValue !== $data) {
$oldValue = $data;
echo 'id: ' . $id++ . PHP_EOL;
echo 'event: ' . $event . PHP_EOL;
echo 'retry: 2000' . PHP_EOL;
echo 'data: ' . json_encode($data) . PHP_EOL;
echo PHP_EOL;
#ob_flush();
#flush();
}
I'm using SSE as I only need one-way communication (so websockets are probably overkill) and I really don't want to use polling. If I can't sort this out, I may have to.
The client side of the SSE connection looks OK as far as I can tell - though I moved the var textarea..... outside of the onmessage handler.
UPDATE: I should have looked closer but the event to monitor is event1 so we need to set an event listener for that event.
<script>
if( typeof( EventSource ) !== "undefined" ) {
var url = 'handler.php'
var source = new EventSource( url );
var textarea = document.getElementById("subtitles");
source.addEventListener('event1', function(e){
textarea.value += e.data;
textarea.scrollTop = textarea.scrollHeight;
console.info(e.data);
},false );
} else {
document.getElementById("subtitles").value = "Server-sent events not supported.";
}
</script>
As for the SSE server script I tend to employ a method like this
<?php
/* make sure the script does not timeout */
set_time_limit( 0 );
ini_set('auto_detect_line_endings', 1);
ini_set('max_execution_time', '0');
/* start fresh */
ob_end_clean();
/* ultility function for sending SSE messages */
function sse( $evtname='sse', $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";
}
}
$id = 0;
$event = 'event1';
$oldValue = null;
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('X-Accel-Buffering: no');
while( true ){
try {
$data = #file_get_contents( 'liveData.txt' );
} catch( Exception $e ) {
$data = $e->getMessage();
}
if( $oldValue !== $data ) {
/* data has changed or first iteration */
$oldValue = $data;
/* send the sse message */
sse( $event, $data );
/* make sure all buffers are cleansed */
if( #ob_get_level() > 0 ) for( $i=0; $i < #ob_get_level(); $i++ ) #ob_flush();
#flush();
}
/*
sleep each iteration regardless of whether the data has changed or not....
*/
sleep(1);
}
if( #ob_get_level() > 0 ) {
for( $i=0; $i < #ob_get_level(); $i++ ) #ob_flush();
#ob_end_clean();
}
?>
When using the loop, handler.php is never loaded so the client doesn't
get sent any data. In the Chrome developer network tab, handler.php is
shown as "Pending" and then "Cancelled". The file itself stays locked
for around 30 seconds.
This is because the webserver (Apache) or the browser or even PHP itself cancel the request when there is no response within 30 seconds.
So I guess the flushing does not work, try to actively start and end the buffer without using # functions so you get a clue when there is an error.
// Start output buffer
ob_start();
// Write content
echo '';
// Flush output buffer
ob_end_flush();
I think you have a problem with the way the web works. The PHP code doesn't run in your browser - it just creates something that the web server hands off to the browser over the wire.
Once the page is loaded from the server that's it. You will need to implement something that polls for changes.
One way I've done this is to put the page in a loop that refreshes and therefore fetches the page again with the new data every second or so (but this could seriously overload your server if there's a lot of folks on that page).
The only other solution is to use push technology and a javascript framework that can take the push and repopulate the relevant parts of the page, or a javascript loop on a timer that pulls the data.
(Posted solution on behalf of the question author).
Success! While debugging for the nth time, I decided to go back to basics and start again. I scrapped the loop and reduced the PHP code to a bare minimum, but kept the client-side code RamRaider provided. And now it all works wonderfully! And by playing around with the retry value, I can specify exactly how often data is pushed.
PHP (server side):
<?php
$id = 0;
$event = 'event1';
$oldValue = null;
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('X-Accel-Buffering: no');
try {
$data = file_get_contents('liveData.txt');
} catch(Exception $e) {
$data = $e->getMessage();
}
if ($oldValue !== $data) {
$oldValue = $data;
echo 'id: ' . $id++ . PHP_EOL;
echo 'event: ' . $event . PHP_EOL;
echo 'retry: 500' . PHP_EOL;
echo "data: {$data}\n\n";
echo PHP_EOL;
#ob_flush();
#flush();
}
?>
Javascript (client side):
<script>
if ( typeof(EventSource ) !== "undefined") {
var url = 'handler.php'
var source = new EventSource( url );
var textarea = document.getElementById("subtitles");
source.addEventListener('event1', function(e){
textarea.value += e.data;
textarea.scrollTop = textarea.scrollHeight;
console.info(e.data);
}, false );
} else {
document.getElementById("subtitles").value = "Server-sent events not supported.";
}
</script>

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
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:
Send new value to phpScript1 with clientScript1.
Store new value with phpScript1.
Connect clientScript2 to phpScript2.
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);
}
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";

Reading Server Side Events in Python using sseclient

I'm new to Server Side Events and started some tests with PHP on the server side and Python on the client side using the sseclient library.
Using a very basic PHP script, based on the w3schools tutorial I can see the data being received in Python:
<?php
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
function sendMsg($id, $msg) {
echo "id: $id" . PHP_EOL;
echo "data: $msg" . PHP_EOL;
echo PHP_EOL;
ob_flush();
flush();
}
$time = date('r');
// echo "data: The server time is: {$time}\n\n";
// flush();
sendMsg(time(),"The server time is: {$time}\n\n");
?>
and in Python:
#!/usr/bin/env python
from sseclient import SSEClient
messages = SSEClient('http://pathto/myscript.php')
for msg in messages:
print msg
As a second step I've tried sending data read from an array stored in the $_SESSION variable. This seems to work when I connect to the SSE stream from javascript in the browser, but it doesn't work and I'm not sure why.
Here's my basic PHP script:
<?php
session_start();
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
function sendMsg($id, $msg) {
echo "id: $id" . PHP_EOL;
echo "data: $msg" . PHP_EOL;
echo PHP_EOL;
ob_flush();
flush();
}
// check for session data
if (isset($_SESSION["data"])){
#as long there are elements in the data array, stream one at a time, clearing the array (FIFO)
while(count($_SESSION["data"]) > 0){
$serverTime = time();
$data = array_shift($_SESSION["data"]);
sendMsg($serverTime,$data);
}
}
?>
and the Python script is the same.
Why isn't the sseclient Python script picking up the events from the above PHP script (while a basic JS one does) ?
The PHP session variable is sent as a cookie; if you view your JavaScript version with Firebug (or equivalent) you should see the cookie being sent to the SSE server script.
So you'll need to set up a session for the Python script, and have it send that in a cookie too.
You could confirm this guess by adding some error handling to your PHP script:
...
if (isset($_SESSION["data"])){
//current code here
}else{
sendMsg(time(), "Error: no session");
}

PHP Ajax: live report of the PHP algorithm

If I have the next algorithm in the file test.php:
for ($i = 0; $i < 1000; ++$i)
{
//Operations...
ALERT_TO_MAIN_PAGE($i);
}
I call test.php with AJAX form the page main.html
How can I track the progress of $i, with live values?
So main.html will show like this, very time the PHP file has completed one iteration:
Done 0.
Done 1.
Done 2.
...
You can try using HTML5 Server Sent Events to send messages like that to the browser.
Right off the website:
<?php
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache'); // recommended to prevent caching of event data.
/**
* Constructs the SSE data format and flushes that data to the client.
*
* #param string $id Timestamp/id of this connection.
* #param string $msg Line of text that should be transmitted.
*/
function sendMsg($id, $msg) {
echo "id: $id" . PHP_EOL;
echo "data: $msg" . PHP_EOL;
echo PHP_EOL;
ob_flush();
flush();
}
$serverTime = time();
sendMsg($serverTime, 'server time: ' . date("h:i:s", time()));
You can't do it this way, because ajax waits for the operation to complete on the php side.
You should run your php offline (in background) and use ajax to simply ask for state (i.e. read info about progress from session).

Categories