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 a 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, a 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 a 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