My professional experience hasn't included a lot of "push" technology, but a friend mentioned an upcoming hockey draft and how he wants to be able to push updates to a webpage. I've heard of SSE (Server-Sent Events) and wanted to implement it.
Server-Sent Events is a technology that allows data to be "pushed" from the server to a webpage. The browser calls an endpoint and a connection is made where the browser listens to events publish through this connection.
The connection is made with Javascript by creating an EventSource. This new object takes a URL as its parameter. A callback function is set to the onmessage method of the new EventSource. When an event is received, the onmessage callback function is passed an MessageEvent object.
// Creating an EventSource by calling a different endpoint
const eventSource = new EventSource("sse.php");
// Registering the function that gets called when an event is sent from the server
eventSource.onmessage = function(event) {
// Do something with the event
console.log(event.data);
};
The page is pretty easy to set up; the server is a tad more involved and depends on your technology. For a Node-based solution, visit this page where Zach has a demo of it.
As I don't have a server that runs Node, I created my example using PHP. I started with an example that just sent a periodic message to the browser.
<?php
// Set file mime type event-stream
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
// Loop until the client close the stream
while (true) {
if(connection_aborted()) exit();
// Echo time
$time = date('r');
echo "data: The server time is: {$time}\n\n";
// Flush buffer (force sending data to client)
flush();
// Wait 2 seconds for the next message / event
sleep(2);
}
?>
This example was ok, but missing discussion of sessions. To get to my final solution I had to first find this article. I had to disable sessions.
// make session read-only
session_start();
session_write_close();
If my friend is running a draft, he'd need a way to update the data. I decided to use MySQL and myPhpAdmin. The SSE server side code would now call the database instead of just incrementing a number. This was put into the loop so any DB updates would be created and sent as an event.
$sql = "SELECT * FROM messages\n"
. "order by id desc\n"
. "limit 1";
$result = $conn->query($sql);
if ($result->num_rows > 0) {
if ($row = $result->fetch_assoc()) {
$latestEventId = $row['id'];
...
As a way to update the data, I created a simple PHP page that would insert a record each time the page was refreshed.
<?php
$servername = "<your value>";
$username = "<your value>";
$password = "<your value>";
$dbname = "<your value>";
// Create connection
$conn = new mysqli($servername, $username, $password, $dbname);
// Check connection
if ($conn->connect_error) {
die("Connection failed: " . $conn->connect_error);
}
$sql = "INSERT INTO messages () VALUES ()";
if ($conn->query($sql) === TRUE) {
$data = "New record created successfully";
} else {
$data = "Error: " . $sql . "<br>" . $conn->error;
}
$conn->close();
?>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<section><?= $data ?></section>
<section>
<button type="button" onclick="location.reload()">Reload</button> the page for a new entry
</section>
</body>
</html>
This was a pretty easy POC. Other concerns before this could be used would be the number of connections to the PHP server as well as the database connections.
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SSE</title>
</head>
<body>
<h1>SSE demo with PHP</h1>
<a href="db.php" target="_blank">Click here to open a window which will create a DB entry</a>
<br />
Each time a new entry is added to the database, an event is pushed to this page.
<br />
The link above can be refreshed to add a new event/record in the DB.
<ol id="list">
</ol>
<script>
// Create new event, the server script is sse.php
var eventSource = new EventSource("sse.php");
// Event when receiving a message from the server
eventSource.onmessage = function (event) {
// Append the message to the ordered list
document.getElementById("list").innerHTML += '<li>' + event.data + "</li>";
};
</script>
</body>
</html>
<?php
// Lots of code taken from https://kevinchoppin.dev/blog/server-sent-events-in-php
$servername = "<your value>";
$username = "<your value>";
$password = "<your value>";
$dbname = "<your value>";
try {
// make session read-only
session_start();
session_write_close();
// disable default disconnect checks
ignore_user_abort(true);
// set headers for stream
header("Content-Type: text/event-stream");
header("Cache-Control: no-cache");
header("Access-Control-Allow-Origin: *");
// Is this a new stream or an existing one?
$lastEventId = floatval(isset($_SERVER["HTTP_LAST_EVENT_ID"]) ? $_SERVER["HTTP_LAST_EVENT_ID"] : 0);
if ($lastEventId == 0) {
$lastEventId = floatval(isset($_GET["lastEventId"]) ? $_GET["lastEventId"] : 0);
}
echo ":" . str_repeat(" ", 2048) . "\n"; // 2 kB padding for IE
echo "retry: 2000\n";
// start stream
while (true) {
if (connection_aborted()) {
exit();
} else {
$conn = new mysqli($servername, $username, $password, $dbname);
if ($conn->connect_error) {
die("Connection failed: " . $conn->connect_error);
}
$sql = "SELECT * FROM messages\n"
. "order by id desc\n"
. "limit 1";
$result = $conn->query($sql);
if ($result->num_rows > 0) {
if ($row = $result->fetch_assoc()) {
$latestEventId = $row['id'];
if ($lastEventId < $latestEventId) {
echo "id: " . $latestEventId . "\n";
$time = $row['timestamp'];
echo "data: A record was recorded at: {$time}\n\n";
$lastEventId = $latestEventId;
ob_flush();
flush();
} else {
// no new data to send
echo ": heartbeat\n\n";
ob_flush();
flush();
}
}
}
}
// 2 second sleep then carry on
sleep(2);
}
} catch (Exception $e) {
echo 'Message: ' . $e->getMessage();
}
<?php
$servername = "<your value>";
$username = "<your value>";
$password = "<your value>";
$dbname = "<your value>";
$conn = new mysqli($servername, $username, $password, $dbname);
if ($conn->connect_error) {
die("Connection failed: " . $conn->connect_error);
}
$sql = "INSERT INTO messages () VALUES ()";
if ($conn->query($sql) === TRUE) {
$data = "New record created successfully";
} else {
$data = "Error: " . $sql . "<br>" . $conn->error;
}
$conn->close();
?>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<section><?= $data ?></section>
<section>
<button type="button" onclick="location.reload()">Reload</button> the page for a new entry
</section>
</body>
</html>