AsyncFW users, please also see the CometWorld tutorial.

 

Quick and Dirty Comet!

In 60 minutes or less!

 

Comet is simply a mechanism for suspending a request on the server.  Combine that with an Asynchronous Ajax request from the client, and you have a “fire and forget” interface to the server.  The server will reply when new data is available, without the client (seemingly) needing to ask for it.  The caveat being “Seemingly” - In reality, the client is simply staging the request ahead of time.

 

Why use Comet?

 

That is the tricky part – If you are currently “polling” the server for data Comet may provide a better model.  If you search for ‘Comet’ on the web, you might soon think that all Comet is good for is “Chat” applications.  While I don’t think I would use Comet on every site, and certainly not on every page, I do think it has some merit. 

 

When a user accesses a web page, they may sit idly reading for some time, 99.9% of the time that is perfect, but what if they are looking at a Temperature Gauge, Stock Price, Airline fare, Ticket availability, etc.  These are values that can change at any time. Normally, you would simply set up a timed poll of the server to keep the information accurate (at least accurate to the time of the last poll). With Comet, you can broadcast the change to all online users in real-time. 

 

While polling is a workable solution, you have to balance the needs of the users, versus the load on the server. Setting a sub-one second poll, would place a tremendous burden on a server with many users. Setting a long polling time reduces that burden but also reduces the “potential” accuracy of the data. 

 

Comet resolves this conflict. By setting up a “tell me when the data changes” connection, you don’t need to poll. The data will simply update when there is need. But, it too has it’s costs. With Comet, you have to maintain the connection state on the server, so every user will have one permanent connection (dedicated to Comet), and at least one additional transient connection for normal processing. 

 

Using Comet

 

Searching the web for information on Comet can be a bit of a challenge. You will find the obligatory “Chat” examples (like the world needs more of those) and a bunch of theory and arcane discussions. If you are like me, and just want to make it work, it can be a challenge.  I spent over week trying to wedge Comet into the FrameWork, I thought this would be nice little niche addition that some would find useful. While most of the issues and difficulties I had were mine, it was a bit of a challenge to find any examples other than JSP “Chat” with iFrames.  I had no interest in JSP, nor a Chat application (as I am a servlet purist, and I already more chat applications than I need) , and additionally - I didn’t want to require the use of iFrames.  I also wanted to maintain state, return something other than static HTML and I wanted to make the entire process painless for those that use the Framework for web site creation.

 

Setup

 

Glass Fish v2

 

Version Info:

Sun Java System Application Server 9.1 Update 2

Open ESB v2

Portlet Container 2.0 Update 1

Sun Java System Access Manager 7.1 Patch 1

Java BluePrints

Java Engineering Edition 5 Samples

Your First Cup: An Introduction to the Java EE Platform

API Documentation

Java SE Development Kit 6 Update 10

 

First, I have only used Comet on Sun Application Server/GlassFish, how well it works in other servers - I have no idea. So for this tutorial I will assume GlassFish v2 (v3 should also work, and seems to be the recommended solution).

 

Modify domain.xml

 

The first think you must do is enable Comet for your site. This is done by modifying your domain.xml file located in the /config directory of your site. For my installation that means going to C:\Sun\SDK\domains\domain1\config and opening the file domain.xml.

 

 

Locate the “http-listener” entry for port 8080, and add the “commetSupport” property:

 

 <property name="cometSupport" value="true" />

 

Optionally I also added the following Java-config setting:

 

<jvm-options>-Dcom.sun.enterprise.server.ss.ASQuickStartup=false</jvm-options>

 

Verify that  you have the Comet Libraries

 

There are at least 2 versions of the Comet libraries. I am using the default libraries that I got with Glassfish:

 

import com.sun.enterprise.web.connector.grizzly.comet.*;

 

Make the above import will resolve in your IDE by simply creating an empty servlet and adding the above import, and compiling.

 

 

Comet de-mystified:

 

Ok,  de-mystified may be a bit aggressive, but I will give it a try.  If you don’t understand client-side Ajax, then I strongly suggest learning about it before you dive into Comet. There are a ton of good references both on this site, and the web in general. For an introduction to FrameWork Ajax, for a more general introduction, try this!

 

From the client (Java Script) you will want to establish two connections. The first, is the long waiting Comet connection. The other will be your transient, or active connection.  As a general rule, you may want to establish your long-lived Comet connection on load of your page.  This connection should be Asynchronous so that the browser does not lock waiting for a reply.

 

req.open(method, url, true);

 

The key to opening an asynchronous javascript connection is to use “true” as the third parameter of the req.open command.

 

