JSR 356, Java API for WebSocket
by Johan Vos
Learn how to integrate WebSockets into your applications.
Published April 2013
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
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:
- A
javax.websocket.Session
parameter, specifying the createdSession
- 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()); }
A 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 Session
instances 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. - Zero or more string parameters annotated with
@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. - A
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.
The 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 onOpen
, onClose
, 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 partsReader
to receive the whole message as a blocking stream- any object parameter for which the endpoint has a text decoder (
Decoder.Text
orDecoder.TextStream
).
- if the method is handling binary messages:
byte[]
orByteBuffer
to receive the whole messagebyte[]
and boolean pair, orByteBuffer
and boolean pair to receive the message in partsInputStream
to receive the whole message as a blocking stream- any object parameter for which the endpoint has a binary decoder (
Decoder.Binary
orDecoder.BinaryStream
).
- if the method is handling pong messages:
PongMessage
for 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.
@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 } }
Listing 1
The Encoder
interface has a number of subinterfaces:
Encoder.Text
for converting Java objects into text messagesEncoder.TextStream
for adding Java objects to a character streamEncoder.Binary
for converting Java objects into binary messagesEncoder.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 objectDecoder.TextStream
for reading a Java object from a character streamDecoder.Binary
for converting a binary message into a Java objectDecoder.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.
See Also
- JSR 356 on jcp.org
- JSR 356 specification work on java.net
- Tyrus (JSR 356 Reference Implementation) on java.net
- JSR 342, the Java EE 7 umbrella specification on jcp.org
- GlassFish, the Reference Implementation for Java EE
About the Author
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.
Join the Conversation
Join the Java community conversation on Facebook, Twitter, and the Oracle Java Blog!