SSE echo from PHP duplicating at random intervals - php

I'm trying to set up a PHP server send event, which works okay. But at random intervals it is pushing the same data repeatedly.
Here's a quick scenario to clarify what I'm describing: Let's say I insert a db record at 1:00:00. The record's data is pushed as it should. However, at 1:03:00 that record's data is pushed a second time. Then at 1:03:17, it is pushed again. And I now have 3 instances of the record displayed.
Why is this happening, and why at random intervals?
I increased php execution time, but the issue is still occurring.
In the browser console, I'm getting this error: net::ERR_INCOMPLETE_CHUNKED_ENCODING.
I have this for client side:
<script>
var source = new EventSource('pdo_updates.php');
var pdo_updates;
source.onmessage = function(e) {
pdo_updates = e.lastEventId + '' + e.data + '<br>';
document.getElementById("videoID").innerHTML += pdo_updates;
};
evtSource.close();
</script>
And this for server side:
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache, must-revalidate, post-check=0, pre-check=0');
function send_msg($id, $msg) {
echo "data: $msg" . PHP_EOL;
echo PHP_EOL;
ob_flush();
flush();
}
$last_event_id = floatval(isset($_SERVER["HTTP_LAST_EVENT_ID"]) ? $_SERVER["HTTP_LAST_EVENT_ID"] : False);
if ($last_event_id == 0) {
$last_event_id = floatval(isset($_GET["lastEventId"]) ? $_GET["lastEventId"] : False);
}
$last_id = 0;
try {
$conn = new PDO('mysql:host=localhost;dbname=my_db', $username, $password);
$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
while(1) {
$id = $last_event_id != False ? $last_event_id : $last_id;
$stmt = $conn->prepare("SELECT id, message FROM messages WHERE id > :id ORDER BY id DESC LIMIT 1");
$result = $stmt->execute(array('id' => $id));
$stmt->bindValue('id', $id);
if ($result) {
while($data = $stmt->fetch(PDO::FETCH_ASSOC)) {
if ($data) {
send_msg($data['id'], $data['message']);
$last_id = $data['id'];
}
}
}
sleep(1);
}
} catch(PDOException $e) {
echo 'ERROR: ' . $e->getMessage();
}

So, after trying just about every keepalive related header, and every apache timeout adjustment config, I ended up running a packet capture. I discovered there was a TCP reset was being triggered by the remote end. I have my site behind Cloudflare, and once I disabled Cloudflare the issue partially disappeared. The TCP session was being refreshed every 100 seconds and would cause the last message to appear again when that happened. This was responsible for the browser console error: net::ERR_INCOMPLETE_CHUNKED_ENCODING
At the same time, however, there was an issue in my submit code. But I'm not 100% sure why.
$.ajax({
type: "POST",
url: url,
data: $("#submitmessage").serialize(), // serializes the form's elements.
success: function(data)
{
$("#myvideo").val("");
}
});
I needed to clear the value in my input field after submit. I haven't had any duplicates since.

Related

How do you update an SSE (Server Sent Event) message with POST request

Please excuse me, I am quite new with backend development.
What I try to accomplish is pretty straight forward. I want to send a POST request to my server. As an example; www.domain.com/api.php?id=foo
And with that value update my SSE with that parameter. But I can't seem to figure it out. I guess I need to store that value somehow in a database or a text file? Preferably in a text file for simplicity.
What I have now:
HTML file:
<!DOCTYPE html>
<html>
<body>
<h1>Getting server updates</h1>
<div id="result"></div>
<script>
if(typeof(EventSource) !== "undefined") {
var source = new EventSource("demo_sse.php");
source.onmessage = function(event) {
document.getElementById("result").innerHTML += event.data + "<br>";
};
} else {
document.getElementById("result").innerHTML = "Sorry, your browser does not support server-sent events...";
}
</script>
</body>
</html>
PHP file:
<?php
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
$test = "One";
echo "data: Page: {$test}\n\n";
flush();
?>
So my question is: How do I change the value of the variable 'test' from a POST request so that the SSE is updated?
It works perfectly now, if I change the value in the PHP file it starts spitting that out in the HTML. But how do I change that value with something external like a POST call?
Thank you! And let me know if I need to clarify.
Okay, so I managed to make it work with a SQL database. :) This is my solution:
<?php
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
// QUERY THE DATABASE FOR CURRENT PAGE NUMBER
$db = new mysqli($host, $username, $password, $database_name); // connect to the DB
$query = $db->prepare("SELECT number FROM page WHERE itemId=1"); // prepate a query
$query->execute(); // actually perform the query
$result = $query->get_result(); // retrieve the result so it can be used inside PHP
$r = $result->fetch_array(MYSQLI_ASSOC); // bind the data from the first result row to $r
// ECHO SERVER SENT EVENT
$pageNumber = $r['number'];
echo "data: Page: {$pageNumber}\n\n";
flush();
// UPDATE QUERY BASED ON GET PARAMATERS
$siteURL = $_GET['page'];
$updateQuery = $db->prepare("UPDATE `page` SET `number` = '$siteURL' WHERE `page`.`itemId` = 1;");
$updateQuery->execute();
?>

