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

Writing Portable HTML5 WebSocket Applications Using the Atmosphere Framework

05.15.2010
| 8580 views |
  • submit to reddit

The Atmosphere Framework now supports the HTML5 WebSocket specification. If you don't know what  WebSocket is, I recommend you take a look at this introduction.  As with Ajax Push/Comet, all major webservers are starting supporting the specification. And guess what, all webservers are doing it their own way. Sound familiar?

Back in 2006, Jetty first introduced it's Continuation API, closely followed by my Grizzly Comet Framework, and eventually we saw Tomcat AIO, Resin Comet and JBossWeb AIO native implementation. It took almost four years before Comet got standardized with the little-over-complicated Servlet 3.0 Async API.  The exact same pattern is now happening, e.g. Jetty, Grizzly/GlassFish and Resin now support WebSocket, and again there is no portability across WebServer. Hence if you plan to build a WebSocket application, make sure you pick the right WebServer as your application will not be portable, and you may have to wait another four years before the Servlet Specification catch up :-).

As with Ajax Push/Comet, the Atmosphere Framework can save your life by exposing APIs that are portable across WebServer. Originally, the Atmosphere Framework's  goal was to bring Ajax Push/Comet portability across WebServer. You can currently write a Comet application and deploy it inside your favorite WebServer and be guarantee the application will works. What's new now is the Atmosphere Framework is also supporting WebSocket  portability across WebServer. Yaaa!! Currently we support Jetty, Grizzly and soon Resin and will add support as soon as WebSocket implementation pops up. There is also Netty that I would eventually like to support. So don't get LOCKED into a WebServer's native API, start on the right feet by using the Atmosphere Framework! Event more interesting: you can currently deploy your Atmosphere's WebSocket application into a WebServer that doesn't support WebSocket and your application will works as-it-is, the only part that will need to be modified will be the client side. But that's temporary as our upcoming Atmosphere JQuery based client library will  support detection of WebSocket vs Comet.

The Server Side

First, let's recall some Atmosphere concepts as they are the same independently of the technology used, e.g WebSocket or Comet:

  • Suspend: The action of suspending consist of telling the underlying Web Server to not commit the response, e.g. to not send back to the browser the final bytes the browser is waiting for before considering the request completed.
  • Resume: The action of resuming consist of completing the response, e.g. committing the response by sending back to the browser the final bytes the browser is waiting for before considering the request completed.
  • Broadcast: The action of broadcasting consists of producing an event and distributing that event to one or many suspended response. The suspended response can then decide to discard the event or send it back to the browser.
  • Long Polling: Long polling consists of resuming a suspended response as soon as event is getting broadcasted.
  • Http Streaming: Http Streaming, also called forever frame, consists of resuming a suspended response after multiples events are getting broadcasted.
  • Native Asynchronous API: A native asynchronous API (Comet or WebSocket) means an API that is proprietary, e.g. if you write an application using that API, the application will not be portable across Web Server.

In Atmosphere, one of the main concept is called AtmosphereHandler. An AtmosphereHandler can be used to suspend, resume and broadcast and allow the use of the usual HttpServletRequest and HttpServletResponse set of APIs. Note that Atmosphere 0.7 will also support non Servlet API like the one available with Netty, Play!, etc.

public interface AtmosphereHandler<F,G>
{
  public void onRequest(AtmosphereResource<F,G> event) throws IOException;

  public void onStateChange(AtmosphereResourceEvent<F,G> event) throws IOException;
}

