Spring Boot 整合 WebSocket 如何实现点对点与群聊功能?

来源:MongoDB教程作者:上海GEO公司头衔:草根站长
导读:本期聚焦于上海GEO公司创作的《Spring Boot 整合 WebSocket 如何实现点对点与群聊功能?》,敬请观看详情。为什么你的即时通讯功能总是轮询接口、性能差还延迟高?答案往往是没用对通信方式。WebSocket 提供的全双工长连接能从根本上解决服务端主动推送的问题,而 Spring Boot 提供的 spring-boot-starter-websocket 模块让整合过程变得相当简洁。本文将手把手带你搭建一个完整的聊天服务:从引入依赖、编写 WebSocket 配置类开始,逐步实现连接建立时的会话管理、基于 Session 的点对点私聊,以及通过 ConcurrentHashMap 维护在线用户列表实现的群聊广播。文中还会给出心跳检测、异常断开清理、并发安全等生产环境必须注意的细节,并附带可直接运行的完整代码示例,帮你避开常见坑点。

在传统的 HTTP 请求响应模式下,客户端想获取服务端的新消息,只能靠轮询或者长轮询,这种方式不仅浪费带宽,实时性也很难保证。WebSocket 协议通过一次 HTTP 握手升级为全双工长连接,服务端可以随时主动推送消息,天然适合聊天、通知推送这类场景。Spring Boot 对 WebSocket 提供了开箱即用的支持,本文将基于原生 WebSocket API(而不是 STOMP 子协议),完整实现点对点私聊和群聊广播两个核心功能,并讨论生产环境中的注意事项。

Spring Boot 整合 WebSocket 如何实现点对点与群聊功能?

一、搭建基础环境与 WebSocket 配置

首先创建一个 Spring Boot 项目,在 pom.xml 中引入 WebSocket starter 依赖:

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

这个 starter 会自动引入 spring-websocket 和容器相关的支持包。接下来需要编写一个配置类,向容器注册 ServerEndpointExporter,它的作用是把标注了 @ServerEndpoint 注解的类注册为 WebSocket 端点。需要特别注意:如果项目将来部署到外置 Tomcat(打 war 包),这个 Bean 就不要注册了,否则会报重复端点的错误,因为外置容器会自己完成端点扫描和发布。

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

@Configuration
public class WebSocketConfig {

    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

这里采用的是 JSR-356 标准的注解式开发方式,优点是代码直观、不需要额外封装消息处理器。另一种方案是使用 Spring 封装的 WebSocketHandler 体系,通过继承 TextWebSocketHandler 来处理消息,配合 WebSocketConfigurer 注册路径。两种方式功能等价,本文选择注解方式,因为它的会话管理更直接。

二、会话管理:点对点通信的核心

点对点私聊的前提是服务端知道每个在线用户对应的 WebSocket 会话。最常用的做法是用一个线程安全的 Map 来维护用户标识和会话的映射关系。这里有几个细节容易踩坑:第一,必须使用 ConcurrentHashMap 而不是普通 HashMap,因为 WebSocket 的连接、断开、收发消息是由不同线程触发的,普通 HashMap 在并发写入时可能出现数据丢失甚至死循环;第二,连接地址建议带上用户名参数,例如 ws://localhost:8080/ws/chat?userId=zhangsan,服务端在连接建立时取出参数作为 Map 的 key。

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@ServerEndpoint("/ws/chat/{userId}")
@Component
public class ChatWebSocketServer {

    // 全局在线用户会话表,静态保证所有实例共享
    private static final Map<String, Session> ONLINE_SESSIONS = new ConcurrentHashMap&ltgt();;

    @OnOpen
    public void onOpen(Session session, @PathParam("userId") String userId) {
        ONLINE_SESSIONS.put(userId, session);
        System.out.println("用户上线:" + userId + ",当前在线人数:" + ONLINE_SESSIONS.size());
    }

    @OnClose
    public void onClose(@PathParam("userId") String userId) {
        ONLINE_SESSIONS.remove(userId);
        System.out.println("用户下线:" + userId);
    }

