Getting Started with AtmosphereHandler, WebSocket and Long Polling - Atmosphere/atmosphere Wiki

It is recommended to start with @ManagedService tutorial before reading this article.

This document describes how to get started with the Atmosphere Framework. This document will demonstrates how to use atmosphere.js client and the AtmosphereHandler extension point. If you aren't planning to use the AtmosphereHandler extension, see Getting started with Atmosphere Jersey, Meteor or WebSocketProtocol documents.

Note

This sample uses WebSocket for main transport and long-polling for fallback transport. You can change both transport at any time, e.g use sse (Server Side Events) as transport and streaming as fallback transport.

Prerequisites

Atmosphere applications are provided as .war files and can be deployed on all servers that support the .war format (e.g. Tomcat, Jetty, GlassFish). For this tutorial it is assumed you already worked with Maven. If you want to use WebSockets, make sure you deploy the application using Tomcat 7.0.27, GlassFish 3.1.2 or Jetty 7.5.1 or an higher version of those servers.

Building a WebSockets Chat Application

Server side

Let's build a really simple chat to demonstrate how simple the Atmosphere Framework is. All we need to do is to write an implementation of AtmosphereHandler

  1 import org.atmosphere.cpr.AtmosphereHandler;
  2 import org.atmosphere.cpr.AtmosphereRequest;
  3 import org.atmosphere.cpr.AtmosphereResource;
  4 import org.atmosphere.cpr.AtmosphereResourceEvent;
  5 import org.atmosphere.cpr.AtmosphereResponse;
  6 
  7 import java.io.IOException;
  8 import java.util.Date;
  9 @AtmosphereHandlerService(path="/chat")
 10 public class ChatAtmosphereHandler implements AtmosphereHandler {
 11 
 12     @Override
 13     public void onRequest(AtmosphereResource r) throws IOException {
 14     
 15         AtmosphereRequest req = r.getRequest();
 16         
 17         // First, tell Atmosphere to allow bi-directional communication by suspending.
 18         if (req.getMethod().equalsIgnoreCase("GET")) {
 19             // We are using HTTP long-polling with an invite timeout
 20             r.suspend();
 21         // Second, broadcast message to all connected users.
 22         } else if (req.getMethod().equalsIgnoreCase("POST")) {
 23             r.getBroadcaster().broadcast(req.getReader().readLine().trim());
 24         }   
 25     }   
 26     
 27     @Override
 28     public void onStateChange(AtmosphereResourceEvent event) throws IOException {
 29         AtmosphereResource r = event.getResource();
 30         AtmosphereResponse res = r.getResponse();
 31         
 32         if (r.isSuspended()) {
 33             String body = event.getMessage().toString();
 34             
 35             // Simple JSON -- Use Jackson for more complex structure
 36             // Message looks like { "author" : "foo", "message" : "bar" }
 37             String author = body.substring(body.indexOf(":") + 2, body.indexOf(",") - 1);
 38             String message = body.substring(body.lastIndexOf(":") + 2, body.length() - 2);
 39             
 40             res.getWriter().write(new Data(author, message).toString());
 41             switch (r.transport()) {
 42                 case JSONP:
 43                 case LONG_POLLING:
 44                     event.getResource().resume();
 45                     break;
 46                 case WEBSOCKET:
                        break;
 47                 case STREAMING:
 48                     res.getWriter().flush();
 49                     break;
 50             }       
 51         } else if (!event.isResuming()){
 52             event.broadcaster().broadcast(new Data("Someone", "say bye bye!").toString());
 53         }   
 54     }   
 55     
 56     @Override
 57     public void destroy() {
 58     }
 59     
 60     private final static class Data {
 61     
 62         private final String text;
 63         private final String author;
 64         
 65         public Data(String author, String text) {
 66             this.author = author;
 67             this.text = text;
 68         }   
 69         
 70         public String toString() {
 71             return "{ \"text\" : \"" + text 
 72                   + "\", \"author\" : \"" + author + "\" , \"time\" : " + new Date().getTime() + "}";
 73         }   
 74     }   
 75 }   

Line by line explanation:

Line 20 : One of the main concept of the Atmosphere Framework is an AtmosphereResource. An AtmosphereResource represents a suspended connection (or live connection) between the client and the server. An AtmosphereResource is needed to suspend/resume and broadcast events. AtmosphereResource.suspend is used to tell the framework to suspend the connection, or, in other words, to not commit the response and leave the connection open for other write operations. Suspending a connection with HTTP means leaving, not writing anything back. Whereas with WebSockets it means execute the handshake operation and let the connection open. Here we are telling Atmosphere to suspend the connection indefinitely (no timeout).

Line 23 : Use a Broadcaster to broadcast (or publish) the request's body to all suspended connections.

