Tutorial Series

JSR 356, Java API for WebSocket

JohanVos
socket

Johan Vos shows us how to integrate WebSockets into your applications, with one of Java EE 7’s newest JSRs

Johan Vos shows us how to integrate WebSockets into your
applications, with one of Java EE 7’s newest
JSRs. 
Reprinted with permission from the Oracle
Technology Network, Oracle Corporation.

For many Web-based client-server applications, the old HTTP
request-response model has its limitations. Information has to be
transmitted from the server to the client in between requests,
rather than upon request only.

A number of “hacks” have been used in the past to circumvent
this problem, for example, long polling and Comet. However, the
need for a standards-based, bidirectional and full-duplex channel
between clients and a server has only increased.

In 2011, the IETF standardized the WebSocket protocol as RFC
6455. Since then, the majority of the Web browsers are implementing
client APIs that support the WebSocket protocol. Also, a number of
Java libraries have been developed that implement the WebSocket
protocol.

The WebSocket protocol leverages the HTTP upgrade technology to
upgrade an HTTP connection to a WebSocket. Once it is upgraded, the
connection is capable of sending messages (data frames) in both
directions, independent of each other (full duplex). No headers or
cookies are required, which considerably lowers the required
bandwidth. Typically, WebSockets are used to periodically send
small messages (for example, a few bytes). Additional headers would
often make the overhead larger than the payload.

JSR 356

JSR 356, Java API for WebSocket, specifies the API that Java
developers can use when they want to integrate WebSockets into
their applications—both on the server side as well as on the Java
client side. Every implementation of the WebSocket protocol that
claims to be compliant with JSR 356 must implement this API. As a
consequence, developers can write their WebSocket-based
applications independent of the underlying WebSocket
implementation. This is a huge benefit, because it prevents a
vendor-lock and allows for more choices and freedom of libraries
and application servers.

JSR 356 is a part of the upcoming Java EE 7 standard; hence, all
Java EE 7–compliant application servers will have an implementation
of the WebSocket protocol that adheres to the JSR 356 standard.
Once they are established, WebSocket client and server peers are
symmetrical. The difference between a client API and a server API
is, therefore, minimal. JSR 356 defines a Java client API as well,
which is a subset of the full API required in Java EE 7.

A client-server application leveraging WebSockets typically
contains a server component and one or more client components, as
shown in Figure 1:

Figure 1 – A client-server
application leveraging WebSockets

In this example, the server application is written in Java, and
the WebSocket protocol details are handled by the JSR 356
implementation contained in the Java EE 7 container.

A JavaFX client can rely on any JSR 356–compliant client
implementation for handling the WebSocket-specific protocol issues.
Other clients (for example, an iOS client and an HTML5 client) can
use other (non-Java) implementations that are compliant with RFC
6455 in order to communicate with the server application.

Programming Model

The Expert Group that defined JSR 356 wanted to support patterns
and techniques that are common to Java EE developers. As a
consequence, JSR 356 leverages annotations and injection.

In general, two different programming models are supported:

  • Annotation-driven. Using annotated POJOs, developers can
    interact with the WebSocket lifecycle events.

  • Interface-driven. Developers can implement
    the Endpoint interface
    and the methods that interact with the lifecycle events.

Lifecycle Events

The typical lifecycle event of a WebSocket interaction goes as
follows:

  • One peer (a client) initiates the connection by sending an HTTP
    handshake request.

  • The other peer (the server) replies with a handshake
    response.

  • The connection is established. From now on, the connection is
    completely symmetrical.

  • Both peers send and receive messages.

  • One of the peers closes the connection.

Most of the WebSocket lifecycle events can be mapped to Java
methods, both in the annotation-driven and interface-driven
approaches.

Annotation-Driven Approach

An endpoint that is accepting incoming WebSocket requests can be
a POJO annotated with the @ServerEndpoint annotation.
This annotation tells the container that the given class should be
considered to be a WebSocket endpoint. The
required value element
specifies the path of the WebSocket endpoint.

Consider the following code snippet:

@ServerEndpoint("/hello") 
public class MyEndpoint { }

This code will publish an endpoint at the relative
path hello.
The path can include path parameters that are used in subsequent
method calls; for example, /hello/{userid} is
a valid path, where the value of {userid} can
be obtained in lifecycle method calls using the@PathParam annotation.

In GlassFish, if your application is deployed with the
contextroot mycontextroot in
a Web container listening at port 8080 oflocalhost,
the WebSocket will be accessible using ws://localhost:8080/mycontextroot/hello

An endpoint that should initiate a WebSocket connection can be a
POJO annotated with the @ClientEndpoint annotation.
The main difference between @ClientEndpoint and
ServerEndpoint is
that the ClientEndpoint does
not accept a path value element, because it is not listening to
incoming requests.

@ClientEndpoint 
public class MyClientEndpoint {}

Initiating a WebSocket connection in Java leveraging the
annotation-driven POJO approach can be done as follows:

javax.websocket.WebSocketContainer container = 
javax.websocket.ContainerProvider.getWebSocketContainer();

container.conntectToServer(MyClientEndpoint.class, 
new URI("ws://localhost:8080/tictactoeserver/endpoint"));

Hereafter, classes annotated with @ServerEndpoint or @ClientEndpoint will
be called annotated endpoints.

Once a WebSocket connection has been established,
Session is
created and the method annotated with @OnOpen on
the annotated endpoint will be called. This method can contain a
number of parameters:

  • javax.websocket.Session parameter,
    specifying the created Session

  • An EndpointConfig instance
    containing information about the endpoint configuration

  • Zero or more string parameters annotated
    with @PathParam,
    referring to path parameters on the endpoint path

The following method implementation will print the identifier of
the session when a WebSocket is “opened”:

@OnOpen
public void myOnOpen (Session session) {
   System.out.println ("WebSocket opened: "+session.getId());
}

Session instance
is valid as long as the WebSocket is not closed.
The Session class
contains a number of interesting methods that allow developers to
obtain more information about the connection. Also,
the Session contains
a hook to application-specific data, by means of
the getUserProperties() method
returning a Map<String,
Object>
. This allows developers to
populate Sessioninstances
with session- and application-specific information that should be
shared among method invocations.

When the WebSocket endpoint receives a message, the method
annotated with @OnMessage will
be called. A method annotated with@OnMessage can
contain the following parameters:

  • The javax.websocket.Session parameter.

  • @PathParam,
    referring to path parameters on the endpoint path.
  • The message itself. See below for an overview of possible
    message types.

When a text message has been sent by the other peer, the content
of the message will be printed by the following code snippet:

@OnMessage
public void myOnMessage (String txt) {
   System.out.println ("WebSocket received message: "+txt);
} 

If the return type of the method annotated
with @OnMessage is
not void,
the WebSocket implementation will send the return value to the
other peer. The following code snippet returns the received text
message in capitals back to the sender:

@OnMessage
public String myOnMessage (String txt) {
   return txt.toUpperCase();
} 

Another way of sending messages over a WebSocket connection is
shown below:

RemoteEndpoint.Basic other = session.getBasicRemote();
other.sendText ("Hello, world");

In this approach, we start from the Session object,
which can be obtained from the lifecycle callback methods (for
example, the method annotated with @OnOpen).
The getBasicRemote() method
on the Session instance
returns a representation of the other part of the WebSocket,
the RemoteEndpoint.
That RemoteEndpoint instance
can be used for sending text or other types of messages, as
described below.

When the WebSocket connection is closing, the method annotated
with @OnClose is
called. This method can take the following parameters:

  • The javax.websocket.Session parameter.
    Note that this parameter cannot be used once the WebSocket is
    really closed, which happens after the @OnClose annotated
    method returns.

  • javax.websocket.CloseReason parameter
    describing the reason for closing the WebSocket, for example,
    normal closure, protocol error, overloaded service, and so on.

  • Zero or more string parameters annotated
    with @PathParam,
    referring to path parameters on the endpoint path.

Thee following code snippet will print the reason why a
WebSocket is closing:

@OnClose
public void myOnClose (CloseReason reason) {
   System.out.prinlnt ("Closing a WebSocket due to "+reason.getReasonPhrase());
}

To be complete, there is one more lifecycle annotation: in case an
error is received, the method annotated
with @OnError will
be called.

Interface-driven approach

The annotation-driven approach allows us to annotate a Java
class and methods with lifecycle annotations. Using the
interface-driven approach, a developer
extends javax.websocket.Endpoint and
overrides the onOpenonClose,
and onError methods:

public class myOwnEndpoint extends javax.websocket.Endpoint {
   public void onOpen(Session session, EndpointConfig config) {...}
   public void onClose(Session session, CloseReason closeReason) {...}
   public void onError (Session session, Throwable throwable) {...}
}

In order to intercept messages, a javax.websocket.MessageHandler needs
to be registered in the onOpen implementation:

public void onOpen (Session session, EndpointConfig config) {
   session.addMessageHandler (new MessageHandler() {...});
}

MessageHandler is
an interface with two subinterfaces: MessageHandler.Partial and MessageHandler.Whole.
TheMessageHandler.Partial interface
should be used when the developer wants to be notified about
partial deliveries of messages, and an implementation
of MessageHandler.Whole should
be used for notification about the arrival of a complete
message.

The following code snippet listens to incoming text messages and
sends the uppercase version of the text message back to the other
peer:

public void onOpen (Session session, EndpointConfig config) {
   final RemoteEndpoint.Basic remote = session.getBasicRemote();
   session.addMessageHandler (new MessageHandler.Whole<String>() {
      public void onMessage(String text) {
                 try {
                     remote.sendString(text.toUpperCase());
                 } catch (IOException ioe) {
                     // handle send failure here
                 }
             }

   });
}

Message Types, Encoders and Decoders

The Java API for WebSocket is very powerful, because it allows
any Java object to be sent or received as a WebSocket message.

Basically, there are three different types of messages:

  • Text-based messages

  • Binary messages

  • Pong messages, which are about the WebSocket connection
    itself

When using the interface-driven model, each session can register
at most one MessageHandler for
each of these three different types of messages.

When using the annotation-driven model, for each different type
of message, one @onMessage annotated
method is allowed. The allowed parameters for specifying the
message content in the annotated methods are dependent on the type
of the message.

The Javadoc
for the 
@OnMessage annotation
 clearly
specifies the allowed message parameters based on the message type
(the following is quoted from the Javadoc):

  • “if the method is handling text messages: 


    • String
       to
      receive the whole message

    • Java primitive or class equivalent to receive the whole message
      converted to that type

    • String and
      boolean pair to receive the message in parts


    • Reader
       to
      receive the whole message as a blocking stream

    • any object parameter for which the endpoint has a text decoder
      (Decoder.Text or Decoder.TextStream).

  • if the method is handling binary messages: 

  • if the method is handling pong messages: 

Any Java object can be encoded into a text-based or binary
message using an encoder. This text-based or binary message is
transmitted to the other peer, where it can be decoded into a Java
object again—or it can be interpreted by another WebSocket library.
Often, XML or JSON is used for the transmission of WebSocket
messages, and the encoding/decoding then comes down to marshaling a
Java object into XML or JSON and back.

An encoder is defined as an implementation of
the javax.websocket.Encoder interface,
and a decoder is an implementation of thejavax.websocket.Decoder interface.
Somehow, the endpoint instances need to know what the possible
encoders and decoders are. Using the annotation-driven approach, a
list of encoders and decoders is passed via the encoder and decoder
elements in the@ClientEndpoint and @ServerEndpoint annotations.

The code in Listing 1 shows how to register
MessageEncoder class
that defines the conversion of an instance
of MyJavaObject to
a text message. A MessageDecoder class
is registered for the opposite conversion.

Listing 1

@ServerEndpoint(value="/endpoint", encoders = MessageEncoder.class, decoders= MessageDecoder.class)
public class MyEndpoint {
...
}

class MessageEncoder implements Encoder.Text<MyJavaObject> {
   @override
   public String encode(MyJavaObject obj) throws EncodingException {
      ...
   }
}

class MessageDecoder implements Decoder.Text<MyJavaObject> {
   @override 
   public MyJavaObject decode (String src) throws DecodeException {
      ...
   }

   @override 
   public boolean willDecode (String src) {
      // return true if we want to decode this String into a MyJavaObject instance
   }
}

The Encoder interface
has a number of subinterfaces:

  • Encoder.Text for
    converting Java objects into text messages

  • Encoder.TextStream for
    adding Java objects to a character stream

  • Encoder.Binary for
    converting Java objects into binary messages

  • Encoder.BinaryStream for
    adding Java objects to a binary stream

Similarly, the Decoder interface
has four subinterfaces:

  • Decoder.Text for
    converting a text message into a Java object

  • Decoder.TextStream for
    reading a Java object from a character stream

  • Decoder.Binary for
    converting a binary message into a Java object

  • Decoder.BinaryStream for
    reading a Java object from a binary stream

Conclusion

The Java API for WebSocket provides Java developers with a
standard API to integrate with the IETF WebSocket standard. By
doing so, Web clients or native clients leveraging any WebSocket
implementation can easily communicate with a Java back end.

The Java API is highly configurable and flexible, and it allows
Java developers to use their preferred patterns.

Further reading

Author Bio: Johan Vos started working with Java in 1995. He
is a cofounder of LodgON, where he is working on Java-based
solutions for social networking software. An enthusiast of both
embedded and enterprise development, Vos focuses on end-to-end Java
using JavaFX and Java EE. Learn more at his 
blog or follow him at http://twitter.com/johanvos.

Reprinted with permission from the Oracle Technology
Network, Oracle Corporation

Author
JohanVos
Johan Vos started working with Java in 1995, as part of his PhD research. He joined the Blackdown team and ported Java 1.2 to Linux/SPARC. Johan has been a Java consultant ever since, and worked for a number of companies (e.g. The Reference, Acunia). He co-founded LodgON, where the main focus is on social networking software and recently became a Java Champion. He was part of the Core Platform Expert Group of OSGi that created the OSGi platform specifications. His main technology interests are currently GlassFish and JavaFX. He holds an Msc in civil engineering (mining) and a PhD in Applied Physics.
Comments
comments powered by Disqus