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.
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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
21CTO
21CTO (21CTO.com) offers developers community, training, and services, making it your go‑to learning and service platform.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
