Skip to content

WebSocket 使用指南

WebSocket 是 sz-admin 中的可选实时通信能力,主要用于在线通知、字典刷新、前端配置刷新、权限同步、强制下线和站内消息等场景。

设计理念

NOTE

WebSocket 在框架中被设计为一项轻量的基础通信能力,而不是承载全部业务规则的独立业务中心。sz-service-websocket 负责连接管理、鉴权、心跳和消息转发;Admin 业务端继续负责具体业务判断和消息内容组织。

为了让 WebSocket 服务与 Admin 业务端保持解耦,框架使用 Redis Pub/Sub 作为服务间消息通道。业务端只需要发布标准化的推送消息,WebSocket 服务订阅后再分发给在线客户端。这样既避免了服务之间的直接强依赖,也更适合多实例部署时的消息转发和节点协同。

关键要点:

  • sz-service-websocket 只提供基础 WebSocket 通讯能力,不直接绑定具体业务流程。
  • Admin 业务端通过 SocketServiceWebsocketRedisService 发布消息,业务含义由业务端决定。
  • Redis Pub/Sub 负责连接两个边界:业务服务不需要感知具体在线连接,WebSocket 服务也不需要侵入业务模块。
  • 扩展实时通信能力时,可以在保持整体架构轻量的前提下,自定义频道、消息内容和前端处理逻辑。

实际使用时可以先按下面的顺序理解:

  1. 前端配置 VITE_SOCKET_URL,登录后自动连接 WebSocket。
  2. 后端业务代码优先调用 SocketService 推送消息。
  3. 前端通过内置频道处理器或 mittBus 监听业务频道。
  4. 需要扩展时,再新增频道枚举、后端推送逻辑和前端处理逻辑。

一、快速启用

1. 启动 WebSocket 服务

后端 WebSocket 能力由独立服务 sz-service-websocket 提供,默认端口为 9993,连接端点为 /socket

yaml
server:
  port: 9993

spring:
  application:
    name: websocket-service

该服务会加载 Redis 和 Sa-Token 配置,用于校验登录态、维护在线连接和进行服务间消息转发。因此 WebSocket 服务需要与业务服务使用同一套登录态相关 Redis 数据。

2. 配置前端连接地址

前端通过 VITE_SOCKET_URL 控制是否启用 WebSocket。该环境变量不存在或为空时,前端不会创建 WebSocket 连接。

properties
VITE_SOCKET_URL=ws://127.0.0.1:9993/socket

如果生产环境使用 HTTPS,应使用 wss://。前端代码会根据当前协议把相对地址转换为 ws:wss:

3. 前端自动建立连接

前端布局加载时会调用 useSocketStore().open(),当前用户存在 token 且 VITE_SOCKET_URL 已配置时,会创建连接。

ts
const ws = new WebSocket(socketUrl, [userStore.token]);

由于浏览器原生 WebSocket API 不支持自定义请求头,token 会通过 Sec-WebSocket-Protocol 子协议头传递给后端。

二、后端如何推送消息

业务代码优先使用 SocketService。它位于 sz-module-admin,对外统一接收 Long 类型用户 ID,内部负责转换为 WebSocket 链路使用的 loginId 字符串。

java
// 全员同步前端配置
socketService.syncFrontendConfig();

// 全员同步字典
socketService.syncDict();

// 定向同步用户权限
socketService.syncPermission(userId);

// 强制指定用户下线
socketService.kickOff(userId);

// 发送站内消息
socketService.sendMessage(body, senderId, receiverIds);

// 通知消息已读
socketService.readMessage(fromUserId, toUsers);

如果是框架级或特殊场景,需要绕过业务封装,也可以直接使用 WebsocketRedisServiceSocketUtil

java
// 全员广播
websocketRedisService.sendServiceToWs(
    SocketUtil.broadcast(SocketPushMessage.of(SocketChannelEnum.SYNC_DICT))
);

// 定向推送
websocketRedisService.sendServiceToWs(
    SocketUtil.toUsers(
        SocketPushMessage.of(SocketChannelEnum.UPGRADE_CHANNEL, "系统即将升级"),
        List.of("1"),
        "system"
    )
);

普通业务优先使用 SocketService,只有在需要自定义底层路由、跨模块封装或演示接口时,再直接使用 WebsocketRedisService

三、前端如何接收消息

前端消息入口在 src/stores/modules/socket/socket.ts,消息解析后会交给 channelHandlers.ts 分发。

