Chapter 4. WebSocket API Endpoints, Sessions and MessageHandlers

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.

4.1. Endpoint Classes

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.

4.1.1. jakarta.websocket.server.ServerEndpoint

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);
    }
}


4.1.1.1. value

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) {
        ...
    }
}


4.1.1.2. decoders

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.
    }
}


4.1.1.3. encoders

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.
    }
}


4.1.1.4. subprotocols

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.

4.1.1.5. configurator

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
    }
}

4.1.2. jakarta.websocket.ClientEndpoint

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));
    }
}


4.1.2.1. decoders

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.

4.1.2.2. encoders

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.

4.1.2.3. subprotocols

List of names (Strings) of supported sub-protocols.

4.1.2.4. configurator

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
    }
}

4.2. Endpoint method-level annotations

4.2.1. @OnOpen

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();
    }
}


4.2.2. @OnClose

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.
    }
}


4.2.3. @OnError

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();
    }
}


4.2.4. @OnMessage

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);
    }
}


4.3. MessageHandlers

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.