• About Us
  • Blog
  • Basket
  • Account
  • Sign In
  •  

Blog

Published on Thursday, August 16, 2012

How to use XMLHttpRequest and XDomainRequest to stream messages

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>
Comments (0)

Author: Products Team

Categories: Development

Tags: HTTP

James Rosewell
>

Products Team

Other posts by Products Team
Contact author

Name:
Email:
Subject:
Message:
x

Tags

.NET 2013 2014 4G 51Degrees 5G A.C.Roma A7 ABI Acer Affiliate Marketing Alcatel Amazon AMP Analysis Analytics Android Android 5.0 Lollipop Android Kitkat Android Lollipop Android Media Stick Apache API Apple Apple TV Archos Asha Asian Market ASP.NET Asus Australia Big Data Black Friday Blackberry Blink Browser C C# Case Study CeBIT CES Chrome Cloud CMS combinations Comparison Competition CoolPad COTW Cron CSS3 Data Data Blog Data File Data Model Daydream Denver Design Desire Eye Desktop Detection Device Device Data Device Detection Device Intelligence Device Popularity Device property Device Types Device Use Display DoCoMo Doogee DotNetNuke Download Drupal Email EReader E-Reader Ericsson Evaluation Event Examples EXPLAY Rio Facebook feature Firefox Firefox OS Fly Foundation Framework France Galaxy S3 Galaxy S5 Galaxy Tab A Galaxy Tab A 8.0 Galaxy Tab A 9.7 Germany Global Google Google Daydream GSMA HAProxy Hardware Hisense HTC HTC ONE MAX HTC OS HTML5 HTTP Huawei HUAWEI. UPDATE HUDL Huwaei IBC Icemobile Prime 4.0 IE IFA IIS Image Optimiser Image Optimizer India Infographic Ingeniux Internet usage iOS iOS 7 iOS 8 ipad iPhone iPhone 6 IsEmailBrowser IsWebApp Italy Japan Java Javascript Jolla Kentico Keynote Kindle Kindle Fire Kindle Fire HD Leagoo Lenovo LG Location Log File Analysis LTE Lumia Map Memory Meta Data Mi 4S Micromax Microsoft Miia Style Mobile Mobile Analysis Mobile Analytics Mobile Devices Mobile Marketing Mixer Module Motorola MVC4 MWC MWC 2017 MWC16 MyPhone Native Native Apps NET New Release News News Letter Nexus Nexus 6 Nexus 9 NFC NGINX Nokia Non-Mobile NVIDIA Omate On7 Opera Opera Mini Operating System Optimisation OS OSX 10.10 OTA Panasonic Patent PC Pebble Performance phablet phone PHP Poland Presentation Press Release Price Band PRIV PS4 Python QMobile QR Codes Redirection Research Reseller Responsive Images RESS Review reviews RIM Ringmark RWD Samsung Scala Screen Screen resolution Screen Size SEO Server Set Box Set Top Box Sharepoint Shark 1 SHIFT phones Sitecore SLUSH Smart TV Smartphone Smartphones Smartwatches Snapdragon Sony Sony Xperia Spain Swedish Beers Symbian Tablet Tablets Tesco Testing Top 5 TOTW TV UDS UK Umbraco Update updates US User Agent UserAgent User-Agent Vendors Version 3 VoLTE VR Wearable Web Web Apps WebKit WebMatrix White Paper Widgets Widnows WiFi Wiko Wileyfox Windows Windows Phone Xbox XBox One Xiaomi Xperia Xperia z Yosemite Z10 ZenFon 2 ZOPO ZTE