Jakarta WebSocket

This chapter describes Jakarta WebSocket, which provides support for creating WebSocket applications. WebSocket is an application protocol that provides full-duplex communications between two peers over the TCP protocol.

Introduction to WebSocket

In the traditional request-response model used in HTTP, the client requests resources, and the server provides responses. The exchange is always initiated by the client; the server cannot send any data without the client requesting it first. This model worked well for the World Wide Web when clients made occasional requests for documents that changed infrequently, but the limitations of this approach are increasingly relevant as content changes quickly and users expect a more interactive experience on the Web. The WebSocket protocol addresses these limitations by providing a full-duplex communication channel between the client and the server. Combined with other client technologies, such as JavaScript and HTML5, WebSocket enables web applications to deliver a richer user experience.

In a WebSocket application, the server publishes a WebSocket endpoint, and the client uses the endpoint’s URI to connect to the server. The WebSocket protocol is symmetrical after the connection has been established; the client and the server can send messages to each other at any time while the connection is open, and they can close the connection at any time. Clients usually connect only to one server, and servers accept connections from multiple clients.

The WebSocket protocol has two parts: handshake and data transfer. The client initiates the handshake by sending a request to a WebSocket endpoint using its URI. The handshake is compatible with existing HTTP-based infrastructure: web servers interpret it as an HTTP connection upgrade request. An example handshake from a client looks like this:

GET /path/to/websocket/endpoint HTTP/1.1
Host: localhost
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: xqBt3ImNzJbYqRINxEFlkg==
Origin: http://localhost
Sec-WebSocket-Version: 13

An example handshake from the server in response to the client looks like this:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: K7DJLdLooIwIG/MOpvWFB3y3FE8=

The server applies a known operation to the value of the Sec-WebSocket-Key header to generate the value of the Sec-WebSocket-Accept header. The client applies the same operation to the value of the Sec-WebSocket-Key header, and the connection is established successfully if the result matches the value received from the server. The client and the server can send messages to each other after a successful handshake.

WebSocket supports text messages (encoded as UTF-8) and binary messages. The control frames in WebSocket are close, ping, and pong (a response to a ping frame). Ping and pong frames may also contain application data.

WebSocket endpoints are represented by URIs that have the following form:

ws://host:port/path?query
wss://host:port/path?query

The ws scheme represents an unencrypted WebSocket connection, and the wss scheme represents an encrypted connection. The port component is optional; the default port number is 80 for unencrypted connections and 443 for encrypted connections. The path component indicates the location of an endpoint within a server. The query component is optional.

Modern web browsers implement the WebSocket protocol and provide a JavaScript API to connect to endpoints, send messages, and assign callback methods for WebSocket events (such as opened connections, received messages, and closed connections).

Creating WebSocket Applications in the Jakarta EE Platform

The Jakarta EE platform includes Jakarta WebSocket, which enables you to create, configure, and deploy WebSocket endpoints in web applications. The WebSocket client API specified in Jakarta WebSocket also enables you to access remote WebSocket endpoints from any Java application.

Jakarta WebSocket consists of the following packages.

  • The jakarta.websocket.server package contains annotations, classes, and interfaces to create and configure server endpoints.

  • The jakarta.websocket package contains annotations, classes, interfaces, and exceptions that are common to client and server endpoints.

WebSocket endpoints are instances of the jakarta.websocket.Endpoint class. Jakarta WebSocket enables you to create two kinds of endpoints: programmatic endpoints and annotated endpoints. To create a programmatic endpoint, you extend the Endpoint class and override its lifecycle methods. To create an annotated endpoint, you decorate a Java class and some of its methods with the annotations provided by the packages mentioned previously. After you have created an endpoint, you deploy it to an specific URI in the application so that remote clients can connect to it.

In most cases, it is easier to create and deploy an annotated endpoint than a programmatic endpoint. This chapter provides a simple example of a programmatic endpoint, but it focuses on annotated endpoints.

Creating and Deploying a WebSocket Endpoint

The process for creating and deploying a WebSocket endpoint:

  1. Create an endpoint class.

  2. Implement the lifecycle methods of the endpoint.

  3. Add your business logic to the endpoint.

  4. Deploy the endpoint inside a web application.

The process is slightly different for programmatic endpoints and annotated endpoints, and it is covered in detail in the following sections.

As opposed to servlets, WebSocket endpoints are instantiated multiple times. The container creates an instance of an endpoint per connection to its deployment URI. Each instance is associated with one and only one connection. This facilitates keeping user state for each connection and makes development easier, because there is only one thread executing the code of an endpoint instance at any given time.

Programmatic Endpoints

The following example shows how to create an endpoint by extending the Endpoint class:

public class EchoEndpoint extends Endpoint {
   @Override
   public void onOpen(final Session session, EndpointConfig config) {
      session.addMessageHandler(new MessageHandler.Whole<String>() {
         @Override
         public void onMessage(String msg) {
            try {
               session.getBasicRemote().sendText(msg);
            } catch (IOException e) { ... }
         }
      });
   }
}

This endpoint echoes every message received. The Endpoint class defines three lifecycle methods: onOpen, onClose, and onError. The EchoEndpoint class implements the onOpen method, which is the only abstract method in the Endpoint class.

The Session parameter represents a conversation between this endpoint and the remote endpoint. The addMessageHandler method registers message handlers, and the getBasicRemote method returns an object that represents the remote endpoint. The Session interface is covered in detail in the rest of this chapter.

The message handler is implemented as an anonymous inner class. The onMessage method of the message handler is invoked when the endpoint receives a text message.

To deploy this programmatic endpoint, use the following code in your Jakarta EE application:

ServerEndpointConfig.Builder.create(EchoEndpoint.class, "/echo").build();

When you deploy your application, the endpoint is available at ws://<host>:<port>/<application>/echo; for example, ws://localhost:8080/echoapp/echo.

Annotated Endpoints

The following example shows how to create the same endpoint from Programmatic Endpoints using annotations instead:

@ServerEndpoint("/echo")
public class EchoEndpoint {
   @OnMessage
   public void onMessage(Session session, String msg) {
      try {
         session.getBasicRemote().sendText(msg);
      } catch (IOException e) { ... }
   }
}

The annotated endpoint is simpler than the equivalent programmatic endpoint, and it is deployed automatically with the application to the relative path defined in the ServerEndpoint annotation. Instead of having to create an additional class for the message handler, this example uses the OnMessage annotation to designate the method invoked to handle messages.

WebSocket Endpoint Lifecycle Annotations lists the annotations available in the jakarta.websocket package to designate the methods that handle lifecycle events. The examples in the table show the most common parameters for these methods. See the API reference for details on what combinations of parameters are allowed in each case.

WebSocket Endpoint Lifecycle Annotations
Annotation Event Example

OnOpen

Connection opened

@OnOpen
public void open(Session session,
                 EndpointConfig conf) { }

OnMessage

Message received

@OnMessage
public void message(Session session,
                    String msg) { }

OnError

Connection error

@OnError
public void error(Session session,
                  Throwable error) { }

OnClose

Connection closed

@OnClose
public void close(Session session,
                  CloseReason reason) { }

Sending and Receiving Messages

WebSocket endpoints can send and receive text and binary messages. In addition, they can also send ping frames and receive pong frames. This section describes how to use the Session and RemoteEndpoint interfaces to send messages to the connected peer and how to use the OnMessage annotation to receive messages from it.

Sending Messages

Follow these steps to send messages in an endpoint.

  1. Obtain the Session object from the connection.

    The Session object is available as a parameter in the annotated lifecycle methods of the endpoint, like those in Websocket Endpoint Lifecycle Annotations. When your message is a response to a message from the peer, you have the Session object available inside the method that received the message (the method annotated with @OnMessage). If you have to send messages that are not responses, store the Session object as an instance variable of the endpoint class in the method annotated with @OnOpen so that you can access it from other methods.

  2. Use the Session object to obtain a RemoteEndpoint object.

    The Session.getBasicRemote method and the Session.getAsyncRemote method return RemoteEndpoint.Basic and RemoteEndpoint.Async objects respectively. The RemoteEndpoint.Basic interface provides blocking methods to send messages; the RemoteEndpoint.Async interface provides nonblocking methods.

  3. Use the RemoteEndpoint object to send messages to the peer.

    The following list shows some of the methods you can use to send messages to the peer.

    • void RemoteEndpoint.Basic.sendText(String text)

      Send a text message to the peer. This method blocks until the whole message has been transmitted.

    • void RemoteEndpoint.Basic.sendBinary(ByteBuffer data)

      Send a binary message to the peer. This method blocks until the whole message has been transmitted.

    • void RemoteEndpoint.sendPing(ByteBuffer appData)

      end a ping frame to the peer.

    • void RemoteEndpoint.sendPong(ByteBuffer appData)

      Send a pong frame to the peer.