How to increase Server-Sent Event's reopen time?

I'm working to show notifications from Server-Sent Event. I checked that every time the browser tries to reconnect with the source about 3 seconds after each connection is closed. That event is getting a call too fast, so my server is loaded too.
So how do I change the reopening time to increase? I have to do at least 60 seconds, so tell me how to do it?
I'm trying the following code.
<table class="table" id="notification"></table>
<script type="text/javascript">
var ssevent = null;
if (!!window.EventSource) {
ssevent = new EventSource('ssevent.php');
ssevent.addEventListener('message', function(e){
if(e.data){
json = JSON.parse(e.data);
if(json){
json.forEach(function(v,i){
html = "<tr><td>"+ v.text +"</td><td>"+ v.date +"</td></tr>";
});
$('#notification').append(html);
}
}
}, false);
ssevent.addEventListener('error', function(e) {
if (e.readyState == EventSource.CLOSED){
console.log("Connection was closed.");
}
}, false);
} else {
console.log('Server-Sent Events not support in your browser');
}
</script>
The file of event stream is as follow.
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
include_once "config.php";
$time = isset($_SESSION['last_event_time']) ? $_SESSION['last_event_time'] : time();
$result = $db->quesry("SELECT * FROM event_noti WHERE event_time < {$time} ")->rows;
$_SESSION['last_event_time'] = time();
if($result){
$json = array();
foreach ($result as $row){
$json[] = array(
'text' => $row['event_text'],
'date' => date("Y-m-d H:i", $row['event_time']),
);
}
echo "data: ". json_encode($json) . "\n\n";
flush();
}
Fundamentally you cannot control this: it is a browser-specific setting.
If your browser is Firefox it appears to be controlled by this setting: dom.server-events.default-reconnection-time with a default of 5000ms.
Taking a step back: the reconnect only happens if the server closes the connection. Why are you closing the connection? (*) Why is a 3-second re-connection too fast?
The point of SSE is to minimize latency; the trade-off is more resource usage, in particular having to keep a dedicated socket open for each client.
So it sounds like you don't want to be using SSE, and instead want to use a simple AJAX poll, on a 60-second setInterval() call?
*: If you did intend to keep it open, you need to wrap your query and processing the result code in a while(true){...} loop. Put e.g. a one-second sleep at the end of the while loop to stop the DB server being overloaded.
Now I have my answer.
Controlling Reconnection-Timeout:
The browser attempts to reconnect the connection to the source within about 3 seconds after each server-sent event connection is closed. Before trying to reconnect, you can change that timeout by starting the line with retry: and then adding the millisecond number to wait.
I changed the code below and started working as I wanted.
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
include_once "config.php";
$time = isset($_SESSION['last_event_time']) ? $_SESSION['last_event_time'] : time();
$result = $db->quesry("SELECT * FROM event_noti WHERE event_time < {$time} ")->rows;
$_SESSION['last_event_time'] = time();
echo "retry: 60000\n"; // 60 seconds, to wait for next connection.
$json = array();
if($result){
foreach ($result as $row){
$json[] = array(
'text' => $row['event_text'],
'date' => date("Y-m-d H:i", $row['event_time']),
);
}
}
echo "data: ". json_encode($json) . "\n\n";
flush();
Source from

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";

Check for database change with SSE

Trying to create a small notification system. When user fills out the profile then his verification status is set to 1 in database and then I would like to show a notification once that "hey you are now verified". Been searching a lot on the internet, but nothing has helped me to reach my goal. If the status is 1 in database I get the Event: verification_ok in the test.php but if it is 0 I get Maximum execution time of 120 seconds exceeded. Also I don't see any response in my client side code.
This is the server side code (test.php).
<?php
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header("Connection: Keep-alive");
require_once $_SERVER['DOCUMENT_ROOT'].'/PHP/scripts/no_session_redirect.php';
$key = true;
$ver = $user_home->runQuery("SELECT verification_status FROM verification WHERE user_id=:user_id");
$ver->execute(array(":user_id"=>$user_id));
$status = $ver->fetch(PDO::FETCH_ASSOC);
while($key){
if($status["verification_status"] == 1){
pushNotification($status["verification_status"]);
$key = false;
}else{
$status["verification_status"];
sleep(10);
}
}
function pushNotification() {
echo "Event: verification_ok\n";
}
And here is the client side code:
$(document).ready(function() {
if (typeof(EventSource) !== "undefined") {
// Yes! Server-sent events support!
var source = new EventSource("test.php");
source.addEventListener("verification_ok", function(e) {
console.log(e.data);
}, false);
source.addEventListener("open", function(e) {
}, false);
source.addEventListener("error", function(e) {
if (e.readyState == EventSource.CLOSED) {
console.log("Error - connection was lost.");
}
}, false);
} else {
// Sorry! No server-sent events support..
}
});

PHP MYSQL JQuery Long Polling - Not working as expected

My long polling implementation isn't working. Been having a very difficult time understanding where to look toward debugging said code.
Key Points
No Errors
Long polling working randomly (only responds to some changes in MySQL with no distinct pattern)
MySQL is updating correctly
I'm testing this via Localhost WAMP and two browsers with two different sessions
PHP Portion -
$path= $_SERVER[ 'DOCUMENT_ROOT'];
$path .= "/config.php" ;
require_once($path);
require_once(PHP_PATH . "/classes/user.php");
session_start();
require_once(PHP_PATH . "/functions/database.php");
// Return to Login if no Session
if(!isset($_SESSION['user'])){
header("Location: /login");
die();
}
$db = connectdatabase();
$timeout = 40;
// if no post ids kill the script // Should never get here
if(!isset($_POST['post_ids'])){
die();
}
if(!isset($_POST['timestamp'])){
die();
}
$last_ajax_call = $_POST['timestamp'];
$post_ids = trim(strip_tags($_POST['post_ids']));
$id = $_SESSION['user']->getID();
// Check if there are posts from the last search that need to be updated with a comments or the like number has to be updated
$query = "SELECT posts.*, users.first_name, users.last_name, users.picture
FROM posts
LEFT JOIN users
ON users.id = posts.user_id
WHERE ((UNIX_TIMESTAMP(posts.date) > :last_ajax_call OR UNIX_TIMESTAMP(posts.last_modified) > :last_ajax_call)
AND posts.parent IN (:post_ids)) OR (posts.id IN (:post_ids) AND UNIX_TIMESTAMP(posts.last_modified) > :last_ajax_call)";
while ($timeout > 0) {
$check_for_updates = $db->prepare($query);
$check_for_updates->bindParam(':post_ids', $post_ids);
$check_for_updates->bindParam(':last_ajax_call', $last_ajax_call);
$check_for_updates->execute();
$r = $check_for_updates->fetchAll();
if(!empty($r)){
// Get current date time in mysql format
$unix_timestamp = time();
// Cofigure result array to pass back
$result = array(
'timestamp' => $unix_timestamp,
'updates' => $r
);
$json = json_encode($result);
echo $json;
return;
} else {
$timeout --;
usleep( 250000 );
clearstatcache();
}
}
// you only get here if no data found
$unix_timestamp = time();
// Cofigure result array to pass back
$result = array(
'timestamp' => $unix_timestamp
);
$json = json_encode($result);
echo $json;
JQuery Ajax -
function getUpdates(timestamp) {
var post_ids = $("#newsfeed").find("#post_ids").attr('data-post-ids');
var data = {'timestamp' : timestamp,
'post_ids' : post_ids};
poll = $.ajax({
type: 'POST',
url: '/php/check_for_updates.php',
data: data,
async: true, /* If set to non-async, browser shows page as "Loading.."*/
cache: false,
success: function(data) {
try {
// put result data into "obj"
var obj = jQuery.parseJSON(data);
// put the data_from_file into #response
//$('#response').html(obj.data_from_file);
// repeat
console.log("SQL: " + obj['timestamp']);
setTimeout( function() {
// call the function again, this time with the timestamp we just got from server.php
getUpdates(obj['timestamp']);
}, 1000 );
} catch( e ) {
// repeat
// Get mysql formated date
var unix_timestamp = Math.floor(Date.now() / 1000);
console.log("JS: " + unix_timestamp);
setTimeout( function() {
getUpdates(unix_timestamp);
}, 1000 );
}
}
}
);
}
Thanks for all the help guys! I asked around a lot of people and got a bunch of great places to look to debug the code.
I finally found the answer here -
http://blog.preinheimer.com/index.php?/archives/416-PHP-and-Async-requests-with-file-based-sessions.html
http://konrness.com/php5/how-to-prevent-blocking-php-requests/
It looks like I the PHP checking for updates was blocking any updates from happening till the PHP stop checking for updates.
Couple things you can do is:
1.) Open the Chrome Developer tools and then click on the Network tab and clear everything out. Then click on submit. Look at the network tab and see what is being posted and what isn't. Then adjust accordingly from there.
2.) Echo out different steps in your php script and do the same thing with the Network tab and then click on the "results" area and see what's being echoed out and if it's as expected.
From there, you should be able to debug what's happening and figure out where it's going wrong.

Categories