Line 32 : If the AtmosphereResource is suspended, which means a live connection between the client and the server, we write the AtmosphereResourceEvent#getMessage. The message was originally sent by the broadcast operation (line 23).

Line 41 : Depending on the transport used, we invoke the AtmosphereResource.resume for the transports Long-Polling and JSONP, flush the bytes for transport Streaming and do nothing for WebSocket.

The sequence of events for the chat application looks like:

  1. The client sends a GET request to the server. The request will be delivered to line 20 and the connection will be suspended.
  2. The client sends a POST request with a message. The request will be delivered to line 23 and the chat message will be broadcasted to all suspended connections. Let's now build the client side.

Client side

For the client we are using the atmosphere.js jQuery plugin. The javascript looks like:

 10     var socket = $.atmosphere;
 11     var request = { url: document.location.toString() + 'chat',
 12                     contentType : "application/json",
 13                     logLevel : 'debug',
 14                     transport : 'websocket' ,
 15                     fallbackTransport: 'long-polling'};
 16 
 17 
 18     request.onOpen = function(response) {
 19         content.html($('<p>', { text: 'Atmosphere connected using ' + response.transport }));
 20         input.removeAttr('disabled').focus();
 21         status.text('Choose name:');
 22     };
 23 
 24     request.onMessage = function (response) {
 25         var message = response.responseBody;
 26         try {
 27             var json = JSON.parse(message);
 28         } catch (e) {
 29             console.log('This doesn\'t look like a valid JSON: ', message.data);
 30             return;
 31         }
 32 
 33         if (!logged) {
 34             logged = true;
 35             status.text(myName + ': ').css('color', 'blue');
 36             input.removeAttr('disabled').focus();
 37         } else {
 38             input.removeAttr('disabled');
 39 
 40             var me = json.author == author;
 41             var date =  typeof(json.time) == 'string' ? parseInt(json.time) : json.time;
 42             addMessage(json.author, json.text, me ? 'blue' : 'black', new Date());
 43         }
 44     };
 45 
 46     request.onError = function(response) {
 47         content.html($('<p>', { text: 'Sorry, but there\'s some problem with your '
 48             + 'socket or the server is down' }));
 49     };
 50 
 51     var subSocket = socket.subscribe(request);
 52 
 53     input.keydown(function(e) {
 54         if (e.keyCode === 13) {
 55             var msg = $(this).val();
 56 
 57             // First message is always the author's name
 58             if (author == null) {
 59                 author = msg;
 60             }
 61 
 62             subSocket.push(JSON.stringify({ author: author, message: msg }));
 63             $(this).val('');
 64 
 65             input.attr('disabled', 'disabled');
 66             if (myName === false) {
 67                 myName = msg;
 68             }
 69         }
 70     });
 71 
 72     function addMessage(author, message, color, datetime) {
 73         content.append('<p><span style="color:' + color + '">' + author + '</span> @ ' +
 74             + (datetime.getHours() < 10 ? '0' + datetime.getHours() : datetime.getHours()) + ':'
 75             + (datetime.getMinutes() < 10 ? '0' + datetime.getMinutes() : datetime.getMinutes())
 76             + ': ' + message + '</p>');
 77     }

Line by line explanation:

Line 10: The first things we need to so is to get a pointer to atmosphere.js main element. For the chat we will call it socket. Under the hood Atmosphere will use the best transport depending on the server you are using. For example, deploying the application on Tomcat 7.0.27 or Jetty 7.5 and up will automatically enable the WebSocket transport.

Line 11: Here we define the request we want to make. In our case it will look like http://127.0.0.1:8080/chat with Content-Type header set to JSON. As the preferred transport we will favor WebSocket and fall-back to long-polling in case the server or client does not support WebSocket.

Line 18: Here we are defining a function that will be invoked when the connection is opened and ready to execute request. The onOpen function will be invoked once the server suspend operation successfully completed.

Line 24: Here we are defining a function that will be invoked when a Broadcast operation occurs on the server. The JSON message will be parsed (line 27) and displayed on the screen.

Line 46: Here we are defining a function that will be invoked in case an error occurs (the connection is dropped, the server goes down, etc.).

Line 51: Once we are ready, socket.subscribe(request) will connect to the server, and return a subSocket that can be used to push messages back to the server.

Line 62: Here we are using the subSocket connection retrieved from the socket. If the browser is using WebSocket, the subSocket connection will be the same WebSocket as socket. If long-polling is used, subSocket will use another connection to push messages back to the server.

Line 72: This function displays the received message on the screen.

Note that you don't have to care if WebSocket is supported or not. Both client and server will negotiate the best transport (e.g fallback to long-polling) and transparently used it.

That's all

You can look at the entire application here or download it from here.