GOMS WebSocket 集成设计(STOMP over SockJS)

lishihuan大约 14 分钟

GOMS WebSocket 集成设计(STOMP over SockJS)

笔记归档:基于 Goms-Cloud-springboot2(RuoYi-Cloud 3.6.8 / Spring Boot 2.7 / Vue2)落地实践。
对齐旧项目 LegacyApp ChatMessage 字段,增补大类 category,并理顺点对点路由与鉴权边界。
源码备份见同目录 webSocket_demo/
近期收敛:常量/构建合并为单一 ChatMessages;SockJS 断开噪音由 SockJsExceptionResolver 收口(不动公共 GlobalExceptionHandler);监测浮层「在线 N(M)」+ 指定用户 el-autocomplete(可手输 userId,下拉 z-index 高于浮层)。
2026-08-07:支持 sys_config.websocket_url 独立 WS 通道(nginx /ws/);已配时优先原生 websocket,未配仍走 API 网关 xhr/xdr 回退。
2026-08-10:补齐 主平台二级目录/iws/yj_goms/)场景:主平台反代必须透传 Upgrade,否则只能 xhr 长轮询;正式已验证配置见 §3.3。
同日(工作区增强,周五发包未含)WebSocketService 增加 https 协议纠正、websocket_native_enabled、以及 WS_NATIVE_FAILED_KEY 本会话跳过原生 WS(见 §3.4)。


1. 目标与边界

目标说明
浏览器实时收消息通知弹窗、应急多人同步、建议刷新页面等
业务写操作仍走 REST前端通过 WebSocket 改业务状态;改完由服务端推事件
网关/反代友好默认可走 SockJS HTTP 回退;正式/本地可配独立 websocket_url(nginx /ws/ + Upgrade)走原生 websocket
二级目录挂载微应用挂在主平台 /iws/{app}/ 下时,WS 与静态页共用入口反代,每一层都要透传 Upgrade(见 §3.3)
可观测监测浮层 + 控制台 __gomsWs + 诊断 REST

非目标(当前):跨 JVM 广播(内嵌 SimpleBroker 不跨实例;集群需外置 broker)。


2. 总体架构

2.1 分层职责

职责
主平台 nginx /yj_goms/二级目录入口:静态 + WS Upgrade 透传到 GOMS 机(§3.3,最易漏)
GOMS nginx /ws/独立 WS 入口;须支持 Upgrade,反代到 goms-system /socket/endpointWs
网关回退路径 /goms-system/socket/** 白名单(握手);剥离外部伪造的 from-source/webSocket/** 白名单
goms-systemSTOMP Broker、推送 Service、诊断 REST、模板推送
goms-module / 其它服务业务落库后 Feign 调 RemoteWebSocketService
ruoyi-uiLayout 建连;Navbar 订系统频道;业务页订事件或自订 topic

3. 协议与端点

推荐(独立 WS)sys_config.websocket_url,如 http://127.0.0.1:19095/ws(nginx /ws/goms-system:18093/socket/endpointWs
回退(API 网关){origin}{VUE_APP_BASE_API}/goms-system/socket/endpointWs
服务内路径/socket/endpointWs(网关 StripPrefix / nginx 反代后)
传输已配 websocket_url:默认优先原生 websocket,失败再 xhr/xdr;未配:仅 xhr-streaming / xdr-streaming / xhr-polling / xdr-polling;可用 websocket_native_enabled / 本会话跳过强制仅 xhr(§3.4)
STOMPCONNECT / SUBSCRIBE / MESSAGE

前端解析见 WebSocketService.resolveEndpointUrl() / resolveSockJsTransports();诊断快照含 endpointSourcesys_config.websocket_url | VUE_APP_BASE_API)、nativeTransportEnabled / nativeTransportSkipped

3.1 为何握手不能带 Authorization

浏览器 SockJS 的 /info、xhr 请求由 sockjs-client 发起,无法像 axios 一样自定义 Authorization
因此:

  • 回退走网关时必须白名单 /socket/**(否则握手 401)
  • 独立 /ws/ 路径不经 iws-gateway REST,仍靠 nginx/IWS 暴露;鉴权不变
  • 身份在 STOMP CONNECT 帧校验:前端已传 Authorization: Bearer …,服务端 InboundChannelInterceptorTokenService 解析,Principal 只认 Token 内 userId

3.2 独立 websocket_url 与 nginx(本地示例)

说明
SQLruoyi-modules/goms-system/sql/websocket_sys_config.sql 写入 websocket_url(默认 http://127.0.0.1:19095/ws
nginx监听 19095,location /ws/http://127.0.0.1:18093/socket/endpointWs/,并开启 Upgrade / Connection
正式环境按 IWS 平台 WS 路径填写,如 https://host/iws/yj_goms/ws须 https,见 §3.3)
改后「参数设置」刷新缓存;前端重新登录或拉配置后再建连

未配 websocket_url(空)时行为与旧版一致:经 API 网关、仅 HTTP 回退,避免现场无 Upgrade 时优先原生 ws 拖慢建连。

3.3 二级目录 / 主平台挂载场景(踩坑重点 · 卡了很久)

场景:GOMS 作为微应用挂在主平台 /iws/yj_goms/ 下(VUE_APP_SUB_PATH=/iws/yj_goms/),页面与 WS 同域同入口,中间至少两层 nginx。
对照集成文档:主平台H5方式集成.md §六;对照旧项目 LegacyApp:/iws/BizApp/ws/.../websocket

3.3.1 为什么 Network「很脏」:不是接口变慢

未打通原生 WS 时,SockJS 只能走 xhr-polling,URL 形如:

.../goms-system/socket/endpointWs/{server}/{session}/xhr
现象真相
每条约 25s 结束一次SockJS 默认心跳 / 长轮询超时,协议设计如此
请求很频繁每个开了 WS 的登录用户都在轮转;人多时网关日志会「刷屏」
F12 没有 websocket 类型原生升级失败或被禁用,降级到 HTTP 回退

LegacyApp 线上「干净」是因为走了专用通道:

ws://.../iws/BizApp/ws/{session}/websocket   ← 一条原生 WS

GOMS 若把 SockJS 绑在 iws-gateway/yj_goms_api(REST 网关) 上,网关通常不支持 Upgrade,只能 xhr —— 这是架构差异,不是业务接口性能问题。

3.3.2 正确链路(两层 nginx,缺一不可)

[浏览器]
  wss://主平台/iws/yj_goms/ws/{session}/websocket
  │  sys_config.websocket_url = https://主平台/iws/yj_goms/ws
  ▼
[主平台 nginx]  location /yj_goms/     ← ★ 必须透传 Upgrade / Connection
  │  proxy_pass → GOMS 机 nginx(端口以现场为准)
  │  剥掉 /yj_goms 前缀后,上游收到 /ws/...
  ▼
[GOMS 机 nginx] location /ws/          ← 反代到业务进程
  │  proxy_pass → 127.0.0.1:18093/socket/endpointWs/
  ▼
[goms-system]
  STOMP CONNECT + Authorization → Principal
层级location作用漏配后果
主平台/yj_goms/静态页 + WS Upgrade 透传到 GOMS 机Upgrade 头变 null → 后端 invalid Upgrade header: null → 只能 xhr
GOMS 机/ws/(写在 location / 之前直连 goms-system:18093不经 ruoyi-gateway / iws-gateway/ws 被当静态 SPA 吃掉,或 Upgrade 再次丢失

REST 业务仍走 iws-gateway/yj_goms_api(token / 加解密 / 防篡改不变)。
WebSocket 从不经过 axios 入参加密与防篡改;握手可无 Bearer,身份只在 STOMP CONNECT

3.3.3 正式环境已验证:主平台加这几行就够

现场卡点往往在 主平台 /yj_goms/ 只做了普通 HTTP 反代,没有 Upgrade。补上即可(OPTIONS 等原有块可保留):

location /yj_goms/ {
    # 原 OPTIONS + 静态/页面反代可保留
    proxy_pass http://{GOMS机IP}:{端口}/;   # 正式端口以现场为准,如 18081 / 19095
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
    proxy_read_timeout 36000s;
}

GOMS 机(本机 nginx 端口以现场为准,常见 19095 / 18081)侧:

location /ws/ {
    proxy_pass http://127.0.0.1:18093/socket/endpointWs/;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
    proxy_read_timeout 36000s;
}

3.3.4 websocket_url 怎么填(二级目录)

环境sys_config.websocket_url说明
本地http://127.0.0.1:19095/ws直连本机 nginx /ws/
正式(主平台挂载)https://{主平台域名}/iws/yj_goms/ws与页面同域;必须 https,页面 https 时 SockJS 才走 wss://

易错:

错误写法问题
http://.../iws/yj_goms/ws(正式)HTTPS 页混合内容 / 无法升 wss
.../iws/iws-gateway/yj_goms_api/goms-system/socket/endpointWs又绕回 REST 网关,原生 WS 仍不可用
只配 GOMS 机 /ws/,主平台不透传 Upgrade请求能到 18093,但 Upgrade 已丢 → invalid Upgrade header: null
只改 nginx、不改 websocket_url前端仍走 VUE_APP_BASE_API 回退路径

改后:「参数设置」刷新缓存 → 重新登录或重连;控制台 __gomsWs.reconnect()

3.3.5 与 LegacyApp 对照

LegacyAppGOMS(二级目录正确配法)
浏览器入口/iws/BizApp/ws/.../websocket/iws/yj_goms/ws/.../websocket
主平台/边缘专用 /ws/ + Upgrade/yj_goms/ 整段透传 Upgrade(WS 与静态同入口)
应用机location /ws/busModule/endpointWslocation /ws/socket/endpointWs
前端websocket_url + transports 含 websocket同:sys_config.websocket_url;已配则优先原生
Network一条 WS一条 WS(未配齐时满屏 /xhr

3.3.6 验证清单

检查期望
F12 Network出现 websocket 类型,或 URL 以 /websocket 结尾
__gomsWs.status().transport"websocket"
__gomsWs.status().endpointSource"sys_config.websocket_url"
__gomsWs.status().nativeTransportSkippedUpgrade 通时应为 false;未通且已降级可为 true
后端日志WebSocket CONNECT, userId=不是 invalid Upgrade header: null

临时兜底(主平台短期改不了):见 §3.4(websocket_native_enabled=false / 清空 websocket_url / 本会话自动跳过)—— 能用但 Network 仍会 25s /xhr

3.4 原生 WS 失败兜底(WS_NATIVE_FAILED_KEY 等)

背景:已配 websocket_url 且主平台 Upgrade 未透传时,SockJS 仍会先试原生 websocket → 控制台刷 ws:///wss:// 失败 → 再降级 xhr。
周五发包(上午发布)尚未包含本节前端逻辑;属工作区后续增强,源码备份已同步 webSocket_demo/js/WebSocketService.js

机制说明
_normalizeEndpointProtocol页面为 https: 时,把配置里的 http:// 端点升为 https://,避免混合内容与错误的 ws://
websocket_native_enabledsys_config 开关,默认 true(未配键等同 true);设 false 时即使有 websocket_url用 xhr/xdr
WS_NATIVE_FAILED_KEYsessionStorage.goms_ws_native_failed本会话标记:已确认原生不可用
_markNativeWebsocketFailed连上后若期望原生却实际 transport !== 'websocket',写入上述 key,并记诊断事件 native_ws_skip
_shouldSkipNativeWebsocketnative_enabled=false session 已标记 → resolveSockJsTransports 返回仅 xhr/xdr
已配 websocket_url
  ├─ websocket_native_enabled=false  → 全程 xhrOnly
  ├─ sessionStorage goms_ws_native_failed=1 → 本会话 xhrOnly(刷新标签页前)
  └─ 否则 transports = [websocket, xhr-streaming, …]
       └─ onConnect 发现 transport≠websocket → 标记 failed,后续重连不再试原生

运维建议

场景做法
IWS 短期改不了 Upgrade参数设置 websocket_native_enabled=false(或清空 websocket_url
IWS 已修好保持 true;清 session / 新开标签即可再试原生
正式 websocket_url仍须 https://…/iws/yj_goms/ws;协议纠正只是防配错 http

诊断:__gomsWs.status()nativeTransportEnablednativeTransportSkippedtransport

SQL 键可与现有三项一并维护(前端未配键时默认启用原生);示例:

INSERT INTO sys_config (config_name, config_key, config_value, config_type, create_by, create_time, remark)
VALUES ('WebSocket原生传输', 'websocket_native_enabled', 'true', 'Y', 'admin', NOW(),
  '已配 websocket_url 时是否尝试原生 websocket;IWS 未透传 Upgrade 时可设 false,仅 xhr');

4. Broker 与点对点路由(踩坑重点)

4.1 正确配置

registry.enableSimpleBroker("/topic", "/queue", "/mass");
registry.setUserDestinationPrefix("/user");
前缀角色
/topic/mass广播 / 群发
/queue点对点真实投递前缀
/user UserDestination 逻辑前缀,禁止再写进 enableSimpleBroker

4.2 双投 vs 丢消息

错误配置现象
SimpleBroker /user(LegacyApp)convertAndSendToUser 双投(同一连接收两条)
SimpleBroker 去掉 /user没有 /queue解析成 /pushTask-user{sessionId}无匹配前缀 → 静默丢消息
正确SimpleBroker 含 /queue;服务端发 /queue/pushTask;客户端订 /user/queue/pushTask

4.3 投递路径

服务端: convertAndSendToUser(userId, "/queue/pushTask", ChatMessage)
        ↓ UserDestinationResolver
实际:   /queue/pushTask-user{sessionId}   ← 命中 SimpleBroker /queue
客户端: subscribe("/user/queue/pushTask") ← 按 CONNECT Principal 映射,勿拼 userId

5. 消息信封(ChatMessage)

对齐 LegacyApp,GOMS 增补 大类 category

字段类型说明
idString消息 id,服务端可生成
keyIdString业务实例隔离(如 jeId:123),多场同屏靠它过滤
categoryString大类NOTIFY | BIZ
messageTypeString小类:动作码(SYSTEMEMERGENCY_PAUSEWS_DIAG…)
messString通知纯文本;BIZ 可选摘要
dataObjectBIZ 的 JSON 载荷;NOTIFY 可空
senderId / receiverIdString发送人 / 接收人(与 Principal.nameopen in new window 一致)
sedTimeString发送时间,服务端填充

5.1 两大类行为

大类载荷重心前端默认
NOTIFYmess 纯文本Element Notification(受 websocket_notify_enabledWS_DIAG 始终弹)
BIZdata JSON不弹窗$rootgoms-websocket-biz,业务页同步/刷新

缺省 category:有非空 messWS_DIAG → NOTIFY,否则 → BIZ(兼容旧推送)。

5.2 常量与构建(单一类,勿再拆)

  • Java:com.ruoyi.system.api.domain.ChatMessages(与 ChatMessage 同包)
    • 大类:CATEGORY_NOTIFY / CATEGORY_BIZ
    • 小类:TYPE_SYSTEM / TYPE_WS_DIAG / TYPE_EMERGENCY_* / TYPE_PAGE_REFRESH
    • 工厂:notifyOf / bizOf;发送前 normalize
  • JS:utils/goms/wsMessage.js(镜像常量与解析)
ChatMessages.notifyOf(receiverId, ChatMessages.TYPE_SYSTEM, "纯文本");
ChatMessages.bizOf(receiverId, ChatMessages.TYPE_EMERGENCY_PAUSE, "jeId:123", dataMap);

6. 鉴权与接口矩阵

表面鉴权说明
SockJS /socket/**网关白名单仅握手
STOMP CONNECTToken(CONNECT 头)拒无效 Token;不以客户端自报 userId 为准
GET /webSocket/status登录brokerUserCount(在线人数);onlineUsers / canDiagSend 仅诊断权限;另有 selfOnline/selfSessionCount(面板可不展示)
POST /webSocket/pingdiagSendgoms:webSocket:diagSend浏览器自测 / 指定接收人
POST /webSocket/sendMsgsendToUser@InnerAuth仅 Feign;浏览器伪造 from-source 会被网关剥离

原则:浏览器测推送用 diagSend;业务推送用服务端 Feign,不要让普通登录 Token 直调任意广播入口。


7. 前端设计

模块路径职责
单例连接utils/WebSocketService.jsconnect/disconnect/subscribe;websocket_url / 原生传输开关 / 本会话跳过;诊断事件环;__gomsWs
消息约定utils/goms/wsMessage.jscategory 解析、订阅常量、事件名
Layoutlayout/index.vuewebsocket_enabled 启停连接
Navbarlayout/components/Navbar.vue/user/queue/pushTask,分流 Notification / 事件
监测浮层components/goms/WebSocketMonitor状态、在线人数、自测发送、回执
诊断 APIapi/system/websocket.jsstatus / ping / diagSend

7.1 监测浮层文案与指定用户(易混点)

展示含义说明
顶栏绿点 + WS 已连接/未连接本浏览器 STOMP 连接态本人是否连上,看这里即可
在线 N(M)brokerUserCount + selfSessionCountN=本机在线人数;M=本账号会话数(多标签 >1);标签用「在线」避免与固定宽 label 换行
selfOnlinestatus 仍返回面板主文案不写「我在线」(与顶栏重复)

指定用户推送

  • 控件:el-autocomplete(可直接输入任意 userId,也可点选 onlineUsers
  • 名单:GET /webSocket/statusonlineUsers(须 goms:webSocket:diagSend);约 8s 轮询,切到「指定用户」/输入框聚焦时立刻刷新
  • 下拉挂 body:popper-classz-index> 浮层 4000(否则选项被挡住,看起来像「没数据」)

业务页推荐:

  1. 系统频道(已由 Navbar 订):听 $rootgoms-websocket-biz / goms-websocket-notify
  2. 独立 topicwebsocketService.subscribe('/topic/xxx', …)(少用,注意权限与在线范围)

8. 后端设计

模块路径职责
Configwebsocket/config/WebSocketConfig端点、Broker、线程池
拦截器websocket/interceptor/InboundChannelInterceptorCONNECT 验 Token
推送websocket/service/WebSocketPushServiceisEnabled + send / sendToUser(normalize)
诊断websocket/controller/WebSocketDiagControllerstatus / ping / diagSend
内部推送websocket/controller/WebSocketSendController@InnerAuth send*
FeignRemoteWebSocketService其它服务调用入口
模板推送SysPushModulesServiceImpl#pushWebSocket工作流等模板 → NOTIFY
SockJS 异常websocket/handler/SockJsExceptionResolver/socket/**:warn + 空响应;不改公共 GlobalExceptionHandler

sys_config

  • websocket_enabled:总开关
  • websocket_notify_enabled:是否弹 Notification
  • websocket_url:SockJS 独立入口(不经 iws-gateway REST);空则回退 VUE_APP_BASE_API + xhr/xdr;二级目录正式https://{host}/iws/yj_goms/ws(§3.3)
  • websocket_native_enabled:已配 websocket_url 时是否尝试原生 websocket(默认 true);IWS 未透传 Upgrade 时可 false(§3.4)

公共模块边界:SockJS 客户端断开写帧失败属常态噪音;收口放在 goms-system 本地 HandlerExceptionResolver(非 socket 返回 null 继续走若依全局处理),避免动 ruoyi-common-security


9. 业务协同推荐模式(应急等)

用户 A 点「暂停」→ REST(带 Authorization)→ Service 落库
                              ↓
                    ChatMessages.bizOf(..., EMERGENCY_PAUSE, keyId, data)
                              ↓
                    sendToUser("/queue/pushTask", msg) 或 Feign
                              ↓
用户 B/C 浏览器 ← BIZ 事件 ← 按 keyId + messageType 更新本地 UI

禁止:浏览器直调 sendToUser 伪装推送给他人。


10. 源码备份清单

目录:notes/java/webSocket/webSocket_demo/

webSocket_demo/
├── java/
│   ├── ChatMessage.java          # 信封实体
│   ├── ChatMessages.java         # 大类/小类常量 + notifyOf/bizOf/normalize
│   ├── RemoteWebSocketService.java
│   ├── config/                   # WebSocketConfig / UserPrincipal
│   ├── interceptor/              # InboundChannelInterceptor
│   ├── service/                  # WebSocketPushService
│   ├── controller/               # Diag / Send(InnerAuth)
│   └── handler/                  # SockJsExceptionResolver(仅 goms-system)
├── js/
│   ├── WebSocketService.js
│   ├── wsMessage.js
│   └── websocket.js              # api/system/websocket.js
└── vue/
    └── WebSocketMonitor.vue

仓库内权威路径仍以 Goms-Cloud-springboot2 为准;本目录供笔记对照与离线查阅。


11. 运维与排障摘要

现象排查
能连不能收SimpleBroker 是否含 /queue;发送是否 /queue/pushTask;订阅是否 /user/queue/pushTask
CONNECT 失败Token 是否过期;日志 WebSocket CONNECT 拒绝
收两条是否又把 /user 写进 enableSimpleBroker
自测无权限角色是否勾选 goms:webSocket:diagSend
指定用户「没名单」① 有无 diag 权限(onlineUsers 才下发)② 联想下拉 z-index 是否被浮层挡住 ③ 是否已轮询/聚焦刷新 status
自测无回执先看顶栏是否 WS 已连接diagSend 对离线目标会直接返回「未投递」
xhr 写帧异常客户端已离开时的常态噪音;看是否已由 SockJsExceptionResolver 降为 warn,勿当业务故障
满屏 /xhr 每 ~25s二级目录:主平台 /yj_goms/ 未透传 Upgrade,或未配 websocket_url;属 SockJS 降级,不是接口变慢(§3.3)
invalid Upgrade header: null请求已到 goms-system,但中间反代剥掉了 Upgrade;优先查主平台 /yj_goms/,其次 GOMS /ws/
配了 websocket_url 仍连不上正式是否用 https;GOMS /ws/:18093/socket/endpointWs/endpointSource 是否为 sys_config.websocket_url
控制台反复 ws:///wss:// 失败Upgrade 未通;临时:websocket_native_enabled=false,或等本会话 native_ws_skip 后只走 xhr(§3.4)
nativeTransportSkipped=true 仍 xhr预期:本会话已判定原生不可用,或开关为 false;IWS 修好后清 session / 新标签再试
未配独立地址建连慢/失败属预期:仅 xhr/xdr;勿强开原生 ws;或补齐两层 nginx + websocket_url

项目侧使用说明见 GOMS 仓库:ai-doc/Wiki/WebSocket使用指南.md;运维/安全见 ai-doc/Wiki/WebSocket推送.md(含 websocket_url)。
主平台二级目录落地步骤见:主平台H5方式集成.md §六。


12. 参考对照

项目说明
LegacyAppChatMessage 字段、keyId 多场隔离、应急 messageType 分流;Broker 含 /user(双投);线上 /iws/BizApp/ws/ + 原生 websocket
GOMS/user 进 Broker、补 /queue;CONNECT 验 Token;category 大类(常量集中在 ChatMessages);InnerAuth 保护 send*;SockJS 异常本模块收口;可选 websocket_url 独立 WS 通道;原生失败本会话跳过(§3.4)
GOMS + 主平台二级目录页面挂 /iws/yj_goms/;WS 同入口 .../iws/yj_goms/ws主平台 /yj_goms/ 必须 Upgrade 透传 + GOMS /ws/ → 18093;详见 §3.3