NOTE:req’ is an instance of the XMLHttpRequest or ActiveX object.

 

Server Side: FrameWork Users First…

 

FrameWork users: please also see the CometWorld tutorial.

 

For FrameWork users, this is simple. Simply create a servlet and extend the FWCometServiceLet.

 

public class AjaxDataServer extends FWCometServiceLet implements FWServiceLetInt {

    

Then in your FrameWork Execute method, you simply need to handle the two types of connections

 

    public void Execute(FWSession fwSess) throws FWException {

 

    //fwSess.getSessionReq().toConsole();

   

         try {

         String stAction = fwSess.getRequest().getParameter("sysAction").toLowerCase().trim();

         if ("init".equalsIgnoreCase(stAction)){

               fwSess.getSessionResp().setValue("City", "Servlet Initialized");

         }else if ("openchannel".equalsIgnoreCase(stAction)){

               fwSess.getSessionResp().AddNode("State", "open chat");

         }else if ("postdata".equalsIgnoreCase(stAction)){

               ++cnt;

               fwSess.getSessionResp().setText("City", "POST:"+cnt);

               fwSess.getSessionResp().AddNode("State", "post message");

         }else{

               fwSess.getSessionResp().AddNode("State", "FAILED");

         }

        

    }catch(Exception err){

          fwSess.getSessionResp().AddNode("sysError", err.getMessage());

    }

 

    }

 

…and you are done.  Your long-lived Comet connection will establish a non-blocking, asynchronous connection to the servlet when your client side Java Script executes the following command;

 

AjaxStartListener(“FWCometServiceLetName”, sysAction);

 

The FWCometServiceLetName is simply the name of the Servlet which extends the FWCometServiceLet class on the server. The sysAction is whatever meaningful value you wish to send (see the java code above to see how sysAction is used).

 

For non-Framework Users

 

Assumptions: You are using Eclipse and GlassFish. If you need help setting up this environment, see Workstation Setup. 

 

First let me say, I would not implement Comet in a pure servlet, see the Mini-Servlet Framework for more details – that said, let’s use a pure servlet example J just to keep things simple.

 

 

Setup:

 

1)     Create a new ‘Dynamic Web Project’ in Eclipse called ‘NativeComet

2)     Add a package named com.test

3)     Add a servlet to the new package called ‘Comet’

4)     Create a simple POJO class called CometState

5)     Under Webcontent create an Inde.html page

6)     Under Webcontent create a JavaScript file called Comet.js

 

 

Let’s flesh these all out, then discuss them in some detail.

 

Index.html

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">

<html>

<head>

<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">

<title>Comet basic</title>

<!--  http://localhost:8080/NativeComet/Index.html -->

<script type='text/javascript' src='Comet.js'></script>

</head>

<body>

<form name="index" id="index">

<table>

       <tr>

              <td>Field One

              </td>

              <td>

                     <Input name="fieldone" id="fieldone" value="Send This to all"/>

              </td>

       </tr>

 

       <tr>

              <td>click on input<br>to send notify<br>w/field 1 data.

              </td>

              <td><Input name="fieldtwo" id="fieldtwo" onmousedown="NotifyListeners('Comet','NotifyListeners');" value="Click Me"/>

              </td>

       </tr>

 

</table>

<script type="text/javascript">

try{

       AjaxStartListener("Comet","StartListener");

  

}catch(err){

   alert("[Start.html].script: Failed to load FRAMEWORK:"+err);

}

</script>

</form>

</body>

</html>

 

Comet.js

function getRequestObj(){

       var req = null;

 

       if (window.XMLHttpRequest)

       {

              // browser has native support for XMLHttpRequest object

              req = new XMLHttpRequest();

       }

       else if (window.ActiveXObject)

       {

              // try XMLHTTP ActiveX (Internet Explorer) version

              req = new ActiveXObject("Microsoft.XMLHTTP");

              if (! req){

                     req=new ActiveXObject("Msxml2.XMLHTTP");

                     }

       }

       return req;

}

function AjaxStartListener(servletName, Action){

       try{

              servletName   = "/NativeComet/"+servletName;

              var sysAction = Action;

              var sysAccess = "StartListener";

              var method    = 'POST';

              var url       = servletName.replace(/^http:\/\/[^\/]+\//i, "http://"+window.location.hostname+":"+window.location.port+"/");

              var req       = getRequestObj();

              if(req){  

                     req.onreadystatechange=function(){

                           ListenerReturn(Action, req);

                     }

                     toSend = "sysAction="+sysAction+"&sysAccess="+sysAccess+"&"+"FieldOne="+document.getElementById("fieldone").value;

                     req.open(method, url, true);

                     req.setRequestHeader("Content-length", toSend.length);

                     req.setRequestHeader("content-type","application/x-www-form-urlencoded");         

                     req.send(toSend);

              }

       }catch(err){

              alert("[Comet].[AjaxStartListener] ERROR 0100:"+err);

       }

}

function NotifyListeners(servletName, Action){

       try{

              servletName   = "/NativeComet/"+servletName;

              var sysAction = Action;

              var sysAccess = "NotifyListeners";

              var method    = 'POST';

              var url       = servletName.replace(/^http:\/\/[^\/]+\//i, "http://"+window.location.hostname+":"+window.location.port+"/");

              var req       = getRequestObj();

              if(req){  

                     req.onreadystatechange=function(){

                           NotifyReturn(Action, req);

                     }

                     toSend = "sysAction="+sysAction+"&sysAccess="+sysAccess+"&"+"FieldOne="+document.getElementById("fieldone").value;

                     req.open(method, url, true);

                     req.setRequestHeader("Content-length", toSend.length);

                     req.setRequestHeader("content-type","application/x-www-form-urlencoded");         

                     req.send(toSend);

              }

       }catch(err){

              alert("[Comet].[NotifyListeners] ERROR 0100:"+err);

       }

}

function ListenerReturn(Action, req){

       if (req.readyState == 4 && (req.status == 200)) {

              ProcessXMLResp(req.responseXML);

       } else if (req.readyState == 4 && (req.status != 200)) {

              alert("[Comet].[ListenerReturn]: ERROR 0300: Your request may have timed out. Please try again."+req.status);

       }

      

}

function NotifyReturn(Action, req){

       if (req.readyState == 4 && (req.status == 200)) {

              //just eat it!

       } else if (req.readyState == 4 && (req.status != 200)) {

              alert("[Comet].[ListenerReturn]: ERROR 0300: Your request may have timed out. Please try again."+req.status);

       }

      

}

function ProcessXMLResp(inXML){

       document.forms['index'].elements["fieldone"].value = inXML.getElementsByTagName("FieldOne")[0].childNodes[0].nodeValue;

       AjaxStartListener("Comet","StartListener")

      

}

 

Comet.java

package com.test;

 

import java.io.IOException;

 

import javax.servlet.ServletConfig;

import javax.servlet.ServletException;

import javax.servlet.http.HttpServlet;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import com.sun.enterprise.web.connector.grizzly.comet.*;

/**

 * Servlet implementation class Comet

 */

public class Comet extends HttpServlet {

                private static final long serialVersionUID = 1L;

    private String contextPath;

    /**

     * @see HttpServlet#HttpServlet()

     */

    public Comet() {

        super();

        // TODO Auto-generated constructor stub

    }

                public void init(ServletConfig config) throws ServletException {

                                super.init(config);

                                contextPath = config.getServletContext().getContextPath()+"/Comet";

                                CometEngine cometEngine = CometEngine.getEngine();

                                CometContext context = cometEngine.register(contextPath,cometEngine.AFTER_SERVLET_PROCESSING);   

                                context.setExpirationDelay(120000);

                }

                /**

                 * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)

                 */

                protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

                                doPost(request,response);

                }

 

                /**

                 * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)

                 */

                protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

                                CometState cs = new CometState(request, response);

                                CometEngine cometEngine            = CometEngine.getEngine();

                                CometContext cometContext         = cometEngine.getCometContext(contextPath);

                                System.out.println("Entered Servlet:"+cs.getReply());

                                if(request.getParameter("sysAccess").equalsIgnoreCase("StartListener")){

                                                try{

                                                                CometRequestHandler handler = new CometRequestHandler();

                                                                handler.clientIP                                                 = request.getRemoteAddr();

                                                                handler.attach(cs);

                                                                cometContext.addCometHandler(handler);

                                                }catch(Exception err){

                                                                err.printStackTrace();

                                                }

                                }else if(request.getParameter("sysAccess").equalsIgnoreCase("NotifyListeners")){

                                                try{

                                                                cometContext.notify(cs);

                                                }catch(Exception err){

                                                                err.printStackTrace();

 

                                                }

 

                                }

                }

                private class CometRequestHandler implements CometHandler<CometState> {

                                private CometState cometState;

                                public String clientIP;

                                @Override

                                public void attach(CometState arg0) {

                                                System.out.println("\n**Attached CometState.");

                                                cometState = arg0;

                                               

                                }

 

                                @Override

                                public void onEvent(CometEvent event) throws IOException {

                                                try {

                                                    if (event.getType() == CometEvent.NOTIFY) {

                                                                System.out.println("\n**Notify Client:\n"+((CometState)event.attachment()).getReply());

                                                                cometState.getResponse().setContentType("text/xml");

                                                                cometState.getResponse().setHeader("Cache-Control", "no-cache");

                                                                cometState.getResponse().getWriter().write(((CometState)event.attachment()).getReply());

                                                                cometState.getResponse().getWriter().flush();

                                                                                event.getCometContext().resumeCometHandler(this);

                                                                }

                                                } catch (Throwable t) {

                                                                t.printStackTrace();

                                                }

                                               

                                }

 

                                @Override

                                public void onInitialize(CometEvent event) throws IOException {

                                                // TODO Auto-generated method stub

                                                System.out.println("\n**Initialize CometState.");

                                }

 

                                @Override

                                public void onInterrupt(CometEvent event) throws IOException {

                                                try{

                                                                System.out.println("\n**Interrupt CometState.");

                                                                cometState.getResponse().setContentType("text/xml");

                                                                cometState.getResponse().setHeader("Cache-Control", "no-cache");

                                                                cometState.getResponse().getWriter().write("<INTERRUPTED>FAILED</INTERRUPTED>");

                                                                cometState.getResponse().getWriter().flush();

                                                                event.getCometContext().resumeCometHandler(this);

                                                }catch(Exception err){

                                                                err.printStackTrace();

                                                }

 

                                               

                                }

 

                                @Override

                                public void onTerminate(CometEvent event) throws IOException {

                                                onInterrupt(event);

                                               

                                }

 

                }

}

 

CometState.java

package com.test;

 

import java.util.Enumeration;

 

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

 

public class CometState {

                HttpServletRequest request;

                HttpServletResponse response;

                String reply;

                public CometState(HttpServletRequest inRequest, HttpServletResponse inResponse){

                                request = inRequest;

                                response = inResponse;

                                reply = BuildReply();

                }

 

                public HttpServletRequest getRequest() {

                                return request;

                }

 

                public HttpServletResponse getResponse() {

                                return response;

                }

                public String getReply(){

                                return reply;

                }

                private String BuildReply(){

                                StringBuffer sbReply = new StringBuffer();

                                try{

                                                sbReply.append("<?xml version=\"1.0\" encoding=\"ISO-8859-1\" ?>\n<Start>");

                                                Enumeration e = getRequest().getParameterNames();

                                                while(e.hasMoreElements()){

                                                                String key = (String)e.nextElement();

                                                                String[] value = getRequest().getParameterValues(key);

                                                                sbReply.append("<"+key+" value='"+value[0]+"'>"+value[0]+"</"+key+">");

                                                }

                                                sbReply.append("</Start>");

                                }catch(Exception err){

                                                err.printStackTrace();

                                }

                                return sbReply.toString();

                }

               

}

 

 

The NativeComet Application

 

The NativeComet application is a good exercise for both FrameWork and non-FrameWork users. It is a painfully simple and ugly application, but I think it provides a better Comet-framework to build upon than the “chat” examples.

 

The Index.html is pretty straight forward (Look mom, no iFrames!). It imports the comet.js script, and has two input fields. Input FieldOne is the value that will be broadcast to users. Input FieldTwo is essentially a button, when clicked it will submit the value of FieldOne to be sent to all other clients. If you haven’t guessed, this application is best tested with multiple client instances running J.

 

The comet.js JavaScript file is where the rubber starts to meet the road. This contains the Ajax script for getting an HTTP object, and establishing the connections.  Note that in this example, the notification of a change will be instigated by the client, this is not the only way – some other server side process could also fire notification – but I will leave that to you to play with.

 

Java Script Functions:

 

 

function getRequestObj()

            Returns the HTTP Object for the Ajax Connection

 

function AjaxStartListener(servletName, Action)

            Called to initiate the listener on the Server

 

function NotifyListeners(servletName, Action)

            Called to force client updates for all registered listeners

 

function ListenerReturn(Action, req)

            Called when the client has received an update

 

function NotifyReturn(Action, req)

            Called when the notification has been issued. Nothing is done here.

 

function ProcessXMLResp(inXML)

            Process the returned XML. In this case, simply put the new FieldOne node value in the input box

 

Comet Java Code

 

The Comet java class is simply a servlet with the init(), doGet(), and doPost() overridden and an inner class named CometRequestHandler. The CometRequestHandler implements CometHandler, this is the key to the entire process – more on that in a minute.

 