The example in Annotated Endpoints demonstrates how to use this procedure to reply to every incoming text message.

Sending Messages to All Peers Connected to an Endpoint

Each instance of an endpoint class is associated with one and only one connection and peer; however, there are cases in which an endpoint instance needs to send messages to all connected peers. Examples include chat applications and online auctions. The Session interface provides the getOpenSessions method for this purpose. The following example demonstrates how to use this method to forward incoming text messages to all connected peers:

@ServerEndpoint("/echoall")
public class EchoAllEndpoint {
   @OnMessage
   public void onMessage(Session session, String msg) {
      try {
         for (Session sess : session.getOpenSessions()) {
            if (sess.isOpen())
               sess.getBasicRemote().sendText(msg);
         }
      } catch (IOException e) { ... }
   }
}

Receiving Messages

The OnMessage annotation designates methods that handle incoming messages. You can have at most three methods annotated with @OnMessage in an endpoint, one for each message type: text, binary, and pong. The following example demonstrates how to designate methods to receive all three types of messages:

@ServerEndpoint("/receive")
public class ReceiveEndpoint {
   @OnMessage
   public void textMessage(Session session, String msg) {
      System.out.println("Text message: " + msg);
   }
   @OnMessage
   public void binaryMessage(Session session, ByteBuffer msg) {
      System.out.println("Binary message: " + msg.toString());
   }
   @OnMessage
   public void pongMessage(Session session, PongMessage msg) {
      System.out.println("Pong message: " +
                          msg.getApplicationData().toString());
   }
}

Maintaining Client State

Because the container creates an instance of the endpoint class for every connection, you can define and use instance variables to store client state information. In addition, the Session.getUserProperties method provides a modifiable map to store user properties. For example, the following endpoint replies to incoming text messages with the contents of the previous message from each client:

@ServerEndpoint("/delayedecho")
public class DelayedEchoEndpoint {
   @OnOpen
   public void open(Session session) {
      session.getUserProperties().put("previousMsg", " ");
   }
   @OnMessage
   public void message(Session session, String msg) {
      String prev = (String) session.getUserProperties()
                                    .get("previousMsg");
      session.getUserProperties().put("previousMsg", msg);
      try {
         session.getBasicRemote().sendText(prev);
      } catch (IOException e) { ... }
   }
}

To store information common to all connected clients, you can use class (static) variables; however, you are responsible for ensuring thread-safe access to them.

Using Encoders and Decoders

Jakarta WebSocket provides support for converting between WebSocket messages and custom Java types using encoders and decoders. An encoder takes a Java object and produces a representation that can be transmitted as a WebSocket message; for example, encoders typically produce JSON, XML, or binary representations. A decoder performs the reverse function; it reads a WebSocket message and creates a Java object.

This mechanism simplifies WebSocket applications, because it decouples the business logic from the serialization and deserialization of objects.

Implementing Encoders to Convert Java Objects into WebSocket Messages

The procedure to implement and use encoders in endpoints follows.

  1. Implement one of the following interfaces:

    • Encoder.Text<T> for text messages

    • Encoder.Binary<T> for binary messages

      These interfaces specify the encode method. Implement an encoder class for each custom Java type that you want to send as a WebSocket message.

  2. Add the names of your encoder implementations to the encoders optional parameter of the ServerEndpoint annotation.

  3. Use the sendObject(Object data) method of the RemoteEndpoint.Basic or RemoteEndpoint.Async interfaces to send your objects as messages. The container looks for an encoder that matches your type and uses it to convert the object to a WebSocket message.

For example, if you have two Java types (MessageA and MessageB) that you want to send as text messages, implement the Encoder.Text<MessageA> and Encoder.Text<MessageB> interfaces as follows:

public class MessageATextEncoder implements Encoder.Text<MessageA> {
   @Override
   public void init(EndpointConfig ec) { }
   @Override
   public void destroy() { }
   @Override
   public String encode(MessageA msgA) throws EncodeException {
      // Access msgA's properties and convert to JSON text...
      return msgAJsonString;
   }
}

Implement Encoder.Text<MessageB> similarly. Then, add the encoders parameter to the ServerEndpoint annotation as follows:

@ServerEndpoint(
   value = "/myendpoint",
   encoders = { MessageATextEncoder.class, MessageBTextEncoder.class }
)
public class EncEndpoint { ... }

Now, you can send MessageA and MessageB objects as WebSocket messages using the sendObject method as follows:

MessageA msgA = new MessageA(...);
MessageB msgB = new MessageB(...);
session.getBasicRemote.sendObject(msgA);
session.getBasicRemote.sendObject(msgB);

As in this example, you can have more than one encoder for text messages and more than one encoder for binary messages. Like endpoints, encoder instances are associated with one and only one WebSocket connection and peer, so there is only one thread executing the code of an encoder instance at any given time.

Implementing Decoders to Convert WebSocket Messages into Java Objects

The procedure to implement and use decoders in endpoints follows.

  1. Implement one of the following interfaces:

    • Decoder.Text<T> for text messages

    • Decoder.Binary<T> for binary messages

      These interfaces specify the willDecode and decode methods.

      Unlike with encoders, you can specify at most one decoder for binary messages and one decoder for text messages.
  2. Add the names of your decoder implementations to the decoders optional parameter of the ServerEndpoint annotation.

  3. Use the OnMessage annotation in the endpoint to designate a method that takes your custom Java type as a parameter. When the endpoint receives a message that can be decoded by one of the decoders you specified, the container calls the method annotated with @OnMessage that takes your custom Java type as a parameter if this method exists.

For example, if you have two Java types (MessageA and MessageB) that you want to send and receive as text messages, define them so that they extend a common class (Message). Because you can only define one decoder for text messages, implement a decoder for the Message class as follows:

public class MessageTextDecoder implements Decoder.Text<Message> {
   @Override
   public void init(EndpointConfig ec) { }
   @Override
   public void destroy() { }
   @Override
   public Message decode(String string) throws DecodeException {
      // Read message...
      if ( /* message is an A message */ )
         return new MessageA(...);
      else if ( /* message is a B message */ )
         return new MessageB(...);
   }
   @Override
   public boolean willDecode(String string) {
      // Determine if the message can be converted into either a
      // MessageA object or a MessageB object...
      return canDecode;
   }
}

Then, add the decoder parameter to the ServerEndpoint annotation as follows:

@ServerEndpoint(
   value = "/myendpoint",
   encoders = { MessageATextEncoder.class, MessageBTextEncoder.class },
   decoders = { MessageTextDecoder.class }
)
public class EncDecEndpoint { ... }

Now, define a method in the endpoint class that receives MessageA and MessageB objects as follows:

@OnMessage
public void message(Session session, Message msg) {
   if (msg instanceof MessageA) {
      // We received a MessageA object...
   } else if (msg instanceof MessageB) {
      // We received a MessageB object...
   }
}

Like endpoints, decoder instances are associated with one and only one WebSocket connection and peer, so there is only one thread executing the code of a decoder instance at any given time.

Path Parameters

The ServerEndpoint annotation enables you to use URI templates to specify parts of an endpoint deployment URI as application parameters. For example, consider this endpoint:

@ServerEndpoint("/chatrooms/{room-name}")
public class ChatEndpoint {
   ...
}

If the endpoint is deployed inside a web application called chatapp at a local Jakarta EE server in port 8080, clients can connect to the endpoint using any of the following URIs:

http://localhost:8080/chatapp/chatrooms/currentnews
http://localhost:8080/chatapp/chatrooms/music
http://localhost:8080/chatapp/chatrooms/cars
http://localhost:8080/chatapp/chatrooms/technology

Annotated endpoints can receive path parameters as arguments in methods annotated with @OnOpen, @OnMessage, and @OnClose. In this example, the endpoint uses the parameter in the @OnOpen method to determine which chat room the client wants to join:

@ServerEndpoint("/chatrooms/{room-name}")
public class ChatEndpoint {
   @OnOpen
   public void open(Session session,
                    EndpointConfig c,
                    @PathParam("room-name") String roomName) {
      // Add the client to the chat room of their choice ...
   }
}

The path parameters used as arguments in these methods can be strings, primitive types, or the corresponding wrapper types.

Handling Errors

To designate a method that handles errors in an annotated WebSocket endpoint, decorate it with @OnError:

@ServerEndpoint("/testendpoint")
public class TestEndpoint {
   ...
   @OnError
   public void error(Session session, Throwable t) {
      t.printStackTrace();
      ...
   }
}

This method is invoked when there are connection problems, runtime errors from message handlers, or conversion errors when decoding messages.

Specifying an Endpoint Configurator Class

