news 2026/9/23 3:23:52

RuoYi-Vue-Plus WebSocket实战:从鉴权到微服务跨节点推送

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
RuoYi-Vue-Plus WebSocket实战:从鉴权到微服务跨节点推送

被标题里的“(2)”点进来的朋友,应该都看过我之前那篇WebSocket入门文章了。简单回顾一下:上一篇主要讲了怎么在RuoYi-Vue-Plus里把原生WebSocket跑起来,前端能连通、服务端能收到消息,算是完成了“从0到1”。但说实话,那篇只是解决了“通”的问题,距离“能用”还很远——鉴权怎么做?握手时怎么拿到用户信息?消息怎么按用户精准推送?重启服务会不会session全丢?更别提微服务版(RuoYi-Cloud-Plus)下面临的跨节点推送问题。这篇我把实际项目里踩过的坑、写过的代码、最后沉淀下来的方案全部展开,内容比较多,建议收藏了慢慢看。

这篇适合谁?已经在RuoYi-Vue-Plus框架上做开发,需要用WebSocket做消息通知、在线状态、扫码登录、实时进度这类功能的后端同学。如果你只是随手搜到这篇、对若依不熟,我也尽量把原理讲明白了,你拿着思路也可以迁移到别的Spring Boot项目里。

1. 先理清思路:WebSocket在若依框架里该怎么定位

1.1 为什么不用现成的WebSocket注解方案

很多教程上来就是@ServerEndpoint@OnOpen@OnMessage,十分钟就跑通。这种方案在普通Spring Boot项目里确实没毛病,但放在RuoYi-Vue-Plus里会有两个问题:

第一,@ServerEndpoint的生命周期不受Spring容器管理。虽然通过ServerEndpointExporter把端点交给Spring管理后可以依赖注入,但拦截器、握手阶段的鉴权逻辑写起来非常别扭,社区里全是自己撸HandshakeInterceptor。第二,若依框架和Spring家族结合得非常深,安全验证用的是Sa-Token,这套东西要跟WebSocket握手阶段打通,用Spring原生WebSocketHandler配合HandshakeInterceptor是最顺的路径。

所以我最终选了Spring WebSocket原生方案,核心组件就三个:

  • WebSocketConfigurer:注册WebSocket路由,绑定Handler和拦截器
  • HandshakeInterceptor:在握手阶段拦截,解析token、绑定用户身份
  • TextWebSocketHandler:处理连接建立、接收消息、连接关闭

下面所有内容都基于这个骨架展开。

1.2 当前方案的架构拆解

我在RuoYi-Vue-Plus里把WebSocket相关功能单独拉了一个模块(ruoyi-websocket),没有塞进system模块。这样做的原因是,WebSocket在业务上往往横跨多个模块——通知、日志推送、在线用户、大屏实时数据,谁都要用。如果塞在某个业务模块里,其他模块引用过来会造成循环依赖,拆成独立模块后谁都能引,清爽很多。

模块内部按职责分了四块:

类名职责
WebSocketConfig路由注册、拦截器装配
WebSocketAuthInterceptortoken解析、用户身份绑定
WebSocketSessionManager会话管理、按用户精准推送
BusinessWebSocketHandler业务消息处理、生命周期回调