内置频道会直接执行对应逻辑:

频道前端行为
PONG清除心跳超时计时器
KICK_OFF关闭连接、清理登录态并跳转登录页
SYNC_FRONTEND_CONF标记并重新拉取前端配置
SYNC_DICT标记字典缓存失效并重新预热字典
SYNC_PERMISSIONS重新获取按钮权限
UPGRADE_CHANNEL通过 mittBus 广播 socket.UPGRADE_CHANNEL
MESSAGE通过 mittBus 广播 socket.MESSAGE
READ通过 mittBus 广播 socket.READ

未显式注册的频道不会丢弃,会统一按 socket.${channel} 广播。

ts
mittBus.on('socket.UPGRADE_CHANNEL', data => {
  // 处理系统升级通知
});

站内消息组件就是通过该机制监听消息变化:

ts
mittBus.on('socket.MESSAGE', handleMessage);
mittBus.on('socket.READ', () => {
  getUnreadCount();
});

四、扩展一个新频道

新增业务频道时,推荐按三个位置修改:

  1. 后端在 SocketChannelEnum 中新增枚举值。
  2. 后端业务侧通过 SocketServiceSocketUtil 推送该频道。
  3. 前端在 channelHandlers.ts 中增加处理器,或在页面中监听 mittBussocket.${channel} 事件。

后端推送给前端时,SocketPushMessage.of(...) 使用的是枚举常量名,例如 SocketChannelEnum.SYNC_DICT 最终发送给前端的频道是 SYNC_DICT

json
{
  "channel": "SYNC_DICT",
  "data": null
}

因此前后端频道名称应以枚举常量名和前端常量为准,不使用枚举里的 name 字段作为传输值。

五、内置业务场景

场景后端触发推送频道前端处理
前端配置变更socketService.syncFrontendConfig()SYNC_FRONTEND_CONF重新拉取前端配置
字典、字典类型、字典来源变更socketService.syncDict()SYNC_DICT标记字典缓存失效并重新加载
用户权限变化socketService.syncPermission(userId)SYNC_PERMISSIONS重新获取权限按钮
强制下线socketService.kickOff(userId)KICK_OFF清理登录态并跳转登录页
站内消息发送socketService.sendMessage(...)MESSAGE弹出通知并刷新未读数量
站内消息已读socketService.readMessage(...)READ刷新未读数量
升级公告演示SocketChannelEnum.UPGRADE_CHANNELUPGRADE_CHANNEL通过 mittBus 交给业务接管

测试演示接口位于 TestController,请求前缀为 /www,仅在 devlocalpreview 环境激活。生产环境不要依赖这些演示接口。

http
POST /www/push/all
POST /www/push/user
POST /www/kick
POST /www/message/send

六、消息结构

框架中有三类消息对象,各自负责不同边界。

类型方向说明
ClientMessage前端到 WebSocket 服务接收客户端消息,channel 为字符串,可兼容 PING 等非枚举频道
SocketPushMessage服务端到前端最终推送给浏览器的消息体,channelSocketChannelEnum.name()
TransferMessage后端服务间Redis Pub/Sub 中传递的信封,包含接收人、是否广播、消息范围等路由信息

前端发送心跳:

json
{
  "channel": "PING"
}

后端返回心跳:

json
{
  "channel": "PONG"
}

业务推送消息:

json
{
  "channel": "UPGRADE_CHANNEL",
  "data": "系统即将升级"
}

data 可以是字符串、对象或数组,前端解析后会直接放入 msg.data,不需要再次手动 JSON.parse,除非某个业务字段本身就是 JSON 字符串。

七、鉴权与会话失效

WebSocket 建连时,后端 WebSocketInterceptor 会从 Sec-WebSocket-Protocol 中解析 token,并执行以下校验:

  1. 校验 JWT 签名和 loginType。
  2. 从 JWT payload 中读取 loginId
  3. 校验 token-session 是否仍存在于 Redis。
  4. 校验账号是否被禁用。
  5. 刷新 Sa-Token 的 active-timeout。
  6. loginId 和 token 写入 WebSocket session attributes。
  7. 将 token 回写到 Sec-WebSocket-Protocol 响应头,完成浏览器子协议确认。

连接建立后,非心跳消息进入业务处理前还会再次校验 token。为减少 Redis 写入压力,updateLastActiveToNow 在非心跳消息中按 5 分钟节流刷新;签名校验、Redis session 存在性和禁用状态判断仍会执行。

如果鉴权失效,服务端会用自定义关闭码 4401 关闭连接。前端识别该关闭码后会停止重连,并复用统一登录失效处理逻辑清理登录态、跳转登录页。

ts
if (event.code === CLOSE_CODE_AUTH_EXPIRED) {
  canReconnect.value = false;
  handleAuthExpired();
  return;
}

此外,WebSocketSessionGuard 会每 60 秒扫描当前节点的在线 session。如果发现 token 已注销、被踢、被替换、active-timeout 失效或账号被禁用,也会主动用 4401 断开连接。

八、心跳与重连

前端连接建立后会启动心跳:

  • 每 30 秒发送一次 { "channel": "PING" }
  • 发送 PING 后等待 10 秒 PONG,超时则认为连接异常并触发重连。
  • 页面从后台切回前台时,如果连接不是 OPEN 状态,会立即尝试重新连接。
  • 只有服务端返回 4401 时才停止重连并进入登录失效流程。

后端对 PING 走快速路径:不进入完整业务分发,只刷新 active-timeout 并立即返回 PONG

PINGPONG 是心跳信令,不在 SocketChannelEnum 中维护,前后端都使用字符串常量处理。

九、CORS 与 Nginx

WebSocket 跨域由 sz-service-websocketsz.cors.allowed-origins 控制。

yaml
sz:
  cors:
    allowed-origins: "*"

本地开发可以使用 *。生产环境应显式配置前端实际访问域名,多个来源用英文逗号分隔。

yaml
sz:
  cors:
    allowed-origins: "https://admin.example.com,https://preview.example.com"

这里配置的是浏览器页面的真实 Origin,不是 Nginx 地址,也不是后端服务地址。

Nginx 反向代理 WebSocket 时,需要保留协议升级头。

nginx
location /socket {
    proxy_pass http://sz-service-websocket:9993;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
    proxy_cache_bypass $http_upgrade;
    proxy_read_timeout 3600s;
    proxy_send_timeout 3600s;
}

十、服务间通信原理

WebSocket 服务只负责连接管理、鉴权、心跳和消息转发;具体业务逻辑仍在 Admin 业务服务中。

后端服务间通信依赖 Redis Pub/Sub:

Redis channel方向说明
channel:service_to_ws业务服务到 WebSocket 服务业务服务发布 TransferMessage,WebSocket 服务订阅后推送给浏览器
channel:ws_to_serviceWebSocket 服务到业务服务浏览器上行消息经 WebSocket 服务透传给业务服务

服务端推送浏览器的主链路如下:

mermaid
flowchart LR
  A["业务代码"] --> B["SocketService / WebsocketRedisService"]
  B --> C["Redis: channel:service_to_ws"]
  C --> D["sz-service-websocket"]
  D --> E["浏览器 WebSocket 连接"]

浏览器上行消息的链路如下:

mermaid
flowchart LR
  A["浏览器"] --> B["sz-service-websocket"]
  B --> C["Redis: channel:ws_to_service"]
  C --> D["业务服务监听器"]

如果只是做服务端通知前端,大多数场景只需要使用 SocketService,不需要关心 Redis 信封细节。

十一、排查清单

连接没有建立时,优先检查:

  • VITE_SOCKET_URL 是否存在且非空。
  • sz-service-websocket 是否已启动,端口是否为 9993
  • 前端 token 是否存在。
  • WebSocket 服务是否能访问同一套 Sa-Token Redis 数据。
  • sz.cors.allowed-origins 是否包含当前前端页面的真实 Origin。
  • Nginx 是否正确配置 UpgradeConnection 头。

连接后收不到消息时,优先检查:

  • 后端是否调用了 SocketServicesendServiceToWs
  • redis.listener.enable 是否开启,Redis Pub/Sub 监听是否生效。
  • 定向推送的 toUsers 是否为登录用户的 loginId 字符串。
  • 后端发送的 channel 是否与前端常量一致。
  • 前端是否在 channelHandlers.tsmittBus 中处理了该频道。

频繁掉线或反复重连时,优先检查:

  • 服务端是否返回了 4401,如果是,说明登录态已失效或账号状态不允许继续连接。
  • 浏览器是否能在 10 秒内收到 PONG
  • 代理层是否存在较短的 WebSocket 读写超时。
  • WebSocket 服务、业务服务和登录服务是否连接到同一 Redis 环境。