Spring WebSocket(STOMP)

Spring WebSocket(STOMP)

一句话定义

WebSocket :底层双向长连接协议,通过HTTP握手升级协议,建立TCP长通道。传统HTTP只能前端发起请求、后端响应;WebSocket建立连接之后,服务端可以主动向前端推送消息。

STOMP (Simple Text Oriented Messaging Protocol,面向简单文本消息协议),是运行在WebSocket之上的应用层子协议 ,相当于一套消息通信规范,类似HTTP定义请求响应格式。原生WebSocket只提供字节流通道,没有消息路由、订阅机制;STOMP补齐了发布/订阅模型,消息依靠destination目的地进行路由,Spring框架对STOMP做了封装,也就是我们常说的 Spring WebSocket STOMP。

通俗类比:WebSocket相当于一根双向通话电话线 ;STOMP是电话双方约定好的通话指令规范,定义连接、订阅、发消息、断开、心跳等指令。

一、核心原理

  1. 握手阶段 :客户端发起普通HTTP请求,请求头携带Upgrade: websocket标识。后端校验通过,返回101状态码,协议升级,TCP长连接正式建立。>

握手是HTTP,通信阶段是WebSocket长连接。

  1. STOMP帧通信:长连接通道内传输STOMP帧,每一帧代表一条指令。
  2. 消息代理Broker :Spring内置消息代理,接收消息,根据destination目的地,将消息分发到所有订阅该地址的客户端。

二、STOMP常用指令帧

指令 作用
CONNECT 客户端发起STOMP连接认证
SUBSCRIBE 订阅某个目的地destination,监听推送消息
SEND 客户端向指定目的地发送消息
MESSAGE 服务端推送消息帧,发给订阅客户端
DISCONNECT 客户端断开STOMP连接

三、目的地destination前缀含义(面试重点)

  1. /topic:广播主题,一对多。消息推送给所有订阅该topic的客户端,适合全局通知、群聊、大屏广播。
  2. /queue:点对点消息,一对一私聊、用户私有消息。
  3. /app:业务前缀。消息不会交给消息代理,转发给后端@MessageMapping控制器执行业务逻辑。
  4. /user:用户专属私有目的地,结合登录认证,消息只推送给指定登录用户 。前端订阅地址格式:/user/queue/xxx

四、核心注解

  1. @EnableWebSocketMessageBroker:开启STOMP消息代理,放在SpringBoot启动类或者配置类,开启WebSocket STOMP功能,核心注解。
  2. @MessageMapping:接收客户端通过STOMP的SEND指令发送过来的消息,作用类比SpringMVC中的@RequestMapping。
    @RequestMapping匹配HTTP请求URL;@MessageMapping匹配STOMP消息的目的地destination。客户端发送消息到/app/xxx,配置里设置前缀/app,注解只需要填写后面路径/xxx,匹配成功就进入该方法执行业务。>

区别:@RequestMapping是HTTP短连接,一问一答;@MessageMapping基于WebSocket长连接,一次握手建立连接后,可以反复发送消息,多次触发该方法。

  1. @SendTo:方法处理业务完成后,自动将返回结果推送到指定topic,广播给所有订阅客户端。
  2. @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");

十一、踩坑点与注意事项

  1. 跨域问题
    前后端分离项目,前端与后端域名端口不一致,需要setAllowedOriginPatterns()配置跨域。生产环境不要直接写*,指定前端域名提升安全性。
  2. 认证鉴权
    STOMP身份校验在握手阶段完成。连接建立之后,普通SpringMVC拦截器不再生效 ,需要使用ChannelInterceptor拦截STOMP消息帧做权限校验。
  3. 集群部署大坑
    内置SimpleBroker存储在JVM内存,多台服务实例之间互不感知。集群环境必须接入RabbitMQ作为外部STOMP消息代理,才能实现跨服务器消息推送。
  4. 连接资源限制
    每一条WebSocket长连接占用一个TCP文件句柄。在线用户量大时,需要调整服务器操作系统最大文件句柄数,防止连接打满。
  5. Nginx反向代理配置
    Nginx代理WebSocket长连接,必须添加升级相关配置,否则握手失败、连接很快断开,需要配置Upgrade头、保持长连接超时时间。
  6. 消息丢失
    SimpleBroker消息保存在内存,服务重启消息全部丢失;需要消息持久化场景,使用RabbitMQ外部Broker。
  7. 什么是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实现浏览器降级,心跳机制保活连接。多用于实时通知、在线聊天、实时数据大屏。

十四、高频面试题

  1. WebSocket 和 STOMP 的区别?
    WebSocket是底层双向长连接TCP协议;STOMP是运行在WebSocket之上的应用层文本子协议,定义消息帧、发布订阅、目的地路由。原生WebSocket没有订阅和消息路由能力,STOMP做了上层封装。
  2. /topic /queue /app /user 四个目的地前缀分别是什么?
  • /topic:广播,一对多,群发消息;
  • /queue:点对点消息;
  • /app:交给后端@MessageMapping执行业务逻辑;
  • /user:用户私有目的地,一对一推送私聊消息。
  1. SockJS的作用?
    浏览器、防火墙不支持原生WebSocket时,自动降级为HTTP长轮询,业务代码无需修改,兼容老旧环境。
  2. SimpleBroker为什么集群不能用?
    SimpleBroker消息存储在当前JVM内存,多台服务实例之间不共享消息,A服务收到消息,B服务上建立连接的客户端收不到广播,集群环境必须接入RabbitMQ。
  3. STOMP心跳的作用?
    TCP连接保活,识别掉线客户端,及时释放无效TCP连接,避免服务器连接资源泄漏。
  4. STOMP连接认证为什么不能用普通MVC拦截器?
    WebSocket握手完成后,通信走消息通道,不再经过SpringMVC的Servlet链路,MVC拦截器失效,要使用ChannelInterceptor拦截消息帧。
  5. @MessageMapping 和 @RequestMapping 的区别?
    @RequestMapping处理HTTP请求,匹配URL,属于短连接,一问一答;@MessageMapping接收STOMP的SEND消息,匹配destination目的地,基于WebSocket长连接,一次握手建立连接后可以反复收发消息。

相关推荐
Xudde.2 小时前
PwnLab: init 靶机渗透测试(Vulnhub Writeup)
笔记·学习·安全·web安全
2402_846295772 小时前
eDNA常用的基因片段
笔记
AI视觉网奇2 小时前
3Dconnexion 按照笔记
笔记
海海不掉头发3 小时前
软件开发方法第七周笔记_CSDN博客
人工智能·笔记·学习·软件工程
Vae_Mars3 小时前
幂等性学习笔记
笔记·学习·wpf
Xudde.3 小时前
HackMe 靶机渗透测试(Vulnhub Writeup)
笔记·学习·安全·web安全
先途检测认证4 小时前
塑料、硅胶、不锈钢材质,欧盟食品接触的测试项目有什么区别?
笔记·材质
hellojackjiang20114 小时前
IM聊天不掉线?生产级WebSocket断线重连、心跳保活实战
websocket·网络协议·网络编程·html5·即时通讯·im
跨知易知识产权4 小时前
美国发明专利和外观专利保护期限分别是多久,起算时间怎么界定?
笔记