51Degrees-Logo

How to use XMLHttpRequest and XDomainRequest to stream messages

Engineering

8/16/2012 10:00 AM

PHP Development

Introduction

Our PHP device detection API includes a feature to update the rules and data used to identify requesting browsers, operating systems and hardware via a single PHP script (51Degrees.mobi.update.php). Plug-in developers want to use this script to provide a simple button to update the device data from within their favourite CMS' administration interface. However our update script simply writes status messages back to the browser as plain text. Some browsers don't display these messages as they arrive, instead waiting until the entire message has been received. This is no good if we want to provide the user update status messages as the update is happening. For example; "Calculating Delta" or "Verifying Changes". We therefore need a method to display these messages as they arrive. This blog explains a surprisingly simple solution.

All code, including comments, is available at the end of the blog post.

XMLHttpRequest

XMLHttpRequest has been available to web developers since the 90s. Therefore it's a great way to get HTTP responses from javascript. However version 1 did not allow browsers to access part of the response before the entire response has been received.

XMLHttpRequest Version 2

Version 2 overcomes this limitation in version 1. However the W3C specification is still a working draft and implementations vary. I'll explain the different solutions we used for each of the 5 major browsers to achieve the desired result.

Firefox, Opera and Safari

These browsers all share a consistent implementation of XMLHttpRequest enabling the onprogress event to be used to get the current responseText even when the response has not been fully received.

_request = new XMLHttpRequest(); _request.onprogress = function() { var text = _request.responseText; };

Chrome

Chrome will not fire the onprogress event until at least 2048 bytes of data have been received. Therefore if the amount of data being transmitted is small the messages will not be display. To work around this we send a 2048 character empty string as the initial response from the server and then ignore these characters when processing the messages.

Internet Explorer

XMLHttpRequest does not support the onprogress event yet in IE. However the Microsoft specific XDomainRequest object type has been introduced to IE8 and above. XDomainRequest is primarily designed to enable requests to be made across domains. However it has the added advantage of supporting an onprogress event and enabling access to partially received responses. The only minor complexity is that the server needs to respond with an additional HTTP header to inform Internet Explorer that it's okay for cross domain access. In PHP the following line will add the necessary header enabling all domains to access the page.

header('Access-Control-Allow-Origin: *');

Summary

The solution shown in this post will work for any design which requires small volumes of data to be transferred over a comparatively long period of time using a single HTTP requests from a modern web browser. It does not rely on heavy weight frameworks, or more complex server side configuration, and importantly will work will on mobile devices where bandwidth is limited and multi HTTP requests for small amounts of data are inefficient.

The solution could be enhanced to only send the 2048 byte prefix when the request is from a Chrome based browser.

About 51Degrees.mobi

51Degrees.mobi encourage others to integrate our solutions into their products. APIs are provided for .NET, C, Java and PHP with 3rd parties like Apache Mobile Filter (AMF) provide Perl implementations. Our freemium open source business model enables 3rd parties to generate revenue in several ways. Please contact us to find out more.

PHP Server Code

<?php // Ensure the page does not time out after the default 30 seconds. set_time_limit(0); // Set the headers to produce plain text. header('Content-Type: text/plain'); header('Cache-Control: no-cache, must-revalidate'); // Needed for the XDomainRequest object used by IE instead of // XMLHttpRequest which does not allow the responseText to be // queried before the response is fully received. header('Access-Control-Allow-Origin: *'); // Disable compression and ensure all new data is immediately // sent to the response stream and not buffered. @apache_setenv('no-gzip', 1); @ini_set('zlib.output_compression', 0); @ini_set('implicit_flush', 1); for ($i = 0; $i < ob_get_level(); $i++) { ob_end_flush(); } ob_implicit_flush(1); // Needed by Chrome to start processing progress events from the // XMLHttpRequest object. echo(str_repeat(' ', 2048)); flush(); // Send 100 messages waiting 1 second between them. for($i = 0; $i <= 100; $i++) { echo("New Message : " . $i . "\r\n"); flush(); sleep(1); } ?>

Javascript Example HTML Page

<!DOCTYPE html> <html> <head> <title>Messages</title> <script type="text/javascript"> // Count of the number of messages recieved by previous // progress event calls. var _counter = 0; // The XMLHttpRequest or XDomainRequest object. var _request; // Called if the request is aborted to display a message to the user. function a() { document.getElementById('message').innerHTML = "Update Aborted"; } // Called when new data arrives from the server. function p() { // Remove the white space prefix and then seperate by carriage return // line feed to get an array of the seperate messages. var messages = _request.responseText.replace(/^\s+|\s+$/g, "").split("\r\n"); // If messages are present in the data then add any new ones to the // display div. Record the number of messages so that only new ones // are displayed the next time the event is fired. if (messages) { var ctrl = document.getElementById('message'); if (messages.length > _counter) { var html = ''; for (var i = _counter; i < messages.length; i++) { html += messages[i] + '<br/>'; } ctrl.innerHTML = html; _counter = messages.length; } } } // Initiates the server request. function submitForm(button) { try { // Will only work for IE. Used to access partial // HTTP responses. _request = new XDomainRequest(); _request.onprogress = p; _request.open("GET", "messages.php", true); _request.send(null); } catch (e) { // Will come here for all other browsers, and use // XmlHttpRequest which will support partial response // in the progress event. _request = new XMLHttpRequest(); _request.onprogress = p; _request.onabort = a; _request.open("GET", "messages.php", true); _request.send(null); } } </script> </head> <body> <form action="" method="POST" name="ajax"> <input onclick="javascript:submitForm(this);" type="BUTTON" value="Submit"></input> <div id="message"></div> </form> </body> </html>