欢迎阅读我的文章!更多精彩内容,欢迎关注:
• B站主页
小枫Geek
• 微信公众号Procode  


  WebSocket 是前后端实时通信的利器,但凡你写过在线聊天、系统通知、看板监控,十有八九都用过它。 然而实际开发中,掉线、卡顿、消息延迟、内存暴涨等问题层出不穷。

        很多人以为是“服务器太烂”,其实更多时候,是 WebSocket 的连接机制和 Spring Boot 默认实现没搞懂。

        本文我们就从底层原理出发,系统讲清楚——为什么 WebSocket 会掉线?心跳机制怎么做才靠谱?消息广播如何不炸内存?


1、为什么选择 WebSocket?

HTTP 是“请求-响应”模型,前端必须主动发起请求,服务端被动回应。 而 WebSocket 属于“全双工通信”,一旦建立连接,客户端和服务端就能相互推消息。

这让很多实时场景成为可能:

  • 聊天室消息推送

  • 系统状态监控

  • 股票/交易数据实时刷新

  • 即时通知提醒

但别忘了:连接一旦长期存在,就要面对断线重连、心跳检测、资源回收等麻烦事。

2、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 {

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

接着写个简单的处理器:

@Component
public class ChatHandler extends TextWebSocketHandler {

    private final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();

    @Override
    public void afterConnectionEstablished(WebSocketSession session) {
        sessions.put(session.getId(), session);
        System.out.println("连接建立:" + session.getId());
    }

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws IOException {
        for (WebSocketSession s : sessions.values()) {
            if (s.isOpen()) {
                s.sendMessage(message);
            }
        }
    }

    @Override
    public 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");

一切似乎完美——直到你上线。

3、连接为什么总掉?

WebSocket 本身的协议很稳定,问题往往出在 网络环境和中间层 上。

常见掉线原因包括:

  1. 反向代理或负载均衡超时比如 Nginx 默认 proxy_read_timeout 只有 60s,超过这个时间没数据传输就断。

  2. 客户端断线未检测到浏览器或移动端断网后,TCP 连接其实已死,但服务端 session 仍“以为”它活着。

  3. 心跳机制缺失WebSocket 没有内置心跳,需要手动实现。

  4. 服务器重启或集群切换session 保存在内存中,节点切换就断。

4、正确的心跳机制:前后端都要配合

心跳包本质是“定期发送一条确认连接活性的消息”,防止连接假死。

前端实现(示例)

let ws = new WebSocket("ws://localhost:8080/chat");
let heartbeatInterval;

ws.onopen = () => {
    heartbeatInterval = setInterval(() => {
        ws.send(JSON.stringify({ type: "ping" }));
    }, 30000);
};

ws.onclose = () => clearInterval(heartbeatInterval);

服务端处理心跳

@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,既能维持连接,也能检测超时。

5、集群部署下的“广播失效”问题

当你把服务从单机部署改成集群,就会发现: 用户 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;
    }

    @Override
    public 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,延迟太高。

6、消息泛滥与内存泄漏

WebSocket 消息广播虽然方便,但如果你不控制消息频率、缓存清理和 session 生命周期,很容易出现:

  • 内存逐步升高;

  • GC 频繁;

  • CPU 飙高;

  • “Ghost session”(僵尸连接)堆积。

优化思路:

  1. 使用 ConcurrentHashMap 存储 session,并在 afterConnectionClosed 时及时清理;

  2. 定期检测 session.isOpen(),关闭无效连接;

  3. 对消息频率较高的场景(如行情推送)增加队列缓冲;

  4. 不建议在内存中维护大量 session,可考虑使用 Redis + Channel 分布式方案

7、Spring Boot + STOMP 简化方案(可选)

Spring Boot 其实提供了更“高级”的封装:STOMP + SockJS + MessageBroker

简单来说:

  • STOMP 是一种基于 WebSocket 的消息协议;

  • SockJS 兼容性更强,支持自动降级;

  • Spring Boot 自动帮你做 session 管理、广播、订阅等操作。

只需配置:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketStompConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws").setAllowedOriginPatterns("*").withSockJS();
    }

    @Override
    public 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 自动处理订阅、广播、心跳等问题,极大减少手写代码。

8、总结:稳定的 WebSocket 要点

问题

原因

解决方案

长连接掉线

反向代理超时、无心跳

定时心跳 + Nginx proxy_read_timeout

假死连接

客户端断网未检测

定期 ping/pong

集群广播失败

Session 不共享

Redis Pub/Sub

内存泄漏

Session 未清理

定期检测 + 主动关闭

兼容性差

浏览器/代理不支持

使用 SockJS/STOMP

        WebSocket 并不是“配置一下就能用”的黑盒技术。 一旦连接长期存在,它就和数据库连接池一样,需要监控、清理、心跳、限流。

        在 Spring Boot 项目中,用好心跳机制、合理维护 session、配合 Redis 实现广播,才能真正让你的实时通信系统稳如老狗。

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