Search This Blog

Tuesday 12 April 2016

Reverse AJAX - The Comet Technique

In my series on Reverse AJAX, I covered Polling and Piggybacking. We saw the pros and cons of either technique. In this post I shall take a look at a third technique - Comet.
According to wikipedia:
Comet is a web application model in which a long-held HTTP request allows a 
web server to push data to a browser, without the browser explicitly requesting it.
In this approach:
  1. Client sends a request to the server.
  2. When server has some information to send back, it will return the information as a response to this request. If there is no data for a certain amount of time, the request times out.
  3. For the client, the request ends either with a 200 OK (and some data) or a time -out error. In either case, client will initiate another request to the server.
Thus with Comet, web servers can send the data to the client without having to explicitly request it. One of the requirements on server side is the ability to keep the client request on till it has some data to send the client. The server should be able to suspend the client request for use when needed.
Prior to servlet 3.x, Servlets did not have the ability to suspend requests. So most container providers built their own implementations to support Comet behavior. For e.g. Tomcat provided the CometProcessor interface which needs to be implemented by Servlets.
public void event(CometEvent event)
This method is the on where all the magic happens. There is an example here. JBoss and Jetty servers among others also provide similar techniques.
With Servlet 3.x async support however, there is a uniform way to do this across the various containers.
The first step was to create an asynchronous endpoint at the server.
@SuppressWarnings("serial")
public class CometPushServlet extends HttpServlet {
    
  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {
    System.out.println("Request received for a check at " + new Date());
    AsyncContext asyncContext = req.startAsync();
    //Save for later processing
    CometEventNotifier.suspendRequestForLaterProcessing(asyncContext);
    
  }
}
As seen here, we call the startAsync method on the request.
Puts this request into asynchronous mode, and initializes its AsyncContext with the original 
(unwrapped) ServletRequest and ServletResponse objects.
Calling this method will cause committal of the associated response to be delayed until 
AsyncContext#complete is called on the returned AsyncContext, or the asynchronous operation 
has timed out.
Every request that arrives here is converted into an asynchronous one (creating the AsyncContext) and then passed to the CometEventNotifier class.
public class CometEventNotifier implements ServletContextListener {
  
  // A Queue to hold all the AyncContexts
  private static final BlockingQueue<AsyncContext> queue = new LinkedBlockingQueue<>();

  //using a separate thread to avoid blocking the main guy
  private Thread workerThread;
  
  public static void suspendRequestForLaterProcessing( AsyncContext asyncContext) {
    queue.add(asyncContext);
  }
  
  @Override
  public void contextDestroyed(ServletContextEvent arg0) {
    workerThread.interrupt();// no point in running it any further
    }

  @Override
  public void contextInitialized(ServletContextEvent arg0) {
    
    workerThread = new Thread(new Runnable() {
      @Override
      public void run() {
        
        while (true) { //Runs on an infinite loop
          try {
            //Check if there is any event
            int eventC = EventUtils.getEvents();
            if(eventC==0) {
              //sleep for 2 seconds
              Thread.sleep(2000);
              continue;
            }
            //Inform all waiting requests about the invite
            AsyncContext context;
            while ((context = queue.poll()) != null) {
              try {
                ServletResponse response = context.getResponse();
                response.setContentType(" application/xml");
                PrintWriter out = response.getWriter();
                out.printf("<data><eventC>" + eventC +"</eventC></data>");
                out.flush();
                out.close();
              } catch (Exception e) {
                throw new RuntimeException(e.getMessage());
              } finally {
                context.complete();
              }
            }
          } catch (InterruptedException e) {
            //skip
            return;
          }
        }
      }
    });
    //Start the Thread
    workerThread.start();
  }

}
Several Points of note in this code:
  1. The class is an instance of ServletContextListener. However on initialization it simply transfers control to a worker thread responsible for event processing.
  2. The Thread runs an infinite loop. Every few seconds it checks if any events have been generated. In case of events, for each of  AsyncContext, it retrieves the ServletRequest and ServletResponse.
  3. It then writes the output on the response stream.
  4. Processing is completed by calling the complete() method - this indicates that response associated with the AsyncContext can be closed. In case the operation had timed out, its the container's responsibility to call the complete() method.
The configurations in web.xml is as follows:
<servlet>
      <servlet-name>CometPushServlet</servlet-name>
      <servlet-class>com.app.web.CometPushServlet</servlet-class>
      <async-supported>true</async-supported>
   </servlet>
   <servlet-mapping>
      <servlet-name>CometPushServlet</servlet-name>
      <url-pattern>/comet</url-pattern>
   </servlet-mapping>

   <listener>
      <description> Responsible for completing all long requests</description>
      <listener-class>com.app.web.CometEventNotifier</listener-class>
   </listener>
As seen, there is a special async-supported tag that must be added - this must be present in all servlets and filters through which async requests pass through. Last is the client which makes the call:
<html>
<head>
<script>
  var rec =0;
  
  var cometCheck = function() {
      rec= rec+1;
      if (window.XMLHttpRequest) {
          // code for IE7+, Firefox, Chrome, Opera, Safari
          xmlhttp = new XMLHttpRequest();
      } else { // code for IE6, IE5
          xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
      }
      xmlhttp.onreadystatechange = function() {
          if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {  
              var xmlDoc = xmlhttp.responseXML;
              var eventCount = xmlDoc.getElementsByTagName("eventC")[0].childNodes[0].nodeValue
              document.getElementById("eventHighlighter").innerHTML = " Call No " + rec + " : <b>"
                  + eventCount
                  + " Event Invites received ! Goto Events page to review. </b>";
             // processing complete - restart the connection
                cometCheck();
          }
      }
      xmlhttp.open("GET", "comet", true);
      xmlhttp.timeout = 4000;
      xmlhttp.ontimeout = function () {
       alert("TimedOut");
       //In case of a timeout - restart the connection
       cometCheck(); 
    }
      xmlhttp.send();
  };

cometCheck();
  
</script>

</head>

<body>
     <h3>Hello, This is a sample page</h3>
      <br />
      <br /> Is your session active ??
      <div id="sessionActve"></div>
      <div id="eventHighlighter"></div>
</body>
</html>
As seen here, the java script includes a method that opens a connection to our comet servlet. On completion it updates the UI and then reopens the connection. If there was a timeout, than the client will reopen a different connection.
There are several advantages to this technique. Unlike polling we aren't sending several requests to the server. At any given time there is just one request open at the server, which is processed only when data needs to be sent back.
Unlike the piggyback technique, the client receives fast updates when an event occurs.

Reference and Guide: http://www.ibm.com/developerworks/library/wa-reverseajax1/

4 comments:

  1. It's very nice blog. I'm so happy to gain some knowledge from here.

    ReplyDelete
  2. This comment has been removed by a blog administrator.

    ReplyDelete
  3. This comment has been removed by a blog administrator.

    ReplyDelete
  4. This comment has been removed by a blog administrator.

    ReplyDelete