导读:本期聚焦于苹果创作的《Spring Boot 如何整合 WebSocket 实现点对点与群聊功能?》,敬请观看详情。实时聊天场景下,HTTP 轮询不仅延迟高,还会给服务端带来大量无效请求。Spring Boot 结合 WebSocket 与 STOMP 协议,可以利用消息代理把消息精准发往指定用户或广播到公共频道。本文将从依赖引入、端点配置、消息体设计、控制器编写到前端订阅,完整展示点对点私聊和群聊两种功能的实现方式。重点说明 /user 前缀如何与 convertAndSendToUser 配合实现单播,以及 /topic 前缀如何完成群发。同时还会分析消息确认、心跳检测和连接异常处理等细节,帮助开发者快速落地实时通信模块。

在 Web 应用里实现即时聊天,传统做法是客户端每隔几秒发一次 HTTP 请求拉取新消息,也就是短轮询。这种方式不仅延迟不稳定,而且大量请求会消耗服务器资源。WebSocket 在客户端和服务端之间建立一条持久连接,服务端可以随时向客户端推送数据,非常适合点对点私聊和群聊场景。Spring Boot 通过 spring-boot-starter-websocket 模块提供了对 WebSocket 的完整支持,再配合 STOMP 协议和内置消息代理,开发者无需手动处理底层帧,就能实现清晰的消息路由。

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

接下来会从服务端配置、消息模型、控制器逻辑以及前端订阅方式逐步展开,重点说明点对点消息如何利用 /user 目的地精准送达,群聊消息如何通过 /topic 广播给所有在线用户。

一、引入依赖并创建 WebSocket 配置类

在 pom.xml 中加入 spring-boot-starter-websocket 依赖,这个 starter 会传递引入 spring-messaging、spring-websocket 以及 Tomcat 的相关支持。如果项目使用 Gradle,可以添加对应的 implementation 配置。依赖就绪后,需要创建一个配置类实现 WebSocketMessageBrokerConfigurer 接口,重写 configureMessageBroker 和 registerStompEndpoints 方法。

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

上面这段 XML 可以直接放进 pom.xml 的 dependencies 节点。添加依赖后,如果使用 Spring Boot 的自动配置,也可以不写额外配置,但为了控制消息前缀和端点路径,建议自定义配置类。

创建 WebSocketConfig 类并标注 @Configuration 和 @EnableWebSocketMessageBroker。在 configureMessageBroker 方法中调用 enableSimpleBroker("/topic", "/queue") 开启基于内存的消息代理,并设置 setApplicationDestinationPrefixes("/app"),让客户端发送到 /app 开头的消息进入注解方法处理。注册端点时使用 addEndpoint("/ws").withSockJS(),这样即便浏览器或网络环境不支持原生 WebSocket,也可以降级到 SockJS 的轮询或长连接。

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;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/topic", "/queue");
        registry.setApplicationDestinationPrefixes("/app");
    }

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

这里把 /topic 和 /queue 都配置成简单消息代理的目的地。简单消息代理基于内存,适合单机部署;如果后面要横向扩展,可以换成外部消息代理,例如 RabbitMQ 或 ActiveMQ。客户端在连接时访问的端点是 /ws,这个路径与普通 HTTP 接口不冲突,因为它专门处理 WebSocket 握手请求。

二、设计消息模型并编写服务端控制器

为了让点对点和群聊共用一套接口,可以定义一个 ChatMessage 类,包含消息类型、发送者、接收者和内容四个字段。消息类型用来区分 CHAT 和 JOIN 等动作,发送者和接收者使用用户标识,接收者为空时表示群聊消息。内容字段保存文本,后续扩展图片或文件时再增加附件字段即可。保持消息体简洁有助于前后端快速对接。

public class ChatMessage {
    private MessageType type;
    private String sender;
    private String receiver;
    private String content;

    public enum MessageType {
        CHAT,
        JOIN,
        LEAVE
    }

