Introduction
The Atmosphere Framework is a Java/Javascript framework which allows the creation of portable asynchronous applications using Groovy, Scala and Java. The Atmosphere Framework ships with a JavaScript component supporting all modern browsers and several server components supporting all major Java-based WebServers and Framework. The aim of the framework is to allow a developer to write an application and let the framework discover the best communication channel (transport) between the client and the server, transparently.
This article will use NettoSphere, a framework build on top of the popular Netty Framework and Atmosphere with support of WebSockets, Server Side Events and Long-Polling. NettoSphere allows Atmosphere's applications to run on top of the Netty Framework.
The game we will develop is the famous Snake(*). The difference with the original game is instead of having to avoid walls and obstacles, your snake will have to avoid hitting other "social snake", e.g snakes from other users. Each browser will control a snake and share it's position with all other snake, e.g Browsers!!
This article assumes you have a little understanding of what is the Atmosphere Framework. See this article for an introduction to Atmosphere
The Games will supports for it's primary transport WebSocket and will fallback to HTML5 Server Side Events (SSE) and Long-Polling. Long-Polling is required because only IE version 10 supports WebSockets and SSE is not supported at all by IE.
The game will use NettoSphere 2.0.0.RC1, which is build on top of Netty 3.6.3.Final. The game can be downloaded here.
Note that we won't go into the details of the game itself and instead focus on how games can be build using Atmosphere API..
Snakes on the NettoSphere!
Before going into the details of the Snake, let's just explain how an Atmosphere Snake can be installed on the Netty Framework. As simple as
Listing 0: Bootstrap.java
Config.Builder b = new Config.Builder(); b.resource(SnakeManagedService.class) .resource("./webapps") .port(8080) .host("127.0.0.1") .build(); Nettosphere s = new Nettosphere.Builder().config(b.build()).build(); s.start();
That's all we need to do: create Config, associate our server component (SnakeManagedService.class), client component ("./webapps"), build and start!
[main] INFO o.atmosphere.cpr.AtmosphereFramework - Atmosphere is using org.atmosphere.cpr.DefaultAnnotationProcessor for processing annotation [main] INFO o.a.cpr.DefaultAnnotationProcessor - Found Annotation in org.nettosphere.samples.games.SnakeManagedService being scanned: interface org.atmosphere.config.service.ManagedService [main] INFO o.atmosphere.cpr.AtmosphereFramework - Installed AtmosphereHandler org.atmosphere.handler.ManagedAtmosphereHandler mapped to context-path: /snake [main] INFO o.atmosphere.cpr.AtmosphereFramework - Installed the following AtmosphereInterceptor mapped to AtmosphereHandler org.atmosphere.handler.ManagedAtmosphereHandler [main] INFO o.atmosphere.cpr.AtmosphereFramework - AtmosphereResourceLifecycleInterceptor : Atmosphere LifeCycle [main] INFO o.atmosphere.cpr.AtmosphereFramework - BroadcastOnPostAtmosphereInterceptor : Broadcast POST Body Interceptor [main] INFO o.atmosphere.cpr.AtmosphereFramework - TrackMessageSizeInterceptor : Track Message Size Interceptor using | [main] INFO o.atmosphere.cpr.AtmosphereFramework - HeartbeatInterceptor : Heartbeat Interceptor Support [main] INFO o.atmosphere.cpr.AtmosphereFramework - : Managed Event Listeners [main] INFO o.atmosphere.cpr.AtmosphereFramework - Installed WebSocketProtocol org.atmosphere.websocket.protocol.SimpleHttpProtocol [main] INFO o.atmosphere.cpr.AtmosphereFramework - Atmosphere is using async support: org.atmosphere.nettosphere.NettyAtmosphereHandler$1 running under container: Nettosphere/2.0 [main] INFO o.atmosphere.cpr.AtmosphereFramework - Installing Default AtmosphereInterceptor [main] INFO o.atmosphere.cpr.AtmosphereFramework - org.atmosphere.interceptor.OnDisconnectInterceptor : Browser disconnection detection [main] INFO o.atmosphere.cpr.AtmosphereFramework - org.atmosphere.interceptor.JavaScriptProtocol : Atmosphere JavaScript Protocol [main] INFO o.atmosphere.cpr.AtmosphereFramework - org.atmosphere.interceptor.JSONPAtmosphereInterceptor : JSONP Interceptor Support [main] INFO o.atmosphere.cpr.AtmosphereFramework - org.atmosphere.interceptor.SSEAtmosphereInterceptor : SSE Interceptor Support [main] INFO o.atmosphere.cpr.AtmosphereFramework - org.atmosphere.interceptor.AndroidAtmosphereInterceptor : Android Interceptor Support [main] INFO o.atmosphere.cpr.AtmosphereFramework - org.atmosphere.interceptor.PaddingAtmosphereInterceptor : Browser Padding Interceptor Support [main] INFO o.atmosphere.cpr.AtmosphereFramework - org.atmosphere.interceptor.DefaultHeadersInterceptor : Default Response Headers Interceptor [main] INFO o.atmosphere.cpr.AtmosphereFramework - Set org.atmosphere.cpr.AtmosphereInterceptor.disableDefaults in your xml to disable them. [main] INFO o.atmosphere.cpr.AtmosphereFramework - Using BroadcasterCache: org.atmosphere.cache.UUIDBroadcasterCache [main] INFO o.atmosphere.cpr.AtmosphereFramework - Shared ExecutorService supported: true [main] INFO o.atmosphere.cpr.AtmosphereFramework - HttpSession supported: false [main] INFO o.atmosphere.cpr.AtmosphereFramework - Using BroadcasterFactory: org.atmosphere.cpr.DefaultBroadcasterFactory [main] INFO o.atmosphere.cpr.AtmosphereFramework - Using WebSocketProcessor: org.atmosphere.websocket.DefaultWebSocketProcessor [main] INFO o.atmosphere.cpr.AtmosphereFramework - Using Broadcaster: org.atmosphere.cpr.DefaultBroadcaster [main] INFO o.atmosphere.cpr.AtmosphereFramework - Atmosphere Framework 1.1.0.RC1 started.
The Server Side
Let's start by first writing the server component, as described by listing 1. Remember, our goal is to write a Snake game that can be played by ALL browsers including mobile, using the best transport available.
Listing 1: SnakeManagedService.java
1 package org.nettosphere.samples.games; 2 3 import org.atmosphere.config.service.Get; 4 import org.atmosphere.config.service.ManagedService; 5 import org.atmosphere.config.service.Post; 6 import org.atmosphere.cpr.AtmosphereRequest; 7 import org.atmosphere.cpr.AtmosphereResource; 8 import org.atmosphere.cpr.AtmosphereResourceEvent; 9 import org.atmosphere.cpr.AtmosphereResourceEventListenerAdapter; 10 import org.atmosphere.cpr.AtmosphereResourceFactory; 11 import org.atmosphere.cpr.HeaderConfig; 12 13 import java.io.IOException; 14 import java.util.concurrent.ConcurrentLinkedQueue; 15 16 @ManagedService(path = "/snake") 17 public class SnakeManagedService extends SnakeGame { 18 19 private final ConcurrentLinkedQueue>String< uuids = new ConcurrentLinkedQueue>String<(); 20 21 @Get 22 public void onOpen(final AtmosphereResource resource) { 23 resource.addEventListener(new AtmosphereResourceEventListenerAdapter() { 24 @Override 25 public void onSuspend(AtmosphereResourceEvent event) { 26 try { 27 if (!uuids.contains(resource.uuid())) { 28 SnakeManagedService.super.onOpen(resource); 29 uuids.add(resource.uuid()); 30 } 31 } catch (IOException e) { 32 e.printStackTrace(); 33 } 34 35 } 36 37 @Override 38 public void onDisconnect(AtmosphereResourceEvent event) { 39 AtmosphereRequest request = event.getResource().getRequest(); 40 String s = request.getHeader(HeaderConfig.X_ATMOSPHERE_TRANSPORT); 41 if (s != null && s.equalsIgnoreCase(HeaderConfig.DISCONNECT)) { 42 SnakeManagedService.super.onClose(resource); 43 uuids.remove(resource.uuid()); 44 } 45 } 46 }); 47 } 48 49 @Post 50 public void onMessage(AtmosphereResource resource) { 51 try { 52 // Here we need to find the suspended AtmosphereResource 53 super.onMessage(AtmosphereResourceFactory.getDefault().find(resource.uuid()), 54 resource.getRequest().getReader().readLine()); 55 } catch (IOException e) { 56 e.printStackTrace(); 57 } 58 } 59 60 }
The ManagedService annotation (line 16) tells the framework to route every '/snake' request to the SnakeManagedService class. The ManagedService is a meta component in Atmosphere which transparently enable the following services (called AtmosphereInterceptor):
- Messages Caching: a connection between the browsers and the server can always be closed by a third party. Proxy, Firewall, Network outage etc. can always cause the connection to be unexpected closed. If messages where about to be send to the client, those messages will be lost if the server doesn't cache them and make them available when the client reconnect. This is quite important for any real time application, and event more important for our Snakes' users!!!
- Connection LifeCycle Support: Atmosphere's allow the management of the connection between the browsers and the server. For example, you can add special headers to the WebSocket's Handshake response, add some padding to a Server Side Events connection etc. Since most of the application doesn't need to do such thing, it is better to delegate the handling to the LifeCycle Support component and focus on the application business logic instead.
- Track Messages Size: Messages sent by the server back to the browser may be chunked. In order to help the browser with incomplete messages (or messages spawn in more than one packet), this service adds meta information to the message. The browser use this meta information to reconstruct the message before delivering it to the application.
- Heartbeat: some Proxy closes idle connections between the browser and the server. This service make sure there are always activities on the connection, preventing unexpected closes, reconnects, etc.
- Browser disconnect: The Atmosphere's Javascript client is able to sent messages when a tab/window is getting closed. This service use this information to clean up resources associated with that browser.
As you can see, you SAVES a lot of code by using the @ManagedService annotation!
Next, on line 21, we use the @Get annotation to tell the framework to route GET request to the onOpen method. When the browser's connect, the onOpen will be invoked with an AtmosphereResource, who represent a connection between the browser and server. The 'onOpen' method's role is to attach an Atmosphere's listener to the connection and gets invoked first when the connection is ready to be manipulated (line 25). Inside the onSuspend, we track our current alive Snake by using the browser's unique is 'AtmosphereResource.uuid()' and initiate the game (listing 2).
Listing 2: SnakeGame.java
78 public void onOpen(AtmosphereResource resource) throws IOException { 79 int id = snakeIds.getAndIncrement(); 80 resource.session().setAttribute("id", id); 81 Snake snake = new Snake(id, resource); 82 83 resource.session().setAttribute("snake", snake); 84 snakeBroadcaster.addSnake(snake); 85 StringBuilder sb = new StringBuilder(); 86 for (Iterator>Snake< iterator = snakeBroadcaster.getSnakes().iterator(); 87 iterator.hasNext(); ) { 88 snake = iterator.next(); 89 sb.append(String.format("{id: %d, color: '%s'}", 90 Integer.valueOf(snake.getId()), snake.getHexColor())); 91 if (iterator.hasNext()) { 92 sb.append(','); 93 } 94 } 95 snakeBroadcaster.broadcast(String.format("{'type': 'join','data':[%s]}", 96 sb.toString())); 97 } 98 99 public void onClose(AtmosphereResource resource) { 100 snakeBroadcaster.removeSnake(snake(resource)); 101 snakeBroadcaster.broadcast(String.format("{'type': 'leave', 'id': %d}", 102 ((Integer) resource.session().getAttribute("id")))); 103 } 104 105 protected Snake snake(AtmosphereResource resource) { 106 return (Snake) resource.session().getAttribute("snake"); 107 } 108 109 protected void onMessage(AtmosphereResource resource, String message) { 110 Snake snake = snake(resource); 111 if ("west".equals(message)) { 112 snake.setDirection(Direction.WEST); 113 } else if ("north".equals(message)) { 114 snake.setDirection(Direction.NORTH); 115 } else if ("east".equals(message)) { 116 snake.setDirection(Direction.EAST); 117 } else if ("south".equals(message)) { 118 snake.setDirection(Direction.SOUTH); 119 } 120 }
In the onOpen method we store some state information about the current Snake user and add our Snake to an Atmosphere's Broadcaster (called SnakeBroadcaster). A Broadcaster implements the publish/subscribe paradigm. An application can subscribe to one or many Broadcasters to get notified about events. By default, a single Broadcaster is created by the framework and associated with every new AtmosphereResource. For our game, we just create one Broadcaster called "/snake", which maps our original request's URI. The important code here is line 95, where the SnakeBroadcaster is used here to broadcast the position of all snakes to all connected users. We use JSON for encoding our data.
The onDisconnect method (listing 1, line 38) is simply used to clean our resources when the browser gets closed (tab or window). We do check for the 'disconnect' message in order to differentiate when the browser close the connection versus when the connection get closed unexpectedly. When closed unexpectedly, we know the client will reconnect so we don't kill the associated Snake! Listing 2 line 99 broadcast to all remaining connection browser that snake X is now gone, so the browser can kill that snake by stopping rendering it!
Finally, the @Post method (listing 1, line 50) just broadcast the information received from a browser to others. That means every time a snake is moving, it's position will be broadcasted to all others and vice versa (Listing 2, line 109)
That's it for the server side component, which TRANSPARENTLY support WebSocket, Server Side Events and Long-Polling!
Wait!!!!!!!
There will be a lot of information exchanged between snakes: every time a snake move its information will be send to the server, and broadcasted back to all other snakes. For a WebSocket connection this is quite easy to handle because the connection is bi-directional. But realize how complex the code can be when Server Side Events and Long-Polling are used: one connection is used to receive messages, and another connection is used for sending information. Realize that without Atmosphere, a lot of code would have been required to makes the game work!
The client side
That’s it for the server side. Now let’s use the atmosphere.js to write the client side. First, let’s look at the code (Listing 3). As for the server side, the game's logic won't be described
Listing 3: Atmosphere.js Client Code
188 Game.connect = (function (host) { 189 var request = {url: host, 190 transport: 'websocket', 191 enableProtocol: true, 192 trackMessageLength: true, 193 logLevel: 'debug'}; 194 195 request.onOpen = function (response) { 196 // Socket open.. start the game loop. 197 Console.log('Info: ' + Game.transport + ' connection opened.'); 198 Console.log('Info: Press an arrow key to begin.'); 199 Game.startGameLoop(); 200 }; 201 202 request.onClose = function (response) { 203 if (response.state == "unsubscribe") { 204 Console.log('Info: ' + Game.transport + ' closed.'); 205 Game.stopGameLoop(); 206 } 207 }; 208 209 request.onTransportFailure = function (errorMsg, request) { 210 jQuery.atmosphere.info(errorMsg); 211 if (window.EventSource) { 212 request.fallbackTransport = "sse"; 213 } else { 214 request.fallbackTransport = 'long-polling' 215 } 216 Game.transport = request.fallbackTransport; 217 }; 218 219 request.onMessage = function (response) { 220 var message = response.responseBody; 221 var packet; 222 try { 223 packet = eval('(' + message + ')'); //jQuery.parseJSON(message); 224 } catch (e) { 225 console.log('Message: ', message); 226 return; 227 } 228 229 switch (packet.type) { 230 case 'update': 231 for (var i = 0; i < packet.data.length; i++) { 232 Game.updateSnake(packet.data[i].id, packet.data[i].body); 233 } 234 break; 235 case 'join': 236 for (var j = 0; j < packet.data.length; j++) { 237 Game.addSnake(packet.data[j].id, packet.data[j].color); 238 } 239 break; 240 case 'leave': 241 Game.removeSnake(packet.id); 242 break; 243 case 'dead': 244 Console.log('Info: Your snake is dead, bad luck!'); 245 Game.direction = 'none'; 246 break; 247 case 'kill': 248 Console.log('Info: Head shot!'); 249 break; 250 } 251 }; 252 Game.socket = $.atmosphere.subscribe(request) 253 254 });
There is a lot of extra in the code in Listing 3 related to the game's logic itself, so let’s only describe the Atmosphere's client JavaScript called atmosphere.js important parts. First, we initialize a connection (line 252)
Game.socket = $.atmosphere.subscribe(request)
The next step is to define some functions callback. For this game, we will define the important one: onOpen, onClose, onTransportFailure and onMessage. First, we define an onOpen function that gets invoked when the underlying transport is connected to the server. There we just initialize the Snake. The preferred transport is specified on the request object, which is defined as:
var request = { url: host, transport: 'websocket', enableProtocol: true, trackMessageLength: true };
Here we want to use the WebSocket transport by default, and fallback to Server Side events or Long-Polling if not supported
request.onTransportFailure = function (errorMsg, request) { jQuery.atmosphere.info(errorMsg); if (window.EventSource) { request.fallbackTransport = "sse"; } else { request.fallbackTransport = 'long-polling' } Game.transport = request.fallbackTransport; };
The beauty here is: you don’t need to use a special API. All transports are handled the same way using the atmosphere.js.
Next we define the onMessage function, which will be invoked every time we receive data from the server:
request.onMessage = function (response) { var message = response.responseBody; var packet; try { packet = jQuery.parseJSON(message); } catch (e) { console.log('Message: ', message); return; } switch (packet.type) { case 'update': for (var i = 0; i < packet.data.length; i++) { Game.updateSnake(packet.data[i].id, packet.data[i].body); } break; case 'join': for (var j = 0; j < packet.data.length; j++) { Game.addSnake(packet.data[j].id, packet.data[j].color); } break; case 'leave': Game.removeSnake(packet.id); break; case 'dead': Console.log('Info: Your snake is dead, bad luck!'); Game.direction = 'none'; break; case 'kill': Console.log('Info: Head shot!'); break; }
Here we just displaying snake's position. To send Snake's position to the server, all we need to do is to invoke:
120 Game.setDirection = function (direction) { 121 Game.direction = direction; 122 Game.socket.push(direction); 123 Console.log('Sent: Direction ' + direction); 124 };
That's it, we have both client and server components ready to Snake!
Back in 1970! As good as Space Invaders!

Supported Browsers and their associate transports
Our Snake application will first negotiate the best transport to use between the client and the server. For example, the following transport will be used
- Chrome 21 : WebSockets
- Internet Explorer 9 : Long-Polling
- FireFox 15: Server Side Events
- Safari/iOS 6: WebSockets
- Internet Explorer 10: WebSockets
- Android 2.3: Long-Polling
- FireFox 3.5 : Long-Polling
All of this transparently, allowing a developer to focus on the application instead of transport/portability issues.
For this article we have used Netty, but the same code will run UNMODIFIED in Play! Framework and any WebServer supporting Servlet 2.4 Specification.
Conclusions and Considerations
WebSockets and Server Sides Events are technologies on the rise and their adoption within the enterprise is accelerating. Some things to think about before jumping in:
- Is the API portable, e.g. will it work on all well-known WebServer?
- Is the framework already offering a transport fallback mechanism? For example, Internet Explorer 7/8/9 neither support WebSockets and Server Side Events, and unfortunately for us, those browsers are still widely used.
- Is the framework cloud enabled, and more important, will it scale?
- Is it easy to write application, is the framework well established?
- Do we really need a Java EE Server? Why not using NettoSphere or Play!
Clearly, the Atmosphere Framework is the response for those five really important questions.