Sock it to 'em

JSR 356 Java API for WebSocket or Atmosphere?

The recently released JSR 356 Java API for WebSocket[1], recommended for supporting the WebSocket protocol in Java [3], already ships with recent versions of Servlet Containers such as Tomcat, Wildfly, Jetty, and GlassFish. Although this is a really good step in the right direction, there are many areas where the API either lacks functionality or could benefit from significant improvement. In this article originally published in JAX Magazine, Jean Francis Arcand walks us through a few of them. 

Before the API, applications that wanted to use the WebSocket Protocol needed to either use proprietary WebSocket API, sacrificing portability, or use a framework such as Atmosphere[2], which abstracted an API layer allowing WebSocket applications across Servlet Container, and also frameworks like Vert.x, Play! and Netty.

Another important difference is the fact that a JSR 356 WebSocket application will only work when the browser supports the WebSocket Protocol. If a browser doesn't, the application won't work. That's not a problem with Atmosphere - the framework is able to transparently fallback to another transport like HTTP, without any change required to your application. Be aware that if you are planning to go on production, live on the web, with a pure JSR 356 websocket application, a lot of browsers won't be able to communicate with your application.

Server Side API

Let’s use a Chat Application to compare the use of JSR 356 and Atmosphere, and see where the JSR would benefit from some improvements. We‘ll start the comparison by writing the simplest endpoints. It is assumed here you have a little knowledge of JSR 356 annotationṡ.

First, let’s write our skeleton class. With JSR 356, our endpoint will look like:

Listing 1

@ServerEndpoint(value = "/chat")

public class ChatRoom {

 

@OnOpen

public void ready(Session s) {

s.getAsyncRemote().sendText("You are connected");

}

 

@OnMessage

public void message(String message, Session session) {

// Sent the message back to all connected users.

for(Session s: session.getOpenSessions()) {

if (s.isOpen()) s.getAsyncRemote().sendText(message);

}

}

 

@OnClose

public void close(CloseReason reason) {

}

 

}

End

With Atmosphere, our endpoint will look like:

Listing 2

@ManagedService(path="/chat")

public class ChatRoom {

 

@Ready

public String ready(AtmosphereResource r) {

return "You are connected";

}

 

@Message

public String message(String message) {

// Sent the message back to all connected users.

return message;

}

 

@Disconnect

public void close(AtmosphereResourceEvent event) {

}

}

End

There is not much difference between JSR 356 and Atmosphere in terms of coding, with the exception that with Atmosphere you don't need to implement the write operation, all that you need to do is to set the return value of your annotated method. But the @ManagedService annotation gives you much more than @ServerEndpoint in terms of quality of service. The @ManagedService annotation starts transparently, though under the hood, some functionality is needed for an application running on the web. One of the biggest issues with WebSockets is Proxy. Proxy that aren't supporting the protocol or Proxy configured with timeout will eventually kill a websocket connection if no activity occurs on it. In this case, you need to make sure both the client and server code can re-handshake their state transparently, so that your application can survive disconnection. For JSR 356, that means we need a little more work Server side to prevent evil Proxy. One naive way would be to add:

Listing 3

@ServerEndpoint(

value = "/chat"

)

public class ChatRoom {

 

// Warning, the Scheduler will never be shutdown. Add a ShutdownHook.

private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(10);

private final AtomicBoolean isStated = new AtomicBoolean();

 

@OnOpen

public void ready(final Session session) {

session.getAsyncRemote().sendText("You are connected");

aliveSession.offer(session);

 

if (!isStated.getAndSet(true)) {

scheduler.schedule(new Runnable() {

@Override

public void run() {

// Send heartbeat every 10 seconds

for(Session s: session.getOpenSessions()) {

if (s.isOpen()) s.getAsyncRemote().sendText(" ");

}

}

}, 10, TimeUnit.SECONDS);

}

}

 

@OnMessage

public void message(String message, Session session) {

for(Session s: session.getOpenSessions()) {

if (s.isOpen()) s.getAsyncRemote().sendText(message);

}

}

 

@OnClose

public void close(Session session, CloseReason reason) {

}

 

}