Jakarta WebSocket enables you to configure how the container creates server endpoint instances. You can provide custom endpoint configuration logic to:

  • Access the details of the initial HTTP request for a WebSocket connection

  • Perform custom checks on the Origin HTTP header

  • Modify the WebSocket handshake response

  • Choose a WebSocket subprotocol from those requested by the client

  • Control the instantiation and initialization of endpoint instances

To provide custom endpoint configuration logic, you extend the ServerEndpointConfig.Configurator class and override some of its methods. In the endpoint class, you specify the configurator class using the configurator parameter of the ServerEndpoint annotation.

For example, the following configurator class makes the handshake request object available to endpoint instances:

public class CustomConfigurator extends ServerEndpointConfig.Configurator {

    @Override
    public void modifyHandshake(ServerEndpointConfig conf,
                                HandshakeRequest req,
                                HandshakeResponse resp) {

        conf.getUserProperties().put("handshakereq", req);
    }

}

The following endpoint class configures endpoint instances with the custom configurator, which enables them to access the handshake request object:

@ServerEndpoint(
    value = "/myendpoint",
    configurator = CustomConfigurator.class
)
public class MyEndpoint {

    @OnOpen
    public void open(Session s, EndpointConfig conf) {
        HandshakeRequest req = (HandshakeRequest) conf.getUserProperties()
                                                      .get("handshakereq");
        Map<String,List<String>> headers = req.getHeaders();
        ...
    }
}

The endpoint class can use the handshake request object to access the details of the initial HTTP request, such as its headers or the HttpSession object.

For more information on endpoint configuration, see the API reference for the ServerEndpointConfig.Configurator class.

The dukeetf2 Example Application

The dukeetf2 example application, located in the jakartaee-examples/tutorial/web/websocket/dukeetf2/ directory, demonstrates how to use a WebSocket endpoint to provide data updates to web clients. The example resembles a service that provides periodic updates on the price and trading volume of an electronically traded fund (ETF).

Architecture of the dukeetf2 Sample Application

The dukeetf2 example application consists of a WebSocket endpoint, an enterprise bean, and an HTML page.

  • The endpoint accepts connections from clients and sends them updates when new data for price and trading volume becomes available.

  • The enterprise bean updates the price and volume information once every second.

  • The HTML page uses JavaScript code to connect to the WebSocket endpoint, parse incoming messages, and update the price and volume information without reloading the page.

The Endpoint

The WebSocket endpoint is implemented in the ETFEndpoint class, which stores all connected sessions in a queue and provides a method that the enterprise bean calls when there is new information available to send:

@ServerEndpoint("/dukeetf")
public class ETFEndpoint {
   private static final Logger logger = Logger.getLogger("ETFEndpoint");
   /* Queue for all open WebSocket sessions */
   static Queue<Session> queue = new ConcurrentLinkedQueue<>();

   /* PriceVolumeBean calls this method to send updates */
   public static void send(double price, int volume) {
      String msg = String.format("%.2f / %d", price, volume);
      try {
         /* Send updates to all open WebSocket sessions */
         for (Session session : queue) {
            session.getBasicRemote().sendText(msg);
            logger.log(Level.INFO, "Sent: {0}", msg);
         }
      } catch (IOException e) {
         logger.log(Level.INFO, e.toString());
      }
    }
    ...
}

The lifecycle methods of the endpoint add and remove sessions to and from the queue:

@ServerEndpoint("/dukeetf")
public class ETFEndpoint {
   ...
   @OnOpen
   public void openConnection(Session session) {
      /* Register this connection in the queue */
      queue.add(session);
      logger.log(Level.INFO, "Connection opened.");
   }

   @OnClose
   public void closedConnection(Session session) {
      /* Remove this connection from the queue */
      queue.remove(session);
      logger.log(Level.INFO, "Connection closed.");
   }

   @OnError
   public void error(Session session, Throwable t) {
      /* Remove this connection from the queue */
      queue.remove(session);
      logger.log(Level.INFO, t.toString());
      logger.log(Level.INFO, "Connection error.");
   }
}

The Enterprise Bean

The enterprise bean uses the timer service to generate new price and volume information every second:

@Startup
@Singleton
public class PriceVolumeBean {
   /* Use the container's timer service */
   @Resource TimerService tservice;
   private Random random;
   private volatile double price = 100.0;
   private volatile int volume = 300000;
   private static final Logger logger = Logger.getLogger("PriceVolumeBean");

   @PostConstruct
   public void init() {
       /* Initialize the EJB and create a timer */
       logger.log(Level.INFO, "Initializing EJB.");
       random = new Random();
       tservice.createIntervalTimer(1000, 1000, new TimerConfig());
   }

