9.1. HTTP Streaming

In the time before broadband Internet access was available to the masses, browser developers looked for ways to speed up the perceived rendering time of web pages. One such method is called progressive rendering; in this method rendering of the page begins as soon as the <body> tag is received, ensuring that the site display begins as soon as possible. This is the effect seen on long web pages when the vertical scrollbar continues to grow as the page is being loaded. In this circumstance, the browser is completing the displaying of the page as new information is received, creating a longer and longer page with each passing second. This same effect can be observed when connecting to servers that are experiencing very heavy traffic as the server struggles to keep up with the requests.

Consider what's happening when a page is being rendered progressively. The opening <body> tag is read, and then some more data is received. Some time passes. Some more data is received. This pattern is repeated until the entire page has been downloaded and is being displayed to the user. But how does the browser know how long to wait for new data? Further, how does it know how much more data is coming? The answer to both questions is that the browser has no idea. This is the essence of HTTP streaming.

NOTE

HTTP streaming is frequently mislabeled as Persistent HTTP, which has nothing to do with this technique. Persistent HTTP is simply a way of keeping a connection open so that numerous HTTP requests can be sent without opening and closing connections for each request.

9.1.1. Request Delays

Instead of relying on network latency and server response time to determine the waiting time between data bursts, it's possible to artificially create this delay. The following example comes from the PHP manual for the sleep() method (www.php.net/sleep) and illustrates this technique:

<?php

    // current time
    echo date('h:i:s') . "
";

    // sleep for 10 seconds
    sleep(10);

    // wake up !
    echo date('h:i:s') . "
";

?>

When loaded into the browser, this page outputs the current time, waits 10 seconds, and then outputs the time again. Granted, this isn't a very useful page, but it does illustrate how to force the server to wait before sending the next piece of data. In practice, you should add calls to ob_flush() and flush() immediately after the calls to echo() to force data to be sent to the client:

<?php

    // current time
    echo date('h:i:s') . "
";
    ob_flush();
    flush();

    // sleep for 10 seconds
    sleep(10);

    // wake up !
    echo date('h:i:s') . "
";
    ob_flush();
    flush();
?>

Adding these two function calls ensures that the output buffer is completely flushed, forcing the data to be sent to the client.

NOTE

This feature of PHP, sending chunks of data periodically to the browser, may not be enabled on all servers. For more information, see www.php.net/flush.

Suppose that the same technique were used to output HTML and JavaScript instead of plain text:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title>HTTP Streaming Example 2</title>
    </head>
    <body>
        <script type="text/javascript">
        //<![CDATA[
            document.title = "First message";
        //]]>
        </script>
<?php
    ob_flush();
    flush();

    // sleep for 10 seconds
    sleep(10);
?>
        <script type="text/javascript">
        //<![CDATA[
            document.title = "Second message";
        //]]>
        </script>
    </body>
</html>

The JavaScript in this example simply sets the title of the window two different times. Without the call to sleep(), it would happen so fast that you would only see the title change to "Second message." However, with the delay, it is easy to see that both commands are executed as soon as the client receives the data. This proof-of-concept works but doesn't do anything very interesting. What if some sort of command were coupled with the call to sleep()?

9.1.2. File Modification Example

Suppose that there's a file whose modification time is of interest. Perhaps data is being written into it that should be picked up as soon as it is available. In any event, it's important to know as soon as the file has been modified. The following PHP code implements this solution:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title>HTTP streaming Example 3</title>
    </head>
    <body>
<?php
    //get the file modification time
    $modified = filemtime('details.txt'),
    $lastModified = $modified;

    //clear file statistics
    clearstatcache();

    //check every so often to see if it has changed
    while (true) {

        // sleep for 1 second
        sleep(1);

        //check the modification time
        $lastModified = filemtime('details.txt'),

        //clear file statistics
        clearstatcache();

        //check it against the previous time
        if ($modified != $lastModified) {

            $output = date('h:i:s', $lastModified);
?>
             <script type="text/javascript">
            //<![CDATA[
                document.title = "File was modified at <?php echo $output ?>";
            //]]>
            </script>
<?php
            ob_flush();
            flush();

$modified = $lastModified;

            // sleep for 1 second
            sleep(1);

        }
    }
?>

    </body>
</html>