End

Another critical functionality missing is when a websocket connection is abruptly closed, either by a Proxy, or anything in between the browser and the server. For example, a mobile browser may suffer frequently suffer from unexpected disconnections. By the time the browser reconnects, messages may have been published, and lost messages will never be sent to the browser. The @ManagedService annotation transparently deals with this case, making sure to cache messages and send them back when the browser reconnects. With JSR 356, support for such a scenarion is missing, hence you need to write your own. This is far from simple, as you need to track the browser and make sure all messages will eventually be delivered. An extremely native implementation could be:

Listing 4

@ServerEndpoint(

value = "/chat"

)

public class ChatRoom {

 

private final ConcurrentLinkedQueue<Session> aliveSession = new ConcurrentLinkedQueue<Session>();

// Warning, the Scheduler will never be shutdown

private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(10);

private final AtomicBoolean isStated = new AtomicBoolean();

private final ConcurrentHashMap<String, List<String>> messages = new ConcurrentHashMap<String, List<String>>();

 

@OnOpen

public void ready(final Session session) {

session.getAsyncRemote().sendText("You are connected");

aliveSession.offer(session);

 

checkForCachedMessage(session);

 

if (!isStated.getAndSet(true)) {

scheduler.schedule(new Runnable() {

@Override

public void run() {

// Send heartbeat every 10 seconds

for(Session s: session.getOpenSessions()) {

if (s.isOpen()) s.getAsyncRemote().sendText(" ");

}

}

}, 10, TimeUnit.SECONDS);

}

}

 

private void checkForCachedMessage(Session session) {

// Unique Token

String clientUniqueToken = parseQueryStringForUniqueId(session.getQueryString());

List<String> cachedMessages = messages.get(clientUniqueToken);

for (String m : cachedMessages) {

if (s.isOpen()) session.getAsyncRemote().sendText(m);

}

}

 

private String parseQueryStringForUniqueId(String queryString) {

...

}

 

@OnMessage

public void message(String message, Session session) {

for(Session s: session.getOpenSessions()) {

if (s.isOpen()) s.getAsyncRemote().sendText(message);

}

}

 

@OnClose

public void close(Session session, CloseReason reason) {

aliveSession.remove(session);

startCachingMessageFor(session);

}

 

private void startCachingMessageFor(Session session) {

...

}

 

}

 

End

Not impossible to implement, but consider you get the functionality transparently when using the @ManagedService annotation.

Another functionality missing with JSR 356 is, if an application sends a message larger than the underlying server's I/O buffer, the message can sometimes be delivered to the client in two chunks. When working with text messages, it is probably not an issue, but if you work with JSON for example, receiving a JSON message in two chunks will produce an error when try to decode it on the client side using:

Listing 5

websocket.onmessage(message) {

// Will fail if the browser receives incomplete message.

window.JSON.parse(message);

}

End

You can either add an ugly try/catch around that function, append the next message to the already received one until the parse operation succeed, or you can encode in the message itself the length of the expected message. It requires code on both client and server component. Using Atmosphere, you get that functionality for free transparently built in for you. This means you are guaranteed to receive the complete message when defining (more details on the client next section)

Listing 6

atmosphereSocket.onMessage(response) {

var message = response.responseBody;

// Will always work

window.JSON.parse(message);

}

End

Talking of JSON, let's stop working with String and instead add some Encoder and Decoder. For example, let's define the following Message class:

Listing 7

public class Message {

 

private String message;

 

public Message(String message) {

this.message = message;

}

 

public String getMessage() {

return message;

}

 

public void setMessage(String message) {

this.message = message;

}

}

End

JSR 356 defined the notion of Message's Encoder and Decoder. For example, let's assume the browser is sending JSON message. To handle those messages, let's write a JSR 356 Decoder using Jackson:

Listing 8

public class MessageDecoder implements Decoder.Text<Message> {

 

private ObjectMapper mapper = new ObjectMapper();

 

@Override

public Message decode(String s) {

return mapper.readValue(s, Message.class);;

}

 

@Override

public boolean willDecode(String s) {

return true;

}

 

@Override

public void init(EndpointConfig config) {

}

 

@Override

public void destroy() {

}

}

 

with Atmosphere,

 

public class ChatDecoder implements Decoder<String, Message> {

 

private ObjectMapper mapper = new ObjectMapper();

 

@Override

public Message decode(String s) {

return mapper.readValue(s, Message.class);

}

}

End

The major difference here is with JSR 356 you can decide to not decode an object based on its String content. With Atmosphere, we instead support chaining for Decoder, e.g. a decoder decoded object be the input of another decoder. That way, the message doesn't have to be parsed several times as with JSR 356. For encoding the Message class, use JSR 356 expose Encoder:

 

Listing 9

public class ChatEncoder implements Encoder.Text<Message> {



private ObjectMapper mapper = new ObjectMapper();



@Override

public String encode(Message s) {

return mapper.writeValueAsString(s);

}



@Override

public void init(EndpointConfig config) {

}



@Override

public void destroy() {

}

}



with Atmosphere,



public class ChatEncoder implements Encoder<Message, String> {



private ObjectMapper mapper = new ObjectMapper();



@Override

public String encode(Message s) {

return mapper.writeValueAsString(s);

}

}

End

 

The main difference here is as with Atmosphere's Decoder, you can chain Encoder and pass an encoded object to the next Encoder. With JSR 356, you can only encode as String or PrintWriter. With Atmosphere, you don't have such restriction.

Putting the piece together, the JSR 356 implementation looks like:

 

Listing 10

@ServerEndpoint(

value = "/chat",

encoders = {ChatEncoder.class},

decoders = {ChatDecoder.class}

)

public class ChatRoom {



@OnOpen

public void ready(final Session session) {

session.getAsyncRemote().sendText("You are connected");

}



@OnMessage

public void message(Message message, Session session) {

for(Session s: session.getOpenSessions()) {

if (s.isOpen()) s.getAsyncRemote().sendText(message);

}

}



@OnClose

public void close(Session session, CloseReason reason) {

}



}

End

 

with Atmosphere,

Listing 11

@ManagedService(path="/chat")

public class BasicChatRoom {



@Ready

public String ready(AtmosphereResource r) {

return "You are connected";

}



@Message(encoders = {ChatEncoder.class}, decoders = {ChatDecoder.class})

public String message(String message) {

return message;

}



@Disconnect

public void close(AtmosphereResourceEvent event) {

}

}

End

The main difference here is with Atmosphere, encoders/decoders can apply at method level, where with JSR 356 it applies only at class level.

 

Browser Client API

Let's now explore the client side of a WebSocket application. JSR 356 doesn't ship with a Javascript client library, so we will use the W3C interface that the majority of browsers support. For our simple application, we use:

Listing 11

var websocket = new WebSocket(uri);



websocket.onopen = function(evt) {

...

};



websocket.onmessage = function(evt) {

...

};



websocket.onclose = function(evt) {

...

};



websocket.onerror = function(evt) {

...

};



websocket.send(...);

End

With Atmosphere:

Listing 12

var request = { url: url

transport: 'websocket',

fallbacktransport: 'long-polling'};



request.onOpen = function (response) {

...

};



request.onMessage = function (response) {

...

};



request.onClose = function (response) {

...

};



request.onError = function (response) {

...

};



socket = atmosphere.subscribe(request);

socket.push(...);

End

The API looks similar, with a major difference: If the WebSocket API is not supported by the browser, Atmosphere will transparently fallback and use long-polling instead. But not all applications need to use a fallback transport, so let's not focus on that. Atmosphere client-side Javascript comes with nice functionality

For example, as with the server, if the connection get interrupted, most of the time the client needs to reconnect. With W3C API, you have to implement the logic inside your onclose or onerror function. Nothing complicated, but you have to take care of it. Since the server may not be available automatically, you may need to try reconnecting after a five second pause. Put a limit on when trying to reconnect, etc. All this code needs to be implemented. With Atmosphere, it's already built into the Javascript, so all you need to do is:

Listing 13

var request = { url: url

reconnectInterval : 5000 // 5 seconds before reconnecting

maxReconnectOnClose : 5 // Stop after 5 unsuccesful reconnect

transport: 'websocket',

fallbacktransport: 'long-polling'};



request.onOpen = function (response) {

...

};



request.onReOpen = function (response) {

// Invoked when the client successfully reconnect

};



request.onReconnect = function (response) {

// Invoked before trying to reconnect.

};



request.onMessage = function (response) {

...

};



request.onClose = function (response) {

...

};



request.onError = function (response) {

...

};



socket = atmosphere.subscribe(request);

socket.push(...);

End

Again, you can do it with W3C API, but that will require a lot of code. As pointed in the previous section, you may also have to make sure the websocket.onmessage(data) contains the full response send by the server before trying to parse it using a JSON parser.

Java Client Side Adoption

JSR 356 ships with a Client Side API as well, but I won’t go into the details in this article. Atmosphere also has a Client Side API called wAsync[4], which support websocket as well as other transport like Server Side Events, long-polling etc.

Adoption

As of November 2013, JSR 356 is supported by Tomcat 7/8 (Beta), Jetty 9.1.0 (Beta), GlassFish 4, Wildfly/Undertow (beta) and Resin 4.

Atmosphere supports WebSocket natively Tomcat 7/8, Jetty 7/8/9, GlassFish 3/4, WebLogic 12, JBoss 7.1.x, Netty 3.x, Vert.x 2.x and Play! Framework 2.x and up. With Atmosphere, your application is portable and you can use server that are already production ready, without waiting for a newer version of it which support JSR 356.

Conclusion

JSR 356 is a good step toward WebSocket adoption in the Java land, but there is a lack of important features like fallback transport for browsers that don't support the websocket protocol, caching of messages to prevent message's lost, proper life cycle reconnection, and wide adoption with servers deployed in production.

Atmosphere is production ready and can be deployed almost anywhere. It already supports functionalities like message caching, proper reconnection life cycle, fallback transport, and much more besides. Atmosphere can run on servers supporting JSR 356, as well as well established server and framework. More importantly, Atmosphere ships with a client side Javascript which can solve a lot of issues. Finally, Atmosphere is supported by the majority of existing frameworks, and can transparently add WebSocket support to framework like PrimeFaces, RestEasy, Wicket, Jersey, etc. This means you can write a good old Servlet Application that transparently runs on top of the WebSocket protocol.

Jeanfrançois Arcand has been working in software engineering for the last 18 years. He studied pure mathematics and worked for a Canadian research centre, doing mathematical modeling in C++ until someone in- troduced him to a new language called Java. He never stopped using it. Jeanfrançois worked for Sun Microsystems for almost 10 years, before writing one of the first NIO frameworks, Grizzly, Jeanfrançois also developed the Grizzly Comet Framework, which was an early way to implement asynchronous web applications. He then started the Atmosphere Framework, which brings portability across Servlet container and allows the creation of WebSocket and Comet applications. He can be followed on twitter at http://twitter.com/jfarcand

References

[1] http://jcp.org/en/jsr/detail?id=356

[2] http://github.com/Atmosphere/atmosphere

[3] http://tools.ietf.org/html/rfc6455

[4] http://github.com/Atmosphere/wasync

Jean Francis Arcand

What do you think?

JAX Magazine - 2014 - 03 Exclucively for iPad users JAX Magazine on Android

Comments

Latest opinions