blogger templates blogger widgets
This is part of a list of blog posts.
To browse the contents go to

Reverse AJAX - New school

3. Comet

Comet is a web application model where a request is sent to the server and kept alive for a long time until a time-out or a server event occurs.
When the request is completed, another long-lived Ajax request is sent to wait for other server events. With Comet, web servers can send the data to the client without having to explicitly request it.

The big advantage of Comet is that each client always has a communication link open to the server.
The server can push events on the clients by immediately committing (completing) the responses when they arrive, or it can even accumulate and send bursts.
Because a request is kept open for a long time, special features are required on the server side to handle all of these long-lived requests.



Long Polling Technique:

The long polling mode involves techniques that open a connection.
The connection is kept open by the server, and, as soon as an event occurs, the response is committed and the connection is closed. Then, a new long-polling connection is reopened immediately by the client waiting for new events to arrive.
(Request #2 in above image)


The basic life cycle of an application using "long polling" is as follows:

1. The client makes an initial request and then waits for a response (usually expected time to process data on the server).
2. The server defers its response until an update is available, or a particular status or timeout has occurred.
3. When an update is available, the server sends a complete response to the client.
4. The client typically sends a new long poll request, either immediately or after a pause to allow an acceptable latency period.

There are 2 ways to implement long polling:

1. Script tags

The goal is to append a script tag in your page to get the script executed.
The server will: suspend the connection until an event occurs, send the script content back to the browser, and then reopen another script tag to get the next events.
Advantages: Because it's based on HTML tags, this technique is very easy to implement and works across domains (by default, XMLHttpRequest does not allow requests on other domains or sub-domains).
Disadvantages: Similar to the iframe technique, error handling is missing, and you can't have a state or the ability to interrupt a connection.

2. XMLHttpRequest long polling

The second, and recommended, the method to implement Comet is to open an Ajax request to the server and wait for the response.
The server requires specific features on the server side to allow the request to be suspended.
As soon as an event occurs, the server sends back the response in the suspended request and closes it, exactly like you close the output stream of a servlet response.
The client then consumes the response and opens a new long-lived Ajax request to the server.

Advantages: It's easy to implement on the client side with a good error-handling system and timeout management. This reliable technique also allows a round-trip between connections on the server side, since connections are not persistent (a good thing, when you have a lot of clients on your application). It also works on all browsers; you only make use of the XMLHttpRequest object by issuing a simple Ajax request.

Disadvantage: Like all techniques we've discussed, this one still relies on a stateless HTTP connection, which requires special features on the server side to be able to temporarily suspend it. I'm running on Tomcat 8 which supports Servlets 3.1 spec in other words it supports Asynchronous processing.

Read about Async Servlets here


@WebServlet(asyncSupported=true, urlPatterns="/events")
public final class ReverseAjaxServlet extends HttpServlet {

    private final Queue<asynccontext> asyncContexts = new ConcurrentLinkedQueue<asynccontext>();

    private final Random random = new Random();
    private final Thread generator = new Thread("Event generator") {
        @Override
        public void run() {
         println("Listening thread started");
            while (!Thread.currentThread().isInterrupted()) {
                try {
                    Thread.sleep(random.nextInt(7000));
                    while (!asyncContexts.isEmpty()) {
                     println("Removing asyncContext from queue");
                     //removing only one at a time
                        AsyncContext asyncContext = asyncContexts.poll(); 
                        HttpServletResponse peer = (HttpServletResponse) asyncContext.getResponse();
                        peer.getWriter().write(new JSONArray().put("At " + new Date()).toString());
                        peer.setStatus(HttpServletResponse.SC_OK);
                        peer.setContentType("application/json");
                        asyncContext.complete();
                    }
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } catch (IOException e) {
                    throw new RuntimeException(e.getMessage(), e);
                }
            }
        }
    };

    @Override
    public void init() throws ServletException {
        generator.start();
    }

    @Override
    public void destroy() {
        generator.interrupt();
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        AsyncContext asyncContext = req.startAsync();
        asyncContext.setTimeout(0);
        println("Inserting asyncContext into queue");
        asyncContexts.offer(asyncContext);
    }
    
    public static void println(String output) {
  System.out.println("[" + Thread.currentThread().getName() + "]" + output);
 }
}


<html>
<head>
    <title>HTTP Polling</title>
    <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js"></script>
    <script type="text/javascript" src="http://jquery-json.googlecode.com/files/jquery.json-2.2.min.js"></script>
    <script type="text/javascript">
        jQuery(function($) {

            function processEvents(events) {
                if (events.length) {
                    $('#logs').append('<span style="color: blue;">[client] ' + events.length + ' events</span><br/>');
                } else {
                    $('#logs').append('<span style="color: red;">[client] no event</span><br/>');
                }
                for (var i in events) {
                    $('#logs').append('<span>[event] ' + events[i] + '</span><br/>');
                }
            }

            function long_polling() {
                $.get('events', function(events) {
                    processEvents(events);
                    long_polling();
                });
            }

            long_polling();

        });
    </script>
</head>
<body>
<div id="logs" style="font-family: monospace;">
</div>
</body>
</html>

[client] 1 events
[event] At Thu Aug 27 08:41:52 IST 2015
[client] 1 events
[event] At Thu Aug 27 08:41:57 IST 2015
[client] 1 events
[event] At Thu Aug 27 08:41:57 IST 2015
[client] 1 events
[event] At Thu Aug 27 08:42:02 IST 2015

[Event generator]Listening thread started
[http-nio-8080-exec-3]Inserting asyncContext into queue
[Event generator]Removing asyncContext from queue
[http-nio-8080-exec-5]Inserting asyncContext into queue
[Event generator]Removing asyncContext from queue
[http-nio-8080-exec-7]Inserting asyncContext into queue
[Event generator]Removing asyncContext from queue
[http-nio-8080-exec-9]Inserting asyncContext into queue
[Event generator]Removing asyncContext from queue
[http-nio-8080-exec-1]Inserting asyncContext into queue
[Event generator]Removing asyncContext from queue


HTTP Streaming Technique:

The server keeps a request open indefinitely; that is, it never terminates the request or closes the connection,
even after it pushes data to the client. (request #1 in image)

The basic life cycle of an application using "HTTP streaming" is as follows:

1. The client makes an initial request and then waits for a response.
2. The server defers the response to a poll request until an update is available, or a particular status or timeout has occurred.
3. Whenever an update is available, the server sends it back to the client as a part of the response.
4. The data sent by the server does not terminate the request or the connection. The server returns to step 3.

There are 2 ways to implement streaming:

1. Forever Iframes

The Forever Iframes technique involves a hidden Iframe tag put in the page with its src attribute pointing to the servlet path returning server events.
Each time an event is received, the servlet writes and flushes a new script tag with the JavaScript code inside.
The iframe content will be appended with this script tag that will get executed.

Advantages: Simple to implement, and it works in all browsers supporting iframes.
Disadvantages: There is no way to implement reliable error handling or to track the state of the connection because all connection and data are handled by the browser through HTML tags. You then don't know when the connection is broken on either side.


2.Multi-part XMLHttpRequest

The second technique, which is more reliable, is to use the multi-part flag supported by some browsers (such as Firefox) on the XMLHttpRequest object.
An Ajax request is sent and kept open on the server side. Each time an event comes, a multi-part response is written through the same connection.

On the server side, things are a little more complicated. You must first set up the multi-part request and then suspend the connection.

Advantage: Only one persistent connection is opened. This is the Comet technique that saves the most bandwidth usage.
Disadvantage: The multi-part flag is not supported by all browsers. Some widely used libraries, such as CometD in Java, reported issues in buffering.

For example, chunks of data (multi-parts) may be buffered and sent only when the connection is completed or the buffer is full, which can create higher latency than expected.

Below code utilizes Asynchronous servlets. I'm running on Tomcat 8 which supports Servlets 3.1 spec, in other words it supports Asynchronous processing.

Read about Async Servlets here


@WebServlet(asyncSupported = true, urlPatterns="/events")
public final class ReverseAjaxServlet extends HttpServlet {

    private final Queue<asynccontext> asyncContexts = new ConcurrentLinkedQueue<asynccontext>();
    private final String boundary = "0.1.2.3.4.5.6.7.8.9"; // generated

    private final Random random = new Random();
    private final Thread generator = new Thread("Event generator") {
        @Override
        public void run() {
            while (!Thread.currentThread().isInterrupted()) {
                try {
                    Thread.sleep(random.nextInt(5000));
                    for (AsyncContext asyncContext : asyncContexts) {
                     println("Removing asyncContext from queue");
                        HttpServletResponse peer = (HttpServletResponse) asyncContext.getResponse();
                        peer.getOutputStream().println("Content-Type: application/json");
                        peer.getOutputStream().println();
                        peer.getOutputStream().println(new JSONArray().put("At " + new Date()).toString());
                        peer.getOutputStream().println("--" + boundary);
                        peer.flushBuffer();
                    }
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } catch (IOException e) {
                    throw new RuntimeException(e.getMessage(), e);
                }
            }
        }
    };

    @Override
    public void init() throws ServletException {
        generator.start();
    }

    @Override
    public void destroy() {
        generator.interrupt();
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        AsyncContext asyncContext = req.startAsync();
        asyncContext.setTimeout(0);
        println("Inserting asyncContext into queue");
        resp.setContentType("multipart/x-mixed-replace;boundary=\"" + boundary + "\"");
        resp.setHeader("Connection", "keep-alive");
        resp.getOutputStream().print("--" + boundary);
        resp.flushBuffer();

        asyncContexts.offer(asyncContext);
    }
    
    public static void println(String output) {
  System.out.println("[" + Thread.currentThread().getName() + "]" + output);
 }
}


<html>
<head>
    <title>HTTP Streaming</title>
    <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js"></script>
    <script type="text/javascript" src="http://jquery-json.googlecode.com/files/jquery.json-2.2.min.js"></script>
    <script type="text/javascript">
        jQuery(function($) {

            if (!('XMLHttpRequest' in window && 'multipart' in window.XMLHttpRequest.prototype)) {
                alert('Comet Http Streaming is not supported in your browser !');
                throw new Error('Comet Http Streaming is not supported in your browser !');
            }

            function processEvents(events) {
                if (events.length) {
                    $('#logs').append('<span style="color: blue;">[client] ' + events.length + ' events</span><br/>');
                } else {
                    $('#logs').append('<span style="color: red;">[client] no event</span><br/>');
                }
                for (var i in events) {
                    $('#logs').append('<span>[event] ' + events[i] + '</span><br/>');
                }
            }

            var xhr = $.ajaxSettings.xhr();
            xhr.multipart = true;
            xhr.open('GET', 'events', true);
            xhr.onreadystatechange = function() {
                if (xhr.readyState == 4) {
                    processEvents($.parseJSON(xhr.responseText));
                }
            };
            xhr.send(null);

        });
    </script>
</head>
<body>
 <div id="logs" style="font-family: monospace;"></div>
</body>
</html>

This didn't work as expected. I opened a question in SO: How to Comet http streaming

For comet to work, we need support on browsers and also on the server. jQuery and Dojo provides libraries to do it.
Also some container specific libraries exist like CometProcessor (in Tomcat), Continuations (Jetty), etc.

Finally, got a solution working. Looks like I was overdoing it. The below code works fine and serves my purpose.


@WebServlet(urlPatterns = "/async21", asyncSupported = true)
public class Async21 extends HttpServlet {

 private static final long serialVersionUID = 1L;

 @Override
 protected void service(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException {
  response.setContentType("text/html");

  response.getWriter().write("Before starting job 
");
  response.getWriter().flush();

  final AsyncContext actx = request.startAsync();
  actx.setTimeout(Long.MAX_VALUE);
  actx.start(new HeavyTask(actx, request.getParameter("clientID")));

 }

 class HeavyTask implements Runnable {
  AsyncContext actx;
  String user;
  HeavyTask(AsyncContext actx, String clientID) {
   this.actx = actx;
   this.user = clientID;
  }

  @Override
  public void run() {
   try {
    Thread.currentThread().setName("Job-Thread-" + actx.getRequest().getParameter("user"));
    for (int i = 0; i < 7; i++) {
     actx.getResponse().getWriter().write("Doing work (" + i + ") for client "+user+"
");
     actx.getResponse().getWriter().flush();
     Thread.sleep(1000);
     println("work " + i + " completed");
    }
    println("Job finished now dispatching...");
    actx.complete();
   } catch (InterruptedException e) {
    e.printStackTrace();
   } catch (IllegalStateException e) {
    e.printStackTrace();
   } catch (IOException e) {
    e.printStackTrace();
   }
  }
 }

 public static void println(String output) {
  System.out.println("[" + Thread.currentThread().getName() + "]" + output);
 }
}



<%@ page language="java" contentType="text/html; charset=UTF-8"
 pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<title>Insert title here</title>
</head>
<body>
 Enter user identification here: <input id="clientID" type="text" /><br />
 <a href="#" onclick="fetchFromServer();">start</a><br />
 <div id="myDiv"></div>
</body>
<script>

 function getAjaxClient() {

  var client = null;
  try {
   // Firefox, Opera 8.0+, Safari
   client = new XMLHttpRequest();
  } catch (e) {
   // Internet Explorer
   try {
    client = new ActiveXObject("Msxml2.XMLHTTP");
   } catch (e) {
    client = new ActiveXObject("Microsoft.XMLHTTP");
   }
  }
  return client;
 };

 function fetchFromServer() {
  this.ajax = getAjaxClient();

  try {
   var params = escape("user") + "=" + document.getElementById("clientID").value;
   var url = "async21?" + params;
   this.ajax.onreadystatechange = handleMessage;
   this.ajax.open("GET", url, true); //true means async, which is the safest way to do it

   //  Before we needed to set all these but on newer browsers its not needed
   //  this.ajax.setRequestHeader("Connection", "Keep-Alive");
   //  this.ajax.setRequestHeader("Keep-Alive", "timeout=999, max=99");
   //  this.ajax.setRequestHeader("Transfer-Encoding", "chunked");

   //send the GET request to the server
   this.ajax.send(null);
  } catch (e) {
   alert(e);
  }
 };

 function handleMessage() {
  //states are:
  // 0 (Uninitialized) The object has been created, but not initialized (the open method has not been called).
  // 1 (Open) The object has been created, but the send method has not been called.
  // 2 (Sent) The send method has been called. responseText is not available. responseBody is not available.
  // 3 (Receiving) Some data has been received. responseText is not available. responseBody is not available.
  // 4 (Loaded)
  try {
   if (this.readyState == 0) {
    console.log("ready state 0");
   } else if (this.readyState == 1) {
    console.log("ready state 1");
   } else if (this.readyState == 2) {
    console.log("ready state 2");
   } else if (this.readyState == 3) {
    
    var myDiv = document.getElementById("myDiv");
    if (this.status == 200) {
     console.log("ready state 3");
     //for chunked encoding, we get the newest version of the entire response here, 
     //rather than in readyState 4, which is more usual.
     //so we don't do this
     myDiv.innerHTML = myDiv.innerHTML + "<br>"
       + (this.responseText);
     //instead
     myDiv.innerHTML = this.responseText;
     
    } else {
     console.log("ready state 3 not ready");
    }
   } else if (this.readyState == 4) {
    var myDiv = document.getElementById("myDiv");
    if (this.status == 200) {
     //the connection is now closed.
     console.log("ready state 4");
     //start again - we were just disconnected!
    } else {
     console.log("ready state 4 not ready");
    }
   }
  } catch (e) {
   alert(e);
  }
 };
</script>
</html>

Note: removing the flush() method on the writer will cause the output to buffer and send only when it's completed. This helps in improving latency.



1 comment:

  1. Thank you for very nice article!

    I am trying to implement XMLHttpRequest long polling. But there is one issue: sometimes event occur after AsyncListener onComplete (when peers are removed and peers.isEmpty() is true) and before ReverseAjaxServlet receives new Get request.
    How to manage this situation?

    ReplyDelete