这套结构下,WebSocket连接从建立到关闭的完整流程是:

  1. 客户端发起握手请求,URL带token参数(ws://ip:port/ws?token=xxx
  2. WebSocketAuthInterceptor拦截握手,取出token调用Sa-Token的StpUtil.getLoginIdByToken解析用户
  3. 解析成功,把userId写进WebSocketSession的attributes里,握手放行
  4. BusinessWebSocketHandler收到连接建立事件,把session注册进WebSocketSessionManager
  5. 后续所有消息收发、用户推送、状态变更都通过WebSocketSessionManager完成

这个流程最核心的一点:握手阶段必须完成身份识别,绝不在收到第一条消息时才去鉴权。否则连接占用了,消息才被拒绝,客户端体验是“连上了但用不了”,而且Service层的推送根本不知道往哪个session发。

2. 核心实现:鉴权握手、会话管理与消息推送

2.1 握手鉴权,和Sa-Token打通

RuoYi-Vue-Plus用的是Sa-Token,和原版若依的JWT方案不一样。JWT拿到token字符串自己解析就行,Sa-Token的token是随机字符串,真实用户信息存在Redis里,必须调用它的API才能取到登录用户。这一点要注意。

握手拦截器我写成了这样:

@Component public class WebSocketAuthInterceptor implements HandshakeInterceptor { @Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) { if (request instanceof ServletServerHttpRequest) { ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request; String token = servletRequest.getServletRequest().getParameter("token"); if (StrUtil.isBlank(token)) { return false; } try { // Sa-Token按token解析登录用户 LoginHelper loginHelper = SpringUtils.getBean(LoginHelper.class); LoginUser loginUser = loginHelper.getLoginUser(token); // 把用户ID、用户类型、登录端写进attributes,Handler里可以直接用 attributes.put("userId", loginUser.getUserId()); attributes.put("loginUser", loginUser); return true; } catch (Exception e) { // 解析失败直接拒绝握手 return false; } } return false; } @Override public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) { // 握手中的后置逻辑,一般不需要处理 } }

注意几个细节:

  • 握手阶段的HTTP请求是ServletServerHttpRequest,拿参数和普通HttpServletRequest一样
  • loginHelper.getLoginUser(token)这个方法是RuoYi-Vue-Plus里LoginHelper提供的,有的版本API叫做StpUtil.getLoginIdByToken,按你用的版本调整
  • token从URL参数传,能避免WebSocket API无法自定义Header的困境。原生WebSocket构造函数不支持带Header,这也是很多WebSocket鉴权都走URL参数的原因
  • 握手失败返回false时,客户端会收到握手失败的异常,前端要在onerror里给出友好提示,不能只靠onclose

2.2 会话管理,不重复造轮子但要做对

若依框架本身带一个WebSocketUsers工具类,维护了一个Map<String, WebSocketSession>。但我实际用下来觉得过于简单,有两个硬伤:一是key用的是sessionId,想按userId精准推送到人非常别扭;二是没有保存用户维度信息,多个端(比如PC端、手机端)同时在线时,后登录的会把先登录的顶掉。

我自己重写的WebSocketSessionManager长这样:

@Component public class WebSocketSessionManager { /** * userId -> 多端session列表 * ConcurrentHashMap避免并发问题 */ private final Map<Long, CopyOnWriteArrayList<WebSocketSession>> sessionMap = new ConcurrentHashMap<>(); public void addSession(Long userId, WebSocketSession session) { CopyOnWriteArrayList<WebSocketSession> sessions = sessionMap.computeIfAbsent(userId, k -> new CopyOnWriteArrayList<>()); sessions.add(session); } public void removeSession(Long userId, WebSocketSession session) { CopyOnWriteArrayList<WebSocketSession> sessions = sessionMap.get(userId); if (sessions != null) { sessions.remove(session); if (sessions.isEmpty()) { sessionMap.remove(userId); } } } public void sendToUser(Long userId, String message) { CopyOnWriteArrayList<WebSocketSession> sessions = sessionMap.get(userId); if (sessions == null || sessions.isEmpty()) { return; } for (WebSocketSession session : sessions) { if (session.isOpen()) { session.sendMessage(new TextMessage(message)); } } } public void sendToAll(String message) { sessionMap.forEach((userId, sessions) -> { sessions.forEach(session -> { if (session.isOpen()) { try { session.sendMessage(new TextMessage(message)); } catch (IOException e) { // 发送失败,记录日志,继续推下一个连接 } } }); }); } }

为什么用CopyOnWriteArrayList而不是普通的ArrayList?因为WebSocket的连接和断开是高频操作,而且sentToUser在推送时可能正在遍历列表,如果用ArrayList,遍历时另一个线程remove会抛ConcurrentModificationExceptionCopyOnWriteArrayList虽然写操作性能稍差,但读操作不加锁,在这个场景下最合适。

提醒:发送消息无法保证绝对成功。如果网络断开,session.isOpen()可能仍然返回true,但sendMessage会抛IOException。实际项目中我给所有异常都加了try-catch,并且会统计连续失败的session,超过阈值就主动关闭。

2.3 连接建立与关闭,别漏了清理工作

TextWebSocketHandler里有三个生命周期方法,对应连接建立、消息到达、连接关闭。我把重点业务逻辑放在这里:

@Component public class BusinessWebSocketHandler extends TextWebSocketHandler { @Resource private WebSocketSessionManager sessionManager; @Override public void afterConnectionEstablished(WebSocketSession session) { Long userId = (Long) session.getAttributes().get("userId"); if (userId == null) { // 理论上握手拦截器已经保证userId存在,但防御一下 session.close(); return; } sessionManager.addSession(userId, session); // 可以顺便记录日志、发一条欢迎消息、推个上线通知给其他用户 log.info("WebSocket连接建立: userId={}, sessionId={}", userId, session.getId()); } @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) { // 收到客户端消息,按业务协议处理 String payload = message.getPayload(); Long userId = (Long) session.getAttributes().get("userId"); // 这里做一个简单的ping/pong处理 if ("PING".equals(payload)) { session.sendMessage(new TextMessage("PONG")); return; } // 其他业务消息,交给对应的Service处理 // 注意:不要在这里做耗时操作,如果需要,用异步线程池 } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { Long userId = (Long) session.getAttributes().get("userId"); if (userId != null) { sessionManager.removeSession(userId, session); log.info("WebSocket连接关闭: userId={}, sessionId={}", userId, session.getId()); } } }

有个容易被忽略的点:afterConnectionClosed并不是只有客户端主动断开时才触发。服务端重启、网络异常、防火墙掐断,都会触发这个方法。所以关闭会话时一定不能只做“移除session”一件事,还要考虑这个userId可能在别的地方有业务状态要清理——比如用户下线标记。

2.4 心跳保活,防Nginx和运营商掐连接

WebSocket虽然基于TCP长连接,但中间链路(Nginx、云服务商的LVS、防火墙)都可能因为空闲超时把连接掐断。所以一定要心跳保活。最常见的方案是客户端定时发PING,服务端回PONG,这个我在handleTextMessage里已经写了。

但光有业务心跳不够,还要在Spring层面配置好空闲超时:

@Configuration public class WebSocketContainerConfig { @Bean public ServletServerContainerFactoryBean createWebSocketContainer() { ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean(); // 空闲超时,单位毫秒,超过这个时间没有收到任何消息,容器会自动关闭连接 container.setMaxSessionIdleTimeout(30000L); return container; } }

这样设置后,容器会在30秒内没有消息交互时自动断开连接。配合前端的定时PING(比如每15秒发一次),就能保证连接始终活跃,又不会因为异常连接占用资源。

注意:setMaxSessionIdleTimeout的超时时间是容器层面的空闲检测,跟业务心跳不冲突。如果你设了30秒空闲超时,客户端每15秒ping一次,连接永远不会被容器断开;但如果客户端崩了、网络断了,容器最迟30秒后就会回收这个连接,不用等TCP层慢慢超时。

2.5 服务端主动推送的两种姿势

WebSocket最常见的使用场景就是服务端主动推消息。在RuoYi-Vue-Plus里,我主要用两种姿势:

第一种是直接注入WebSocketSessionManager,在任意Service里调用:

@Service public class NoticeService { @Resource private WebSocketSessionManager sessionManager; public void sendNotice(Long userId, String title, String content) { NoticeMessage message = new NoticeMessage(); message.setType("NOTICE"); message.setTitle(title); message.setContent(content); message.setTimestamp(System.currentTimeMillis()); sessionManager.sendToUser(userId, JSON.toJSONString(message)); } }

第二种是配合Spring的事件机制。业务模块发出领域事件,WebSocket模块监听事件后统一推送,实现业务逻辑和推送逻辑的解耦。比如用户下单成功后,订单模块发布OrderCreatedEvent,WebSocket的监听器收到后给用户推送订单状态变化。这个在微服务场景下还有妙用,后面讲。

消息格式我统一用JSON:

{ "type": "NOTICE", "data": { "id": 123, "title": "你有一个新任务", "content": "请尽快处理" }, "timestamp": 1712390400000 }

type字段用于前端路由不同的处理逻辑,data放业务数据,timestamp供前端做消息排序。这个协议建议提前定好,不要等对接时再改。

3. 分布式场景:单节点够用,微服务就得换思路

3.1 单节点部署时的注意点

如果RuoYi-Vue-Plus就部署一个节点,不搞集群,那上面的方案完全够用。session都在本机内存里,sendToUser直接遍历本机map即可。

但即便如此也有几个坑:

  • 服务重启session全丢。客户端不会自动重连,要前端做重连机制,比如监听onclose后延迟3秒重新发起握手
  • 负载均衡下不能多实例。如果前面挂了Nginx轮询到后端两个节点,客户端的两次连接可能落在不同节点上,sendToUser推给A节点,但用户session在B节点,消息就丢了。必须做集群场景的处理
  • 注意Nginx的配置。Nginx默认对Upgrade协议支持不够,需要单独配置:
location /ws/ { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 3600s; }

proxy_read_timeout如果不设大,默认60秒,超过60秒没有数据交互,Nginx就会断开连接。心跳机制只是让应用层维持活跃,Nginx这层的超时也要一并调。

3.2 微服务版本(RuoYi-Cloud-Plus)如何做跨节点推送

在微服务架构下,session管理不能继续用本地内存了。比如ruoyi-system服务有两个实例,客户端连到了实例A,但推送请求打到了实例B,B的session map里根本没有这个用户,消息就丢了。

解决思路有两种:

方案一:粘性会话(Sticky Session)。让Nginx或网关保证同一个userId的连接都路由到同一个后端实例。配置简单,但一旦实例挂了,该实例上的所有session全部丢失,而且粘性路由是基于IP的,多实例扩容缩容时会话分布会不均匀。适合对可用性要求不高的内部系统。

方案二:Redis发布订阅(Pub/Sub)。所有实例订阅同一个Redis频道。需要推送时,实例A往频道里发布消息;所有实例都收到这个通知,然后检查本地session map里有没有目标用户,有就推送,没有就忽略。这个方案不需要session全局同步,每个实例只管自己的连接,跨节点推送交给Redis广播,实现成本很低,是RuoYi-Cloud-Plus下最务实的方案。

@Component public class RedisWebSocketPublisher { @Resource private StringRedisTemplate stringRedisTemplate; private static final String CHANNEL = "ws:push"; public void publish(String userId, String message) { // 组装一个带路由信息的对象 String payload = JSONUtil.toJsonStr(new PushMessage(userId, message)); stringRedisTemplate.convertAndSend(CHANNEL, payload); } } @Component public class RedisWebSocketSubscriber { @Resource private WebSocketSessionManager sessionManager; @PostConstruct public void init() { // 订阅频道,收到消息后,先判断目标用户是否在本机 stringRedisTemplate.getConnectionFactory().getConnection().subscribe( (message, pattern) -> { String payload = new String(message.getBody(), StandardCharsets.UTF_8); PushMessage pushMessage = JSONUtil.toBean(payload, PushMessage.class); sessionManager.sendToUser(pushMessage.getUserId(), pushMessage.getMessage()); }, CHANNEL.getBytes(StandardCharsets.UTF_8) ); } }

这里有个容易踩的坑:subscribe是阻塞的,如果直接写在业务线程里,会把线程堵住。建议在@PostConstruct里新开一个线程去订阅,或者用ThreadPoolTaskExecutor异步初始化。另外,Redis pub/sub的消息是即发即弃的,如果某个实例正在重启,期间发布的消息它会漏掉。好在WebSocket推送场景一般对实时性要求高、对可靠性要求不那么苛刻,真要丢一条消息,最多是用户没看到一个通知,可接受。如果业务要求更高可靠性,就得换Redis Stream或MQ了。

3.3 微服务网关的WebSocket转发设置

在RuoYi-Cloud-Plus里,前端连接WebSocket要经过ruoyi-gateway。网关的默认路由是HTTP转发,WebSocket的握手请求虽然也能转发,但要确认spring-cloud-starter-gateway支持WebSocket转发(它天然支持),关键是路由配置:

spring: cloud: gateway: routes: - id: ruoyi-websocket uri: lb://ruoyi-websocket predicates: - Path=/ws/**

这里有个细节:WebSocket的Sec-WebSocket-Protocol头和普通Header一样需要透传,网关默认会转发所有Header,一般不需要特殊配置。但如果你在Nginx层再挡一道,就要注意Nginx是否把UpgradeConnection头透传了,漏一个都握手失败,报Unexpected response code: 200,这个错误是非常经典的WebSocket排障线索。

另外,网关层建议做一次token校验,避免无效请求一路打到后端服务。在网关的GlobalFilter里对/ws/**路径做Sa-Token校验,失败直接返回401,后端服务只需要信任网关传过来的用户信息即可。这个在RuoYi-Cloud-Plus自带的SaTokenFilter基础上扩一下就行。

4. 前端对接:Vue3 + TS怎么优雅地接入

4.1 封装WebSocket客户端类

若依前端用的是Vue3 + TypeScript,原生WebSocket在TS下用起来有几个痛点:没有类型提示、没有自动重连、消息分发要靠一堆if-else。我的做法是封装一个WebSocketClient类,把公共逻辑全收进去。

type MessageHandler = (data: any) => void class WebSocketClient { private socket: WebSocket | null = null private url: string = '' private handlers: Map<string, MessageHandler> = new Map() private heartbeatTimer: number | null = null private reconnectTimer: number | null = null private manualClose: boolean = false constructor(path: string) { const protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://' const token = getToken() // 从Pinia或localStorage取token this.url = `${protocol}${window.location.host}${path}?token=${token}` } connect() { this.manualClose = false this.socket = new WebSocket(this.url) this.socket.onopen = () => { // 连接建立后,启动心跳 this.startHeartbeat() } this.socket.onmessage = (event) => { const message = JSON.parse(event.data) const handler = this.handlers.get(message.type) if (handler) { handler(message.data) } } this.socket.onclose = () => { this.stopHeartbeat() if (!this.manualClose) { // 非手动关闭,自动重连 this.scheduleReconnect() } } this.socket.onerror = () => { // 错误后一般会触发onclose,这里只做日志 } } on(type: string, handler: MessageHandler) { this.handlers.set(type, handler) } send(type: string, data: any) { if (this.socket && this.socket.readyState === WebSocket.OPEN) { this.socket.send(JSON.stringify({ type, data })) } } close() { this.manualClose = true this.socket?.close() } private startHeartbeat() { this.heartbeatTimer = window.setInterval(() => { if (this.socket?.readyState === WebSocket.OPEN) { this.socket.send(JSON.stringify({ type: 'PING' })) } }, 15000) } private stopHeartbeat() { if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer) this.heartbeatTimer = null } } private scheduleReconnect() { if (this.reconnectTimer) { clearTimeout(this.reconnectTimer) } this.reconnectTimer = window.setTimeout(() => { this.connect() }, 3000) } }

用的时候,在需要的地方new一个实例,订阅消息类型:

const ws = new WebSocketClient('/ws') ws.on('NOTICE', (data) => { // 更新通知中心 }) ws.connect()

页面销毁时记得调用ws.close(),否则组件卸载了消息还在往上冒,容易引发内存泄漏。

4.2 解决Vue3 + TS的报错问题

“若依vue3 ts报错”这个词条搜的人很多,我实际踩下来的典型报错有两个:

第一个是TS不认识window上的自定义属性或者WebSocket实例类型不匹配。如果你有全局挂载socket的需求,不要在window上乱挂,正确姿势是写一个类型声明:

// env.d.ts declare global { interface Window { webSocketClient?: InstanceType<typeof WebSocketClient> } }

第二个报错是Property 'send' does not exist on type 'WebSocket'之类,多半是@types/websocket和浏览器内置WebSocket类型定义冲突了。解决方法是显式使用WebSocket全局类型,别引入websocket这个npm包——浏览器环境根本不需要它,直接new WebSocket()就是原生实现。

4.3 Postman 调试WebSocket的小技巧

服务端写好了,前端没对接之前,怎么验证?Postman从2020年就支持WebSocket调试了,用法如下:

  1. 新建请求,协议选择WebSocket,URL填ws://localhost:8080/ws?token=xxxx,这里token要是真实有效的
  2. 点击Connect,如果握手成功,消息区会出现“Connected to ws://...”
  3. 在消息输入框发送{"type":"PING"},如果服务端回了PONG,说明通道是通的
  4. 如果连接失败,注意看返回码:101是握手成功;200说明被Nginx或网关拦截了;401是鉴权失败

Postman除了能发消息,还能查看WebSocket的帧内容,排查中文乱码非常实用。

5. 常见问题、排查思路与避坑实录

5.1 高频问题速查表

现象可能原因解决办法
握手失败,报Unexpected response code: 200Nginx未配置Upgrade头检查proxy_set_header UpgradeConnection配置
握手失败,报401token缺失或已过期确认token参数名、Sa-Token会话是否有效
连接建立后十几秒就被断开容器空闲超时过短调整setMaxSessionIdleTimeout或启用心跳
消息发不出去,服务端无日志服务端连接实际已断开遍历时增加session.isOpen()判断并关掉无效连接
集群部署时消息时有时无session未跨节点同步用Redis Pub/Sub或MQ做广播推送
前端连接一直CONNECTING不触发open服务根本没启动或网关路由错误检查后端tomcat端口、网关路由、防火墙
中文消息乱码字符集不一致统一使用UTF-8,请求头Sec-WebSocket-Protocol不参与编码

5.2 一个典型的“连上就断”排查实录

有次做扫码登录功能,前端扫描二维码后,手机端确认,PC端页面自动刷新。功能开发完联调时发现:PC端的WebSocket连上一两秒就被断开,服务端日志完全没打出来。

排查过程是这样的:

第一步,查看浏览器Network面板,WebSocket的连接记录显示“Closed”状态,但没有具体错误码。

第二步,打印了服务端日志,发现afterConnectionClosed被触发了,这说明连接确实建立过,然后又在短时间内被关闭。但afterConnectionEstablished的日志没打出来?这就奇怪了。

第三步,回头检查代码,原来我在afterConnectionEstablished里加了用户状态校验:如果用户已经在别处登录,就把当前连接关闭。而测试时Postman和浏览器同时连着同一个账号,后面的连接把前面的顶掉了。

这个“顶掉”逻辑其实本意是好的,避免同一账号反复建立无效连接,但实现太粗暴。改成多端共存后问题消失。后来我又在关闭连接前给客户端发了一条带type: KICK_OUT的消息,让前端能给用户提示“你的账号在别处登录”,体验就好多了。

这个坑说明:WebSocket的生命周期很多回调是异步的,日志也好、连接状态也好,不要想当然。出现“连上就断”先确认是不是自己代码里主动关了连接,再看框架层面配置,最后才排查网络链路。顺序反了,排查效率会很低。

5.3 关于RuoYi-Vue-Plus版本差异的提醒

若依生态有RuoYi-Vue、RuoYi-Vue-Plus、RuoYi-Cloud-Plus等好多个版本,不同版本的依赖和工具类命名有差异。比如LoginHelperLoginUser这些类,在老版本里可能是SecurityUtilsLoginUser的搭配。如果是老版本RuoYi-Vue(基于Spring Security),握手鉴权那段要写Spring Security的token解析逻辑,不能照抄Sa-Token这版。

文章里用的SpringUtils.getBean方式获取Bean,是为了在拦截器这种非Spring管理的场景下也能拿到容器里的Bean,这个在RuoYi-Vue-Plus是有的。如果你用的版本没有,可以试着手动注入HandshakeInterceptor为Spring Bean,然后在WebSocketConfigurer里通过构造器或@Resource注入,也是一样的效果。

5.4 如果遇到“Error adding module to project: null”

这个报错虽然不是WebSocket直接相关的,但搜若依的人常碰到。发生在把RuoYi-Plus导入IDE时,多半是Maven的模块识别出了问题。解决办法一般是:先把项目根目录的pom.xml用Maven reimport,然后mvn clean install -Dmaven.test.skip=true,最后在IDE里重新导入。如果还不行,检查JDK版本是否匹配(RuoYi-Vue-Plus一般要求JDK17,继续用JDK8会出各种幺蛾子)。

6. 从WebSocket到更多实时能力的一点扩展

写完基础的消息推送之后,我顺手把扫码登录也用同一套WebSocket通道做掉了,效果出奇地好。这里分享一下扩展思路:把WebSocket当作一个“实时通道底座”,上面可以跑很多业务协议,而不只是“通知推送”这一种。

我按message.type区分业务:

  • NOTICE:站内通知
  • SCAN_LOGIN:扫码登录。手机端确认后,服务端把登录凭证推给PC端
  • ONLINE_STATUS:好友/协作者在线状态变更
  • PROGRESS:大文件导入导出的实时进度

协议统一的好处是,前端只需要维护这一个连接,不用为每个业务场景都建立新的WebSocket。后端也只用维护一个WebSocketSessionManager,避免session管理逻辑散落在各个模块。

当然也有代价:连接是所有业务共用的,一个handler里会积压越来越多的if-elseswitch-case。建议在handleTextMessage里做一层分发,把不同type的消息派发到对应的Service,而不是把所有业务代码堆在Handler里。

再往后如果实时性要求不再满足于“秒级”,比如要做多人协作编辑这种“毫秒级”同步,Spring WebSocket自带的简单消息代理就不够用了,得换STOMP协议配合消息代理(RabbitMQ/ActiveMQ),那是另一套架构了。大多数若依项目的业务场景用不上,知道有这回事就行。

最后分享一个实际运营中的心得:WebSocket上线后一定要加监控。我见过生产环境一天两万多条推送,某个session泄露(连接没关闭、引用还在Map里)导致内存涨了20%,最后靠监控发现才定位到是有个定时任务里new了WebSocketSessionManager但没复用Spring的单例。这类问题如果不提前埋点,排起查来真的头大。你在接入WebSocket时,至少把当前在线连接数推送成功/失败数连接断开原因分布这几个指标打出来,后面运维会感谢你的。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/23 3:23:27

Compton X Render后端优化指南:X11桌面合成性能提升

1. Wayland时代还在用X11&#xff1a;Compton的适用场景与性能困境1.1 Compton到底是什么&#xff0c;为什么老兵不死很多接触Linux桌面不到两三年的朋友&#xff0c;可能压根没听说过Compton这个名字。它是X11环境下的一款合成管理器&#xff08;compositor&#xff09;&#…

作者头像 李华
网站建设 2026/9/23 3:20:34

YOLOv8车流检测毕设实战:CPU环境零GPU部署指南

简介&#xff1a;本资源是一套基于YOLOv8的多端车流检测系统完整实现&#xff0c;面向计算机、人工智能、自动化等专业的本科生及初阶开发者&#xff0c;适用于毕业设计、课程设计与项目实践。系统支持多路视频流实时检测&#xff0c;涵盖模型训练、推理部署、GUI界面及报警机制…

作者头像 李华
网站建设 2026/9/23 3:19:53

WebGPU+Transformers.js:在浏览器中端侧运行DeepSeek-R1蒸馏模型

聊一个我最近一直在折腾的事&#xff1a;在浏览器里直接跑 DeepSeek-R1 的推理。先说清楚&#xff0c;完整版 R1 是六百多亿参数的 MoE 模型&#xff0c;普通电脑那点内存连模型文件都放不下&#xff0c;更别说单靠浏览器去做端侧推理。但我真正跑通的是 DeepSeek 官方蒸馏出来…

作者头像 李华
网站建设 2026/9/23 3:19:50

动态换肤组件化实战:基于CSS变量的主题Token设计与ThemeProvider实现

这年头做应用&#xff0c;不做个“白天摸鱼晚上蹦迪”的暗色模式&#xff0c;都不好意思说自己搞过前端。但你说做个换肤吧&#xff0c;需求方拿着截图跟你说“就按这个色儿来”&#xff0c;然后第二天又换成另一个色儿&#xff0c;这时候你要是还给每个页面写死颜色样式&#…

作者头像 李华
网站建设 2026/9/23 3:14:10

Aspire + Azure Web PubSub 构建高并发群聊系统

1. 项目概述&#xff1a;为什么一个“群聊”值得用 Aspire Azure Web PubSub 重做一遍&#xff1f;我去年在给一家在线教育平台做实时互动模块时&#xff0c;被逼着把 WebSocket 服务从 SignalR 换成 Azure Web PubSub。当时心里是抵触的——不就是发个消息、推个通知&#xf…

作者头像 李华
网站建设 2026/9/23 3:13:23

OpenClaw的tools与skills详解:用TaoToken统一Key跑通配置骨架

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华