The PHP code in this example first checks to see when the file in question, details.txt, has been modified. This value is stored in two variables: $modified and $lastModified. Two variables are used so that there will be a point of comparison later on: $modified holds the modification time from the last change (or when the page was first loaded), while $lastModified holds the most recent modification time. It's then possible to compare $lastModified with $modified to see if they're the same; if they're not, that means the file has changed.

After calling filemtime() to retrieve the modification time of the file, clearstatcache() is called. This is a PHP-only necessity, since PHP caches the results of many file operations for faster execution. Then, a while loop begins. This loop will never exit because the control condition is hard-coded to true. In this way, the page will continue to check and report on the modification time of the file indefinitely.

Inside of the loop, sleep() is called to create an artificial delay and free up CPU cycles for other operations. Then, filemtime() is called again, and the value is compared to the previously stored value in $modified. If the modification time is different, a JavaScript call is made to change the document's title. After the JavaScript code is output, calls to ob_flush() and flush() ensure that the data in the buffer is sent across the HTTP stream (instead of being buffered for later transmission). Then, $modified is updated with the new modification time and the thread pauses for another second to prevent sending too much data at one time.

To test this functionality, try uploading details.txt periodically and watch the title bar of the browser. No changes to details.txt are necessary because the process of uploading the file changes the modification time.

Keep in mind that this is a simplified example. The point to take away is that any JavaScript code can be executed in place of the code in this example. Most likely, the code to execute would be a call to some function that specifically keeps track of this data.

9.1.3. Using Iframes

It may seem strange that the previous example continues to execute indefinitely because this is not the way developers learn to create web applications. However, HTTP streaming is a completely different paradigm, just as Ajax is a completely different paradigm from traditional web applications. To make the most of this technique, a change in thought process is necessary. Part of that thought process questions how long an indefinite loop can run on a server, since computers deal only with finites.

Eventually, the infinite loop in the previous example will stop running because servers have a built-in timeout mechanism that prevents long-running scripts from continuing to run and eat up CPU cycles. This is a stopgap system designed to ensure that a single page on a site cannot bring the entire site down. The exact timeout setting is specific to the server being used and can be changed by the server administrators, so there isn't a hard number that can be depended upon. Basically, there's no way to tell when the script will stop running and at what point during its execution that stop will occur. For this reason, it's important not to execute too much code using HTTP streaming. It's also for this reason that a heartbeat is absolutely necessary.

A heartbeat is essentially a small piece of code executed periodically to inform some other code that the process is still running. In terms of HTTP streaming, the heartbeat indicates that the request is still being processed, and code execution on the server continues. When a heartbeat fails to be registered, the client must recognize this and restart the server process.

Setting up this sort of system requires two pages. The first page is the main client, the one in which most of the JavaScript code exists. Inside of that page is an iframe that contains the second page, which is the HTTP streaming connection to the server. The inner page is responsible for calling functions on the outer page to display information as well as register heartbeats. The outer page, in turn, must keep track of the heartbeats being sent and know when to reset the server connection. Here's what the outer page looks like:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title>HTTP streaming Example 4</title>
         <script type="text/javascript">
        //<![CDATA[

        var iTimeoutId = null;

        function heartbeat() {
            clearTimeout(iTimeoutId);
            iTimeoutId = setTimeout(resetConnection, 10000);
        }

        function resetConnection() {
            frames["connection"].location.replace(
              "ProgressiveHTTPExample4Connection.php?t=" + (new Date()).getTime());
            heartbeat();
        }

        function modifiedAt(sDateTime) {
            document.getElementById("divStatus").innerHTML =
                                                        "Modified at " + sDateTime;
        }

        window.onload = resetConnection;

        //]]>
        </script>
    </head>

<body>
        <div id="divStatus">Waiting for first message...</div>
        <iframe src="about:blank" name="connection"></iframe>
    </body>
</html>

Most of this page is JavaScript code; the only HTML necessary in the <body/> is a <div/> to display status information and the <iframe/> to contain the connection page.

This example's JavaScript code consists of one variable and three functions.

  • The iTimeoutId variable holds a reference to the timeout instance in charge of checking for dead connections.

  • The heartbeat() function is the one to be called by the connection page periodically, letting it know that the connection is still alive. You find only two lines inside of this function: one to cancel the current timeout and one to start a new one. While the connection is alive, the timeout should never fire (it's set to 10 seconds, and since the heartbeat() function should be called roughly once every second, there shouldn't be an overlap). The timeout is set to call the resetConnection() function if and when the timeout executes.

  • The resetConnection() function is also quite simple. It resets the iframe's URL to the connection page and appends a timestamp to the end (the timestamp is necessary to avoid getting a cached version of the page). Then, there's a single call to heartbeat(), which resets the timeout. The connection page should then begin sending heartbeat signals, and the process will continue.

  • The last JavaScript function is modifiedAt(), which is a function that is called when the file has been modified. The data is passed in and displayed in divStatus, so the connection page isn't responsible for displaying this data itself.

As a final step, the window's onload event handler is set to call resetConnection(), ensuring that a connection will begin as soon as the page loads.

The other part of this example, the connection page, looks very similar to the previous example (the differences are highlighted):

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title>HTTP streaming Example 4 Connection</title>
    </head>
    <body>
<?php
    //get the file modification time
    $modified = filemtime('details.txt'),
    $lastModified = $modified;

    //clear file statistics
    clearstatcache();

    //check every so often to see if it has changed
    while (true) {

?>
         <script type="text/javascript">
        //<![CDATA[
        parent.heartbeat();
        //]]>
        </script>
<?php
        ob_flush();
        flush();

        // sleep for 1 second
        sleep(1);

        //check the modification time
        $lastModified = filemtime('details.txt'),

        //clear file statistics
        clearstatcache();

        //check it against the previous time
        if ($modified != $lastModified) {
            $output = date('h:i:s', $lastModified);
?>
             <script type="text/javascript">
            //<![CDATA[
                parent.modifiedAt("<?php echo $output ?>");
            //]]>
            </script>
<?php
            ob_flush();
            flush();
            $modified = $lastModified;

            // sleep for 1 second
            sleep(1);

        }
    }
?>

    </body>
</html>

In this page, a call is made to the heartbeat() function every time the while loop executes. This call uses the parent object to access heartbeat() because it is contained in an iframe and the function exists in its parent page. When the file is modified, another call is made to the parent frame, this time to modifiedAt(), which is passed the timestamp.

NOTE

It's very important that each code block be contained within its own <script/> tag. Code execution will not begin in most browsers until the closing </script> tag is read. Even though it seems redundant, you must provide a complete <script/> tag for every function call or logical group of function calls.

9.1.3.1. Dynamically Created Iframes

In the previous examples, an iframe was already present in the page. It is possible to accomplish the same functionality using dynamically created iframes that aren't visible on the page. To do so, you need to create an iframe using the DOM createElement() method, set the display property to none, and then set the src property:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title>HTTP streaming Example 5</title>
         <script type="text/javascript">
        //<![CDATA[

        var iTimeoutId = null;
        var oFrame = null;

        function heartbeat() {
            clearTimeout(iTimeoutId);
            iTimeoutId = setTimeout(resetConnection, 10000);
        }

        function resetConnection() {
            oFrame.src =
               "ProgressiveHTTPExample4Connection.php?t=" + (new Date()).getTime();
            heartbeat();
        }

        function modifiedAt(sDateTime) {
            document.getElementById("divStatus").innerHTML =
                                                        "Modified at " + sDateTime;
        }

        window.onload = function () {
            oFrame = document.createElement("iframe");
            oFrame.style.display = "none";
            document.body.appendChild(oFrame);
            resetConnection();
        };

        //]]>
        </script>
    </head>
    <body>
        <div id="divStatus">Waiting for first message...</div>
    </body>
</html>

The first step is to define a global variable called oFrame that holds a reference to the dynamically created iframe. In the window's onload event handler, the iframe is created and stored in oFrame. Then, it is hidden from view by setting display to "none". The iframe is then added to the body of the document (which is required for the iframe to work). Finally, the resetConnection() function is called, which in turn sets the src attribute of the iframe.

9.1.3.2. Usability Issues

Although using iframes to implement a web-based push architecture can yield some interesting results, it has a major usability flaw. While the connection is open, the browser indicates that it is busy:

  • In Internet Explorer, the throbber (the animated icon in the upper-right corner) continues to move and the progress bar shows up at the bottom of the screen.

  • In Firefox, the cursor changes to display an arrow and an hourglass and a message is displayed in the status bar indicating that the browser is waiting for more information.

  • In Safari, the progress bar continues to expand and the message "Loading" appears in the title bar.

  • In Opera, the cursor changes to display an arrow and an hourglass.

