Using Server-Sent Events for Logged-in Users

php
Published on November 23, 2016

HTML5 Server-Sent Events is one of the coolest things ever. A web server automatically updating a web application with new content sounds like magic. This is how Facebook and Twiitter update your timeline - they don't send AJAX requests to get new content, rather the serves pushes new content to them.

There are a lot of good resources that will give you a good idea of Server-Sent Events. The best one I find is Stream Updates with Server-Sent Events.

However at the start I got confused with how to implement Server-Sent Events for logged-in users. For example each Facebook user will have its unique timeline of content. The updates are not common to every user. So will Server-Sent Events work here ?

Yes, Server-Sent Events will work good for unique users also. Actually it is pretty simple - when a user logs-in he creates a session. The server can use the session to stream updates for that specific user.

I'll be using a demo application to show this. The application like Facebook, shows a timeline of posts to a certain user. On the server side a post is created every 10 seconds that is streamed back to the user.

A demo of the application is shown at the end. Sample codes are also provided.

Starting the Session and Getting Posts

The user has logged-in and has created a session. Posts related to his account are fetched from the database and shown.

We also need to save the id of the latest post shown to the user. This is required to get newer posts starting from that post id. The latest post id is saved as a session variable.

<?php // Start the user session session_start(); $_SESSION['user_id'] = 1234567; // Sample existing posts $existing_posts = array(); for($i=0; $i<2; $i++) { $post_id = uniqid() . rand(1111111, 9999999); $existing_posts[] = array('post_id' => $post_id, 'post_message' => 'This is the post with ID ' . $post_id); } // Save the last post_id for future use // In practical cases you will need to get the new posts from the database after a certain post_id // The last post_id will be helpful in such cases $_SESSION['last_post_id'] = $existing_posts[0]['post_id']; // Show posts $html = ''; for($i=0; $i<sizeof($existing_posts); $i++) { $html .= '<div class="post">' . $existing_posts[$i]['post_message'] . '</div>'; } echo $html; ?>

Subscribing to updates

The user is now subscribed to the stream by creating an EventSource object and registering to the message event handler. When message events are sent from the server, they are handled with this function.

<script> var event_source = new EventSource("sse-backend.php"); event_source.addEventListener('message', function(e) { // Parse received message to JSON var sse_response = JSON.parse(e.data); console.log(sse_response); // On error if(sse_response.error == 1) { alert(sse_response.error_message); // Terminate the connection. No more messages will be requested from the server event_source.close(); return; } // Prepend new posts for(var i=0; i<sse_response.new_posts.length; i++) { $("#feed-container").prepend('<div class="post">' + sse_response.new_posts[i]['post_message'] + '</div>'); } }, false); <script>

The Server Side

You create an open connection with the server by creating an infinite loop in the message sending PHP script. Output buffering has to be disabled in the PHP script so that the server sends back the new content (and not wait for the script to finish).

Every few seconds new content is checked for using the user_id and last_post_id. If new content comes up, it is flushed out from the server using the flush and ob_flush functions.

In this sample application a new post is created every 10 seconds and flushed out.

PS : I've ran the code on an Apache server that is running PHP as Apache Module (mod_php). Disabling output buffering in this case can be done through ob_flush() and flush() functions. (See the section below on output buffering)

<?php session_start(); header('Content-Type: text/event-stream'); header('Cache-Control: no-cache'); // Check user session validity if(!isset($_SESSION['user_id'])) { $sse_response = "data: " . json_encode(array('error' => 1, 'error_message' => 'User Authentication Failed')) . PHP_EOL . PHP_EOL; echo $sse_response; exit(); } // Get the user id $user_id = $_SESSION['user_id']; while(1) { // Get the last post_id that was shown to the user $last_post_id = $_SESSION['last_post_id']; $sse_response = GetNewPosts($user_id, $last_post_id); echo $sse_response; ob_flush(); // Sends output data from PHP to Apache. flush(); // Sends output from Apache to browser. // Check every 10 seconds sleep(10); } // Use the last_post_id and user_id accordingly (database queries) function GetNewPosts($user_id, $last_post_id) { $new_posts = array(); $post_id = uniqid() . rand(1111111, 9999999); $new_posts[] = array('post_id' => $post_id, 'post_message' => 'This is the post with ID ' . $post_id); // Re-save the new last post_id $_SESSION['last_post_id'] = $post_id; $output = "data: " . json_encode(array('new_posts' => $new_posts, 'error' => 0)) . PHP_EOL . PHP_EOL; return $output; } ?>

Thats it. Every user will see his new unique posts coming up.

Server-Sent Events & Output Buffering

A common problem is to disable output buffering on a PHP script. Some can easily disable output buffering on their servers, while for others it doesn't come so easily. Methods that work on one server might not work on another server. Mostly it is related to server configurations like whether PHP is running as an Apache Module (mod_php) / FastCGI and compression of output by Apache.

When PHP runs as mod_php, in most of the cases disabling output buffering is simple. The above codes using flush and ob_flush should hold good.

But in the case of FastCGI implementation, there can be some issues. Googling "output buffering fast cgi" will give many unanswered results unfortunately.

In case you face issues with regard to output buffering, please add your comment below.

Demo

Click here to go to the demo page

Download Sample Codes

Download
In this Tutorial