How to Build a Distributed WebSocket Cluster with Spring Cloud, Eureka, and Consistent Hashing

This article explores practical approaches for enabling multi‑user communication in a distributed WebSocket cluster, covering session handling challenges, architecture design, technology stack choices, Netty versus Spring WebSocket implementations, consistent‑hashing load balancing, and detailed code examples for Spring Cloud Gateway and Ribbon integration.

21CTO
21CTO
21CTO
How to Build a Distributed WebSocket Cluster with Spring Cloud, Eureka, and Consistent Hashing

System Architecture

Technology Stack

Eureka service discovery

Redis session sharing

Redis pub/sub

Spring Boot

Zuul gateway

Spring Cloud Gateway

Spring WebSocket

Ribbon load balancing

Netty NIO framework

Consistent Hash algorithm

Technical Feasibility Analysis

In Spring's WebSocket implementation each connection has a WebSocketSession, which cannot be serialized to Redis, so true WebSocket session sharing across a cluster is impossible. By contrast, HttpSession can be shared using spring-session-data-redis and spring-boot-starter-redis.

Solution Evolution

Netty vs Spring WebSocket

Netty offers high‑performance NIO threads but integrates poorly with Spring Cloud services, requires custom RPC handling, and duplicates business logic. Spring WebSocket is fully integrated with Spring Boot, making development simpler.

Spring WebSocket Implementation

Step 1: Add Dependency

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

Step 2: Configuration Class

@Configuration
public class WebSocketConfig implements WebSocketConfigurer {
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(myHandler(), "/").setAllowedOrigins("*");
    }
    @Bean
    public WebSocketHandler myHandler() {
        return new MessageHandler();
    }
}

Step 3: Message Handler

@Component
public class MessageHandler extends TextWebSocketHandler {
    private List<WebSocketSession> clients = new ArrayList<>();
    @Override
    public void afterConnectionEstablished(WebSocketSession session) {
        clients.add(session);
        System.out.println("uri :" + session.getUri());
        System.out.println("Connection established: " + session.getId());
        System.out.println("Current sessions: " + clients.size());
    }
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
        clients.remove(session);
        System.out.println("Disconnected: " + session.getId());
    }
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) {
        String payload = message.getPayload();
        Map<String, String> map = JSONObject.parseObject(payload, HashMap.class);
        System.out.println("Received data" + map);
        for (WebSocketSession s : clients) {
            try {
                System.out.println("Sending to: " + session.getId());
                s.sendMessage(new TextMessage("Server reply: " + payload));
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

From Zuul to Spring Cloud Gateway

Zuul 1.x does not support WebSocket forwarding; Zuul 2.x supports it but lacks Spring Boot integration, so migration to Spring Cloud Gateway is required. The gateway must handle SSL termination and dynamic routing.

Gateway SSL Configuration (application.yml)

server:
  port: 443
  ssl:
    enabled: true
    key-store: classpath:xxx.jks
    key-store-password: xxxx
    key-store-type: JKS
    key-alias: alias
spring:
  application:
    name: api-gateway
  cloud:
    gateway:
      httpclient:
        ssl:
          handshake-timeout-millis: 10000
          close-notify-flush-timeout-millis: 3000
          close-notify-read-timeout-millis: 0
          useInsecureTrustManager: true
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true
      routes:
        - id: dc
          uri: lb://dc
          predicates:
            - Path=/dc/**
        - id: wecheck
          uri: lb://wecheck
          predicates:
            - Path=/wecheck/**

HTTPS‑to‑HTTP Filter

@Component
public class HttpsToHttpFilter implements GlobalFilter, Ordered {
    private static final int HTTPS_TO_HTTP_FILTER_ORDER = 10099;
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        URI originalUri = exchange.getRequest().getURI();
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpRequest.Builder mutate = request.mutate();
        String forwardedUri = request.getURI().toString();
        if (forwardedUri != null && forwardedUri.startsWith("https")) {
            try {
                URI mutatedUri = new URI("http",
                    originalUri.getUserInfo(),
                    originalUri.getHost(),
                    originalUri.getPort(),
                    originalUri.getPath(),
                    originalUri.getQuery(),
                    originalUri.getFragment());
                mutate.uri(mutatedUri);
            } catch (Exception e) {
                throw new IllegalStateException(e.getMessage(), e);
            }
        }
        ServerHttpRequest build = mutate.build();
        ServerWebExchange webExchange = exchange.mutate().request(build).build();
        return chain.filter(webExchange);
    }
    @Override
    public int getOrder() {
        return HTTPS_TO_HTTP_FILTER_ORDER;
    }
}

Session Broadcast Solution

Each server receives a broadcast request from the gateway, iterates over all cluster nodes, and forwards the message to any local sessions that match the target users. This approach is simple but wastes CPU cycles when no matching sessions exist.

@Resource
private EurekaClient eurekaClient;
Application app = eurekaClient.getApplication("service-name");
InstanceInfo instanceInfo = app.getInstances().get(0);
System.out.println("ip address: " + instanceInfo.getIPAddr());

Consistent Hashing Implementation (Key Idea)

Use a hash ring to map user IDs to specific cluster nodes, handling node up/down events via Eureka listeners that update the ring stored in Redis. Clients obtain the target node IP either through a custom Ribbon rule (currently limited) or by first making an HTTP request to the gateway that returns the appropriate IP for the subsequent WebSocket connection.

Node Down Handling : Remove the failed node and its virtual nodes from the ring.

Node Up Handling : Add the new node and its virtual nodes, then force affected clients to reconnect so they bind to the updated ring.

Two possible placements for the hash ring:

Local to each gateway (requires push notifications for updates).

Shared in Redis with pub/sub for change events (preferred).

Ribbon Limitations in Finchley.RELEASE

Custom load‑balancing rules may mix requests from different services, and the choose method lacks a key parameter for consistent hashing, forcing a two‑step client approach (HTTP to obtain IP, then WebSocket).

Conclusion

The article presents two distributed WebSocket solutions: a simple session‑broadcast method and a more elegant consistent‑hashing approach, each with trade‑offs, and demonstrates how to integrate them with Spring Cloud components without relying on external message queues.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Distributed SystemsjavaSpring BootWebSocketSpring Cloudconsistent hashing
21CTO
Written by

21CTO

21CTO (21CTO.com) offers developers community, training, and services, making it your go‑to learning and service platform.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.