前端websocket(使用websocket做群聊、单聊应用)

前端websocket(使用websocket做群聊、单聊应用)
使用websocket做群聊、单聊应用

SpringBoot 集成 WebSocket 实现群聊单聊功能,核心是基于 Spring 官方的spring-websocket模块,结合SimpleBroker(简单消息代理)实现消息的发布 / 订阅,再通过用户标识绑定处理单聊、广播地址处理群聊。

下面给出完整可运行的实现方案,包含配置类、消息实体、WebSocket 处理器、前端测试页面,兼顾易用性和 SpringBoot 最佳实践,适配 SpringBoot 2.x/3.x(3.x 仅需调整依赖坐标,代码无差异)。

一、核心依赖引入

首先在pom.xml中添加 WebSocket 核心依赖,SpringBoot 已做自动配置,无需额外引入容器依赖。

SpringBoot 2.x 依赖

<!-- WebSocket核心依赖 --><dependency>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-websocket</artifactId></dependency><!-- Web基础依赖(用于前端页面访问) --><dependency>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-web</artifactId></dependency>

SpringBoot 3.x 依赖(仅调整 jakarta 包,依赖坐标不变)

上述依赖直接使用即可,SpringBoot 3.x 会自动适配 jakarta.servlet-api,无需额外修改。

二、WebSocket 核心配置类

配置消息代理(处理消息转发)、端点注册(前端连接的入口)、消息映射(前端发送消息的路径),并开启用户认证绑定(为单聊做准备,通过Principal标识用户)。

import org.springframework.context.annotation.Configuration;import org.springframework.messaging.simp.config.MessageBrokerRegistry;import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;import org.springframework.web.socket.config.annotation.StompEndpointRegistry;import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;/** * WebSocket配置类:开启STOMP协议的WebSocket消息代理,实现发布/订阅 * STOMP:简单文本定向消息协议,基于WebSocket封装,简化消息通信 */@Configuration@EnableWebSocketMessageBroker // 开启WebSocket消息代理,支持STOMP协议public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {    /**     * 配置消息代理:设置消息的订阅前缀(前端订阅此前缀的地址接收消息)     */    @Override    public void configureMessageBroker(MessageBrokerRegistry config) {        // 启用简单消息代理(内存级,生产环境可替换为RabbitMQ/ActiveMQ)        // 配置**群聊/单聊的消息接收前缀**,前端订阅以"/topic"(群聊)、"/user"(单聊)开头的地址        config.enableSimpleBroker("/topic", "/user");        // 配置**前端发送消息的前缀**,前端发送消息的地址需以"/app"开头,后端通过@MessageMapping匹配        config.setApplicationDestinationPrefixes("/app");        // 配置**单聊的用户前缀**,后端向指定用户发送消息时,会自动拼接"/user/{userId}/",前端订阅"/user/自己的id/xxx"        config.setUserDestinationPrefix("/user");    }    /**     * 注册WebSocket端点:前端通过此地址建立WebSocket连接     * setAllowedOriginPatterns:允许跨域(生产环境可指定具体域名)     * withSockJS:开启SockJS支持,当浏览器不支持WebSocket时,自动降级为轮询     */    @Override    public void registerStompEndpoints(StompEndpointRegistry registry) {        registry.addEndpoint("/ws/connect") // 核心连接端点                .setAllowedOriginPatterns("*") // 允许所有跨域(生产环境谨慎使用)                .withSockJS(); // 开启SockJS降级    }}

关键概念说明

  1. STOMP 协议:WebSocket 是底层协议,无统一的消息格式,STOMP 为其封装了发布 / 订阅点对点的消息模型,简化开发;
  2. /topic:广播前缀,发送到此前缀的消息,所有订阅的客户端都能接收(用于群聊);
  3. /user:用户前缀,发送到此前缀的消息,仅指定用户能接收(用于单聊);
  4. /app:前端发送消息的前缀,后端通过@MessageMapping匹配此路径下的消息。

三、消息实体定义

定义前端发送的消息后端推送的消息实体,统一消息格式,使用lombok简化 get/set(可手动实现)。

