Bidirectional communication between services using Jakarta WebSocket

duration 25 minutes

Learn how to use Jakarta WebSocket to send and receive messages between services without closing the connection.

Prerequisites:

What you’ll learn

Jakarta WebSocket enables two-way communication between client and server endpoints. First, each client makes an HTTP connection to a Jakarta WebSocket server. The server can then broadcast messages to the clients. Server-Sent Events (SSE) also enables a client to receive automatic updates from a server via an HTTP connection however WebSocket differs from Server-Sent Events in that SSE is unidirectional from server to client, whereas WebSocket is bidirectional. WebSocket also enables real-time updates over a smaller bandwidth than SSE. The connection isn’t closed meaning that the client can continue to send and receive messages with the server, without having to poll the server to receive any replies.

The application that you will build in this guide consists of the client service and the system server service. The following diagram depicts the application that is used in this guide.

Application architecture where system and client services use the Jakarta Websocket API to connect and communicate.

You’ll learn how to use the Jakarta WebSocket API to build the system service and the scheduler in the client service. The scheduler pushes messages to the system service every 10 seconds, then the system service broadcasts the messages to any connected clients. You will also learn how to use a JavaScript WebSocket object in an HTML file to build a WebSocket connection, subscribe to different events, and display the broadcasting messages from the system service in a table.

Getting started

The fastest way to work through this guide is to clone the Git repository and use the projects that are provided inside:

Copied to clipboard
git clone https://github.com/openliberty/guide-jakarta-websocket.git
cd guide-jakarta-websocket

The start directory contains the starting project that you will build upon.

The finish directory contains the finished project that you will build.

Before you begin, make sure you have all the necessary prerequisites.

Try what you’ll build

The finish directory in the root of this guide contains the finished application. Give it a try before you proceed.

To try out the application, go to the finish directory and run the following Maven goal to build the system service and deploy it to Open Liberty:

Copied to clipboard
mvn -pl system liberty:run

Next, open another command-line session and run the following command to start the client service:

Copied to clipboard
mvn -pl client liberty:run

After you see the following message in both command-line sessions, both your services are ready.

The defaultServer is ready to run a smarter planet.

Check out the service at the http://localhost:9080 URL. See that the table is being updated for every 10 seconds.

After you are finished checking out the application, stop both the system and client services by pressing CTRL+C in the command-line sessions where you ran them. Alternatively, you can run the following goals from the finish directory in another command-line session:

Copied to clipboard
mvn -pl system liberty:stop
mvn -pl client liberty:stop

Creating the WebSocket server service

In this section, you will create the system WebSocket server service that broadcasts messages to clients.

Navigate to the start directory to begin.

When you run Open Liberty in dev mode, dev mode listens for file changes and automatically recompiles and deploys your updates whenever you save a new change. Run the following command to start the system service in dev mode:

Copied to clipboard
mvn -pl system liberty:dev

After you see the following message, your Liberty instance is ready in dev mode:

**************************************************
*     Liberty is running in dev mode.

The system service is responsible for handling the messages produced by the client scheduler, building system load messages, and forwarding them to clients.

Create the SystemService class.
View Code
Copied to clipboard
system/src/main/java/io/openliberty/guides/system/SystemService.java

Annotate the SystemService class with a @ServerEndpoint annotation to make it a WebSocket server. The @ServerEndpoint value attribute specifies the URI where the endpoint will be deployed. The encoders attribute specifies the classes to encode messages and the decoders attribute specifies the classes to decode messages. Provide methods that define the parts of the WebSocket lifecycle like establishing a connection, receiving a message, and closing the connection by annotating them with the @OnOpen, @OnMessage and @OnClose annotations respectively. The method that is annotated with the @OnError annotation is responsible for tackling errors.

The onOpen() method stores up the client sessions. The onClose() method displays the reason for closing the connection and removes the closing session from the client sessions.

The onMessage() method is called when receiving a message through the option parameter. The option parameter signifies which message to construct, either system load, memory usage data, or both, and sends out the JsonObject message. The sendToAllSessions() method uses the WebSocket API to broadcast the message to all client sessions.

