Table of Contents
This chapter presents an overview of the core WebSocket API concepts - endpoints, configurations and message handlers.
The JAVA API for WebSocket specification draft can be found online here.
Server endpoint classes
are POJOs (Plain Old Java Objects) that are annotated with jakarta.websocket.server.ServerEndpoint
.
Similarly, client endpoint classes are POJOs annotated with jakarta.websocket.ClientEndpoint.
This section shows how to use Tyrus to annotate Java objects to create WebSocket web services.
The following code example is a simple example of a WebSocket endpoint using annotations. The example code shown here is from echo sample which ships with Tyrus.
Example 4.1. Echo sample server endpoint.
@ServerEndpoint("/echo") public class EchoEndpoint { @OnOpen public void onOpen(Session session) throws IOException { session.getBasicRemote().sendText("onOpen"); } @OnMessage public String echo(String message) { return message + " (from your server)"; } @OnError public void onError(Throwable t) { t.printStackTrace(); } @OnClose public void onClose(Session session) { } }
Let's explain the JAVA API for WebSocket annotations.
jakarta.websocket.server.ServerEndpoint has got one mandatory field - value and four optional fields. See the example below.
Example 4.2. jakarta.websocket.server.ServerEndpoint with all fields specified
@ServerEndpoint( value = "/sample", decoders = ChatDecoder.class, encoders = DisconnectResponseEncoder.class, subprotocols = {"subprtotocol1", "subprotocol2"}, configurator = Configurator.class ) public class SampleEndpoint { @OnMessage public SampleResponse receiveMessage(SampleType message, Session session) { return new SampleResponse(message); } }
Denotes a relative URI path at which the server endpoint will be deployed. In the example
"jakarta.websocket.server.ServerEndpoint with all fields specified", the
Java class will be hosted at the URI path
/sample
. The field value must begin with a '/' and may or may
not end in a '/', it makes no difference. Thus request URLs that end or do not end in a '/' will both
be matched. WebSocket API for JAVA supports level 1 URI templates.
URI path templates are URIs with variables embedded within the URI syntax. These variables are substituted at runtime in order for a resource to respond to a request based on the substituted URI. Variables are denoted by curly braces. For example, look at the following @ServerEndpoint annotation:
@ServerEndpoint("/users/{username}")
In this type of example, a user will be prompted to enter their name, and then a Tyrus web
service configured
to respond to requests to this URI path template will respond. For example, if the user entered their
username as "Galileo", the web service will respond to the following URL:
http://example.com/users/Galileo
To obtain the value of the username variable the jakarta.websocket.server.PathParam
may be used on method parameter
of methods annotated with one of @OnOpen, @OnMessage, @OnError, @OnClose.
Example 4.3. Specifying URI path parameter
@ServerEndpoint("/users/{username}") public class UserEndpoint { @OnMessage public String getUser(String message, @PathParam("username") String userName) { ... } }
Contains list of classes that will be used to decode incoming messages for the endpoint. By decoding we mean transforming from text / binary websocket message to some user defined type. Each decoder needs to implement the Decoder interface.
SampleDecoder
in the following example decodes String message and produces
SampleType message - see decode method on line 4.
Example 4.4. SampleDecoder
public class SampleDecoder implements Decoder.Text<SampleType> { @Override public SampleType decode(String s) { return new SampleType(s); } @Override public boolean willDecode(String s) { return s.startsWith(SampleType.PREFIX); } @Override public void init(EndpointConfig config) { // do nothing. } @Override public void destroy() { // do nothing. } }
Contains list of classes that will be used to encode outgoing messages. By encoding we mean transforming message from user defined type to text or binary type. Each encoder needs to implement the Encoder interface.
SampleEncoder
in the following example decodes String message and produces
SampleType message - see decode method on line 4.
Example 4.5. SampleEncoder
public class SampleEncoder implements Encoder.Text<SampleType> { @Override public String encode(SampleType message) { return data.toString(); } @Override public void init(EndpointConfig config) { // do nothing. } @Override public void destroy() { // do nothing. } }
List of names (Strings) of supported sub-protocols. The first protocol in this list that matches with sub-protocols provided by the client side is used.
Users may provide their own implementation of ServerEndpointConfiguration.Configurator. It allows them to control some algorithms used by Tyrus in the connection initialization phase:
public String getNegotiatedSubprotocol(List<String> supported, List<String> requested)
allows the user to provide their own algorithm for selection of used subprotocol.
public List<Extension> getNegotiatedExtensions(List<Extension> installed, List<Extension> requested)
allows the user to provide their own algorithm for selection of used Extensions.
public boolean checkOrigin(String originHeaderValue)
.
allows the user to specify the origin checking algorithm.
public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response)
.
allows the user to modify the handshake response that will be sent back to the client.
public <T> T getEndpointInstance(Class<T> endpointClass) throws InstantiationException
.
allows the user to provide the way how the instance of an Endpoint is created
public class ConfiguratorTest extends ServerEndpointConfig.Configurator{ public String getNegotiatedSubprotocol(List<String> supported, List<String> requested) { // Plug your own algorithm here } public List<Extension> getNegotiatedExtensions(List<Extension> installed, List<Extension> requested) { // Plug your own algorithm here } public boolean checkOrigin(String originHeaderValue) { // Plug your own algorithm here } public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) { // Plug your own algorithm here } public <T> T getEndpointInstance(Class<T> endpointClass) throws InstantiationException { // Plug your own algorithm here } }
The @ClientEndpoint class-level annotation is used to turn a POJO into WebSocket client endpoint. In the following sample the client sends text message "Hello!" and prints out each received message.
Example 4.6. SampleClientEndpoint
@ClientEndpoint( decoders = SampleDecoder.class, encoders = SampleEncoder.class, subprotocols = {"subprtotocol1", "subprotocol2"}, configurator = ClientConfigurator.class) public class SampleClientEndpoint { @OnOpen public void onOpen(Session p) { try { p.getBasicRemote().sendText("Hello!"); } catch (IOException e) { e.printStackTrace(); } } @OnMessage public void onMessage(String message) { System.out.println(String.format("%s %s", "Received message: ", message)); } }
Contains list of classes that will be used decode incoming messages for the endpoint. By decoding we mean transforming from text / binary websocket message to some user defined type. Each decoder needs to implement the Decoder interface.
Contains list of classes that will be used to encode outgoing messages. By encoding we mean transforming message from user defined type to text or binary type. Each encoder needs to implement the Encoder interface.
Users may provide their own implementation of ClientEndpointConfiguration.Configurator. It allows them to control some algorithms used by Tyrus in the connection initialization phase. Method beforeRequest allows the user to change the request headers constructed by Tyrus. Method afterResponse allows the user to process the handshake response.
public class Configurator { public void beforeRequest(Map<String, List<String>> headers) { //affect the headers before request is sent } public void afterResponse(HandshakeResponse hr) { //process the handshake response } }
This annotation may be used on certain methods of @ServerEndpoint or @ClientEndpoint, but only once per endpoint. It is used to decorate a method which is called once new connection is established. The connection is represented by the optional Session parameter. The other optional parameter is EndpointConfig, which represents the passed configuration object. Note that the EndpointConfig allows the user to access the user properties.
Example 4.7. @OnOpen with Session and EndpointConfig parameters.
@ServerEndpoint("/sample") public class EchoEndpoint { private Map<String, Object> properties; @OnOpen public void onOpen(Session session, EndpointConfig config) throws IOException { session.getBasicRemote().sendText("onOpen"); properties = config.getUserProperties(); } }
This annotation may be used on any method of @ServerEndpoint or @ClientEndpoint, but only once per endpoint. It is used to decorate a method which is called once the connection is being closed. The method may have one Session parameter, one CloseReason parameter and parameters annotated with @PathParam.
Example 4.8. @OnClose with Session and CloseReason parameters.
@ServerEndpoint("/sample") public class EchoEndpoint { @OnClose public void onClose(Session session, CloseReason reason) throws IOException { //prepare the endpoint for closing. } }
This annotation may be used on any method of @ServerEndpoint or @ClientEndpoint, but only once per endpoint. It is used to decorate a method which is called once Exception is being thrown by any method annotated with @OnOpen, @OnMessage and @OnClose. The method may have optional Session parameter and Throwable parameters.
Example 4.9. @OnError with Session and Throwable parameters.
@ServerEndpoint("/sample") public class EchoEndpoint { @OnError public void onError(Session session, Throwable t) { t.printStackTrace(); } }
This annotation may be used on certain methods of @ServerEndpoint or @ClientEndpoint, but only once per endpoint. It is used to decorate a method which is called once new message is received.
Example 4.10. @OnError with Session and Throwable parameters.
@ServerEndpoint("/sample") public class EchoEndpoint { @OnMessage public void onMessage(Session session, String message) { System.out.println("Received message: " + message); } }
Implementing the jakarta.websocket.MessageHandler
interface is one of the ways how to receive messages
on endpoints (both server and client). It is aimed primarily on programmatic endpoints, as the annotated ones
use the method level annotation jakarta.websocket.OnMessage
to denote the method which
receives messages.
The MessageHandlers get registered on the Session instance:
Example 4.11. MessageHandler basic example
public class MyEndpoint extends Endpoint { @Override public void onOpen(Session session, EndpointConfig EndpointConfig) { session.addMessageHandler(new MessageHandler.Whole<String>() { @Override public void onMessage(String message) { System.out.println("Received message: "+message); } }); } }
There are two orthogonal criterions which classify MessageHandlers.
According the WebSocket Protocol (RFC 6455) the message may be sent either complete, or in chunks. In Java API for WebSocket this fact is reflected
by the interface which the handler implements. Whole messages are processed by handler which implements
jakarta.websocket.MessageHandler.Whole
interface. Partial
messages are processed by handlers that implement jakarta.websocket.MessageHandler.Partial
interface. However, if user registers just the whole message handler, it doesn't mean that the handler will
process solely whole messages. If partial message is received, the parts are cached by Tyrus until the final
part is received. Then the whole message is passed to the handler. Similarly, if the user registers just the
partial message handler and whole message is received, it is passed directly to the handler.
The second criterion is the data type of the message. WebSocket Protocol (RFC 6455) defines four message data type - text message, According to Java API for WebSocket the text messages will be processed by MessageHandlers with the following types:
java.lang.String
java.io.Reader
any developer object for which there is a corresponding jakarta.websocket.Decoder.Text or jakarta.websocket.Decoder.TextStream.
The binary messages will be processed by MessageHandlers with the following types:
java.nio.ByteBuffer
java.io.InputStream
any developer object for which there is a corresponding jakarta.websocket.Decoder.Binary or jakarta.websocket.Decoder.BinaryStream.
The Java API for WebSocket limits the registration of MessageHandlers per Session to be one MessageHandler per native websocket message type. In other words, the developer can only register at most one MessageHandler for incoming text messages, one MessageHandler for incoming binary messages, and one MessageHandler for incoming pong messages. This rule holds for both whole and partial message handlers, i.e there may be one text MessageHandler - either whole, or partial, not both.