Jim Driscoll's Blog

Notes on Technology and the Web

Archive for the ‘comet’ Category

Comet based TicTacToe in Atmosphere

leave a comment »

About a year ago, I gave a talk at JavaOne (and blogged about) writing a Comet powered TicTacToe (naughts and crosses to you Anglophones) game using the Grizzly Comet APIs.


In preparation for writing a Comet app with JSF 2, I thought I’d revisit that application and update it to use the newest Atmosphere APIs.


Atmosphere is a multiplatform Comet framework that allows you to write once, run anywhere on a number of Java based web containers. Sure, you could do the same thing using Servlet 3.0, but working on older servers like Tomcat 6 was something I wanted to support, so I chose that instead.


Since this program doesn’t really do anything that I haven’t already covered in previous blogs, except for the atmosphere calls, let me go over them quickly, and then just attach the program for you to look at.


Also – a quick note: This program works fine on Tomcat 6, but there seems to be a bug when running it on the latest build of Glassfish v3. I haven’t tested it on any other platforms.


The entirety of the Atmosphere API calls are held in the TTTHandler class, just as I’d previously put the Grizzly calls there. This class is responsible for doing two things: setting up a response when a GET request is received, and setting up the response when a POST comes in as well. The GET call is made by a hidden iframe, just as in my previous Comet examples from a year ago. That GET remains unsatisfied until a POST comes from an Ajax request triggered by a move. Then, the responses are sent to all GET requests. Let’s look at the code:

  34        public AtmosphereEvent onEvent(
  35                AtmosphereEvent<HttpServletRequest, HttpServletResponse> event) throws IOException {
  36    
  37            HttpServletRequest req = event.getRequest();
  38            HttpServletResponse res = event.getResponse();
  39    
  40            res.setContentType("text/html");
  41            res.addHeader("Cache-Control", "private");
  42            res.addHeader("Pragma", "no-cache");
  43            if (req.getMethod().equalsIgnoreCase("GET")) {
  44                // Junk for IE and Safari to chew on
  45                res.getWriter().write("<!-- Comet is a programming technique that enables web " +
  46                        "servers to send data to the client without having any need " +
  47                        "for the client to request it. -->\n");
  48                res.getWriter().flush();
  49                event.suspend();
  50            }


This first code snippet is the onEvent method of the handler. In this first part, we receive the event method call on any request. After a bit of setup, we check if it's a GET request. When it is, we send a bit of data back, and then call suspend on line 49. That pauses the response before it closes. Now, let's see what happens on a POST:


  51            if (req.getMethod().equalsIgnoreCase("POST")) {
  52    
  53                // There are better ways to do this, but it's the simplest way to
  54                // ensure that there is data consistency
  55                synchronized (game) {
  56                    int cell = -1;
  57                    String cellStr = req.getParameter("cell");
  58                    PrintWriter writer = res.getWriter();
  59                    writer.println("cell is '" + cellStr + "'");
  60                    if (cellStr == null) {
  61                        writer.println("error - cell not set");
  62                        return event;
  63                    }
  64                    try {
  65                        cell = Integer.parseInt(cellStr);
  66                    } catch (NumberFormatException nfe) {
  67                        writer.println("error - cellStr not an int: " + cellStr);
  68                        return event;
  69                    }
  70                    if (!game.turn(cell)) {
  71                        writer.println("warning - invalid move");
  72                    }
  73                    writer.println(game.getJSON());



When we receive a POST, we do the basic game logic, primarily using a different class that we've used for that. We return the move to the POST request - though that's primarily for debugging.


  75                    Broadcaster bc = event.getBroadcaster();
  76    
  77                    String response = game.getJSON();
  78    
  79                    // broadcast the updated game state
  80                    bc.broadcast(response);
  81    
  82                    writer.flush();



Next, we get a Broadcaster instance, and use it to send the game information to all the waiting GET requests that have set suspend, on line 49.


  98        public AtmosphereEvent onMessage(
  99                AtmosphereEvent<HttpServletRequest, HttpServletResponse> event) throws IOException {
 100    
 101    
 102            // Client closed the connection.
 103            if (event.isCancelled()) {
 104                return event;
 105            }
 106    
 107            String response = (String) event.getMessage();
 108            response = "<script type='text/javascript'>parent.chImg(" + response + ")</script>\n";
 109            PrintWriter writer = event.getResponse().getWriter();
 110            writer.write(response);
 111            writer.flush();
 112    
 113            if (!event.isResumedOnTimeout()) {
 114                event.resume();
 115            }
 116    
 117            return event;
 118        }



This is the part were we're actually doing the heavy lifting of Comet. When a Broadcaster sends a broadcast, the onMessage method is called, with the message sent passed to it.


What happens after that is simple - we retrieve the message, and use it to write out a response to the client, which, if you recall, is patiently waiting to place that text into the iframe, where it will be evaluated by the browser. Lastly, we resume the connection on line 114. This closes and flushes the connection - meaning that the client will have to make a new GET request.


Hope that covers the basic differences to the Comet TicTacToe for the Atmosphere API. You can download the full code here, as a Netbeans project. Feel free to ask any questions below.

(This article originally published on my java.net blog on July 18, 2009.)

Written by Jim Driscoll

February 9, 2010 at 9:26 PM

Posted in comet, Java

Comet TicTacToe

leave a comment »

Here’s the Comet TicTacToe that I went over in my BOF on Comet on Wednesday night. It’s pretty simple (though not as simple as my first example, or even the somewhat improved version) – just 200 lines of Java code (including the game logic), 50 lines of JavaScript (embedded in an HTML page), 50 lines of HTML, and a 75 line CSS file. Simple stuff, but if you’re looking to write your own Comet app, this might help get you started.


You can find the full Netbeans project here.


Essentially, the program logic is contained in only two files: ttt1.html, and CometServlet.java. Check ‘em out. After my initial example two posts ago, these should be pretty self explanatory. If you have any questions, by all means ask in the comments below.

(This article was originally published on my java.net blog on May 8, 2008.)

Written by Jim Driscoll

February 9, 2010 at 6:27 PM

Posted in comet, Java, web

Solving the Comet timeout problem

leave a comment »

In my previous blog, I mentioned that I didn’t like the hack of reloading the iframe via the post action – it’s hacky, and it’s not hard to imagine it messing things up in a more complex program.


Turns out the answer is both easy and blindingly obvious once you think of it: the iframe onload event. And while we’re add it, we’ll add a onerror event too.


In my previous program, I had had a hidden iframe, and on every update, I would reset the source for the iframe using the location property.


We’ll still do that, but add a new function:

       <iframe name="hidden" src="CometCount" frameborder="0" height="0" width="100%"
       onload="restartPoll()" onerror="restartPoll()" ></iframe>


Note the onload and onerror events – whenever the server closes the connection, these will be called. And here’s the function that’s called:

            var retries = 0;
            function restartPoll() {
                if (retries++ > 10) {
                    alert("The connection has errored out too many times");                    
                } else {
                    hidden.location = url;                    
                }
            }


Also, I’ve added a retry limit in there – it wouldn’t do to have the client go into a fatal spin just because the server is down.


Now when the server closes the connection (from a timeout, or an error), the client will continue to function, automatically calling back into the server. Not a solution you’ll want for every situation, but useful enough, especially for our small example.


Lastly, there was a bug in my previous version under IE – sorry about that. It turns out that if you send a POST via IE, you need to have a content body, or IE gets fussy. The fix is to change the line

            xhReq.send(null);
to
            xhReq.send("null");

I’ve uploaded new versions of the files index.html and CometCount.java, so you can see the complete code in context.

(This article was originally published on my java.net blog on May 5, 2008.)

Written by Jim Driscoll

February 9, 2010 at 6:22 PM

Posted in comet, Java, web

Dead Simple Comet Example on Glassfish v3 / Grizzly

with 2 comments

I was looking at a recent blog by Shing Wai Chan and going through the Comet example, when I noticed that the example wasn’t working correctly. Although he updated his example to get around that problem, I was still a bit unsatisfied, and decided to sit down, using his basic example, and see if I could make it even simpler.

I’ve whittled it down to about 100 lines, and only 2 files, and I thought I’d go over it here. The full example (both files) are
index.html and CometCount.java. So this will be a little long, but if you’re tired of reading my rambling, just look at the files, and the code should speak for itself.

First, about the app: It’s a simple counter, which is updated every time you hit a button on the page. Pretty basic, except – every other web browser viewing that page will have the counter updated as well, through the magic of Comet.
About setting it up: make sure that the url mapping points to /CometCount, the value is hardcoded in a few places. Also, to compile you’ll need access to the Grizzly Comet APIs – you can either get them from Grizzly, or Glassfish v3 tp2. You’ll need to also add the jar in the modules directory named grizzly-optionals to your classpath in order to build, along with the standard Servlet API. You’ll also need to update the domain.xml of the v3 instance to add the property cometSupport=true, as you see below:


 

Now on to the description of the program flow. On startup, the servet is initialized, and registers itself with with the Comet Engine (make sure the servlet is installed with a url CometCount, or it won’t work). We set a timeout of 2 minutes.

 

    public void init(ServletConfig config) throws ServletException {
        super.init(config);
        ServletContext context = config.getServletContext();
        contextPath = context.getContextPath() + "/MyComet";

        CometEngine engine = CometEngine.getEngine();
        CometContext cometContext = engine.register(contextPath);
        cometContext.setExpirationDelay(120 * 1000);
    }

Then on the first load of index.html in the browser, the hidden iframe makes a call to the doGet of the servlet – this call suspends, awaiting further action. It does this by attaching the response to a handler (of type CounterHandler), and attaching the handler to the servlet’s CometContext. This also gives rise to the first bug of the program – there’s no display of initial result.

So this:
<iframe name="hidden" src="CometCount" frameborder="0" height="0" width="100%"></iframe>
calls this:
   protected void doGet(HttpServletRequest req, HttpServletResponse res)
            throws ServletException, IOException {

        CounterHandler handler = new CounterHandler();
        handler.attach(res);

        CometEngine engine = CometEngine.getEngine();
        CometContext context = engine.getCometContext(contextPath);

        context.addCometHandler(handler);
    }

Next, someone, somewhere, who’s also using the program, hits the button marked “click”. This calls the onclick method, postMe(). postMe sends an empty POST to the servlet, triggering the doPost method.

So this:
<input type="button" onclick="postMe();" value="Click">
Calls this:
            var url = "CometCount";
            function postMe() {
                function createXMLHttpRequest() {
                    try { return new XMLHttpRequest(); } catch(e) {}
                    try { return new ActiveXObject("Msxml2.XMLHTTP"); } catch (e) {}
                    try { return new ActiveXObject("Microsoft.XMLHTTP"); } catch (e) {}
                    alert("Sorry, you're not running a supported browser - XMLHttpRequest not supported");
                    return null;
                };
                var xhReq = new createXMLHttpRequest();
                xhReq.open("POST", url, false);
                xhReq.send(null);
                hidden.location = url;
            };

The doPost method increments the counter, and then sends a notify event to the servlet’s CometHandler.

    protected void doPost(HttpServletRequest req, HttpServletResponse res)
            throws ServletException, IOException {

        counter.incrementAndGet();

        CometEngine engine = CometEngine.getEngine();
        CometContext context = engine.getCometContext(contextPath);
        context.notify(null);
    }

This event now wakes up that initial GET request, and sends a bit of javascript down the line. Lastly, we call resumeCometHandler, which marks the current event as completed, removing it from the active queue.

        public void onEvent(CometEvent event) throws IOException {
            if (CometEvent.NOTIFY == event.getType()) {
                int count = counter.get();
                PrintWriter writer = response.getWriter();
                writer.write("<script type='text/javascript'>parent.updateCount('" + count + "')</script>\n");
                writer.flush();
                event.getCometContext().resumeCometHandler(this);
            }
        }

Back at the client, that javascript that got sent down gets put into the hidden iFrame, and then executed. This calls the updateCount function, which updates the counter with the new value. Then, we set the iframes’ location object, which reconnects with GET to the servlet, and we’re ready for our next request.

            function updateCount(c) {
                document.getElementById('count').innerHTML = c;
                hidden.location = url;
            }

That is, unless we time out. If we time out (remember, we set the timeout to 2 minutes, not “infinite”), the iframe goes dead, the GET polling loop is broken, and we never again update the counter on the webpage, though the user’s increasing frustrated button pushing on the client will happily update counter on the server. So, to get around this, we add a single line at the end of our postMe function, updating the hidden iframe’s location again. This is the one really hacky part of the program, and I’d love to know a better way to do it. Also, it gives rise to the second bug of our program, related to the first – if the connection dies, you need to click the button twice to see an update of the counter on the client.
So – ta da! We have a bare bones, long polling comet application in two files and about 100 lines.
Again, here’s the programs
Download file index.html
Download file CometCount.java
Jeanfrancois Arcand and I have a BOF on Wednesday night (May 7th, 2008) at JavaOne. If you’re at JavaOne and are just getting started with Comet, come on by.

(This article was originally published on my java.net blog on May 1, 2008.)

Written by Jim Driscoll

February 9, 2010 at 6:04 PM

Posted in comet, Java, web

Follow

Get every new post delivered to your Inbox.

Join 411 other followers