Although these may seem like minor annoyances, such obvious indications of browser activity can easily confuse inexperienced users. There are, however, other ways of achieving push functionality.

9.1.4. Browser-Specific Approaches

HTTP streaming is still a fairly new concept, and as such, there is no consistency in browser implementation. For some browsers, HTTP streaming is nothing more than a hack using existing technology (such as using iframes in the previous examples); in others, it's a planned feature being implemented in one of many ways. Depending on your individual requirements for browser support, you may need to use combinations of these techniques.

9.1.4.1. Internet Explorer HTTP Streaming

HTTP streaming support in Internet Explorer was not an intentional design decision but rather was achieved by some enterprising engineers at Google using existing and less documented browser-features.

When Google added chat capabilities to its Gmail web client, developers immediately began to dissect what was happening behind the scenes. It was Alex Russell who first posted a message (http://alex.dojotoolkit.org/?p=538) on his blog about the inner workings of the Gmail chat client. He discovered the use of a little-known, safe-for-the-web ActiveX control called HTMLFile.

The HTMLFile ActiveX object is exactly what it sounds like: an implementation of an HTML document that mimics the functionality of the document object in an external form. Because this object exists outside of the normal page flow, it has no ties to the browser window and, thus, can be used to perform all kinds of operations without disturbing the browser's user interface. The Google engineers used this to their advantage, inserting an iframe into this object that could be used to achieve HTTP streaming without involving the browser window. The basic technique involves creating an HTMLFile object with an iframe in it and using that iframe to create a streaming HTTP connection, such as:

var oPage = new ActiveXObject("htmlfile");
oPage.open();
oPage.write("<html><body></body></html>");
oPage.close();

oPage.body.innerHTML = "<iframe src="connection.php"></iframe>";

This sample code illustrates creating an HTMLFile object and initializing it for Comet communication. After creating the object, it behaves just like document, so you're able to use open(), write(), and close() to set the HTML of the page. Then, the body's innerHTML is set to an iframe containing the connection. This connection will remain open and receive information without influencing the browser window or indicating to the user that something is going on. The only thing left is to use the connection to call JavaScript. This is where a problem occurs.

Because the page containing the iframe is technically not part of the browser hierarchy, there is no way to access the JavaScript in the main page from the iframe. Using parent or top simply returns the HTMLFile object. To access the main page, you need to assign a new property to the HTMLFile object:

oPage.parentWindow._parent = self;

This one line assigns a reference to the current window into the _parent property of the HTMLFile object's parentWindow. The connection page can now access anything in the main page by using code like this:

parent._parent.heartbeat();

Thus, the connection file must be modified slightly:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title>IE HTTP Streaming Example Connection</title>
    </head>
    <body>
<?php
    //get the file modification time
    $modified = filemtime('details.txt'),
    $lastModified = $modified;

    //clear file statistics
    clearstatcache();

    //check every so often to see if it has changed
    while (true) {
?>
         <script type="text/javascript">
        //<![CDATA[
        parent._parent.heartbeat();
        //]]>
        </script>
<?php
        ob_flush();
        flush();

        // sleep for 1 second
        sleep(1);

        //check the modification time

$lastModified = filemtime('details.txt'),

        //clear file statistics
        clearstatcache();

        //check it against the previous time
        if ($modified != $lastModified) {
            $output = date('h:i:s', $lastModified);
?>
             <script type="text/javascript">
            //<![CDATA[
                parent._parent.modifiedAt("<?php echo $output ?>");
            //]]>
            </script>
<?php
            ob_flush();
            flush();

            // sleep for 1 second
            sleep(1);

            $modified = $lastModified;
        }
    }
?>

    </body>
</html>

The client-side code also must be modified to use the HTMLFile object:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title>IE HTTP Streaming Example 1</title>
         <script type="text/javascript">
        //<![CDATA[

        var iTimeoutId = null;
        var oPage = null;

        function heartbeat() {
            clearTimeout(iTimeoutId);
            iTimeoutId = setTimeout(resetConnection, 10000);
        }

        function resetConnection() {
            oPage.body.innerHTML = "<iframe src="IEExampleConnection.php?t="
                                    + (new Date()).getTime() + ""></iframe>";
            heartbeat();
        }

        function modifiedAt(sDateTime) {

document.getElementById("divStatus").innerHTML =
                                                   "Modified at " + sDateTime;
        }

        window.onload = function () {
            oPage = new ActiveXObject("htmlfile");
            oPage.open();
            oPage.write("<html><body></body></html>");
            oPage.close();
            oPage.parentWindow._parent = self;
            resetConnection();
        };

        //]]>
        </script>
    </head>
    <body>
        <div id="divStatus">Waiting for first message...</div>
    </body>
</html>

This example now works the same way as the previous one but without involving the browser window.

NOTE

PHP uses chunk encoding by default, which means that it may buffer the output and send it in chunks. This can cause Comet that uses the HTMLFile object not to behave as expected (script execution can be delayed). If using PHP, try disabling chunk encoding for the connection file.

9.1.4.2. Firefox HTTP Streaming

Firefox supports HTTP streaming in a clean, though not terribly obvious, way. It is possible to open an HTTP stream using XHR and monitor the readyState property to determine when new data has arrived. Unlike other browsers, the readystatechange event fires every time the browser receives data from the server. While the actual readyState property remains set at 3, the event fires repeatedly, indicating that there is new data ready to be accessed. Consider the following:

var oXHR = new XMLHttpRequest();
oXHR.open("get", "connection.php", true);
oXHR.onreadystatechange = function () {
    switch (oXHR.readyState) {
        case 3:
            alert(oXHR.responseText);
            break;

        case 4:
            alert("Done");
    }
};
oXHR.send(null);

Whenever the readystatechange event fires and the readyState is 3, an alert displays the returned text. If the page is streaming content, the alert would show an ever-growing amount of text each time through. This can be problematic since, chances are, you are only interested in the most recently received text. For this reason, the output must be delimited to allow easy access to the most recent data. In the case of JavaScript code, it makes sense to delimit each call with a semicolon (;), so that the returned data looks something like this:

;heartbeat();heartbeat();heartbeat();modifiedAt("10:34:56");heartbeat()

With this data, it's possible to use an array to quickly get the most recent command:

var aCommands = oXHR.responseText.split(";");
var sCommand = aCommands.pop();

After this code has run, sCommand contains the most recent command from the server (pop() always returns the last item in an array). Assuming that semicolon delimitation is used in the commands, the file modification example can be rewritten to use Firefox's HTTP streaming support. First, the client:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title>Firefox HTTP Streaming Example</title>
         <script type="text/javascript">
        //<![CDATA[

        var iTimeoutId = null;
        var oXHR = null;

        function heartbeat() {
            clearTimeout(iTimeoutId);
            iTimeoutId = setTimeout(resetConnection, 10000);
        }

        function resetConnection() {
            oXHR.abort();
            oXHR.open("get",
                 "FirefoxExampleConnection.php?t=" + (new Date()).getTime(), true);
            oXHR.onreadystatechange = function () {
                switch(oXHR.readyState) {
                    case 3:
                        var aCommands = oXHR.responseText.split(";");
                        var sCommand = aCommands.pop();
                        eval(sCommand);
                        break;
                    case 4:
                        resetConnection();
                        break;
                }
            };
            oXHR.send(null);
            heartbeat();
        }

function modifiedAt(sDateTime) {
            document.getElementById("divStatus").innerHTML =
                                                        "Modified at " + sDateTime;
        }

        window.onload = function () {
            oXHR = new XMLHttpRequest();
            resetConnection();
        };

        //]]>
        </script>
    </head>
    <body>
        <div id="divStatus">Waiting for first message...</div>
    </body>
</html>

The major changes in this version of the example are the creation of an XHR object in the onload event handler and the parsing of the returned data/evaluation of the command using eval(). Whenever the readyState of the XHR object is 3, an array is created containing all commands received to that point. The most recent command must be passed into eval() to be interpreted as a JavaScript call.

If readyState ever reaches 4, it means that the connection timed out and the connection must be reset. Note that the first line of code inside of resetConnection() is a call to the abort() method, which effectively resets the XHR object to make it ready for another connection.

Next, take a look at the new server portion of the example:

<?php
    header("Content-type: text/javascript");

    //get the file modification time
    $modified = filemtime('details.txt'),
    $lastModified = $modified;

    //clear file statistics
    clearstatcache();

    //check every so often to see if it has changed
    while (true) {

        echo(";heartbeat()");
        ob_flush();
        flush();

        // sleep for 1 second
        sleep(1);

        //check the modification time
        $lastModified = filemtime('details.txt'),

        //clear file statistics

clearstatcache();

        //check it against the previous time
        if ($modified != $lastModified) {
            $output = date('h:i:s', $lastModified);

            echo(";modifiedAt("$output")");
            ob_flush();
            flush();

            $modified = $lastModified;

            // sleep for 1 second
            sleep(1);
        }

    }
?>

The changes in this example are subtle: all HTML has been removed. Since each command must be manually interpreted using eval(), there is no need for the HTML tags anymore. The content type of the page has been set to "text/javascript" to indicate the type of data being returned. Further, a semicolon precedes each text output so that it will always be the last item in the commands array on the client.

When you run this example, you will notice that no user interface changes as the page continues to load and send information to the client.

NOTE

Some servers put limits on the amount of time that a server process can run, which can cause errors to occur during the execution of this example as well as other Comet processes. Often times when this happens, the server returns an HTML string describing the problem, which can cause an error when passed into eval(). Always check your server's settings to determine the best way to implement Comet solutions.

9.1.4.3. LiveConnect HTTP Streaming

LiveConnect is a little-known and underutilized technology supported by Firefox, Safari, and Opera, allowing Java objects to be used from within JavaScript. To use LiveConnect, the client machine must have a Java Runtime Environment (JRE) installed, and Java must be enabled in the browser. Most of the objects in the java package and its subpackages are available for use from within JavaScript using LiveConnect, enabling functionality that may not be possible using native JavaScript objects. For a cross-browser, cross-platform method of HTTP streaming, LiveConnect can be used very effectively, thanks to the availability of the java.net package.

The key to using LiveConnect for HTTP streaming is to open a stream over HTTP. This is done by creating a new java.net.URL object and then calling openStream(). Doing so returns an instance of java.io.InputStream, which can then be passed into a java.io.InputStreamReader object. Then, this reader must be passed into a java.io.BufferedReader object for easy access. After that, the reader must be checked periodically to determine when new data is available. Here's the rewritten file modification page:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title>Live Connect Example</title>
         <script type="text/javascript">
        //<![CDATA[

        var iTimeoutId = null;
        var oReader = null;

        function resetConnection() {
            var oURL = new java.net.URL(
                              "http://localhost/LiveConnectExampleConnection.php");
            var oStream = oURL.openStream();
            if (oReader != null) {
                oReader.close();
            }
            oReader = new java.io.BufferedReader(
                                           new java.io.InputStreamReader(oStream));

            checkInput();
        }

        function checkInput() {
            try {
                var sLine = oReader.readLine();
                if (sLine != null) {
                    eval(sLine + "");
                }
                setTimeout(checkInput, 500);
            } catch (oEx) {
                resetConnection();
            }
        }

        function heartbeat() {
            clearTimeout(iTimeoutId);
            iTimeoutId = setTimeout(resetConnection, 10000);
        }

        function modifiedAt(sDateTime) {
            document.getElementById("divStatus").innerHTML =
                                                        "Modified at " + sDateTime;
        }

        window.onload = resetConnection;

        //]]>
        </script>
    </head>
    <body>
        <div id="divStatus">Waiting for first message...</div>
    </body>
</html>

The key to this example is the global oReader object, which contains a reference to a java.io.BufferedReader. When resetConnection() is called, a new java.net.URL object is created with the URL to send the request to. Note that this must be an absolute path to the page, since these Java objects don't know the context of the page in which the JavaScript code is running.

When the openStream() method is called, it returns a reference to an input stream for the URL. Before continuing on, any existing instance of oReader must be closed (by calling close()) to free any remaining memory. Once it's a sure thing that there are no other readers still in memory, a new java.io.BufferedReader is created and stored in oReader. Then, checkInput() is called to see if there's any data.

The checkInput() function does the important part of the process: checking for data and executing JavaScript commands based on that data. Each time this function is called, readLine() returns any available data. If any data is available, it is stored in sLine, which is then passed into eval() to call the JavaScript command returned from the server. Since sLine is returned from a Java method, it's actually not a JavaScript string but rather an instance of java.lang.String. To convert it into a JavaScript, an empty string is appended. After that, a timeout is created to call checkInput() in another 500 milliseconds.

All of the logic inside of checkInput() is wrapped in a try block. At some point, the connection will time out, and the call to readLine() will throw an error. The try block will catch this error and call resetConnection() to ensure that the stream is reopened.

The server-side component to this LiveConnect example is very similar to the Firefox equivalent:

<?php
    header("Content-type: text/javascript");

    //get the file modification time
    $modified = filemtime('details.txt'),
    $lastModified = $modified;

    //clear file statistics
    clearstatcache();

    //check every so often to see if it has changed
    while (true) {

        echo("heartbeat()
");
        ob_flush();
        flush();

        // sleep for 1 second
        sleep(1);

        //check the modification time
        $lastModified = filemtime('details.txt'),

        //clear file statistics
        clearstatcache();

        //check it against the previous time

if ($modified != $lastModified) {
            $output = date('h:i:s', $lastModified);

            echo("modifiedAt("$output")
");
            ob_flush();
            flush();

            $modified = $lastModified;

            // sleep for 1 second
            sleep(1);
        }

    }
?>

The important difference in this page is that each JavaScript call is followed by a new line character ( ). Since the reader on the client side reads in data one line at a time, it's very important that this character be appended to each line of output so that it is read in a timely manner.

9.1.5. Server-Sent DOM Events

The Web Hypertext Application Technology Working Group (known as WHATWG) is a group of developers, companies, and others, interested in pushing browser development toward a platform more suitable for applications. WHATWG publishes a specification called Web Applications 1.0, which is a working draft as of October 2006. While Web Applications 1.0 introduces some very interesting concepts, one of the most interesting is called server-sent DOM events.

Server-sent DOM events allow a server to stream data to the client, which fires events in response to that data, allowing developers easy access to server information. Essentially, the browser opens a persistent connection to a particular page on the server and listens for new data coming in. The data for server-side DOM events comes in the form of event information, such as:

Event: MyEvent
Name1: value1
name2: value2

Event: MyEvent
data: See you later!

Each time the server sends data it must have an event name (specified by Event:) and some data in name-value pairs. Each part of this data is then made available to the client through JavaScript. There must be one blank line in between events so that the client recognizes an event as being fully received. Also, the content type of the data stream must be "application/x-dom-event-stream".

To receive events from the server, an <event-source/> element is necessary. This is a new element introduced in Web Applications 1.0 and can be accessed using all of the usual DOM methods. The src attribute should be set to the URL providing the streamed data, such as:

<event-source src="connection.php" id="source" />

Once the element is included in a page, you can use the addEventListener() method to assign event handlers for specific events. For example, to respond to an event called "MyEvent", the code would be:

var oSource = document.getElementById("source");
oSource.addEventListener("MyEvent", function (oEvent) {
    alert(oEvent.type);
}, false);

When the event is fired and the event handler called, an event object is passed in as the only argument. This event object is exactly the same as any other DOM event object in terms of the properties and methods, so type is the name of the event that was fired and target points to the <event-source/> element. However, there is some extra information provided on the event object in the form of the name-value pairs received in the data stream. If there is a named value called data in the stream, a property named data is accessible on the event object to retrieve that information.

NOTE

Note that in the case of custom events, the third argument in addEventListener() has no meaning but is typically set to false.

9.1.5.1. Firing UI Events

The true power of server-sent DOM events isn't simply in firing custom events; it's in firing UI events on the client from the server. So at any time, the server can decide that a click event should be fired, or mouseover or keydown ... any event named in the DOM Level 3 Events specification can be fired through server-side DOM events. The complete list of events is:

  • abort (Event)

  • blur (UIEvent)

  • click (MouseEvent)

  • change (Event)

  • DOMActivate (UIEvent)

  • DOMAttrModified (MutationEvent)

  • DOMAttributeNameChanged (MutationNameEvent)

  • DOMCharacterDataModified (MutationEvent)

  • DOMElementNameChanged (MutationNameEvent)

  • DOMFocusIn (UIEvent)

  • DOMFocusOut (UIEvent)

  • DOMNodeInserted (MutationEvent)

  • DOMNodeInsertedIntoDocument (MutationEvent)

  • DOMNodeRemoved (MutationEvent)

  • DOMNodeRemovedFromDocument (MutationEvent)

  • DOMSubtreeModified (MutationEvent)

  • error (Event)

  • focus (UIEvent)

  • keydown (KeyboardEvent)

  • keyup (KeyboardEvent)

  • load (Event)

  • mousedown (MouseEvent)

  • mousemove (MouseEvent)

  • mouseover (MouseEvent)

  • mouseout (MouseEvent)

  • mouseup (MouseEvent)

  • reset (Event)

  • resize (UIEvent)

  • scroll (UIEvent)

  • select (Event)

  • submit (Event)

  • textInput (TextEvent)

  • unload (Event)

To fire one of these events, specify its exact name (including case) as the Event value:

Event: click

Of course, firing a click event isn't very useful without firing it on a particular element. So, in addition to specifying the event, you must also specify a target using the Target attribute:

Event: click
Target: #target

Since the server doesn't have any DOM references, it needs to send the ID of the element upon which to fire the event. The format is the same as using an ID in CSS: precede the ID with the pound sign (#). It's also possible to fire an event on the document itself by specifying Document as the target:

Event: click
Target: Document

Depending on the event, you can also specify additional information to be sent:

Event: click
Target: #target
button : 2
screenX : 0
screenY : 0

In this example, the button, screenX, and screenY properties are filled with specified values. As long as the names of these name-value pairs match properties on the event object, they will be assigned appropriately. Any names that don't match will be ignored.

NOTE

When sending UI events to the browser, it is unnecessary to assign event handlers to the <event-source/> element. Each of the events is transported automatically to the targeted element and is handled by the event handlers on that element.

9.1.5.2. Browser Support

As of October 2006, the only browser supporting server-sent DOM events is Opera 9.01. It was actually an Opera engineer, Ian Hickson, who wrote the original specification back in 2004 (that specification was later incorporated into Web Applications 1.0). While the Opera implementation takes most things into account, there are some limitations to be aware of:

  1. The <event-source/> element must be in the main markup of the page; creating it using document.createElement() doesn't work.

  2. You can only use values named data: for custom events. All other names are ignored.

It should be noted that these limitations are minor and do not interfere significantly with the ability to make use of this extremely powerful feature. The following example runs on Opera 9.01 and later, and presumably will work with other browsers that implement server-sent DOM events in the future.

NOTE

Server-sent DOM events are also on the Mozilla roadmap, though it is unclear what version of Firefox will be the first to implement it.

9.1.5.3. Example

The file modification example becomes extremely simple when using server-sent DOM events. Consider the simplification of the client:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title>Server-Sent DOM Events Example 1</title>
         <script type="text/javascript">
        //<![CDATA[

function modifiedAt(sDateTime) {
            document.getElementById("divStatus").innerHTML =
                                                      "Modified at " + sDateTime;
        }

        window.onload = function () {
            var oSource = document.getElementById("source");

            oSource.addEventListener("modified", function (oEvent) {
                modifiedAt(oEvent.data);
            }, false);

        };

        //]]>
        </script>
    </head>
    <body>
        <div id="divStatus">Waiting for first message...</div>
        <event-source id="source" src="ServerSentDOMEventsConnection.php" />
    </body>
</html>

Here, an <event-source/> element is added in the page with an id of "source" and its src attribute set to ServerSentDOMEventsConnection.php. This is enough to start the information stream from the server to the client; however, an event handler must be added to access the data as it comes in. So, in the onload event handler, a reference to the <event-source/> element is retrieved by using getElementById(). Then, an event handler is added using addEventListener() and passing in the name of the custom event "modified". This handler simply retrieves information from the data value and then passes it to modifiedAt() (which is the same as in previous examples).

On the server, the basic functionality is the same as in previous examples, just with a different format:

<?php
    header("Content-type: application/x-dom-event-stream");

    //get the file modification time
    $modified = filemtime('details.txt'),
    $lastModified = $modified;

    //clear file statistics
    clearstatcache();

    //check every so often to see if it has changed
    while (true) {

        // sleep for 1 second
        sleep(1);

        //check the modification time
        $lastModified = filemtime('details.txt'),

        //clear file statistics

clearstatcache();

        //check it against the previous time
        if ($modified != $lastModified) {
            $output = date('h:i:s', $lastModified);
            echo("Event: modified
");
            echo("data: $output

");
            ob_flush();
            flush();
            $modified = $lastModified;

            // sleep for 1 second
            sleep(1);
        }

    }
?>

The major changes here are the different content type for the page ("application/x-dom-event-stream", which is required by the specification) and the output. As opposed to previous examples, this page outputs plain text in the proper format for interpretation:

Event: modified
data: 5:23:06

That's all it takes to make this example work the same way as the previous ones. The differences are that the browser handles resetting the connection if it dies and access to incoming server data is much easier than using iframes or XHR.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset