Jean-Francois Arcand works for Ning.com. Previously he has worked for Sun Microsystems where he created Grizzly (NIO Framework) , Atmosphere and was a significant contributor to the GlassFish Application Server. Jean-Francois is a DZone MVB and is not an employee of DZone and has posted 23 posts at DZone. You can read more from them at their website. View Full User Profile

Getting Started With Atmosphere CPR: Writing A Chat Application

03.24.2009
| 10759 views |
  • submit to reddit

Currently, writing a portable Comet application is impossible: JBossWeb has AIO, Tomcat has its a different AIO API, Jetty has its Continuation API and pre Servlet 3.0 API support, Grizzly has its Comet Framework and Grizzlet API, etc. So, framework like DWR, ICEFaces and Bindows all added native support and abstracted a layer in order to support different Comet API. Worse, if your application uses those API directly, then you are stuck with one Web Server. Not bad if you are using Grizzly Comet, but if you are using the competitor, then you cannot meet the Grizzly!

The current Servlet EG are working on a proposal to add support for Comet in the upcoming Servlet 3.0 specification, but before the planet fully supports the spec it may takes ages. And the proposal will contains a small subset of the current set of features some containers already supports like asynchronous I/O (Tomcat, Grizzly), container-managed thread pool for concurrently handling the push operations, filters for push operations. etc. Not to say that using Atmosphere, framework will not longer have to care about native implementation, but instead build on top of Atmosphere. Protocol like Bayeux will comes for free, and will run on all WebServer by under the hood using their native API.

Atmosphere is a POJO based framework using Inversion of Control (IoC)  to bring push/Comet to the masses! Finally a framework which can run on any Java based Web Server, including Tomcat,Jetty, GlassFish,Resin, Jersey,RESTlet etc..... without having to wait for Servlet 3.0 or without the needs to learn how push/Comet support has been differently implemented by all those Containers.

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.

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

 

package org.atmosphere.samples.chat;


import java.io.IOException;
import java.util.logging.Logger;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.atmosphere.cpr.AtmosphereEvent;
import org.atmosphere.cpr.AtmosphereHandler;
import org.atmosphere.cpr.Broadcaster;

/**
* Simple AtmosphereHandler that implement the logic to build a Chat application.
*
*/
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

 HttpServletRequest req = event.getRequest();
HttpServletResponse res = event.getResponse();

res.setContentType("text/html");
res.addHeader("Cache-Control", "private");
res.addHeader("Pragma", "no-cache");
if (req.getMethod().equalsIgnoreCase("GET")) {
res.getWriter().write("<!-- Comet is a programming technique that enables web " +
"servers to send data to the client without having any need " +
"for the client to request it. -->\n");
res.getWriter().flush();
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

              res.setCharacterEncoding("UTF-8");
String action = req.getParameterValues("action")[0];
String name = req.getParameterValues("name")[0];

if ("login".equals(action)) {
event.getBroadcaster().broadcast(
"System Message from "
+ <a href="https://atmosphere.dev.java.net/nonav/apidocs/org/atmosphere/cpr/AtmosphereEvent.html#getWebServerName%28%29">event.getWebServerName()</a>, name + " has joined.");
res.getWriter().write("success");
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:

     public AtmosphereEvent
onMessage(AtmosphereEvent event) throws IOException {
HttpServletRequest req = event.getRequest();
HttpServletResponse res = event.getResponse();
res.getWriter().write(event.getMessage().toString());
res.getWriter().flush();
return event;
}

Next is when the user enter some chat messages:

             } else if ("post".equals(action)) {
String message = req.getParameterValues("message")[0];
event.getBroadcaster().broadcast(BEGIN_SCRIPT_TAG + toJsonp(name, message) + END_SCRIPT_TAG);
res.getWriter().write("success");
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:

   <atmosphere-handlers>
<atmosphere-handler context-root="/chat" class-name="org.atmosphere.samples.chat.ChatAtmosphereHandler">
<property name="name" value="Chat"/>
</atmosphere-handler>
</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:

   <?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE html
PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Atmosphere Chat</title>
<link rel="stylesheet" href="stylesheets/default.css" type="text/css" />
<script type="text/javascript" src="javascripts/prototype.js"></script>
<script type="text/javascript" src="javascripts/behaviour.js"></script>
<script type="text/javascript" src="javascripts/moo.fx.js"></script>
<script type="text/javascript" src="javascripts/moo.fx.pack.js"></script>
<script type="text/javascript" src="javascripts/application.js"></script>
</head>
<body>
<div id="container">
<div id="container-inner">
<div id="header">
<h1>Atmosphere Chat</h1>
</div>
<div id="main">
<div id="display">
</div>
<div id="form">
<div id="system-message">Please input your name:</div>
<div id="login-form">
<input id="login-name" type="text" />
<br />
<input id="login-button" type="button" value="Login" />
</div>
<div id="message-form" style="display: none;">
<div>
<textarea id="message" name="message" rows="2" cols="40"></textarea>
<br />
<input id="post-button" type="button" value="Post Message" />
</div>
</div>
</div>
</div>
</div>
</div>
<iframe id="comet-frame" style="display: none;"></iframe>
</body>
</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

      login: function() {
var name = $F('login-name');
if(! name.length > 0) {
$('system-message').style.color = 'red';
$('login-name').focus();
return;
}
$('system-message').style.color = '#2d2b3d';
$('system-message').innerHTML = name + ':';

$('login-button').disabled = true;
$('login-form').style.display = 'none';
$('message-form').style.display = '';

var query =
'action=login' +
'&name=' + encodeURI($F('login-name'));
new Ajax.Request(app.url, {
postBody: query,
onSuccess: function() {
$('message').focus();
}
});
},

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

      post: function() {
var message = $F('message');
if(!message > 0) {
return;
}
$('message').disabled = true;
$('post-button').disabled = true;

var query =
'action=post' +
'&name=' + encodeURI($F('login-name')) +
'&message=' + encodeURI(message);
new Ajax.Request(app.url, {
postBody: query,
requestHeaders: ['Content-Type',
'application/x-www-form-urlencoded; charset=UTF-8'],
onComplete: function() {
$('message').disabled = false;
$('post-button').disabled = false;
$('message').focus();
$('message').value = '';
}
});
},

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

      update: function(data) {    
var p = document.createElement('p');
p.innerHTML = data.name + ':
' + data.message;

$('display').appendChild(p);

new Fx.Scroll('display').down();
}

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

  var rules = {
'#login-name': function(elem) {
Event.observe(elem, 'keydown', function(e) {
if(e.keyCode == 13) {
$('login-button').focus();
}
});
},
'#login-button': function(elem) {
elem.onclick = app.login;
},
'#message': function(elem) {
Event.observe(elem, 'keydown', function(e) {
if(e.shiftKey && e.keyCode == 13) {
$('post-button').focus();
}
});
},
'#post-button': function(elem) {
elem.onclick = app.post;
}
};
Behaviour.addLoadEvent(app.initialize);
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

Atmosphere will make easier for anybody that want to write real time based application without having to wait for Servlet 3.0 official support. Of course Atmosphere will support Servlet 3.0 async API, but Atmosphere offer more than what 3.0 propose. As an example, the Atmosphere’s Broadcaster is very useful when it is time to push/aggregate/filter data between suspended connections. Something you can't get for free with Servlet 3.0 or native Comet implementation right now.
 

Published at DZone with permission of Jean-Francois Arcand, author and DZone MVB.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)

Tags: