Building Real-Time Communication with Java WebSockets: A Guide
In today's fast-paced digital landscape, real-time communication has become a cornerstone of modern web applications. Whether it's live chat functionality, collaborative editing tools, or dynamic data updates, the ability to push information to clients instantaneously is invaluable. Java, with its robust ecosystem and versatility, offers powerful tools for implementing real-time features, and one such tool is WebSockets.
Understanding WebSockets
WebSockets provide a full-duplex communication channel over a single, long-lived TCP connection. Unlike traditional HTTP requests, which are stateless and short-lived, WebSockets maintain a persistent connection between the client and the server, allowing for bi-directional communication. This persistent connection eliminates the need for repeated HTTP requests, reducing latency and overhead.
How it works
- When a client wishes to establish a WebSocket connection, it sends an initial HTTP request to the server, known as the WebSocket handshake request.
- The server recognizes this handshake request and responds with a WebSocket handshake response, indicating that it's willing to upgrade the connection to the WebSocket protocol.
- Once the handshake is completed successfully, the connection transitions from HTTP to the WebSocket protocol, allowing full-duplex communication.
package com.edu.retail.ws;
import io.quarkus.scheduler.Scheduled;
import javax.enterprise.context.ApplicationScoped;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
@ServerEndpoint("/dashboard/{clientId}")
@ApplicationScoped
public class DashboardWebSocket {
private Map sessions = new ConcurrentHashMap<>();
private AtomicInteger totalOrders = new AtomicInteger();
@OnOpen
public void onOpen(Session session, @PathParam("clientId") String clientId) {
sessions.put(clientId, session);
}
@OnClose
public void onClose(Session session, @PathParam("clientId") String clientId) {
sessions.remove(clientId);
}
@OnError
public void onError(Session session, @PathParam("clientId") String clientId, Throwable throwable) {
sessions.remove(clientId);
}
@Scheduled(every="5s")
void increment() {
if (sessions != null) {
totalOrders.incrementAndGet();
broadcast(String.valueOf(totalOrders));
}
}
private void broadcast(String message) {
sessions.values().forEach(s -> {
s.getAsyncRemote().sendObject(message, result -> {
if (result.getException() != null) {
System.out.println("Unable to send message: " + result.getException());
}
});
});
}
}
"use strict"
var connected = false;
var socket;
function connect() {
if (! connected) {
var clientId = generateClientId(6);
socket = new WebSocket("ws://" + location.host + "/dashboard/" + clientId);
socket.onopen = function() {
connected = true;
console.log("Connected to the web socket with clientId [" + clientId + "]");
$("#connect").attr("disabled", true);
$("#connect").text("Connected");
};
socket.onmessage =function(m) {
console.log("Got message: " + m.data);
$("#totalOrders").text(m.data);
};
}
}
function generateClientId(length) {
var result = '';
var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
var charactersLength = characters.length;
for ( var i = 0; i < length; i++ ) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
Clicking on the Connect button establishes a WebSocket connection with the server. Since both client and server are running on the same process, we can use localhost as the hostname. The code generates a random string and uses it as the client ID.
socket.onmessage
binds to a callback function that updates the content of a div with received data.
socket.onmessage =function(m) { console.log("Got message: " + m.data); $("#totalOrders").text(m.data); };
Backend Scheduled task
So far, we have looked at the WebSocket server and client. Now we need to push some sales data to clients. To mimic a real-world scenario, I have created a scheduled task to write random values to all the sessions periodically.
Creating a scheduled task in Quarkus is very easy. You need to add the scheduler
extension to the project’s POM file and create the method that needs to executed on the given period.
In this example, I’ve created a new method increment()
in the same DashboardWebSocket
class. It increments the value of totalOrders
and broadcasts to all connected dashboards every 5 seconds.
@Scheduled(every="5s")
void increment() {
if (sessions != null) {
totalOrders.incrementAndGet();
broadcast(String.valueOf(totalOrders));
}
}
Running the application
Now, let’s see our application in action. Using a terminal, navigate to the location where you’ve cloned the repository and issue the following commands.
cd quarkus-websockets-dashboard
./mvnw compile quarkus: dev
Then open your browser window to http://localhost:8080/. Click on the Connect button and see the total number of sales orders changing every 5 seconds.
References
Quarkus — Scheduling Periodic Tasks
Inspiration
https://medium.com/event-driven-utopia/building-a-real-time-sales-dashboard-with-websockets-and-quarkus-d57c3f1554ce
https://github.com/dunithd/edu-samples/tree/main/quarkus-websockets-dashboard
https://docs.quarkiverse.io/quarkus-reactive-messaging-http/dev/reactive-messaging-websocket.html