Monitor progress of long running Php scripts with Html5 server sent events

By | May 1, 2023

Server sent events

When running a long serverside task inside a web application it becomes very useful, if not necessary to report the progress of that task in realtime to the clientside, that is the browser. Earlier there was no easy way to do this and hacks had to be constructed to achieve such a realtime notification. Classical solutions include ajax polling, or even flash. But html5 brings new features to make this easier. And one such feature is server "Server-send Events".

The "Server side events" api allows javascript to generate notifications of events on receiving data from the server. This means that the same connection persists, until it is finished and as the server sends messages, dom events are generated inside the browser so that the browser can handle them. With ajax, the browser cannot do anything till the whole transfer/request finishes. That's the only and the main difference between ajax and server sent events.

At this point you might be wondering about web sockets, a much more spoken about feature of html5 that is capable of bi-directional communication between server and browser. We shall talk about it bit later we shall look into the pros and cons of web sockets vs server sent events.

Quick Demo

Before going into the code, lets first take a look at what exactly are we trying to do. Click on the Start button and it will start a serverside script.

Cool, isn't it. Now that we are done with the demo, its time to understand the technicals.

Code

Client Side

On the client side, create an EventSource object and assign relevant event handlers. A very simple example looks like this

if (!!window.EventSource) 
{
	var source = new EventSource('task.php');
	
	source.addEventListener('message', function(e) 
	{
		console.log(e.data);
		//Do whatever with e.data
	}, false);
}

Note that the EventSource constructor is given the name of the php script to call, which is task.php over here. Now when task.php sends out data using echo for example, the message event shall be triggered which will receive an object containing the message from server. The server has to send messages like this

echo "data: The current status is that we have finished half the worknn";

Thats the protocol. The part after the 'data: ' is interpreted as the message. Every line in the message should have a newline at the end. And when there are 2 newlines, it indicates that one message is complete. It does not mean the end of the whole communication. The server can send another message and keep on doing that. The connection is kept alive as long as the server or client do not decide to close it.

Php serverside script

Lets take a look at a complete php example, task.php

<?php
/**
	EventSource is documented at 
	http://dev.w3.org/html5/eventsource/
*/

//a new content type. make sure apache does not gzip this type, else it would get buffered
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.
*/
function send_message($id, $message, $progress) 
{
	$d = array('message' => $message , 'progress' => $progress);
	
	echo "id: $id" . PHP_EOL;
	echo "data: " . json_encode($d) . PHP_EOL;
	echo PHP_EOL;
	
	//PUSH THE data out by all FORCE POSSIBLE
	ob_flush();
	flush();
}

$serverTime = time();

//LONG RUNNING TASK
for($i = 0; $i < 10; $i++)
{
	send_message($serverTime, 'server time: ' . date("h:i:s", time()) , ($i+1)*10); 
	
	//Hard work!!
	sleep(1);
}

send_message($serverTime, 'TERMINATE');

The send_message function generates messages to send to the client. The loop at the end indicates some long running task what sends out messages periodically. Note that we are sending a json encoded string in the data part of the event message. This is useful since we can send arrays or objects like that.

Another important thing is the header. text/event-stream is a content type of this data transfer. Its very useful to have a different content type so that web servers and browsers can handle the communication accordingly. Many websites configure mod_gzip in apache to compress all css, js, html, xml content. But make sure that it does not compress this event stream content type, or else it would get buffered. To further ensure that no buffering takes place inside php itself, we call ob_flush and flush to push the message out to the client as soon as possible.

Client Side

And next is the html file that initiates this task and reports the progress

