Sock it to 'em

JSR 356 Java API for WebSocket or Atmosphere?

JeanFrançois Arcand
socket

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.

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.

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

Author
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.
Comments
comments powered by Disqus