Spring Boot 与 WebSocket:长连接掉线、心跳与消息广播的问题
Spring Boot 与 WebSocket:长连接掉线、心跳与消息广播的问题
文章目录
- Spring Boot 与 WebSocket:长连接掉线、心跳与消息广播的问题
- 一、前言
- 二、为什么选择 WebSocket?
- 三、Spring Boot 集成 WebSocket 的基础写法
- 1、添加依赖
- 2、 编写配置与端点
- 四、连接为什么总掉?
- 五、正确的心跳机制:前后端都要配合
- 六、集群部署下的“广播失效”问题
- 七、消息泛滥与内存泄漏
- 八、Spring Boot + STOMP 简化方案(可选)
- 九、总结:稳定的 WebSocket 要点
一、前言
WebSocket 是前后端实时通信的利器,但凡你写过在线聊天、系统通知、看板监控,十有八九都用过它。 然而实际开发中,掉线、卡顿、消息延迟、内存暴涨等问题层出不穷。
很多人以为是“服务器太烂”,其实更多时候,是 WebSocket 的连接机制和 Spring Boot 默认实现没搞懂。
本文我们就从底层原理出发,系统讲清楚——为什么 WebSocket 会掉线?心跳机制怎么做才靠谱?消息广播如何不炸内存?
二、为什么选择 WebSocket?
HTTP 是“请求-响应”模型,前端必须主动发起请求,服务端被动回应。 而 WebSocket 属于“全双工通信”,一旦建立连接,客户端和服务端就能相互推消息。
这让很多实时场景成为可能:
- 聊天室消息推送
- 系统状态监控
- 股票/交易数据实时刷新
- 即时通知提醒
但别忘了:连接一旦长期存在,就要面对断线重连、心跳检测、资源回收等麻烦事。
三、Spring Boot 集成 WebSocket 的基础写法
最基本的 WebSocket 服务端实现,通常只需两步:
1、添加依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2、 编写配置与端点
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {@Overridepublic void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {registry.addHandler(new ChatHandler(), "/chat").setAllowedOrigins("*");}
}
接着写个简单的处理器:
@Component
public class ChatHandler extends TextWebSocketHandler {private final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();@Overridepublic void afterConnectionEstablished(WebSocketSession session) {sessions.put(session.getId(), session);System.out.println("连接建立:" + session.getId());}@Overrideprotected void handleTextMessage(WebSocketSession session, TextMessage message) throws IOException {for (WebSocketSession s : sessions.values()) {if (s.isOpen()) {s.sendMessage(message);}}}@Overridepublic void afterConnectionClosed(WebSocketSession session, CloseStatus status) {sessions.remove(session.getId());System.out.println("连接关闭:" + session.getId());}
}
运行后前端连接:
const socket = new WebSocket("ws://localhost:8080/chat");
socket.onmessage = (e) => console.log(e.data);
socket.send("hello");
一切似乎完美——直到你上线。
四、连接为什么总掉?
WebSocket 本身的协议很稳定,问题往往出在 网络环境和中间层 上。
常见掉线原因包括:
- 反向代理或负载均衡超时比如 Nginx 默认
proxy_read_timeout
只有 60s,超过这个时间没数据传输就断。 - 客户端断线未检测到浏览器或移动端断网后,TCP 连接其实已死,但服务端 session 仍“以为”它活着。
- 心跳机制缺失WebSocket 没有内置心跳,需要手动实现。
- 服务器重启或集群切换session 保存在内存中,节点切换就断。
五、正确的心跳机制:前后端都要配合
心跳包本质是“定期发送一条确认连接活性的消息”,防止连接假死。
前端实现(示例)
let ws = new WebSocket("ws://localhost:8080/chat");
let heartbeatInterval;ws.onopen = () => {heartbeatInterval = setInterval(() => {ws.send(JSON.stringify({type: "ping"}));}, 30000);
};
服务端处理心跳
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws IOException {String payload = message.getPayload();if ("ping".equals(payload)) {session.sendMessage(new TextMessage("pong"));return;}// 正常消息广播for (WebSocketSession s : sessions.values()) {if (s.isOpen()) s.sendMessage(message);}
}
这样浏览器每 30 秒发一次 ping
,服务端回复 pong
,既能维持连接,也能检测超时。
六、集群部署下的“广播失效”问题
当你把服务从单机部署改成集群,就会发现: 用户 A 连到节点 1 发消息,用户 B 连到节点 2,消息却收不到。
这是因为每个节点各自维护独立的 sessions
,它们之间不共享。
✅ 解决方案:引入消息中间件
最常见的方式是使用 Redis Pub/Sub:
@Component
public class WebSocketRedisListener implements MessageListener {private final Map<String, WebSocketSession> sessions;public WebSocketRedisListener(Map<String, WebSocketSession> sessions) {this.sessions = sessions;}@Overridepublic void onMessage(Message message, byte[] pattern) {String msg = new String(message.getBody());for (WebSocketSession session : sessions.values()) {try {session.sendMessage(new TextMessage(msg));} catch (IOException ignored) {}}}
}
然后在发送消息时:
redisTemplate.convertAndSend("chat-channel", jsonMessage);
这样,无论消息在哪个节点产生,所有节点都能广播出去。
Redis 的轻量特性非常适合这种场景,不推荐直接上 Kafka 或 RabbitMQ,延迟太高。
七、消息泛滥与内存泄漏
WebSocket 消息广播虽然方便,但如果你不控制消息频率、缓存清理和 session 生命周期,很容易出现:
- 内存逐步升高;
- GC 频繁;
- CPU 飙高;
- “Ghost session”(僵尸连接)堆积。
优化思路:
- 使用
ConcurrentHashMap
存储 session,并在afterConnectionClosed
时及时清理; - 定期检测
session.isOpen()
,关闭无效连接; - 对消息频率较高的场景(如行情推送)增加队列缓冲;
- 不建议在内存中维护大量 session,可考虑使用 Redis + Channel 分布式方案。
八、Spring Boot + STOMP 简化方案(可选)
Spring Boot 其实提供了更“高级”的封装:STOMP + SockJS + MessageBroker。
简单来说:
- STOMP 是一种基于 WebSocket 的消息协议;
- SockJS 兼容性更强,支持自动降级;
- Spring Boot 自动帮你做 session 管理、广播、订阅等操作。
只需配置:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketStompConfig implements WebSocketMessageBrokerConfigurer {@Overridepublic void registerStompEndpoints(StompEndpointRegistry registry) {registry.addEndpoint("/ws").setAllowedOriginPatterns("*").withSockJS();}@Overridepublic void configureMessageBroker(MessageBrokerRegistry registry) {registry.enableSimpleBroker("/topic");registry.setApplicationDestinationPrefixes("/app");}
}
前端直接:
const socket = new SockJS('/ws');
const client = Stomp.over(socket);client.connect({}, () => {client.subscribe('/topic/message', (msg) => {console.log(msg.body);});client.send('/app/chat', {}, JSON.stringify({content: 'hello'}));
});
Spring Boot 自动处理订阅、广播、心跳等问题,极大减少手写代码。
九、总结:稳定的 WebSocket 要点
问题 | 原因 | 解决方案 |
---|---|---|
长连接掉线 | 反向代理超时、无心跳 | 定时心跳 + Nginx proxy_read_timeout |
假死连接 | 客户端断网未检测 | 定期 ping/pong |
集群广播失败 | Session 不共享 | Redis Pub/Sub |
内存泄漏 | Session 未清理 | 定期检测 + 主动关闭 |
兼容性差 | 浏览器/代理不支持 | 使用 SockJS/STOMP |
WebSocket 并不是“配置一下就能用”的黑盒技术。 一旦连接长期存在,它就和数据库连接池一样,需要监控、清理、心跳、限流。
在 Spring Boot 项目中,用好心跳机制、合理维护 session、配合 Redis 实现广播,才能真正让你的实时通信系统稳如老狗。