index.html

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8" />
		
		<script>
		var source = 'THE SOURCE';
		
		function start_task()
		{
			source = new EventSource('task.php');
			
			//a message is received
			source.addEventListener('message' , function(e) 
			{
				var result = JSON.parse( e.data );
				
				add_log(result.message);
				
				document.getElementById('progressor').style.width = result.progress + "%";
				
				if(e.data.search('TERMINATE') != -1)
				{
					add_log('Received TERMINATE closing');
					source.close();
				}
			});
			
			source.addEventListener('error' , function(e)
			{
				add_log('Error occured');
				
				//kill the object ?
				source.close();
			});
		}
		
		function stop_task()
		{
			source.close();
			add_log('Interrupted');
		}
		
		function add_log(message)
		{
			var r = document.getElementById('results');
			r.innerHTML += message + '<br>';
			r.scrollTop = r.scrollHeight;
		}
		</script>
	</head>
	<body>
		This is NOT AJAX
		<br />
		<input type="button" onclick="start_task();"  value="Start Long Task" />
		<input type="button" onclick="stop_task();"  value="Stop Task" />
		<br />
		<br />
		
		Results
		<br />
		<div id="results" style="border:1px solid #000; padding:10px; width:300px; height:200px; overflow:auto; background:#eee;"></div>
		<br />
		
		<div style="border:1px solid #ccc; width:300px; height:20px; overflow:auto; background:#eee;">
			<div id="progressor" style="background:#07c; width:0%; height:100%;"></div>
		</div>
		
	</body>
</html>

The above is what was shown in the demo earlier. It handles the message event and updates an html element to indicate the progress. This is core of the event source api and is very straightforward to implement.

Websockets vs Server side events

Websockets is api meant for bi-directional communication between the browser and server. Unlike server send events, the websockets server allows the client to "push" messages to the connected server anytime, just like the server was doing in the above example.

Bidirectional communication is something that is not always needed in web applications. It is useful in things like games, chat server etc. When only the server needs to send data and not the client, websockets is not necessary. Moreover web sockets requires a complete webserver to run that can speak the web socket protocol. This is a heavy requirement that cannot be easily put into existing applications.

Browser Support

At the time of writing this article, IE 10 the latest, had no support for EventSource. But there are other ways out. And the best one I found to be is ajax streaming without polling.

There are other fallback techniques to imitate the EventSource object where it is not present, but they are more like hacks. Techniques like script tag streaming might produce reliable results depending on the case, but still scalability would be questionable. Check out the wikipedia article on comet and ajaxpatterns.org on http streaming.

Resources

http://dev.w3.org/html5/eventsource/
About Silver Moon

A Tech Enthusiast, Blogger, Linux Fan and a Software Developer. Writes about Computer hardware, Linux and Open Source software and coding in Python, Php and Javascript. He can be reached at [email protected].

7 Comments

Monitor progress of long running Php scripts with Html5 server sent events
  1. Bruce Thompson

    I had the same problem as Qasim Zubair and solved it the same way. You should add:
    header(‘X-Accel-Buffering: no’);
    to send_message() after the echo statements.

    The correct send_message() function is:
    function send_message($id, $message, $progress)
    {
    $d = array(‘message’ => $message , ‘progress’ => $progress);

    echo “id: $id” . PHP_EOL;
    echo “data: ” . json_encode($d) . PHP_EOL;
    echo PHP_EOL;
    //PUSH THE data out by all FORCE POSSIBLE
    header(“X-Accel-Buffering: no”);
    ob_end_flush();
    flush();
    }

    In addition, you have a bug in the php code. The line:
    send_message($serverTime, ‘TERMINATE’);
    should be changed to:
    send_message($serverTime, ‘TERMINATE’, 100);

  2. Ian

    Not sure I understand ithis… when running the client side code… how does the server side code (task.php) get started?

    I get the Error message as its triggering the add_log(‘Error occured’); function.

  3. Qasim Zubair

    I am using nginx. I had to add header(‘X-Accel-Buffering: no’);
    and in send_message() function I had to add ob_flush();
    flush(); to make it work

  4. eli

    Works fine in your example, but ..
    I uploaded the files to powweb. Messages now only appear (all together) after the php has completed.

    help !

      1. Mike

        Useless reply from noup… why even reply if you have no idea or cannot provide any useful information???

        eli, or others, if this problem occurs try turning off virus protection or the firewall to see if it corrects.

Leave a Reply

Your email address will not be published. Required fields are marked *