How to get Server Sent Events working nicely in PHP

Server Sent Events (SSE) (Also known as EventSource) are a great way to send data instantly to the browser, it opens a stream between the browser and the server. The browser then listens for any messages you want to relay to the browser, whether that be a visual notification to the user or just an update to existing content.

However, SSE's can cause some issues, especially with languages like PHP that don't cater for persistent connections by default. It can cause session locks and MySQL crashes depending on what it is you're doing on the server-side. So, before we start, here are a few of the pitfalls that I had to overcome when I recently implemented them.

Session Locks

First and foremost, if you're using sessions for whatever reason you will need to make them read-only on the stream. If they're writable, this will lock them everywhere else, so any page loads will hang while the server waits for them to become writable again. This is easily fixed by calling; session_write_close();

MySQL connection limits

If you need to access a db for the streams and chances are you will want to. You will need to increase your max_connections setting for MySQL. This is because you're keeping a MySQL connection open for every stream. So you will need to set this quite high.

Make sure headers are set correctly

You may run into issues if you don't set the headers to be a stream on the server-side. This is easily achieved in PHP by setting the following at the start of your stream.


header("Content-Type: text/event-stream");
header("Cache-Control: no-cache");
header("Access-Control-Allow-Origin: *");

Run your own checks for client disconnects

I hit some issues where the connection would stay open regardless of whether the user disconnected or not. Which obviously lead to hogged server resources. I overcame this by disabling the default checks and manually checking for a disconnect.


ignore_user_abort(true); // Stops PHP from checking for user disconnect

connection_aborted(); // Checks if user has disconnected or not

OK, so now those little gotcha's are out of the way. Here is an implementation of Server Sent Events in PHP. I used the excellent EventSource polyfill by Yaffle for browsers that don't yet support SSE's. First, create a file called sse.php on the server and enter the following.


<?php

		// 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{

				// here you will want to get the latest event id you have created on the server, but for now we will increment and force an update
				$latestEventId = $lastEventId+1;

				if($lastEventId < $latestEventId)

					echo "id: " . $latestEventId . "\n";
					echo "data: Howdy (".$latestEventId.") \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);

		}
			
?>

This set's all of the appropriate settings mentioned above and does a check to see if a stream has already been established by looking at the HTTP_LAST_EVENT_ID header sent by the browser. If it has one, it is probably a brief disconnect and the stream needs to resume, otherwise it starts a new stream. We also check the query string to see if it has been passed that way from the polyfill.

We then add some padding to the initial response, which is a requirement by IE, then we start the stream with an infinite while loop. Inside we check first to see if the client has disconnected, if not we can carry on and check if we need to supply some data. Here you will need to have an event id stored against any new data that you want to supply to the browser, which should be higher than the last id that was sent. Here I am just incrementing as an example, which forces a new update everytime it checks. If no data is to be sent it just sends a heartbeat to the browser to keep the connection open.

Finally, we flush the data to the browser and sleep for 2 seconds before repeating.

On the client side we will need to establish the stream, first ensure you have implemented the polyfill and then create a stream using the code below in your javascript file.


	// establish stream and log responses to the console
	var es = new EventSource("sse.php");
	var listener = function (event) {
		
		if(typeof event.data !== 'undefined'){
	console.log(event.data);
		}

	};
	es.addEventListener("open", listener);
	es.addEventListener("message", listener);
	es.addEventListener("error", listener);

As a basic implementation, this is all you need. It will establish a stream and then log the response to the console. There are many uses for this API, especially with data that can change often and hopefully this has shed some light on the potential pitfalls that can occur with using SSE's in PHP.