Create the SystemLoadEncoder class.
View Code
Copied to clipboard
system/src/main/java/io/openliberty/guides/system/SystemLoadEncoder.java

The SystemLoadEncoder class implements the Encoder.Text interface. Override the encode() method that accepts the JsonObject message and converts the message to a string.

Create the SystemLoadDecoder class.
View Code
Copied to clipboard
system/src/main/java/io/openliberty/guides/system/SystemLoadDecoder.java

The SystemLoadDecoder class implements the Decoder.Text interface. Override the decode() method that accepts string message and decodes the string back into a JsonObject. The willDecode() override method checks out whether the string can be decoded into a JSON object and returns a Boolean value.

The required websocket and jsonb features for the system service have been enabled for you in the Liberty server.xml configuration file.

Creating the client service

In this section, you will create the WebSocket client that communicates with the WebSocket server and the scheduler that uses the WebSocket client to send messages to the server. You’ll also create an HTML file that uses a JavaScript WebSocket object to build a WebSocket connection, subscribe to different events, and display the broadcasting messages from the system service in a table.

On another command-line session, navigate to the start directory and run the following goal to start the client service in dev mode:

Copied to clipboard
mvn -pl client liberty:dev

After you see the following message, your Liberty instance is ready in dev mode:

**************************************************
*     Liberty is running in dev mode.
Create the SystemClient class.
View Code
Copied to clipboard
client/src/main/java/io/openliberty/guides/client/scheduler/SystemClient.java

Annotate the SystemClient class with @ClientEndpoint annotation to make it as a WebSocket client. Create a constructor that uses the websocket APIs to establish connection with the server. Provide a method with the @OnOpen annotation that persists the client session when the connection is established. The onMessage() method that is annotated with the @OnMessage annotation handles messages from the server.

Create the SystemLoadScheduler class.
View Code
Copied to clipboard
client/src/main/java/io/openliberty/guides/client/scheduler/SystemLoadScheduler.java

The SystemLoadScheduler class uses the SystemClient class to establish a connection to the server by the ws://localhost:9081/systemLoad URI at the @PostConstruct annotated method. The sendSystemLoad() method calls the client to send a random string from either cpuLoad, memoryUsage, or both to the system service. Using the Jakarta Enterprise Beans Timer Service, annotate the sendSystemLoad() method with the @Schedule annotation so that it sends out a message every 10 seconds.

Now, create the front-end UI. The images and styles for the UI are provided for you.

Create the index.html file.
View Code
Copied to clipboard
client/src/main/webapp/index.html

The index.html front-end UI displays a table in which each row contains a time, system load, and the memory usage of the system service. Use a JavaScript WebSocket object to establish a connection to the server by the ws://localhost:9081/systemLoad URI. The webSocket.onopen event is triggered when the connection is established. The webSocket.onmessage event receives messages from the server and inserts a row with the data from the message into the table. The webSocket.onerror event defines how to tackle errors.

The required features for the client service are enabled for you in the Liberty server.xml configuration file.

Running the application

Because you are running the system and client services in dev mode, the changes that you made are automatically picked up. You’re now ready to check out your application in your browser.

Point your browser to the http://localhost:9080 URL to test out the client service. Notice that the table is updated every 10 seconds.

Visit the http://localhost:9080 URL again on a different tab or browser and verify that both sessions are updated every 10 seconds.

Testing the application

Create the SystemClient class.
View Code
Copied to clipboard
system/src/test/java/it/io/openliberty/guides/system/SystemClient.java

The SystemClient class is used to communicate and test the system service. Its implementation is similar to the client class from the client service that you created in the previous section. At the onMessage() method, decode and verify the message.

Create the SystemServiceIT class.
View Code
Copied to clipboard
system/src/test/java/it/io/openliberty/guides/system/SystemServiceIT.java

There are two test cases to ensure correct functionality of the system service. The testSystem() method verifies one client connection and the testSystemMultipleSessions() method verifies multiple client connections.

Running the tests

Because you started Open Liberty in dev mode, you can run the tests by pressing the enter/return key from the command-line session where you started the system service.

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running it.io.openliberty.guides.system.SystemServiceIT
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.247 s - in it.io.openliberty.guides.system.SystemServiceIT