   @Timeout
   public void timeout() {
       /* Adjust price and volume and send updates */
       price += 1.0*(random.nextInt(100)-50)/100.0;
       volume += random.nextInt(5000) - 2500;
       ETFEndpoint.send(price, volume);
   }
}

The enterprise bean calls the send method of the ETFEndpoint class in the timeout method. See Using the Timer Service in Running the Enterprise Bean Examples for more information on the timer service.

The HTML Page

The HTML page consists of a table and some JavaScript code. The table contains two fields referenced from JavaScript code:

<!DOCTYPE html>
<html>
<head>...</head>
<body>
  ...
  <table>
    ...
    <td id="price">--.--</td>
    ...
    <td id="volume">--</td>
    ...
  </table>
</body>
</html>

The JavaScript code uses the WebSocket API to connect to the server endpoint and to designate a callback method for incoming messages. The callback method updates the page with the new information.

var wsocket;
function connect() {
   wsocket = new WebSocket("ws://localhost:8080/dukeetf2/dukeetf");
   wsocket.onmessage = onMessage;
}
function onMessage(evt) {
   var arraypv = evt.data.split("/");
   document.getElementById("price").innerHTML = arraypv[0];
   document.getElementById("volume").innerHTML = arraypv[1];
}
window.addEventListener("load", connect, false);

The WebSocket API is supported by most modern browsers, and it is widely used in HTML5 web client development.

Running the dukeetf2 Example Application

This section describes how to run the dukeetf2 example application using NetBeans IDE and from the command line.

To Run the dukeetf2 Example Application Using NetBeans IDE

  1. Make sure that GlassFish Server has been started (see Starting and Stopping GlassFish Server).

  2. From the File menu, choose Open Project.

  3. In the Open Project dialog box, navigate to:

    jakartaee-examples/tutorial/web/websocket
  4. Select the dukeetf2 folder.

  5. Click Open Project.

  6. In the Projects tab, right-click the dukeetf2 project and select Run.

    This command builds and packages the application into a WAR file (dukeetf2.war) located in the target/ directory, deploys it to the server, and launches a web browser window with the following URL:

    http://localhost:8080/dukeetf2/

    Open the same URL on a different web browser tab or window to see how both pages get price and volume updates simultaneously.

To Run the dukeetf2 Example Application Using Maven

  1. Make sure that GlassFish Server has been started (see Starting and Stopping GlassFish Server).

  2. In a terminal window, go to:

    jakartaee-examples/tutorial/web/websocket/dukeetf2/
  3. Enter the following command to deploy the application:

    mvn install
  4. Open a web browser window and enter the following URL:

    http://localhost:8080/dukeetf2/

    Open the same URL on a different web browser tab or window to see how both pages get price and volume updates simultaneously.

The websocketbot Example Application

The websocketbot example application, located in the jakartaee-examples/tutorial/web/websocket/websocketbot/ directory, demonstrates how to use a WebSocket endpoint to implement a chat. The example resembles a chat room in which many users can join and have a conversation. Users can ask simple questions to a bot agent that is always available in the chat room.

Architecture of the websocketbot Example Application

The websocketbot example application consists of the following elements:

  • The CDI Bean – A CDI bean (BotBean) that contains the logic for the bot agent to reply to messages

  • The WebSocket Endpoint – A WebSocket endpoint (BotEndpoint) that implements the chat room

  • The Application Messages – A set of classes (Message, ChatMessage, InfoMessage, JoinMessage, and UsersMessage) that represent application messages

  • The Encoder Classes – A set of classes (ChatMessageEncoder, InfoMessageEncoder, JoinMessageEncoder, and UsersMessageEncoder) that encode application messages into WebSocket text messages as JSON data

  • The Message Decoder – A class (MessageDecoder) the parses WebSocket text messages as JSON data and decodes them into JoinMessage or ChatMessage objects

  • The HTML Page – An HTML page (index.html) that uses JavaScript code to implement the client for the chat room

The CDI Bean

The CDI bean (BotBean) is a Java class that contains the respond method. This method compares the incoming chat message with a set of predefined questions and returns a chat response.

@Named
public class BotBean {
    public String respond(String msg) { ... }
}

The WebSocket Endpoint

The WebSocket endpoint (BotEndpoint) is an annotated endpoint that performs the following functions:

  • Receives messages from clients

  • Forwards messages to clients

  • Maintains a list of connected clients

  • Invokes the bot agent functionality