The onRequest is invoked every time a request uri match the servlet-mapping of the AtmosphereServlet. So in case of a WebSocket application, you usually use the /* value which means all requests will be sent to the AtmosphereServlet, which in turn will call the AtmosphereHandler.onRequest(). In Atmosphere, an AtmosphereResource encapsulates all the  available operations. I strongly recommend you take a quick look at the Atmosphere White Paper for more information about the framework main classes.

Now for WebSocket, I've added an implementation of that interface called WebSocketAtmosphereHandler in order to introduce some convention over configuration. Note that you can write your own in case that class isn't doing what you want.   By default, the WebSocketAtmosphereHandler is simple doing:

public void upgrade(AtmosphereResource
<HttpServletRequest, HttpServletResponse> r) throws IOException
{
r.suspend();
}

A WebSocket application should normally only have to override the upgrade() method and decide what to do with the request. By default, this handler will assume that the first request is for a static resource like index.html and will forward the request to the appropriate WebServer component. Next, if the browser supports WebSocket, it will send another request asking the server to upgrade to the Websocket protocol. With Atmosphere all you need to do is to invoke the AtmosphereResource.suspend(). It's the same API you usually use with Comet to tell the WebServer to "suspend" the response and not commit it until you resume it. With WebSocket, an upgrade does the exact same things but more "formally".

Once a response has been suspended, independently of Comet or WebSocket, you are ready to broadcast events to that open connection. A broadcast is simply a notification mechanism that push an events back to the browser using the suspended connection. One of the fundamental concept of the Atmosphere Framework is called a Broadcaster. A Broadcaster can be used to broadcast (or push back) asynchronous events to the set or subset of suspended responses. The concept is pretty close to a JMS's queue or topic. A Broadcaster can contain zero or more BroadcastFilter, which can be used to transform the events before they get written back to the browser. For example, any malicious characters can be filtered before they get written back by adding the XSSHtmlFilter as described below

public void upgrade(AtmosphereResource
         <HttpServletRequest, HttpServletResponse>  r) throws IOException
{
   // Upgrade
   super(r);
   // Escape all malicious chars
   r.getBroadcaster().getBroadcasterConfig()
              .addFilter(new XSSHtmlFilter());
}

Internally, a Broadcaster uses an ExecutorService to execute the above chain of invocation. That means a call to Broadcaster.broadcast(..) will not block unless you use the returned Future API, and will use a set of dedicated threads to execute the broadcast. So you push events asynchronously.  By default, everytime a Browser issue a WebSocket.onmessage, which means sending a message, that message will be broadcasted to all upgraded connections. Said differently, everything send by the Browser will be reflected back using the suspended/upgraded connections.

One final word on Broadcaster: by default, a Broadcaster will broadcast using all AtmosphereResource on which the response has been suspended, or for WebSocket, upgraded, e.g. AtmosphereResource.suspend() has been invoked. This behavior is configurable and you can configure it by invoking the Broadcaster.setScope():

  • REQUEST: broadcast events only to the AtmosphereResource associated with the current request.
  • APPLICATION: broadcast events to all AtmosphereResource created for the current web application
  • VM: broadcast events to all AtmosphereResource created inside the current virtual machine.

The default is APPLICATION. Hence, inside the upgrade method you can define your own Broadcaster  and its scope depending on what your application is doing. As an example, you may want to have a single queue per suspended/upgraded connection:

public void upgrade(AtmosphereResource
         <HttpServletRequest, HttpServletResponse>  r) throws IOException
{
   // Upgrade
   super(r);
   // Escape all malicious chars
   r.setBroascaster(BroadcasterFactory.getDefault()
       .get(DefaultBroadcaster.class,"MyEventQueue");
}

In the case above, events will be broadcasted to a single Browser connection. You can always add another one by simply doing, at the time of the upgrade:

public void upgrade(AtmosphereResource
         <HttpServletRequest, HttpServletResponse>  r) throws IOException
{
   // Upgrade
   super(r);
   // Escape all malicious chars
   r.setBroascaster(BroadcasterFactory.getDefault()
       .lookup(DefaultBroadcaster.class,"MyEventQueue");
}

The Famous Chat Application

Now let's write our first WebSocket application, the famous Chat! First, let's define our web.xml as:

<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
  xmlns:j2ee="http://java.sun.com/xml/ns/javaee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
        http://java.sun.com/xml/ns/j2ee/web-app_2.5.xsd">

    <description>Atmosphere Chat</description>
    <display-name>Atmosphere Chat</display-name>
    <servlet>
        <description>AtmosphereServlet</description>
        <servlet-name>AtmosphereServlet</servlet-name>
        <servlet-class>org.atmosphere.cpr.AtmosphereServlet</servlet-class>
        <load-on-startup>0</load-on-startup>
        <init-param>
            <param-name>org.atmosphere.websocket.WebSocketAtmosphereHandler</param-name>
            <param-value>true</param-value>
        </init-param>
    </servlet>
    <servlet-mapping>
        <servlet-name>AtmosphereServlet</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>
</web-app>

Now what we really need next is the server side AtmosphereHandler. But since by default the WebSocketAtmosphereHandler broadcast events to all suspended connections, then for a chat (single room) we don't have to do anything, e.g we just need to deploy the .war and that's it. Everything a Browser will send will be broadcasted to all suspended/upgraded connections.

The client side

For the client side, and to demonstrate how portable an Atmosphere application is, let's just shamelessly copy the Jetty's Chat index.html without making any changes:

  <script type='text/javascript'>

        if (!window.WebSocket)
            alert("WebSocket not supported by this browser");

        function $() {
            return document.getElementById(arguments[0]);
        }
        function $F() {
            return document.getElementById(arguments[0]).value;
        }

        function getKeyCode(ev) {
            if (window.event) return window.event.keyCode;
            return ev.keyCode;
        }

        var room = {
            join: function(name) {
                this._username = name;
                var location = document.location.toString()
                          .replace('http:', 'ws:');
                this._ws = new WebSocket(location);
                this._ws.onopen = this._onopen;
                this._ws.onmessage = this._onmessage;
                this._ws.onclose = this._onclose;
            },

            _onopen: function() {
                $('join').className = 'hidden';
                $('joined').className = '';
                $('phrase').focus();
                room._send(room._username, 'has joined!');
            },

            _send: function(user, message) {
                user = user.replace(':', '_');
                if (this._ws)
                    this._ws.send(user + ':' + message);
            },

            chat: function(text) {
                if (text != null && text.length &;gt 0)
                    room._send(room._username, text);
            },

            _onmessage: function(m) {
                if (m.data) {
                    var c = m.data.indexOf(':');
                    var from = m.data.substring(0, c)
                      .replace('&;lt', '<').replace('&;gt', '>');
                    var text = m.data.substring(c + 1)
                      .replace('&;lt', '<').replace('&;gt', '>');

                    var chat = $('chat');
                    var spanFrom = document.createElement('span');
                    spanFrom.className = 'from';
                    spanFrom.innerHTML = from + ': ';
                    var spanText = document.createElement('span');
                    spanText.className = 'text';
                    spanText.innerHTML = text;
                    var lineBreak = document.createElement('br');
                    chat.appendChild(spanFrom);
                    chat.appendChild(spanText);
                    chat.appendChild(lineBreak);
                    chat.scrollTop = chat.scrollHeight - chat.clientHeight;
                }
            },

            _onclose: function(m) {
                this._ws = null;
                $('join').className = '';
                $('joined').className = 'hidden';
                $('username').focus();
                $('chat').innerHTML = '';
            }
        };

I've put in bold the important piece, which is WebSocket.onopen, WebSocket.onmessage and WebSocket.onclose.Another example can be taken from the Grizzly WebSocket sample:

var app = {
    url: document.location.toString().replace('http:', 'ws:');,
    initialize: function() {
        if ("WebSocket" in window) {
            $('login-name').focus();
            app.listen();
        } else {
            $('missing-sockets').style.display = 'inherit';
            $('login-name').style.display = 'none';
            $('login-button').style.display = 'none';
            $('display').style.display = 'none';
        }
    },
    listen: function() {
        $('websockets-frame').src = app.url + '?' + count;
        count ++;
    },
    login: function() {
        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 = '';

        websocket = new WebSocket(app.url);
        websocket.onopen = function() {
            // Web Socket is connected. You can send data by send() method
            websocket.send('login:' + name);
        };
        websocket.onmessage = function (evt) {
            eval(evt.data);
            $('message').disabled = false;
            $('post-button').disabled = false;
            $('message').focus();
            $('message').value = '';
        };
        websocket.onclose = function() {
            var p = document.createElement('p');
            p.innerHTML = name + ': has left the chat';

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

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

As with the server side, the client side is also fairly simple. You can take a closer look the entire application's code from here.

What's Next!

As with Comet, we will add support native WebSocket support when they will be available. Resin is the next one, and then based on what people wants, we may try Netty. We are also working on a JQuery library that will auto detect if the browser and the server support WebSocket, and if one or both aren't, fall to use a Comet Technique (hint: we are looking for JQuery guru for help!)

More interesting is soon we will be able to write REST application that runs on top of Atmosphere's WebSocket. Soon (in my next upcoming blog) you should be able to define REST-Jersey-WebSocket application by doing:

@GET
@Path("/")
public WebSocketUpgrade<String> upgrade(@PathParam("message") String message)
{
       return r = new WebSocketUpgrade.WebSocketUpgradeBuilder()
                .entity(message)
                .scope(Suspend.SCOPE.REQUEST)
                .resumeOnBroadcast(true)
                .period(30, TimeUnit.SECONDS)
                .build();
}

@Produces("application/xml")
@OnBroadcast
public Broadcastable publishWithXML(@FormParam("message") String message)
{
        return new Broadcastable(new JAXBBean(message));
}

The above code will maybe supported in Atmosphere 0.6 GA, but will for sure make its way in 0.7. I will also work on adding WebSocket support to Akka. Looks promising!

For any questions or to download Atmosphere, go to our main site and use our Nabble forum (no subscription needed) or follow the team or myself and tweet your questions there! You can also checkout the code on Github. Or download our latest presentation to get an overview of what the framework is.

Originally posted at http://jfarcand.wordpress.com/

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.)