Results:

Tests run: 2, Failures: 0, Errors: 0, Skipped: 0

When you are done checking out the services, exit dev mode by pressing CTRL+C in the command-line sessions where you ran the system and client services.

 12package io.openliberty.guides.system;
 13
 14import java.lang.management.ManagementFactory;
 15import java.lang.management.MemoryMXBean;
 16import java.util.Calendar;
 17import java.util.HashSet;
 18import java.util.Set;
 19import java.util.logging.Logger;
 20
 21import jakarta.json.Json;
 22import jakarta.json.JsonObject;
 23import jakarta.json.JsonObjectBuilder;
 24import jakarta.websocket.CloseReason;
 25import jakarta.websocket.OnClose;
 26import jakarta.websocket.OnError;
 27import jakarta.websocket.OnMessage;
 28import jakarta.websocket.OnOpen;
 29import jakarta.websocket.Session;
 30import jakarta.websocket.server.ServerEndpoint;
 31
 32import com.sun.management.OperatingSystemMXBean;
 33
 35@ServerEndpoint(value = "/systemLoad",
 36                decoders = { SystemLoadDecoder.class },
 37                encoders = { SystemLoadEncoder.class })
 39public class SystemService {
 40
 41    private static Logger logger = Logger.getLogger(SystemService.class.getName());
 42
 43    private static Set<Session> sessions = new HashSet<>();
 44
 45    private static final OperatingSystemMXBean OS =
 46        (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean();
 47
 48    private static final MemoryMXBean MEM =
 49        ManagementFactory.getMemoryMXBean();
 50
 52    public static void sendToAllSessions(JsonObject systemLoad) {
 53        for (Session session : sessions) {
 54            try {
 55                session.getBasicRemote().sendObject(systemLoad);
 56            } catch (Exception e) {
 57                e.printStackTrace();
 58            }
 59        }
 60    }
 62
 65    @OnOpen
 67    public void onOpen(Session session) {
 68        logger.info("Server connected to session: " + session.getId());
 69        sessions.add(session);
 70    }
 72
 75    @OnMessage
 77    public void onMessage(String option, Session session) {
 78        logger.info("Server received message \"" + option + "\" "
 79                    + "from session: " + session.getId());
 80        try {
 81            JsonObjectBuilder builder = Json.createObjectBuilder();
 82            builder.add("time", Calendar.getInstance().getTime().toString());
 84            if (option.equalsIgnoreCase("cpuLoad")
 85                || option.equalsIgnoreCase("both")) {
 87                builder.add("cpuLoad", Double.valueOf(OS.getCpuLoad() * 100.0));
 88            }
 90            if (option.equalsIgnoreCase("memoryUsage")
 91                || option.equalsIgnoreCase("both")) {
 93                long heapMax = MEM.getHeapMemoryUsage().getMax();
 94                long heapUsed = MEM.getHeapMemoryUsage().getUsed();
 95                builder.add("memoryUsage", Double.valueOf(heapUsed * 100.0 / heapMax));
 96            }
 98            JsonObject systemLoad = builder.build();
 99            sendToAllSessions(systemLoad);
101        } catch (Exception e) {
102            e.printStackTrace();
103        }
104    }
106
109    @OnClose
111    public void onClose(Session session, CloseReason closeReason) {
112        logger.info("Session " + session.getId()
113                    + " was closed with reason " + closeReason.getCloseCode());
114        sessions.remove(session);
115    }
117
119    @OnError
121    public void onError(Session session, Throwable throwable) {
122        logger.info("WebSocket error for " + session.getId() + " "
123                    + throwable.getMessage());
124    }
125}126

Prerequisites:

Nice work! Where to next?

Nice work! You developed an application that subscribes to real time updates by using Jakarta WebSocket and Open Liberty.

Bidirectional communication between services using Jakarta WebSocket by Open Liberty is licensed under CC BY-ND 4.0

What did you think of this guide?

Extreme Dislike Dislike Like Extreme Like

What could make this guide better?

Raise an issue to share feedback

Create a pull request to contribute to this guide

Need help?

Ask a question on Stack Overflow

Like Open Liberty? Star our repo on GitHub.

Star