    public MessageType getType() { return type; }
    public void setType(MessageType type) { this.type = type; }
    public String getSender() { return sender; }
    public void setSender(String sender) { this.sender = sender; }
    public String getReceiver() { return receiver; }
    public void setReceiver(String receiver) { this.receiver = receiver; }
    public String getContent() { return content; }
    public void setContent(String content) { this.content = content; }
}

消息实体定义好后,创建一个控制器,使用 @MessageMapping 注解映射客户端发来的目的地。例如客户端向 /app/chat.send 发送消息,服务端用 @MessageMapping("/chat.send") 接收。群聊场景中,方法返回 ChatMessage 对象,并配合 @SendTo("/topic/public") 把返回值广播给所有订阅 /topic/public 的客户端。

import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;

@Controller
public class ChatController {

    @MessageMapping("/chat.send")
    @SendTo("/topic/public")
    public ChatMessage sendMessage(@Payload ChatMessage message) {
        return message;
    }

    @MessageMapping("/chat.join")
    @SendTo("/topic/public")
    public ChatMessage join(@Payload ChatMessage message) {
        message.setContent(message.getSender() + " 加入了聊天室");
        return message;
    }
}

群聊逻辑借助 @SendTo 注解已经足够实现。点对点消息则需要更灵活的方式,不能简单返回广播,而是要指定接收者。这时可以注入 SimpMessagingTemplate,使用 convertAndSendToUser 方法把消息发送到指定用户。点对点方法可以直接写在同一个控制器里。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Controller;

@Controller
public class PrivateChatController {

    @Autowired
    private SimpMessagingTemplate messagingTemplate;

    @MessageMapping("/chat.private")
    public void sendPrivateMessage(@Payload ChatMessage message) {
        // 接收者不能为空,否则会发送失败
        messagingTemplate.convertAndSendToUser(
            message.getReceiver(),
            "/queue/messages",
            message
        );
    }
}

调用 convertAndSendToUser 时,第一个参数是接收者的用户名,第二个参数是目的地。Spring 会把这个目的地与用户会话关联,最终转换成该用户订阅的 /user/queue/messages 地址。用户认证信息来源于 WebSocket 握手阶段的 Principal,如果项目没有安全认证,需要手动设置用户身份,否则 user destination 无法工作。

三、前端建立连接并订阅频道

前端页面需要引入 SockJS 和 STOMP.js 两个脚本,也可以使用 npm 安装对应的模块。连接时先创建 SockJS 实例,再通过 Stomp.over 包装成 STOMP 客户端。连接成功后,客户端可以订阅 /topic/public 接收群聊消息,同时订阅 /user/queue/messages 接收点对点消息。注意 /user 前缀是客户端订阅路径,服务端发送时用的是 /queue/messages,框架会自动补齐用户前缀。

var socket = new SockJS('/ws');
var stompClient = Stomp.over(socket);

stompClient.connect({}, function(frame) {
    stompClient.subscribe('/topic/public', function(message) {
        var chat = JSON.parse(message.body);
        showPublicMessage(chat);
    });

    stompClient.subscribe('/user/queue/messages', function(message) {
        var chat = JSON.parse(message.body);
        showPrivateMessage(chat);
    });
});

发送群聊消息时,客户端调用 stompClient.send,目的地写成 /app/chat.send,消息体为 JSON 字符串。发送点对点消息时,目的地改为 /app/chat.private,JSON 中包含 receiver 字段。前端需要获取当前登录用户名,如果在没有安全框架的项目里,可以直接让用户手动输入昵称,再把昵称作为 sender 传给后端。

function sendPublicMessage(content) {
    var message = {
        type: 'CHAT',
        sender: currentUser,
        content: content
    };
    stompClient.send('/app/chat.send', {}, JSON.stringify(message));
}

function sendPrivateMessage(receiver, content) {
    var message = {
        type: 'CHAT',
        sender: currentUser,
        receiver: receiver,
        content: content
    };
    stompClient.send('/app/chat.private', {}, JSON.stringify(message));
}

