Home of Ebyan Alvarez-Buylla

Comet Long Polling with PHP and jQuery

Comet Long Polling with PHP and jQuery

Comet describes a number of techniques with which a web server may push information to a client in a non-transactional format. Long Polling is one of such techniques, in which a browser’s request remains open until the server has information to send.

Long Polling is particularly useful in semi-synchronous applications, such as chat rooms and turn-based games, and is straightforward to implement with PHP and jQuery:

Serverside

The serverside component of Long Polling requires that, contrary to the usual transactional model, the request not be answered right away. To achieve this, we put PHP to sleep():

// How often to poll, in microseconds (1,000,000 μs equals 1 s)
define('MESSAGE_POLL_MICROSECONDS', 500000);

// How long to keep the Long Poll open, in seconds
define('MESSAGE_TIMEOUT_SECONDS', 30);

// Timeout padding in seconds, to avoid a premature timeout in case the last call in the loop is taking a while
define('MESSAGE_TIMEOUT_SECONDS_BUFFER', 5);

// Hold on to any session data you might need now, since we need to close the session before entering the sleep loop
$user_id = $_SESSION['id'];

// Close the session prematurely to avoid usleep() from locking other requests
session_write_close();

// Automatically die after timeout (plus buffer)
set_time_limit(MESSAGE_TIMEOUT_SECONDS+MESSAGE_TIMEOUT_SECONDS_BUFFER);

// Counter to manually keep track of time elapsed (PHP's set_time_limit() is unrealiable while sleeping)
$counter = MESSAGE_TIMEOUT_SECONDS;

// Poll for messages and hang if nothing is found, until the timeout is exhausted
while($counter > 0)
{
    // Check for new data (not illustrated)
    if($data = getNewData($user_id))
    {
        // Break out of while loop if new data is populated
        break;
    }
    else
    {
        // Otherwise, sleep for the specified time, after which the loop runs again
        usleep(MESSAGE_POLL_MICROSECONDS);

        // Decrement seconds from counter (the interval was set in μs, see above)
        $counter -= MESSAGE_POLL_MICROSECONDS / 1000000;
    }
}

// If we've made it this far, we've either timed out or have some data to deliver to the client
if(isset($data))
{
    // Send data to client; you may want to precede it by a mime type definition header, eg. in the case of JSON or XML
    echo $data;
}

This example holds on to the connection, for up to 30 seconds, until new information is found in the database, at which point it is delivered to the client. Note that we are using usleep(), which takes microseconds instead of seconds.

Clientside

The client is responsible for keeping the request alive at all times: once the process begins (usually on DOM ready, although in some browsers it might be offset, as noted below), when the request times out, and when the request returns useful information, at which point it must handle it:

$(function()
{      
    // Main Long Poll function
    function longPoll()
    {
        // Open an AJAX call to the server's Long Poll PHP file
        $.get('longpoll.php', function(data)
        {
            // Callback to handle message sent from server (not illustrated)
            handleServerMessage(data);
            
            // Open the Long Poll again
            longPoll();
        });
    }

    // Make the initial call to Long Poll
    longPoll();
}

In JavaScript, we immediately open another Long Poll upon the previous call returning useful data or timing out; all of the timeouts are handled serverside.

Caveats

At first glance Long Polling is a no-brainer upgrade from the classic polling: it’s more responsive, has less network overhead, and is fairly straightforward to implement. However, it is not without its flaws:

Database is hit more often

In the example above, the database is hit twice every second. If we had 100 concurrent users for one hour, that would be 720,000 database requests, the overwhelming majority of which will return no data. For certain light database systems, this overhead might be negligible, but you’ll want to pair MySQL with a Memcache server that will handle these frequent requests with little overhead. Memcache works well with Long Polling, since the database access consists of all INSERTs, for the part of the program adding new data to be retrieved, and SELECTs, for the part that is fetching the new messages, as illustrated above.

PHP sleeps across the entire session

When you make a call to sleep() or usleep(), PHP will pause execution across the entire session, which means that any other AJAX requests or even pageloads will have to wait until the request returns or times out. To address this, ensure the session is closed before entering the serverside sleep loop.

Chrome and iOS Safari think the page is loading

Chrome and iOS devices’ Safari will display the “waiting” or “loading” messages, along with their respective spinning or bar loaders while a Long Poll is open. In Chrome, this can be combated by offsetting the execution of the initial Long Poll request by a few seconds after DOM load, with setTimeout(). I have not tested whether this approach works in iOS devices as well.

[Edit] set_time_limit() is unreliable when sleeping

Digging around in the sleep() and set_time_limit() function reference has revealed that time sleeping is not counted towards PHP’s internal execution clock in Linux machines, and therefore will not advance the counter for set_time_limit(). You must manually count the time and exit the loop on timeout, as illustrated above. I have left the set_time_limit() code in for Windows machines, and to account for the edge cases in which the script execution takes up the full timeout time.

Header image by Jack Newton.