Spring WebSocket(STOMP)
- 一句话定义
- 一、核心原理
- 二、STOMP常用指令帧
- 三、目的地destination前缀含义(面试重点)
- 四、核心注解
- 五、后端完整配置类
- 六、Controller业务代码示例
- 示例1:广播消息(群聊,@SendTo)
- 示例2:一对一私聊(@SendToUser)
- 七、前端 stomp.js 完整示例
- 八、SockJS是什么
- 九、心跳机制 HeartBeat(生产环境必备)
- 十、两种消息Broker对比
- SimpleBroker(内置内存Broker)
- 十一、踩坑点与注意事项
- 十二、适用场景
- 十三、面试精简背诵版
- 十四、高频面试题
一句话定义
WebSocket:底层双向长连接协议,通过HTTP握手升级协议,建立TCP长通道。传统HTTP只能前端发起请求、后端响应;WebSocket建立连接之后,服务端可以主动向前端推送消息。
STOMP(Simple Text Oriented Messaging Protocol,面向简单文本消息协议),是运行在WebSocket之上的应用层子协议,相当于一套消息通信规范,类似HTTP定义请求响应格式。原生WebSocket只提供字节流通道,没有消息路由、订阅机制;STOMP补齐了发布/订阅模型,消息依靠destination目的地进行路由,Spring框架对STOMP做了封装,也就是我们常说的 Spring WebSocket STOMP。
通俗类比:WebSocket相当于一根双向通话电话线;STOMP是电话双方约定好的通话指令规范,定义连接、订阅、发消息、断开、心跳等指令。
一、核心原理
- 握手阶段:客户端发起普通HTTP请求,请求头携带
Upgrade: websocket标识。后端校验通过,返回101状态码,协议升级,TCP长连接正式建立。>
握手是HTTP,通信阶段是WebSocket长连接。
- STOMP帧通信:长连接通道内传输STOMP帧,每一帧代表一条指令。
- 消息代理Broker:Spring内置消息代理,接收消息,根据
destination目的地,将消息分发到所有订阅该地址的客户端。
二、STOMP常用指令帧
| 指令 | 作用 |
|---|---|
CONNECT | 客户端发起STOMP连接认证 |
SUBSCRIBE | 订阅某个目的地destination,监听推送消息 |
SEND | 客户端向指定目的地发送消息 |
MESSAGE | 服务端推送消息帧,发给订阅客户端 |
DISCONNECT | 客户端断开STOMP连接 |
三、目的地destination前缀含义(面试重点)
/topic:广播主题,一对多。消息推送给所有订阅该topic的客户端,适合全局通知、群聊、大屏广播。/queue:点对点消息,一对一私聊、用户私有消息。/app:业务前缀。消息不会交给消息代理,转发给后端@MessageMapping控制器执行业务逻辑。/user:用户专属私有目的地,结合登录认证,消息只推送给指定登录用户。前端订阅地址格式:/user/queue/xxx
四、核心注解
@EnableWebSocketMessageBroker:开启STOMP消息代理,放在SpringBoot启动类或者配置类,开启WebSocket STOMP功能,核心注解。@MessageMapping:接收客户端通过STOMP的SEND指令发送过来的消息,作用类比SpringMVC中的@RequestMapping。@RequestMapping匹配HTTP请求URL;@MessageMapping匹配STOMP消息的目的地destination。客户端发送消息到/app/xxx,配置里设置前缀/app,注解只需要填写后面路径/xxx,匹配成功就进入该方法执行业务。>
区别:
@RequestMapping是HTTP短连接,一问一答;@MessageMapping基于WebSocket长连接,一次握手建立连接后,可以反复发送消息,多次触发该方法。
@SendTo:方法处理业务完成后,自动将返回结果推送到指定topic,广播给所有订阅客户端。@SendToUser:消息只推送给当前登录认证用户,用于一对一私聊。
五、后端完整配置类
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 WebSocketStompConfig implements WebSocketMessageBrokerConfigurer { /** * 注册STOMP连接端点,前端连接地址:ws://localhost:8080/ws */ @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/ws") // 跨域配置,线上不要写*,指定前端域名 .setAllowedOriginPatterns("*") // SockJS降级方案 .withSockJS() // 设置心跳,单位毫秒,10秒心跳保活 .setHeartbeatTime(10000); } /** * 配置消息代理 */ @Override public void configureMessageBroker(MessageBrokerRegistry registry) { // 开启内置内存消息代理,处理topic、queue目的地 registry.enableSimpleBroker("/topic","/queue"); // 应用消息前缀,客户端发送业务消息以 /app 开头 registry.setApplicationDestinationPrefixes("/app"); } }六、Controller业务代码示例
示例1:广播消息(群聊,@SendTo)
import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.SendTo; import org.springframework.stereotype.Controller; @Controller public class StompChatController { // 前端发送消息目的地:/app/chat @MessageMapping("/chat") @SendTo("/topic/chatRoom") // 处理结果广播推送给所有订阅 /topic/chatRoom 的客户端 public String chatHandle(String msg){ return "收到广播消息:" + msg; } }示例2:一对一私聊(@SendToUser)
@Controller public class StompPrivateMsgController { // 前端发送消息:/app/privateMsg @MessageMapping("/privateMsg") @SendToUser("/queue/private") // 仅推送给当前登录用户 public String privateMsgHandle(String message){ return "私聊消息内容:" + message; } }前端订阅私聊地址:
/user/queue/private
七、前端 stomp.js 完整示例
前端引入sockjs-client和stompjs,建立连接、订阅主题、发送消息。
<!-- 引入依赖js --> <script src="[https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js](https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js)"></script> <script src="[https://cdn.jsdelivr.net/npm/stompjs@6/dist/esm5/stomp.min.js](https://cdn.jsdelivr.net/npm/stompjs@6/dist/esm5/stomp.min.js)"></script> <script> // 1. 创建SockJS连接实例,后端端点/ws const socket = new SockJS('[http://localhost:8080/ws](http://localhost:8080/ws)'); // 2. 创建STOMP客户端 const stompClient = Stomp.over(socket); // 3. 发起STOMP连接 stompClient.connect({}, function(frame) { console.log('STOMP连接成功', frame); // 订阅广播主题 /topic/chatRoom stompClient.subscribe('/topic/chatRoom', function(msg) { console.log("收到广播消息:", msg.body); }); // 订阅个人私聊消息 /user/queue/private stompClient.subscribe('/user/queue/private', function(msg) { console.log("收到私聊消息:", msg.body); }); // 发送消息到后端业务接口 /app/chat stompClient.send("/app/chat", {}, "Hello STOMP!"); }, function(error) { // 连接失败回调 console.error("STOMP连接失败", error); }); </script>八、SockJS是什么
部分老旧浏览器、防火墙、反向代理会拦截原生WebSocket协议。.withSockJS()开启降级策略:优先尝试原生WebSocket;失败自动切换为http长轮询、xhr-stream。
优势:后端、前端业务代码完全不用改动,自动兼容低版本环境。
九、心跳机制 HeartBeat(生产环境必备)
长时间空闲连接,防火墙、Nginx会主动断开TCP连接,服务端无法感知客户端掉线,造成无效连接占用服务器资源。
心跳用于连接保活、检测客户端是否离线。
在注册端点时通过setHeartbeatTime(10000)设置心跳周期(单位ms,示例10s),服务端定时发送心跳包,客户端回复心跳,维持连接。
十、两种消息Broker对比
SimpleBroker(内置内存Broker)
SimpleBroker是Spring STOMP内置、基于JVM内存实现的简易消息代理,Broker即消息代理,负责接收消息、维护客户端订阅关系,并根据目的地destination完成消息分发。消息和订阅信息全部保存在当前JVM内存,不需要额外部署MQ中间件,开箱即用。
- 优点:零额外组件依赖,开发测试快速验证功能,配置简单,轻量易用。
- 缺点:仅单机有效,集群多实例之间内存隔离,消息无法跨服务转发;消息无持久化,服务重启订阅关系、消息全部丢失;缺少消息ACK、死信、消息重试等高级特性。
| 类型 | SimpleBroker(内置内存Broker) | 外部Broker RabbitMQ/ActiveMQ |
|---|---|---|
| 部署 | 内存实现,无需额外中间件,开箱即用 | 需要独立部署MQ,并开启STOMP插件 |
| 集群支持 | ❌ 单机有效,多实例之间消息隔离,A服务消息不能推送到B服务连接的客户端 | ✅ 支持集群多实例,消息跨节点互通 |
| 适用场景 | 单机项目、小流量演示、简单测试 | 高并发、大量长连接、生产环境 |
| 消息持久化 | ❌ 内存消息,服务重启消息丢失 | ✅ 支持消息持久化、消息重试、死信队列 |
| 性能上限 | 连接数量有限 | 支持上万长连接,性能更强 |
开启方式:
registry.enableSimpleBroker("/topic","/queue");
十一、踩坑点与注意事项
- 跨域问题
前后端分离项目,前端与后端域名端口不一致,需要setAllowedOriginPatterns()配置跨域。生产环境不要直接写*,指定前端域名提升安全性。 - 认证鉴权
STOMP身份校验在握手阶段完成。连接建立之后,普通SpringMVC拦截器不再生效,需要使用ChannelInterceptor拦截STOMP消息帧做权限校验。 - 集群部署大坑
内置SimpleBroker存储在JVM内存,多台服务实例之间互不感知。集群环境必须接入RabbitMQ作为外部STOMP消息代理,才能实现跨服务器消息推送。 - 连接资源限制
每一条WebSocket长连接占用一个TCP文件句柄。在线用户量大时,需要调整服务器操作系统最大文件句柄数,防止连接打满。 - Nginx反向代理配置
Nginx代理WebSocket长连接,必须添加升级相关配置,否则握手失败、连接很快断开,需要配置Upgrade头、保持长连接超时时间。 - 消息丢失
SimpleBroker消息保存在内存,服务重启消息全部丢失;需要消息持久化场景,使用RabbitMQ外部Broker。 - 什么是SimpleBroker
SimpleBroker是Spring WebSocket STOMP内置的内存消息代理,在JVM内存维护客户端订阅关系,负责消息路由分发,无需额外MQ中间件。只能单机使用,集群环境下多台实例之间无法互通消息,消息不保持久化,适合开发测试;生产集群项目建议接入RabbitMQ外部Broker。
十二、适用场景
✅ 推荐场景:系统站内实时通知、在线聊天、数据大屏实时刷新、订单状态实时推送、物联网设备状态上报。
❌ 不推荐:超大规模百万级长连接场景(需要专业MQ架构优化)。
十三、面试精简背诵版
Spring WebSocket STOMP,是在WebSocket双向长连接基础上,使用STOMP子协议实现发布订阅模式的实时消息推送。WebSocket依靠HTTP握手升级TCP长连接,解决HTTP只能一问一答的局限;STOMP提供SUBSCRIBE订阅、SEND发送消息,通过destination目的地路由消息。
使用@EnableWebSocketMessageBroker开启功能,@MessageMapping接收客户端消息,@SendTo实现广播推送,@SendToUser实现一对一私聊。SimpleBroker是内存内置代理,仅支持单机;集群生产环境需要接入RabbitMQ作为外部STOMP Broker。搭配SockJS实现浏览器降级,心跳机制保活连接。多用于实时通知、在线聊天、实时数据大屏。
十四、高频面试题
- WebSocket 和 STOMP 的区别?
WebSocket是底层双向长连接TCP协议;STOMP是运行在WebSocket之上的应用层文本子协议,定义消息帧、发布订阅、目的地路由。原生WebSocket没有订阅和消息路由能力,STOMP做了上层封装。 - /topic /queue /app /user 四个目的地前缀分别是什么?
/topic:广播,一对多,群发消息;/queue:点对点消息;/app:交给后端@MessageMapping执行业务逻辑;/user:用户私有目的地,一对一推送私聊消息。
- SockJS的作用?
浏览器、防火墙不支持原生WebSocket时,自动降级为HTTP长轮询,业务代码无需修改,兼容老旧环境。 - SimpleBroker为什么集群不能用?
SimpleBroker消息存储在当前JVM内存,多台服务实例之间不共享消息,A服务收到消息,B服务上建立连接的客户端收不到广播,集群环境必须接入RabbitMQ。 - STOMP心跳的作用?
TCP连接保活,识别掉线客户端,及时释放无效TCP连接,避免服务器连接资源泄漏。 - STOMP连接认证为什么不能用普通MVC拦截器?
WebSocket握手完成后,通信走消息通道,不再经过SpringMVC的Servlet链路,MVC拦截器失效,要使用ChannelInterceptor拦截消息帧。 @MessageMapping和@RequestMapping的区别?@RequestMapping处理HTTP请求,匹配URL,属于短连接,一问一答;@MessageMapping接收STOMP的SEND消息,匹配destination目的地,基于WebSocket长连接,一次握手建立连接后可以反复收发消息。