这里使用 JSON.stringify 将对象转成字符串,STOMP 协议对消息体没有强制格式要求,但是统一使用 JSON 可以减少服务端解析成本。服务端控制器方法通过 @Payload 自动反序列化成 ChatMessage 对象,前提是 Jackson 依赖存在。Spring Boot 默认已经包含 Jackson,一般不需要额外配置。

订阅 /user/queue/messages 时,如果服务端没有认证,客户端拿不到用户身份,订阅会失败或消息无法送达。一个简单的处理方式是继承 DefaultHandshakeHandler,在 determineUser 方法里根据请求参数返回一个 Principal。虽然这种方式不够安全,但适合本地调试和演示。

四、点对点消息的分发过程与注意事项

点对点消息的传递链路比群聊更复杂。客户端订阅的是 /user/queue/messages,但服务端调用 convertAndSendToUser 时写的是 /queue/messages。Spring 的消息处理组件 UserDestinationMessageHandler 会拦截以 /user/ 开头的订阅,把用户会话与目的地关联起来;发送时会根据 Principal 解析出真实的会话 ID,再把消息推送到对应的客户端。这个过程要求 WebSocket 会话在握手阶段绑定用户身份,如果 Principal 为空,消息会因找不到目标而静默丢失。

群聊的广播比较简单,服务端将消息发送到 /topic/public 后,消息代理会检查所有订阅该目的地的会话,并逐个推送。简单消息代理会将订阅信息保存在内存中,因此服务端重启后订阅关系会丢失,但不影响实时消息。对于需要持久化离线消息的场景,建议引入外部代理或数据库,而不是依赖 WebSocket 本身。

另一个容易忽略的点是消息序列化与反序列化。默认情况下,Spring 使用 Jackson 处理 JSON,如果 ChatMessage 类的字段名与前端发送的 JSON key 不一致,需要添加 @JsonProperty 注解。字段类型也要匹配,例如 content 为字符串,前端不要传数字,否则可能抛出转换异常。

五、连接认证与异常处理优化

生产环境中不应该让前端带着用户名参数直接连接 WebSocket。可以在握手拦截器中校验 JWT 或 Session,通过后把用户信息写入 attributes,再在 determineUser 中读取。Spring Security 与 WebSocket 集成后,可以直接从 Principal 获取当前用户。配置 HandshakeInterceptor 可以统一处理跨域、Token 传递等问题。

import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;

import java.util.Map;

public class AuthHandshakeInterceptor implements HandshakeInterceptor {

    @Override
    public boolean beforeHandshake(ServerHttpRequest request,
                                   ServerHttpResponse response,
                                   WebSocketHandler wsHandler,
                                   Map<String, Object> attributes) {
        // 从 URL 或 Header 中提取 token,写入 attributes
        String token = request.getURI().getQuery();
        attributes.put("token", token);
        return true;
    }

    @Override
    public void afterHandshake(ServerHttpRequest request,
                               ServerHttpResponse response,
                               WebSocketHandler wsHandler,
                               Exception exception) {
    }
}

注意这里的 Map<String, Object> 在代码块中已经做了转义,显示为 Map 类型。拦截器返回 true 表示允许握手,false 则拒绝连接。实际项目中应当在 beforeHandshake 里校验 token 有效性,避免匿名用户占用连接资源。

断线重连可以通过前端监听连接断开事件来实现,SockJS 本身提供一定程度的自动重试,但 STOMP 连接过期后需要重新订阅。心跳机制可以在服务端配置 setHeartbeatValue,或在客户端 connect 时设置 heartbeat 参数。例如 stompClient.heartbeat.outgoing = 20000; 可以让客户端每 20 秒发送一次心跳,帮助代理及时清理失效会话。

Spring BootWebSocket点对点群聊修改时间:2026-09-19 19:32:34

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