    @OnError
    public void onError(Session session, Throwable error) {
        error.printStackTrace();
        try {
            session.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

需要提醒的是,@ServerEndpoint 标注的类默认由 WebSocket 容器管理生命周期,每次连接都会创建一个新实例,所以它不能像普通 Spring Bean 那样直接注入 Service。如果需要在 WebSocket 类中使用业务层的 Bean,要通过 SpringUtils.getContext().getBean(xxx.class) 这种工具类手动获取,或者实现一个静态注入的 Handler。这是新手最常遇到的问题之一。

接下来实现私聊逻辑。客户端发送的消息约定为 JSON 格式,包含发送者、接收者和内容三个字段,服务端解析后从 Map 中查找目标用户的 Session,找到就推送,找不到就返回对方不在线的提示:

@OnMessage
public void onMessage(String message, Session session) throws IOException {
    // 实际项目中建议用 Jackson 解析,这里为了直观用简单拆分
    ChatMessage msg = JSON.parseObject(message, ChatMessage.class);

    Session targetSession = ONLINE_SESSIONS.get(msg.getToUserId());
    if (targetSession != null && targetSession.isOpen()) {
        // 同步推送给接收方
        targetSession.getBasicRemote().sendText(message);
        // 回显给发送方,表示发送成功
        session.getBasicRemote().sendText(message);
    } else {
        session.getBasicRemote().sendText("{\"type\":\"system\",\"content\":\"对方不在线\"}");
    }
}

发送消息时有同步和异步两种选择:getBasicRemote().sendText() 是同步阻塞的,发送完成才返回;getAsyncRemote().sendText() 则立即返回,适合高并发场景。如果业务对消息顺序有要求,建议用同步方式,或者给每个会话加发送锁,避免异步发送时消息乱序。

三、群聊广播与消息模型设计

群聊的实现比私聊更简单,本质就是遍历在线用户表,把消息逐一推送出去。但遍历时的并发问题必须重视:如果一边遍历一边有用户上下线,直接 for-each 遍历 Map 可能抛出 ConcurrentModificationException。虽然 ConcurrentHashMap 的迭代器是弱一致性的,不会抛这个异常,但更稳妥的做法是先复制一份快照再遍历:

public void broadcast(String message) {
    ONLINE_SESSIONS.forEach((userId, session) -> {
        try {
            if (session.isOpen()) {
                session.getBasicRemote().sendText(message);
            }
        } catch (IOException e) {
            // 单个用户发送失败不应影响其他人
            System.err.println("推送给 " + userId + " 失败:" + e.getMessage());
        }
    });
}

消息格式建议统一定义一个 DTO,用 type 字段区分消息类型,前端根据类型做不同的渲染。一个典型的消息结构如下:

public class ChatMessage {
    private String type;      // chat 私聊 / group 群聊 / system 系统提示
    private String fromUserId;
    private String toUserId;  // 群聊时可留空
    private String content;
    private Long timestamp;
    // 省略 getter 和 setter
}

把系统提示也纳入统一消息模型有好处,比如用户上下线时广播一条 type 为 system 的消息,前端就能在聊天室里显示某某加入了房间。这样所有消息走同一条通道,前端解析逻辑只需要一份。

四、生产环境必须考虑的几个问题

1. 多实例部署与会话共享。上面方案最大的局限在于会话表是单机内存态的。一旦服务部署两个以上实例,经过 Nginx 负载均衡后,用户 A 的连接可能落在节点一,用户 B 落在节点二,点对点消息就找不到目标会话了。解决方案有两种:一是负载均衡层开启 IP Hash 或者基于用户 ID 的粘性会话,保证同一用户始终连到同一节点;二是引入消息中间件(Redis 发布订阅、RabbitMQ 或 Kafka),每个节点把消息发到中间件,再由各节点在本地会话表中查找并推送。后者是主流做法,扩展性更好。

2. 心跳与僵尸连接清理。网络异常断开时,服务端可能收不到 onClose 事件,Map 里会残留已经失效的会话。建议客户端定时(比如 30 秒)发送一个心跳消息,服务端记录每个会话的最后活跃时间,再用一个定时任务每分钟扫描一次,把超过两分钟没有心跳的会话强制关闭并移除,同时向对方推送离线状态。

3. 安全认证。千万不要直接把用户 ID 暴露在 URL 上,任何人改个参数就能冒充别人。正确做法是连接时携带 Token(放在 URL 参数或首次消息中),服务端在 onOpen 阶段校验 Token 合法性,解析出真实用户 ID 后再写入会话表,校验失败直接调用 session.close() 拒绝连接。

4. 前端连接示例。最后给出浏览器的连接代码,帮助读者快速验证:

const userId = "zhangsan";
const ws = new WebSocket("ws://localhost:8080/ws/chat/" + userId);

ws.onopen = () => console.log("连接已建立");
ws.onmessage = (event) => console.log("收到消息:", event.data);
ws.onclose = () => console.log("连接已关闭");

// 发送私聊消息
ws.send(JSON.stringify({
    type: "chat",
    fromUserId: "zhangsan",
    toUserId: "lisi",
    content: "你好,在吗?"
}));

整体来看,Spring Boot 整合原生 WebSocket 的开发成本并不高,核心就是围绕 Session 的生命周期做好管理。单机场景下本文方案可以直接落地,分布式场景则要在消息路由上多下功夫。如果项目对消息可靠性、离线消息、已读回执有更高要求,可以考虑在 WebSocket 之上引入 STOMP 协议配合消息代理,那会是另一个更完整的架构方案。

Spring BootWebSocket点对点通信修改时间:2026-09-06 09:34:39

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260906/51460.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。