The endpoint specifies its deployment URI and the message encoders and decoders using the ServerEndpoint annotation. The endpoint obtains an instance of the BotBean class and a managed executor service resource through dependency injection:

@ServerEndpoint(
   value = "/websocketbot",
   decoders = { MessageDecoder.class },
   encoders = { JoinMessageEncoder.class, ChatMessageEncoder.class,
                InfoMessageEncoder.class, UsersMessageEncoder.class }
)
/* There is a BotEndpoint instance per connection */
public class BotEndpoint {
   private static final Logger logger = Logger.getLogger("BotEndpoint");
   /* Bot functionality bean */
   @Inject private BotBean botbean;
   /* Executor service for asynchronous processing */
   @Resource(name="comp/DefaultManagedExecutorService")
   private ManagedExecutorService mes;

   @OnOpen
   public void openConnection(Session session) {
       logger.log(Level.INFO, "Connection opened.");
   }
   ...
}

The message method processes incoming messages from clients. The decoder converts incoming text messages into JoinMessage or ChatMessage objects, which inherit from the Message class. The message method receives a Message object as a parameter:

@OnMessage
public void message(Session session, Message msg) {
   logger.log(Level.INFO, "Received: {0}", msg.toString());

   if (msg instanceof JoinMessage) {
      /* Add the new user and notify everybody */
      JoinMessage jmsg = (JoinMessage) msg;
      session.getUserProperties().put("name", jmsg.getName());
      session.getUserProperties().put("active", true);
      logger.log(Level.INFO, "Received: {0}", jmsg.toString());
      sendAll(session, new InfoMessage(jmsg.getName() +
              " has joined the chat"));
      sendAll(session, new ChatMessage("Duke", jmsg.getName(),
              "Hi there!!"));
      sendAll(session, new UsersMessage(this.getUserList(session)));

   } else if (msg instanceof ChatMessage) {
      /* Forward the message to everybody */
      ChatMessage cmsg = (ChatMessage) msg;
      logger.log(Level.INFO, "Received: {0}", cmsg.toString());
      sendAll(session, cmsg);
      if (cmsg.getTarget().compareTo("Duke") == 0) {
         /* The bot replies to the message */
         mes.submit(new Runnable() {
            @Override
            public void run() {
               String resp = botbean.respond(cmsg.getMessage());
               sendAll(session, new ChatMessage("Duke",
                       cmsg.getName(), resp));
            }
         });
      }
   }
}

If the message is a join message, the endpoint adds the new user to the list and notifies all connected clients. If the message is a chat message, the endpoint forwards it to all connected clients.

If a chat message is for the bot agent, the endpoint obtains a response using the BotBean instance and sends it to all connected clients. The sendAll method is similar to the example in Sending Messages to All Peers Connected to an Endpoint.

Asynchronous Processing and Concurrency Considerations

The WebSocket endpoint calls the BotBean.respond method to obtain a response from the bot. In this example, this is a blocking operation; the user that sent the associated message would not be able to send or receive other chat messages until the operation completes. To avoid this problem, the endpoint obtains an executor service from the container and executes the blocking operation in a different thread using the ManagedExecutorService.submit method from Concurrency Utilities for Jakarta EE.

Jakarta WebSocket specification requires that Jakarta EE implementations instantiate endpoint classes once per connection. This facilitates the development of WebSocket endpoints, because you are guaranteed that only one thread is executing the code in a WebSocket endpoint class at any given time. When you introduce a new thread in an endpoint, as in this example, you must ensure that variables and methods accessed by more than one thread are thread safe. In this example, the code in BotBean is thread safe, and the BotEndpoint.sendAll method has been declared synchronized.

Refer to Jakarta Concurrency for more information on the managed executor service and Concurrency Utilities for Jakarta EE.

The Application Messages

The classes that represent application messages (Message, ChatMessage, InfoMessage, JoinMessage, and UsersMessage) contain only properties and getter and setter methods. For example, the ChatMessage class looks like this:

public class ChatMessage extends Message {
    private String name;
    private String target;
    private String message;
    /* ... Constructor, getters, and setters ... */
}

The Encoder Classes

The encoder classes convert application message objects into JSON text using the Java API for JSON Processing. For example, the ChatMessageEncoder class is implemented as follows:

/* Encode a ChatMessage as JSON.
 * For example, (new ChatMessage("Peter","Duke","How are you?"))
 * is encoded as follows:
 * {"type":"chat","target":"Duke","message":"How are you?"}
 */
