Skip to main content

Integrate WebSocket

๐ŸŽฏcontext

Set up a WebSocket server in your Domain Java service using Java Stack 2.0 to enable real-time bidirectional communication.

Descriptionโ€‹

WebSockets enable real-time, two-way communication between clients and servers. Unlike traditional HTTP requests, WebSockets maintain a persistent connection, allowing servers to push data to clients without clients needing to poll for updates.

This How-To demonstrates two approaches for implementing WebSockets in your Java Domain service:

  • A basic implementation without authentication
  • A secure implementation with JWT authentication

The guide covers managing multiple client connections, handling reconnections, keeping connections alive, and sending targeted messages to specific clients.

Preconditionsโ€‹

  • Java Stack 2.0: Your project uses Java Stack 2.0
  • Spring Boot: Your service is built with Spring Boot
  • Maven/Gradle: You use Maven or Gradle for dependency management
  • OpenShift: Your service will be deployed to an OpenShift environment

Step-by-Step Guideโ€‹

1. Add WebSocket Dependenciesโ€‹

1.1. Update Maven Dependencies:

  • Add the Spring Boot WebSocket starter to your pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

2. Configure WebSocket Endpointโ€‹

2.1. Create WebSocket Configuration:

  • Create a configuration class to register WebSocket handlers:
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry
.addHandler(new CustomWebSocketHandler(), "/websocket")
.setAllowedOrigins("*");
}
}

2.2. Expose WebSocket Endpoint:

  • For Kubernetes/OpenShift deployment, expose the WebSocket endpoint using one of these approaches:
  • Add the WebSocket endpoint to your API gateway (if it supports WebSockets)
  • Create an OpenShift route with path "/websocket", termination type "Reencrypt", and port 8443
๐Ÿ’กtip

When connecting to the WebSocket endpoint, clients should use the "wss://" protocol (WebSocket Secure) instead of "https://".

2.3. Disable Security for WebSocket Endpoint:

3. Implement WebSocket Handlerโ€‹

3.1. Create Basic WebSocket Handler:

  • Implement a handler to manage sessions and messages:
        import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

@Service
public class CustomWebSocketHandler extends TextWebSocketHandler {

private static final Logger log = LoggerFactory.getLogger(CustomWebSocketHandler.class);

private static final Map<String, List<WebSocketSession>> sessions = new ConcurrentHashMap<>();
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);

@PostConstruct
public void startKeepAlive() {
this.scheduler.scheduleAtFixedRate(this::sendKeepAliveMessages, 30, 30, TimeUnit.SECONDS);
}

@PreDestroy
public void stopKeepAlive() {
this.scheduler.shutdown();
}

@Override
public void handleTextMessage(WebSocketSession session, TextMessage message) throws IOException {
log.info("Received new message");

String clientId = message.getPayload();
if (clientId.isEmpty() || !sessions.containsKey(clientId)) {
clientId = UUID.randomUUID().toString();
sessions.put(clientId, new ArrayList<>());
session.sendMessage(new TextMessage(clientId));
}
sessions.get(clientId).add(session);
}

@Override
public void afterConnectionEstablished(WebSocketSession session) {
log.info("Connection established for session with ID {}", session.getId());
}

@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
log.info("Session with ID {} has been closed with status code {}", session.getId(), status.getCode());

sessions.entrySet().stream()
.filter(entry -> entry.getValue().contains(session))
.findFirst()
.ifPresent(entry -> entry.getValue().remove(session));
}

public void sendMessageToClient(String clientId, String content) {
log.info("Sending message to client with ID {}", clientId);

List<WebSocketSession> clientSessions = sessions.get(clientId);
if (clientSessions == null || clientSessions.isEmpty()) {
log.error("No sessions found for client ID {}", clientId);
return;
}

TextMessage message = new TextMessage(content);
for (WebSocketSession session : clientSessions) {
try {
session.sendMessage(message);
} catch (IOException e) {
log.error("Exception occurred when sending message to client with ID {}. Error message: {}",
clientId, e.getMessage());
}
}
}

private void sendKeepAliveMessages() {
for (List<WebSocketSession> clientSessions : sessions.values()) {
for (WebSocketSession session : clientSessions) {
try {
if(session.isOpen()) {
session.sendMessage(new TextMessage(WebsocketMessageType.KEEP_ALIVE.toString()));
log.debug("Sent KEEP_ALIVE message to session with ID {}", session.getId());
}
} catch (IOException e) {
log.error("Exception occurred when sending KEEP_ALIVE message to session with ID {}. Error message: {}",
session.getId(), e.getMessage());
}
}
}
}
}
๐Ÿ’กtip

If your service is deployed with multiple replicas, consider storing client sessions in an external cache (like Redis) to enable cross-pod communication.

4. Implement Client-Side Connectionโ€‹

4.1. Client Connection Example (React):

  • For a React frontend, you can use the "react-use-websocket" library:
    • Install with: npm i react-use-websocket

          import useWebSocket from 'react-use-websocket';

      // For basic connection
      const socketUrl = 'wss://YOUR-HOST/websocket';
      let clientId = localStorage.getItem('websocket-client-id') || '';

      // For authenticated connection
      const jwtToken = "YOUR-JWT-TOKEN";

      const { sendMessage } = useWebSocket(socketUrl, {
      onOpen: () => {
      // For basic connection:
      sendMessage(clientId);

      // OR for authenticated connection:
      // sendMessage(jwtToken);
      },
      onMessage: (event) => {
      switch (event.data) {
      case 'KEEP_ALIVE':
      break;
      case 'AUTHENTICATION_SUCCEEDED':
      console.log('Authentication successful');
      break;
      case 'AUTHENTICATION_FAILED':
      console.log('Authentication failed');
      break;
      default:
      // For basic connection, store client ID if new
      if (!clientId) {
      clientId = event.data;
      localStorage.setItem('websocket-client-id', clientId);
      }
      console.log('Message received: ' + event.data);
      break;
      }
      },
      onError: (event) => console.error(event),
      shouldReconnect: (closeEvent) => true,
      onClose: (event) => console.log(event),
      share: true,
      retryOnError: true,
      reconnectInterval: 10,
      reconnectAttempts: 5
      });

Conclusionโ€‹

๐ŸŒŸresult

Congratulations! You have successfully integrated WebSockets into your Java Domain service. Your service can now:

  • Accept real-time WebSocket connections from clients
  • Handle multiple concurrent connections from the same client
  • Manage client reconnections
  • Keep connections alive with heartbeat messages
  • Send targeted messages to specific clients

Further Readingโ€‹