Snakes on the NettoSphere!

Building Games using WebSocket, Server-Side Events and Long-Polling with the Netty Framework and Atmosphere

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.

Help Making The Atmosphere Framework Better: Sponsor to keep the project alive!