Jakarta WebSocket
We are working on a fresh, updated Jakarta EE Tutorial. This section hasn’t yet been updated. |
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:
-
Create an endpoint class.
-
Implement the lifecycle methods of the endpoint.
-
Add your business logic to the endpoint.
-
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.
Annotation | Event | Example |
---|---|---|
|
Connection opened |
|
|
Message received |
|
|
Connection error |
|
|
Connection closed |
|
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.
-
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 theSession
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 theSession
object as an instance variable of the endpoint class in the method annotated with@OnOpen
so that you can access it from other methods. -
Use the
Session
object to obtain aRemoteEndpoint
object.The
Session.getBasicRemote
method and theSession.getAsyncRemote
method returnRemoteEndpoint.Basic
andRemoteEndpoint.Async
objects respectively. TheRemoteEndpoint.Basic
interface provides blocking methods to send messages; theRemoteEndpoint.Async
interface provides nonblocking methods. -
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.
-
Implement one of the following interfaces:
-
Encoder.Text<T>
for text messages -
Encoder.Binary<T>
for binary messagesThese interfaces specify the
encode
method. Implement an encoder class for each custom Java type that you want to send as a WebSocket message.
-
-
Add the names of your encoder implementations to the
encoders
optional parameter of theServerEndpoint
annotation. -
Use the
sendObject(Object data)
method of theRemoteEndpoint.Basic
orRemoteEndpoint.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.
-
Implement one of the following interfaces:
-
Decoder.Text<T>
for text messages -
Decoder.Binary<T>
for binary messagesThese interfaces specify the
willDecode
anddecode
methods.Unlike with encoders, you can specify at most one decoder for binary messages and one decoder for text messages.
-
-
Add the names of your decoder implementations to the
decoders
optional parameter of theServerEndpoint
annotation. -
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 lang="en">
<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
-
Make sure that GlassFish Server has been started (see Starting and Stopping GlassFish Server).
-
From the File menu, choose Open Project.
-
In the Open Project dialog box, navigate to:
jakartaee-examples/tutorial/web/websocket
-
Select the
dukeetf2
folder. -
Click Open Project.
-
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 thetarget/
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
-
Make sure that GlassFish Server has been started (see Starting and Stopping GlassFish Server).
-
In a terminal window, go to:
jakartaee-examples/tutorial/web/websocket/dukeetf2/
-
Enter the following command to deploy the application:
mvn install
-
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
, andUsersMessage
) that represent application messages -
The Encoder Classes – A set of classes (
ChatMessageEncoder
,InfoMessageEncoder
,JoinMessageEncoder
, andUsersMessageEncoder
) 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 intoJoinMessage
orChatMessage
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
-
Make sure that GlassFish Server has been started (see Starting and Stopping GlassFish Server).
-
From the File menu, choose Open Project.
-
In the Open Project dialog box, navigate to:
jakartaee-examples/tutorial/web/websocket
-
Select the
websocketbot
folder. -
Click Open Project.
-
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 thetarget/
directory; deploys it to the server; and launches a web browser window with the following URL:http://localhost:8080/websocketbot/
See To Test the websocketbot Example Application for more information.
To Run the websocketbot Example Application Using Maven
-
Make sure that GlassFish Server has been started (see Starting and Stopping GlassFish Server).
-
In a terminal window, go to:
jakartaee-examples/tutorial/web/websocket/websocketbot/
-
Enter the following command to deploy the application:
mvn install
-
Open a web browser window and type the following address:
http://localhost:8080/websocketbot/
See To Test the websocketbot Example Application for more information.
To Test the websocketbot Example Application
-
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.
-
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!
-
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.
-
Click Show WebSocket Console.
The console shows the messages sent and received as JSON text.