public void init(ServletConfig config) throws ServletException

 

In the init method contains a number of key setup activities, so let’s take them one line at a time;

 

contextPath = config.getServletContext().getContextPath()+"/Comet";

 

The ContextPath refers to the group you are subscribing too.  This is important if you are planning on having multiple conversations.  Think of it as the phone number of the party-line you are joining, or setting up. It reality it is the key to the Hashmap which is the CometContext.

 

CometEngine cometEngine = CometEngine.getEngine();

 

Get an instance of the CometEngine. The Comet Engine is a Singleton, so it is not initialized until it is first invoked.

 

CometContext context = cometEngine.register(contextPath,cometEngine.AFTER_SERVLET_PROCESSING);

 

Create a CometContext

 

context.setExpirationDelay(120000);

 

Set the delay 1000 x Number of seconds (the above is 120 seconds). The default value is 30 seconds.  After the specified time, the connection will expire, and an informational Exception will be thrown.

 

 

doPost         

 

The doPost method does the heavy lifting, the doGet just forwards to post.  Here again we need to get an instance of the CometEngine, and the CometContext. We also see the CometState class for the first time (more on that later).

 

CometState cs                               = new CometState(request, response);

 

CometEngine cometEngine     = CometEngine.getEngine();

 

CometContext cometContext = cometEngine.getCometContext(contextPath);

 

 

                        if(request.getParameter("sysAccess").equalsIgnoreCase("StartListener")){

                                    try{

                                                CometRequestHandler handler = new CometRequestHandler();

                                                handler.clientIP = request.getRemoteAddr();

                                                handler.attach(cs);

                                                cometContext.addCometHandler(handler);

                                    }catch(Exception err){

                                                err.printStackTrace();

                                    }

                        }else if(request.getParameter("sysAccess").equalsIgnoreCase("NotifyListeners")){

                                    try{

                                                cometContext.notify(cs);

                                    }catch(Exception err){

                                                err.printStackTrace();

 

                                    }

 

                        }         

           

Add Listener

 

The rest of the code controls the activites which our servlet can perform, namely “Start Listening”, and “Notify All Listeners of a change”.  Here we use the “sysAccess” value that we passed from the JavaScript, to control which of the two activities to perform.  When sysAccess=”StartListener”, we are telling to server to register this client instance with the CometEngine, and to notify it when any of the other participants issues a “NotifyListeners” command.

 

When a “StartListening” request is received, the servlet creates a new CometRequestHandler (defined as an in-line class), it then attaches an instance of the CometState class (more on this class later) to the new Hander, and adds the handler to the CometContext object. The client connection is suspended, and is now awaiting notification of a change.

 

            Notify

 

When the client issues a “NotifyListeners” request, it simply calls the .notify method of the CometContext. The CometContext, then calls the CometRequestHandler.notify method for every registered listener. Notice that the .notify method is taking an instance of the CometState (cs) for the requestor. This will be the source for the message that will be broadcast to all listeners.

 

CometRequestHandler

 

 

It is the job of the CometContext to marshal the incoming request to all listeners via .notify.  For every listener there is an instance of the CometRequestHandler handler class.  Each instance of the CometRequestHandler class has a copy of its original request, and response object stored in the CometState class. So when the CometRequestHandler receives a notification (i.e .notify is invoked) the CometState.response object is written too, and the thread is resumed. This causes the update to occur on the client.

 

.

.                 cometState.getResponse().getWriter().write(((CometState)event.attachment()).getReply());

.

.

 

In the above line of code, taken from the .notify method, we are writing to an instance of the listener (CometRequestHander) in the CometContext hash - by accessing the instance variable cometState, which was set in the .attach method. What we are writing, is coming from the request event which triggered the .notify.  The Event.Attachment is assigned using the cometContext.notify(cs) (where cs = CometState object created in the doPost). And that completes the circle. If you are confused, simply walk the code a few times, it will all become clear(er).

 

Running the Sample

 

            Start 3 instances of your browser, and run the following URL;

http://localhost:8080/NativeComet/Index.html

 

 

Change one of the “Field One” text fields, and then click on Field Two.

 

 

If all is well, you should see the updates on the page.

Conclusion

 

Comet is a very functional framework for doing server side push, however, I have not done any detailed analysis of the burden imposed on the server.  I believe the need for something like Comet is real, though limited,  but I would caution against over using it. Make sure you really have a need and use it with caution.

 

The source for Comet is available, and would recommend taking a look.

 

 

FrameWork users, please also see the CometWorld tutorial.

 

 

 

Copyright 2009. All rights reserved by

S. Chappell