Sock it to 'em
JSR 356 Java API for WebSocket or Atmosphere?

How does the new Java API for WebSockets compare to the Atmosphere framework? Jean Francis Arcand compares the two.
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.
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) { } } EndWith 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) { } } EndThere 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) { } } EndAnother 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) { ... } } EndNot 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); } EndYou 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); } EndTalking 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; } } EndJSR 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); } } EndThe 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); } } EndThe 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) { } } Endwith 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) { } } EndThe 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(...); EndWith 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(...); EndThe 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(...); EndAgain, 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.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
0 Comments