Introduction
This article outlines the technique of HTTP Streaming with PHP, which I first used in AWC Blackjack (version 0.9 at time of writing). The game was partly proof-of-concept work at the time, back in 2005 – to see if a real-time multi-player game could be implemented with only open technologies found in any modern web browser (ie without proprietary browser technologies like Flash or Java).
A major hurdle was Javascript’s lack of socket support. I expected to implement server polling, and to accept a certain sluggishness on the front end as a result. Thankfully, I discovered HTTP Streaming which enabled the game to deliver a constant stream of data on a dedicated connection to each game client. This made the game more responsive, and brought the game’s performance and efficiency closer to that of a socket connection based game than I had dared hope.
The technique has one major limitation – it is scuppered by proxy servers, which renders the technique unsuitable for mobiles as they tend to access the web via a proxy. I anticipate solving this problem by using long-polling instead of streaming for such clients (whilst abstracting away these implementation details in my Javascript classes).
The Problem
The HTTP protocol doesn’t provide a constant dedicated connection between server and client. In a traditional client/server app, socket connections are usually used to achieve this. The connection is created once, and then both server and client can use it to send messages to each other as a constant stream without having to break off and re-establish the connection every time they want to communicate.
Unfortunately, Javascript doesn’t support sockets. In the web model, if the client wants to send a message to the server, it must open a HTTP connection, send the message and then close the connection. It must repeat this process every time it wants to send a message. The server has an even bigger problem: if it has data to send to a client, it has no way to contact the client – it must wait for the client to contact it, and then give it the data. Often, polling is used to solve this problem – contacting the server periodically (say, every second or so) to check for messages.
This is not an efficient client/server model, as HTTP connections are very expensive, both in terms of how long it takes to open the connection, and the amount of bandwidth required. Establishing a HTTP connection usually takes a non-negligible fraction of a second – on the order of a tenth of a second or so for a desktop PC with a broadband connection. Also, a message might only be a byte long, so wrapping it up in a hundred or more bytes of HTTP headers is a waste of bandwidth.
Fortunately, we can get a dedicated connection between server and client by using HTTP Streaming. Basically, this means keeping a HTTP connection open indefinitely and outputting data to the client as and when it becomes available.
It doesn’t work the other way round though – client to server communication must still use regular HTTP connections to send one message at a time. For an app like AWC Blackjack, which only needs to send data from client to server infrequently – typically in response to a user action, clicking on something say – that’s hardly a problem at all.
The PHP Solution
The key to implementing the HTTP Stream is to keep the connection open indefinitely with an infinite loop in a PHP page. On each iteration of the loop, we check if there are any new messages to output to the client, output them if there are, and then pause for a while before starting the next iteration.
In PHP, the basic loop would look something like this:
while ( 1 )
{
$aMessages = checkForMessages();
if ( !empty($aMessages) )
{
foreach ( $aMessages as $msg )
{
echo $msg . MESSAGE_DELIMITER;
ob_flush();
flush();
}
}
usleep( MESSAGES_CHECK_INTERVAL );
}
Note the calls to ob_flush() and flush() – these are both required to ensure that PHP and the webserver (I’m assuming Apache here) output straight to the client without buffering.
Depending on your app, you may need to detect when the user has disconnected. There are several ways the connection could be interrupted or stopped. If PHP detects a deliberate disconnect (the user pressing ’stop’ or closing the browser) it will shut the script down. It will also stop the script if its execution time exceeds PHP’s max_execution_time setting, or if a fatal error occurs somewhere in the script’s execution.
For these cases, we can use PHP’s register_shutdown_function() function to tell PHP to execute a handler function when the script dies, for whatever reason. In the handler, we can determine what caused the script shutdown, and take any necessary action. A deliberate user abort can be ascertained using PHP’s conection handling features.
register_shutdown_function('handleShutdown');
function handleShutdown()
{
$connectionStatus = connection_status();
if ( $connectionStatus == 1)
{
// User aborted - take appropriate action
}
elseif ( $connectionStatus == 2 )
{
// Script execution timed out - take appropriate action
}
else
{
// Connection appears to be normal - maybe fatal PHP error?
}
}
The case not handled here is where a network error occurred somewhere between the client and server. In this case, PHP doesn’t know the connection has been lost, script execution won’t be halted and so handleShutdown() won’t be called. If your app needs to handle this eventuality you could, as AWC Blackjack does, have the server decide the client has ‘gone away’ if it doesn’t receive any messages from it for a specified length of time, and feed a ’shutdown’ instruction to the still-running script to have it shut itself down.
Note also that PHP won’t detect a deliberate user abort until it tries to output some data to the browser. So if there happens to be no new messages to send to the client for 10 seconds, you may only be notified of a user abort up to 10 seconds after the actual event. This is easily worked around by periodically sending a ‘pulse’ message, which the client would ignore and whose sole purpose is to help PHP decide if the client is still there or not.
If, for example, you need to know about a user abort within two seconds, then you could send the client the pulse message whenever there have been no other messages for two seconds. Or if you need to know immediately about a user abort, send the pulse at the end of every iteration of your main loop.
There is one final thing to add, due to a cross-browser issue that affects our PHP. I’ll detail the issue further in a future post describing how to use Javascript to use the HTTP stream. In short though, some browsers require a ‘kickstart’ to the stream – they must receive a certain number of bytes before they’ll start allowing the data in the stream to be accessed via Javascript. So at the start of the script we output 500 bytes before going into the main loop.
With the kickstart added, and assuming we’re sending a pulse message on every iteration of the main loop, the whole PHP script would look something like:
<?php
define( 'MESSAGES_CHECK_INTERVAL', 250000 );
define( 'MESSAGE_DELIMITER', '#' );
define( 'KICKSTART_LENGTH', 500 );
define( 'PULSE_MESSAGE', '.' );
register_shutdown_function('handleShutdown');
// Kickstart
outputToBrowser( str_repeat( '.', KICKSTART_LENGTH ) );
while ( 1 )
{
$aMessages = checkForMessages();
if ( !empty($aMessages) )
{
foreach ( $aMessages as $msg )
{
outputMessage( $msg );
}
}
// Pulse
outputMessage( PULSE_MESSAGE );
// Pause before next iteration
usleep( MESSAGES_CHECK_INTERVAL );
}
#
# END OF SCRIPT - functions follow
#
function outputToBrowser($out)
{
echo $out;
ob_flush();
flush();
}
function outputMessage($msg)
{
outputToBrowser( $msg . MESSAGE_DELIMITER );
}
function handleShutdown()
{
$connectionStatus = connection_status();
if ( $connectionStatus == 1)
{
// User aborted - take appropriate action
}
elseif ( $connectionStatus == 2 )
{
// Script execution timed out - take appropriate action
}
else
{
// Connection appears to be normal - maybe fatal PHP error?
}
}
?>