<!-- 引入lombok(可选) --><dependency>    <groupId>org.projectlombok</groupId>    <artifactId>lombok</artifactId>    <optional>true</optional></dependency>
import lombok.Data;import java.util.Date;/** * 前端发送的消息实体 */@Datapublic class ChatMessage {    // 消息类型:GROUP(群聊)、SINGLE(单聊)    private MessageType type;    // 发送者ID    private String senderId;    // 接收者ID(群聊时可为空/固定值,单聊时为目标用户ID)    private String receiverId;    // 消息内容    private String content;    // 发送时间    private Date sendTime = new Date();    // 消息类型枚举    public enum MessageType {        GROUP, SINGLE    }}
import lombok.Data;import java.util.Date;/** * 后端推送给前端的消息实体(可与发送实体复用,拆分更灵活) */@Datapublic class ResponseMessage {    // 发送者ID    private String senderId;    // 接收者ID(群聊为"all",单聊为目标用户ID)    private String receiverId;    // 消息内容    private String content;    // 发送时间    private Date sendTime;    // 消息类型    private String type;}

四、WebSocket 消息处理器(核心业务)

通过@Controller+@MessageMapping接收前端消息,结合SimpMessagingTemplate实现群聊广播单聊点对点推送,这是业务处理的核心。

import org.springframework.beans.factory.annotation.Autowired;import org.springframework.messaging.handler.annotation.MessageMapping;import org.springframework.messaging.handler.annotation.SendTo;import org.springframework.messaging.simp.SimpMessagingTemplate;import org.springframework.stereotype.Controller;/** * WebSocket消息处理器:接收前端消息,处理群聊/单聊逻辑,推送消息给目标客户端 */@Controllerpublic class ChatController {    /**     * 核心工具类:用于向指定地址/指定用户发送消息     * Spring自动注入,封装了WebSocket消息发送的底层细节     */    @Autowired    private SimpMessagingTemplate messagingTemplate;    /**     * 群聊接口:前端发送消息到"/app/chat/group",后端接收并广播到所有订阅"/topic/group"的客户端     * @MessageMapping:匹配前端发送的消息路径(拼接配置的/app前缀)     * @SendTo:自动将返回值广播到指定的订阅地址     */    @MessageMapping("/chat/group")    @SendTo("/topic/group")    public ResponseMessage groupChat(ChatMessage chatMessage) {        // 封装返回消息        ResponseMessage response = new ResponseMessage();        response.setSenderId(chatMessage.getSenderId());        response.setReceiverId("all");        response.setContent(chatMessage.getContent());        response.setSendTime(chatMessage.getSendTime());        response.setType("群聊");        return response;    }    /**     * 单聊接口:前端发送消息到"/app/chat/single",后端推送给指定的接收者     * 单聊无需@SendTo,通过SimpMessagingTemplate的convertAndSendToUser方法定向推送     */    @MessageMapping("/chat/single")    public void singleChat(ChatMessage chatMessage) {        // 封装返回消息        ResponseMessage response = new ResponseMessage();        response.setSenderId(chatMessage.getSenderId());        response.setReceiverId(chatMessage.getReceiverId());        response.setContent(chatMessage.getContent());        response.setSendTime(chatMessage.getSendTime());        response.setType("单聊");        /**         * 定向发送给指定用户:convertAndSendToUser(接收者ID, 订阅后缀, 消息体)         * 底层会自动拼接为:/user/{receiverId}/topic/single(与前端订阅地址一致)         * 第三个参数true:表示启用用户前缀(与配置的setUserDestinationPrefix一致)         */        messagingTemplate.convertAndSendToUser(                chatMessage.getReceiverId(),                "/topic/single",                response,                null // 消息头,无需时传null        );    }}

核心方法说明

  1. convertAndSendToUser:Spring 提供的单聊核心方法,自动处理用户标识的绑定,无需手动拼接用户地址;
  2. @SendTo:群聊专用,自动将方法返回值广播到指定的/topic地址,所有订阅该地址的客户端都能接收;
  3. 消息路径匹配规则:前端发送/app/chat/group → 后端@MessageMapping("/chat/group") 匹配(自动拼接/app前缀)。

五、前端测试页面(关键)

编写 HTML+JS 前端页面,使用STOMP.jsSockJS.js实现 WebSocket 连接、消息发送、消息接收,可直接放在resources/static目录下(SpringBoot 默认静态资源目录),启动项目后通过http://localhost:8080/chat.html访问。

创建resources/static/chat.html文件,内容如下:

<!DOCTYPE html><html lang="zh-CN"><head>    <meta charset="UTF-8">    <title>SpringBoot WebSocket 群聊&单聊</title>    <style>        .chat-box { width: 600px; margin: 20px auto; }        .msg-list { height: 400px; border: 1px solid #ccc; padding: 10px; overflow-y: auto; margin-bottom: 10px; }        .msg { margin: 5px 0; padding: 8px; border-radius: 4px; }        .group-msg { background: #e6f7ff; }        .single-msg { background: #f0f9eb; }        .input-box { display: flex; gap: 10px; }        input, button { padding: 8px; font-size: 14px; }        input { flex: 1; }    </style>    <!-- 引入STOMP和SockJS依赖(CDN) -->    <script src="https://cdn.jsdelivr.net/npm/sockjs-client@1.6.1/dist/sockjs.min.js"></script>    <script src="https://cdn.jsdelivr.net/npm/stompjs@2.3.3/lib/stomp.min.js"></script></head><body><div class="chat-box">    <h3>WebSocket 群聊&单聊测试</h3>    <div>        <input type="text" id="senderId" placeholder="请输入自己的ID" value="user1">        <button onclick=connect()">连接WebSocket</button>        <button onclick=disconnect()">断开连接</button>    </div>    <!-- 群聊区域 -->    <div class="msg-list" id="groupMsgList">群聊消息:</div>    <div class="input-box">        <input type="text" id="groupContent" placeholder="请输入群聊内容">        <button onclick=sendGroupMsg()">发送群聊</button>    </div>    <!-- 单聊区域 -->    <div class="msg-list" id="singleMsgList">单聊消息:</div>    <div class="input-box">        <input type="text" id="receiverId" placeholder="请输入接收者ID" value="user2">        <input type="text" id="singleContent" placeholder="请输入单聊内容">        <button onclick=sendSingleMsg()">发送单聊</button>    </div></div><script>    var stompClient = null; // STOMP客户端实例    // 1. 建立WebSocket连接    function connect() {        const senderId = document.getElementById("senderId").value;        if (!senderId) {            alert("请输入自己的ID!");            return;        }        // 创建SockJS连接,指向后端注册的端点        const socket = new SockJS('/ws/connect');        // 初始化STOMP客户端        stompClient = Stomp.over(socket);        // 建立连接(可传递请求头,如token,用于用户认证)        stompClient.connect({senderId: senderId}, function (frame) {            console.log("连接成功:" + frame);            alert("WebSocket连接成功!");            // 2. 订阅**群聊消息**:后端广播到/topic/group的消息,此处接收            stompClient.subscribe('/topic/group', function (msg) {                const data = JSON.parse(msg.body);                showMsg(data, "groupMsgList", "group-msg");            });            // 3. 订阅**自己的单聊消息**:后端推送的/user/{senderId}/topic/single,STOMP自动拼接用户ID            stompClient.subscribe('/user/topic/single', function (msg) {                const data = JSON.parse(msg.body);                showMsg(data, "singleMsgList", "single-msg");            });        }, function (error) {            console.error("连接失败:", error);            alert("WebSocket连接失败,请重试!");        });    }    // 断开WebSocket连接    function disconnect() {        if (stompClient) {            stompClient.disconnect();            stompClient = null;            alert("已断开WebSocket连接!");            console.log("连接已断开");        }    }    // 发送群聊消息    function sendGroupMsg() {        if (!stompClient) {            alert("请先连接WebSocket!");            return;        }        const senderId = document.getElementById("senderId").value;        const content = document.getElementById("groupContent").value;        if (!content) {            alert("请输入群聊内容!");            return;        }        // 发送消息到后端/app/chat/group路径        stompClient.send("/app/chat/group", {}, JSON.stringify({            type: "GROUP",            senderId: senderId,            content: content        }));        // 清空输入框        document.getElementById("groupContent").value = "";    }    // 发送单聊消息    function sendSingleMsg() {        if (!stompClient) {            alert("请先连接WebSocket!");            return;        }        const senderId = document.getElementById("senderId").value;        const receiverId = document.getElementById("receiverId").value;        const content = document.getElementById("singleContent").value;        if (!receiverId || !content) {            alert("请输入接收者ID和单聊内容!");            return;        }        // 发送消息到后端/app/chat/single路径        stompClient.send("/app/chat/single", {}, JSON.stringify({            type: "SINGLE",            senderId: senderId,            receiverId: receiverId,            content: content        }));        // 清空输入框        document.getElementById("singleContent").value = "";    }    // 展示消息到页面    function showMsg(data, listId, className) {        const list = document.getElementById(listId);        const msgDiv = document.createElement("div");        msgDiv.className = "msg " + className;        msgDiv[xss_clean] = `            <span style="color: #999; font-size: 12px;">${data.sendTime}</span>            <br>            <span style="font-weight: bold;">${data.senderId}</span> →             <span style="color: #666;">${data.receiverId}</span>:            <span>${data.content}</span>        `;        list.appendChild(msgDiv);        // 滚动到最新消息        list.scrollTop = list.scrollHeight;    }    // 页面关闭时断开连接    _window.onbeforeunload = function () {        disconnect();    };</script></body></html>

前端核心逻辑

  1. 通过SockJS连接后端/ws/connect端点,降级处理不支持 WebSocket 的浏览器;
  2. 用STOMP.over(socket)创建 STOMP 客户端,通过stompClient.connect建立连接;
  3. 订阅/topic/group(群聊)和/user/topic/single(单聊),接收后端推送的消息;
  4. 通过stompClient.send向/app/xxx路径发送消息,后端@MessageMapping匹配处理。

六、测试运行(一步到位)

  1. 启动 SpringBoot 项目,确保端口为 8080(可在application.yml中修改);
  2. 打开两个及以上浏览器标签页,访问http://localhost:8080/chat.html;
  3. 第一个标签页输入 ID 为user1,点击连接 WebSocket
  4. 第二个标签页输入 ID 为user2,点击连接 WebSocket
  5. 群聊测试:在user1的群聊输入框输入内容,点击发送,user2能收到群聊消息,反之亦然;
  6. 单聊测试:在user1的单聊接收者 ID 输入user2,输入单聊内容发送,仅user2能收到单聊消息;user2向user1发送单聊,仅user1能收到。

七、生产环境优化(关键扩展)

上述方案为基础可运行版本,生产环境需做以下优化,保证稳定性和安全性:

1. 跨域限制(替代通配符 *)

生产环境禁止使用setAllowedOriginPatterns("*"),指定具体的前端域名:

前端websocket(使用websocket做群聊、单聊应用)

// 示例:允许http://localhost:3000和https://your-domain.com跨域registry.addEndpoint("/ws/connect")        .setAllowedOriginPatterns("http://localhost:3000", "https://your-domain.com")        .withSockJS();

2. 用户认证(替代简单的 senderId)

实际项目中需结合Token/JWT/Session做用户认证,避免伪造用户 ID:

  • 前端连接时,在stompClient.connect的请求头中传递 Token:
  • javascript
  • 运行
  • stompClient.connect({token: "用户的JWT Token"}, function (frame) { ... });
  • 后端实现ChannelInterceptor,拦截连接请求,解析 Token 获取真实用户 ID,绑定到Principal:
  • java
  • 运行
  • @Component public class WebSocketAuthInterceptor implements ChannelInterceptor { @Override public Message<?> preSend(Message<?> message, MessageChannel channel) { StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); // 处理CONNECT帧,解析Token if (StompCommand.CONNECT.equals(accessor.getCommand())) { String token = accessor.getFirstNativeHeader("token"); // 解析Token获取用户ID(自己实现JWT解析逻辑) String userId = JwtUtil.getUserIdFromToken(token); if (userId == null) { throw new AccessDeniedException("WebSocket连接认证失败,Token无效"); } // 将用户ID绑定到Principal,供后续单聊使用 accessor.setUser(() -> userId); } return message; } }
  • 在 WebSocket 配置类中注册拦截器:
  • java
  • 运行
  • @Autowired private WebSocketAuthInterceptor authInterceptor; @Override public void configureClientInboundChannel(ChannelRegistration registration) { registration.interceptors(authInterceptor); }

3. 替换消息代理(内存→MQ)

默认的enableSimpleBroker是内存级消息代理,集群部署时会出现消息不通的问题,生产环境需替换为RabbitMQ/ActiveMQ(以 RabbitMQ 为例):

  1. 引入 RabbitMQ 依赖:
  2. xml
  3. <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency>
  4. 配置 RabbitMQ 地址:
  5. yaml
  6. spring: rabbitmq: host: 127.0.0.1 port: 5672 username: guest password: guest
  7. 修改 WebSocket 配置,启用 RabbitMQ 作为消息代理:
  8. java
  9. 运行
  10. // 替换enableSimpleBroker为enableStompBrokerRelay config.enableStompBrokerRelay("/topic", "/user") .setRelayHost("127.0.0.1") // RabbitMQ地址 .setRelayPort(61613) // RabbitMQ的STOMP端口 .setClientLogin("guest") .setClientPasscode("guest");

4. 异常处理与连接监控

  • 实现WebSocketHandlerDecoratorFactory监控连接的建立 / 断开;
  • 增加全局异常处理器,捕获消息处理中的异常,避免服务崩溃;
  • 限制单个用户的连接数,防止恶意连接。

八、核心知识点总结

  1. SpringBoot WebSocket基于spring-websocket模块,结合 STOMP 协议实现发布 / 订阅,简化消息通信;
  2. 群聊通过广播实现:后端@SendTo("/topic/group"),前端订阅/topic/group;
  3. 单聊通过用户定向推送实现:后端messagingTemplate.convertAndSendToUser,前端订阅/user/topic/single;
  4. SockJS用于 WebSocket 降级,保证低版本浏览器的兼容性。

九、常见问题排查

  1. 连接失败:检查端点地址是否正确(前端SockJS('/ws/connect')与后端addEndpoint("/ws/connect")一致)、跨域配置是否正确;
  2. 单聊收不到消息:检查接收者 ID 是否正确、后端convertAndSendToUser的订阅后缀是否与前端一致(如/topic/single);
  3. 群聊收不到消息:检查前端是否订阅了/topic/group、后端是否加了@SendTo("/topic/group");
  4. SpringBoot 3.x 报错:确认依赖为最新版本,3.x 使用 jakarta 包,无需修改代码,直接使用上述配置即可。

文章版权声明:除非注明,否则均为边学边练网络文章,版权归原作者所有