public class ChatMessageEncoder implements Encoder.Text<ChatMessage> {
   @Override
   public void init(EndpointConfig ec) { }
   @Override
   public void destroy() { }
   @Override
   public String encode(ChatMessage chatMessage) throws EncodeException {
      // Access properties in chatMessage and write JSON text...
   }
}

See JSON Processing for more information on the Jakarta JSON Processing.

The Message Decoder

The message decoder (MessageDecoder) class converts WebSocket text messages into application messages by parsing JSON text. It is implemented as follows:

/* Decode a JSON message into a JoinMessage or a ChatMessage.
 * For example, the incoming message
 * {"type":"chat","name":"Peter","target":"Duke","message":"How are you?"}
 * is decoded as (new ChatMessage("Peter", "Duke", "How are you?"))
 */
public class MessageDecoder implements Decoder.Text<Message> {
    /* Stores the name-value pairs from a JSON message as a Map */
    private Map<String,String> messageMap;

    @Override
    public void init(EndpointConfig ec) { }
    @Override
    public void destroy() { }

    /* Create a new Message object if the message can be decoded */
    @Override
    public Message decode(String string) throws DecodeException {
       Message msg = null;
       if (willDecode(string)) {
          switch (messageMap.get("type")) {
             case "join":
                msg = new JoinMessage(messageMap.get("name"));
                break;
             case "chat":
                msg = new ChatMessage(messageMap.get("name"),
                                      messageMap.get("target"),
                                      messageMap.get("message"));
          }
       } else {
          throw new DecodeException(string, "[Message] Can't decode.");
       }
       return msg;
   }

   /* Decode a JSON message into a Map and check if it contains
    * all the required fields according to its type. */
   @Override
   public boolean willDecode(String string) {
      // Convert JSON data from the message into a name-value map...
      // Check if the message has all the fields for its message type...
   }
}

The HTML Page

The HTML page (index.html) contains a field for the user name. After the user types a name and clicks Join, three text areas are available: one to type and send messages, one for the chat room, and one with the list of users. The page also contains a WebSocket console that shows the messages sent and received as JSON text.

The JavaScript code on the page uses the WebSocket API to connect to the endpoint, send messages, and designate callback methods. The WebSocket API is supported by most modern browsers and is widely used for web client development with HTML5.

Running the websocketbot Example Application

This section describes how to run the websocketbot example application using NetBeans IDE and from the command line.

To Run the websocketbot Example Application Using NetBeans IDE

  1. Make sure that GlassFish Server has been started (see Starting and Stopping GlassFish Server).

  2. From the File menu, choose Open Project.

  3. In the Open Project dialog box, navigate to:

    jakartaee-examples/tutorial/web/websocket
  4. Select the websocketbot folder.

  5. Click Open Project.

  6. In the Projects tab, right-click the websocketbot project and select Run.

    This command builds and packages the application into a WAR file, websocketbot.war, located in the target/ directory; deploys it to the server; and launches a web browser window with the following URL:

    http://localhost:8080/websocketbot/

To Run the websocketbot Example Application Using Maven

  1. Make sure that GlassFish Server has been started (see Starting and Stopping GlassFish Server).

  2. In a terminal window, go to:

    jakartaee-examples/tutorial/web/websocket/websocketbot/
  3. Enter the following command to deploy the application:

    mvn install
  4. Open a web browser window and type the following address:

    http://localhost:8080/websocketbot/

To Test the websocketbot Example Application

  1. On the main page, type your name on the first text field and press the Enter key.

    The list of connected users appears on the text area on the right. The text area on the left is the chat room.

  2. Type a message on the text area below the login button. For example, type the messages in bold and press enter to obtain responses similar to the following:

    [--Peter has joined the chat--]
    Duke: @Peter Hi there!!
    Peter: @Duke how are you?
    Duke: @Peter I'm doing great, thank you!
    Peter: @Duke when is your birthday?
    Duke: @Peter My birthday is on May 23rd. Thanks for asking!
  3. Join the chat from another browser window by copying and pasting the URI on the address bar and joining with a different name.

    The new user name appears in the list of users in both browser windows. You can send messages from either window and see how they appear in the other.

  4. Click Show WebSocket Console.

    The console shows the messages sent and received as JSON text.

Further Information about WebSocket

For more information on WebSocket in Jakarta EE, see the Jakarta WebSocket specification: