The Source for Java Technology Collaboration
User: Password:
Register | Login help    

Search

Online Books:
java.net on MarkMail:


Getting started with Atmosphere CPR part 1: Writing the HelloWord of Comet....a Chat application

Posted by jfarcand on March 17, 2009 at 1:19 PM PDT

Time to get started with Atmosphere CPR (Comet Portable Runtime)! In this first part, I will describe how to write a chat application and deploy in on Tomcat, Jetty and GlassFish.

Note: The Atmosphere Framework have evolved since the release of that blog. Please visit our web site for an updated sample and white paper

IMG_0att.JPG

Let do the basic first. Let's use Maven 2 and create the file structure:

 %  mvn archetype:create -DgroupId=org.atmosphere.samples
 -DartifactId=chat -DarchetypeArtifactId=maven-archetype-webapp

Which will create the following structure:

./chat
./chat/pom.xml
./chat/src
./chat/src/main
./chat/src/main/resources
./chat/src/main/webapp
./chat/src/main/webapp/index.jsp
./chat/src/main/webapp/WEB-INF
./chat/src/main/webapp/WEB-INF/web.xml

The next step is to add the required context.xml and save it under META-INF/

.
./chat
./chat/pom.xml
./chat/src
./chat/src/main
./chat/src/main/resources
./chat/src/main/webapp
./chat/src/main/webapp/index.jsp
./chat/src/main/webapp/WEB-INF
./chat/src/main/webapp/WEB-INF/lib
./chat/src/main/webapp/WEB-INF/lib/atmosphere-portable-runtime-0.1-ALPHA1.jar
./chat/src/main/webapp/WEB-INF/web.xml
./chat/src/main/webapp/META-INF
./chat/src/main/webapp/META-INF/context.xml

Finally, let's add Atmosphere CPR library to the pom.xml so it gets added to our WEB-INF/lib

         <dependency>
             <groupId>org.atmosphere</groupId>
             <artifactId>atmosphere-portable-runtime</artifactId>
             <version>0.1-ALPHA1</version>
         </dependency>

         <repositories>
              <repository>
                <id>maven2.java.net</id>
                <name>Java.net Repository for Maven 2</name>
                <url>http://download.java.net/maven/2</url>
              </repository>
         </repositories>

We are now ready to write our first AtmosphereHandler, which is the central piece of any Atmosphere CPR application. Let's just implement this interface

 38 package org.atmosphere.samples.chat;
 39 
 40 import java.io.IOException;
 41 import java.util.logging.Logger;
 42 import javax.servlet.http.HttpServletRequest;
 43 import javax.servlet.http.HttpServletResponse;
 44 import org.atmosphere.cpr.AtmosphereEvent;
 45 import org.atmosphere.cpr.AtmosphereHandler;
 46 import org.atmosphere.cpr.Broadcaster;
 47 
 48 /**
 49  * Simple AtmosphereHandler that implement the logic to build a Chat application.
 50  *
 53  */
 54 public class ChatAtmosphereHandler implements AtmosphereHandler {

As described here, implementing an AtmosphereHandler requires two methods:


    /**
     * When a client send a request to its associated {@link AtmosphereHandler}, it can decide
     * if the underlying connection can be suspended (creating a Continuation)
     * or handle the connection synchronously.
     *
     * It is recommended to only suspend request for which HTTP method is a GET
     * and use the POST method to send data to the server, without marking the
     * connection as asynchronous.
     *
     * @param event an {@link AtmosphereEvent}
     * @return the modified {@link AtmosphereEvent}
     */
    public AtmosphereEvent onEvent(AtmosphereEvent event) throws IOException;


    /**
     * This method is invoked when the {@link Broadcaster} execute a broadcast
     * operations. When this method is invoked its associated {@link Broadcaster}, any
     * suspended connection will be allowed to write the data back to its
     * associated clients.
     *
     * @param event an {@link AtmosphereEvent}
     * @return the modified {@link AtmosphereEvent}
     */
    public AtmosphereEvent onMessage(AtmosphereEvent event) throws IOException;

The onEvent method will be invoked every time a request gets mapped to it associated AtmosphereHandler. There is two way to map request to an AtmosphereHandler. By default, the name of the AtmosphereHandler will be used, e.g. assuming we name our web application chat.war, a request to http://localhost:8080/chat/ChatAtmosphereHandler will invoke the AtmophereHandler.onEvent. For the chat, let's assume we will suspend the response when the browser is sending us GET request

 77         HttpServletRequest req = event.getRequest();
 78         HttpServletResponse res = event.getResponse();
 79 
 80         res.setContentType("text/html");
 81         res.addHeader("Cache-Control", "private");
 82         res.addHeader("Pragma", "no-cache");
 83         if (req.getMethod().equalsIgnoreCase("GET")) {
 84             res.getWriter().write("<!-- Comet is a programming technique that enables web " +
 85                     "servers to send data to the client without having any need " +
 86                     "for the client to request it. -->\n");
 87             res.getWriter().flush();
 88             event.suspend();

The central piece is the AtmosphereEvent, from which we can retrieve the request and response object. Next we do some setup and then once we are ready we just need to invoke the AtmosphereEvent.suspend(), which will automatically tell Atmosphere CPR to not commit the response. Not committing the response means we can re-use it later for writing. In the current exercise, we will use the suspended response when someone enter join or enter sentence inside the chat room. Now let's assume when a user logs in or enter sentences, the browser set a POST (posting some data). So when a user logs in

 90             res.setCharacterEncoding("UTF-8");
 91             String action = req.getParameterValues("action")[0];
 92             String name = req.getParameterValues("name")[0];
 93 
 94             if ("login".equals(action)) {
 95                 event.getBroadcaster().broadcast(
 96                         "System Message from "
 97                         + event.getWebServerName(), name + " has joined.");
 98                 res.getWriter().write("success");
 99                 res.getWriter().flush();

The important piece here is line 94. A Broadcaster's role is to publish data to the suspended responses. As soon as you broadcast data, all suspended responses will be given a chance to write the content of the broadcast. Above we just broadcast the name and also which WebServer we are running on (for demo purpose). Calling Broadcaster.broadcast() will in turn invoke your AtmosphereHandler.onMessage will all suspended response. Here let's assume we just reflect (write) what we receive:

124     public AtmosphereEvent
125             onMessage(AtmosphereEvent event) throws IOException {
126         HttpServletRequest req = event.getRequest();
127         HttpServletResponse res = event.getResponse();
128         res.getWriter().write(event.getMessage().toString());
129         res.getWriter().flush();
130         return event;
131     }

Next is when the user enter some chat messages:

100             } else if ("post".equals(action)) {
101                 String message = req.getParameterValues("message")[0];
102                 event.getBroadcaster().broadcast(BEGIN_SCRIPT_TAG + toJsonp(name, message) + END_SCRIPT_TAG);
103                 res.getWriter().write("success");
104                 res.getWriter().flush();

Here we encode the message using the JSON format so it make simple for the client's javascript to update the page. That's it for the AtmosphereHandler. You can see the complete source code here.

Now let's assume we want to have a more fine grain way to map our AtmosphereHandler to the request. To achieve that, create a file called atmosphere.xml under src/main/webapp/META-INF/ and define the mapping you want:

  1 <atmosphere-handlers>
  2     <atmosphere-handler context-root="/chat" class-name="org.atmosphere.samples.chat.ChatAtmosphereHandler">
  3         <property name="name" value="Chat"/>
  4     </atmosphere-handler>
  5 </atmosphere-handlers>

With this file, all requests to /chat will be mapped to our ChatAtmosphereHandler.

Now let's explore the client side. First, let's write a very simple index.html file:

  1 <?xml version="1.0" encoding="UTF-8" ?>
  2 <!DOCTYPE html
  3 PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
  4 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
  5 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
  6     <head>
  7         <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  8         <title>Atmosphere Chat</title>
  9         <link rel="stylesheet" href="stylesheets/default.css" type="text/css" />
 10         <script type="text/javascript" src="javascripts/prototype.js"></script>
 11         <script type="text/javascript" src="javascripts/behaviour.js"></script>
 12         <script type="text/javascript" src="javascripts/moo.fx.js"></script>
 13         <script type="text/javascript" src="javascripts/moo.fx.pack.js"></script>
 14         <script type="text/javascript" src="javascripts/application.js"></script>
 15     </head>
 16     <body>
 17         <div id="container">
 18             <div id="container-inner">
 19                 <div id="header">
 20                     <h1>Atmosphere Chat</h1>
 21                 </div>
 22                 <div id="main">
 23                     <div id="display">
 24                     </div>
 25                     <div id="form">
 26                         <div id="system-message">Please input your name:</div>
 27                         <div id="login-form">
 28                             <input id="login-name" type="text" />
 29                             <br />
 30                             <input id="login-button" type="button" value="Login" />
 31                         </div>
 32                         <div id="message-form" style="display: none;">
 33                             <div>
 34                                 <textarea id="message" name="message" rows="2" cols="40"></textarea>
 35                                 <br />
 36                                 <input id="post-button" type="button" value="Post Message" />
 37                             </div>
 38                         </div>
 39                     </div>
 40                 </div>
 41             </div>
 42         </div>
 43         <iframe id="comet-frame" style="display: none;"></iframe>
 44     </body>
 45 </html>

Simple form that will send back to the server the login's name and the chat message entered. To update on the fly the interface as soon as our ChatAtmosphereHandler.onMessage write/send us data, let's use prototype and behavior javascript. I'm assuming you are either familiar with those framework or have basic understanding how they work. This will be defined under application.js. As soon as the user enter its login name, let's do

 12     login: function() {
 13         var name = $F('login-name');
 14         if(! name.length > 0) {
 15             $('system-message').style.color = 'red';
 16             $('login-name').focus();
 17             return;
 18         }
 19         $('system-message').style.color = '#2d2b3d';
 20         $('system-message').innerHTML = name + ':';
 21 
 22         $('login-button').disabled = true;
 23         $('login-form').style.display = 'none';
 24         $('message-form').style.display = '';
 25 
 26         var query =
 27         'action=login' +
 28         '&name=' + encodeURI($F('login-name'));
 29         new Ajax.Request(app.url, {
 30             postBody: query,
 31             onSuccess: function() {
 32                 $('message').focus();
 33             }
 34         });
 35     },

When the user write new chat message, let's push

 36     post: function() {
 37         var message = $F('message');
 38         if(!message > 0) {
 39             return;
 40         }
 41         $('message').disabled = true;
 42         $('post-button').disabled = true;
 43 
 44         var query =
 45         'action=post' +
 46         '&name=' + encodeURI($F('login-name')) +
 47         '&message=' + encodeURI(message);
 48         new Ajax.Request(app.url, {
 49             postBody: query,
 50             requestHeaders: ['Content-Type',
 51             'application/x-www-form-urlencoded; charset=UTF-8'],
 52             onComplete: function() {
 53                 $('message').disabled = false;
 54                 $('post-button').disabled = false;
 55                 $('message').focus();
 56                 $('message').value = '';
 57             }
 58         });
 59     },

Now when we get response, we just update the page using

 60     update: function(data) {    
 61         var p = document.createElement('p');
 62         p.innerHTML = data.name + ':
' + data.message; 63 64 $('display').appendChild(p); 65 66 new Fx.Scroll('display').down(); 67 }

The way the index.html and application.js interact is simply defined by:

 69 var rules = {
 70     '#login-name': function(elem) {
 71         Event.observe(elem, 'keydown', function(e) {
 72             if(e.keyCode == 13) {
 73                 $('login-button').focus();
 74             }
 75         });
 76     },
 77     '#login-button': function(elem) {
 78         elem.onclick = app.login;
 79     },
 80     '#message': function(elem) {
 81         Event.observe(elem, 'keydown', function(e) {
 82             if(e.shiftKey && e.keyCode == 13) {
 83                 $('post-button').focus();
 84             }
 85         });
 86     },
 87     '#post-button': function(elem) {
 88         elem.onclick = app.post;
 89     }
 90 };
 91 Behaviour.addLoadEvent(app.initialize);
 92 Behaviour.register(rules);

See the complete source code here. So far so good, now we are ready to deploy our application into our favorite WebServer.

Here are simple pictures from different WebServer:

Glassfish v3

GFv3.jpg

Jetty

jetty.jpg

Tomcat

tomcat.jpg

Grizzly

Grizzly.jpg

Wow that was easy! Download the war or src to get started. Follow us on Twitter for daily update about the project status and ask your questions using users@atmosphere.dev.java.net

technorati:

Related Topics >> Glassfish      
Comments
Comments are listed in date ascending order (oldest first)

Although the chat application works fine (missed a message or 2 in IE, maybe due to prototype/behavior issues) on tomcat 6.0.x with NIO connector the same is not true for APR connector. Is this a known limitation, because COMET in tomcat shld work for both the NIO and APR connectors? (at least according to tomcat 6.0.x documentation at http://tomcat.apache.org/tomcat-6.0-doc/aio.html)

Thank you for your interesting article, and to the team project! I successed some experiments with a simple Groovy script (using HTTPBuilder) that acted like a browser and that communicated with the Chat server. So I was able to send messages and receive messages coming from browsers. Thanks.

After deploying the war to the latest version of Tomcat when I try to login it returns with the following 500 error. javax.servlet.ServletException: Tomcat failed to detect this is a Comet application Please add the following content under your META-INF/context.xml of your war file. This is already there. Any ideas?

After deploying WAR into Tomcat 6.0.18, I added file atmosphere.xml to META-INF folder. When I open http://localhost:8080/chat in browser, I got HTTP 404 error - "requested resource not available." Tomcat Manager shows context as /atmosphere-chat-0.1-ALPHA, which loads the GlassFish login page (index.html). Please advise how to run under Tomcat, or is WAR only meant for GlassFish and runs OK after modifying domain.xml per your blog? Thanks for very useful (scalable) AjaxPush/Comet implementation.

jedeegan - rename the war to just atmosphere-chat.war without the version information. Then put in your webapps directory. You will also need to change the xml Connector node's protocol attribute in server.xml from HTTP/1.1 to "org.apache.coyote.http11.Http11NioProtocol".

Hi, I've deployed this using alpha2 of atmosphere and it seems that Broadcaster.broadcast() only broadcasts to one of the opened browser windows. Is this a client issue? Any ideas ? Thanks!