SignalR 实时通信使用手册
SignalR 实时通信使用手册
实时通信基于 ASP.NET Core SignalR,前端使用
@microsoft/signalr官方客户端。
1. 概述
系统使用 SignalR 实现服务端主动推送能力,当前已支撑以下功能:
| 功能 | 说明 |
|---|---|
| 在线用户统计 | 实时广播当前在线人数与在线用户列表(按租户隔离) |
| 系统通知/公告 | 后台发布通知后实时推送到在线客户端 |
| 日程提醒红点 | 登录时推送当前用户未完成日程数 |
| 站内聊天 | 用户之间实时收发聊天消息 |
| 系统消息 | 业务系统向指定用户推送消息(receiveMessage) |
| 强退/单点登录 | 管理员强退在线用户;开启单点登录后新登录踢掉旧连接 |
2. 架构与连接流程
┌─────────────┐ WebSocket / SSE / LongPolling ┌──────────────────┐
│ ZR.Vue │ ───────────────────────────────────────────▶ │ MessageHub │
│ (前端) │ JWT(accessTokenFactory) 作为连接凭证 │ (/msgHub) │
│ signalR.js │ ◀─────────────────────────────────────────── │ ServiceCore │
└─────────────┘ 服务端通过 Clients.*.SendAsync(事件,数据) └──────────────────┘
│
IHubContext<MessageHub>
│
Controller / Service 主动推送
连接建立时序:
- 前端
main.js调用signalR.init(process.env.VUE_APP_SOCKET_API)创建连接对象(此时未启动)。 - 用户登录后
App.vue监听token变化,调用signalr.start()发起连接。 - 连接携带 JWT(
accessTokenFactory把 token 注入到查询字符串),后端JwtAuthMiddleware校验。 MessageHub.OnConnectedAsync校验IsAuthenticated,未认证直接忽略;认证通过后记录在线连接、加入租户分组,并向当前客户端推送通知列表与日程红点,随后广播在线人数。
3. 服务端配置
3.1 服务注册与端点
文件:ZR.Admin.WebApi/Program.cs
// 注册 SignalR 服务(含 JSON 协议,属性名驼峰)
builder.Services.AddSignalR()
.AddJsonProtocol(options =>
{
options.PayloadSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
});
// 映射 Hub 端点,客户端连接地址即 /msgHub
app.MapHub<MessageHub>("/msgHub");
- Hub 实现:
ZR.ServiceCore/Signalr/MessageHub.cs(继承Hub)。 - 连接端点:
/msgHub(前端VUE_APP_SOCKET_API必须与此对应)。
3.2 连接认证
SignalR 基于 WebSocket,无法像普通 HTTP 请求那样在 Header 中携带 Authorization。项目通过 accessTokenFactory 把 JWT 注入到连接 URL 的查询字符串 ?access_token=xxx,后端 JwtAuthMiddleware 读取该参数完成认证(见 Program.cs 中 access_token 处理)。
因此前端连接地址最终为:
{VUE_APP_SOCKET_API}?access_token={token}(由客户端自动拼接)。
4. 前端接入
4.1 连接封装
文件:ZR.Vue/src/utils/signalR.js
import * as signalR from '@microsoft/signalr';
import { getToken } from '@/utils/auth';
const connection = new signalR.HubConnectionBuilder()
.withUrl(url, { accessTokenFactory: () => getToken() }) // 自动携带 JWT
.withAutomaticReconnect() // 断线自动重连
.configureLogging(signalR.LogLevel.Warning)
.build();
- 全局单例挂在
Vue.prototype.signalr,业务组件中可用this.signalr.SR访问原生HubConnection。 - 连接对象:
this.signalr.SR;调用服务端方法:await this.signalr.SR.invoke("方法名", 参数)。
4.2 初始化与启动
main.js:signalR.init(process.env.VUE_APP_SOCKET_API)—— 仅创建连接,不立即启动。App.vue:watch监听token,登录成功(有 token)后才signalr.start(),保证连接携带有效凭证。
// App.vue
watch: {
token: {
handler(val) { if (val) this.signalr.start() },
immediate: true
}
}
4.3 断线重连
- 启用
withAutomaticReconnect(),网络恢复后自动重连。 - 额外兜底:
onclose中调用start()重试,最多failNum = 4次,每次间隔 5 秒。 onreconnected回调用于重连成功后的处理(如重新拉取数据)。
注意:若 4 次重试均失败,控制台提示断开,建议用户刷新浏览器重建连接。
5. 通信协议详解
5.1 服务端 → 客户端(推送事件)
事件名常量定义在 ZR.ServiceCore/Signalr/HubsConstant.cs。
| 事件名 | 触发时机 | 数据格式 | 前端处理 |
|---|---|---|---|
onlineNum | 有用户上线/下线时广播 | { num, onlineClients, leaveUser } | store.dispatch('socket/changeOnlineNum', data) 更新在线统计 |
moreNotice | 连接建立成功后 | ApiResult(code=200,data=通知列表) | store.dispatch('socket/getNoticeList', data.data) |
receiveNotice | 后台发送通知公告(SysNoticeController.SendNotice) | (title, content) 两个参数 | 弹出 Element Notification 通知 |
dailyScheduleReminder | 登录时推送当前用户未完成日程数 | int(数量) | 用于日程红点提示 |
receiveChat | 聊天消息送达(sendMessage 调用后) | ChatMessageDto(含发送人、消息、时间等) | 聊天组件接收并渲染 |
receiveMessage | 业务系统向指定用户推送消息(MessageNotifier) | 任意 payload(通常为 SysUserMsgDto) | 业务组件按需监听 |
logOut | 单点登录踢出其他设备(logOut 方法) | 无 | 前端退出登录并提示 |
forceUser | 管理员强退在线用户(SysUserOnlineController) | { reason, time } | 前端弹窗提示并被强制退出 |
welcome | 预留欢迎语 | string | 弹 Notification.info |
说明:
HubsConstant中还存在onlineUser、lockUser、connId常量,当前版本服务端未实际调用SendAsync,属预留事件,前端可按需扩展监听。
5.2 客户端 → 服务端(Hub 方法)
定义在 MessageHub.cs,通过 this.signalr.SR.invoke("方法名", ...) 调用。
| 方法名 | 参数 | 说明 |
|---|---|---|
sendMessage | (long toUserId, string message) | 向指定用户发送聊天消息;对方在线则实时推送 receiveChat,不在线则暂记日志(离线消息持久化待扩展) |
getConnId | 无 | 返回当前连接 ConnectionId |
logOut | 无 | 单点登录场景下踢出当前用户的其他设备连接(需 singleLogin=true) |
// 前端调用示例:发送聊天消息
await this.signalr.SR.invoke('sendMessage', toUserId, '你好');
6. 后端主动推送(开发指南)
在 Controller / Service 中向客户端推送消息,有两种常用方式。
6.1 注入 IHubContext<MessageHub>
适用于需要直接控制推送范围(全体 / 指定连接 / 分组)的场景。
public class SysNoticeController : BaseController
{
private readonly IHubContext<MessageHub> _hubContext;
public SysNoticeController(IHubContext<MessageHub> hubContext) => _hubContext = hubContext;
// 向所有在线客户端推送通知
[HttpPut("send/{NoticeId}")]
public IActionResult SendNotice(int NoticeId)
{
var notice = _sysNoticeService.GetFirst(x => x.NoticeId == NoticeId);
if (notice?.Status == 0)
{
// 对应前端监听的 receiveNotice 事件
_hubContext.Clients.All.SendAsync(HubsConstant.ReceiveNotice, notice.NoticeTitle, notice.NoticeContent);
}
return SUCCESS(notice);
}
}
常用推送目标:
_hubContext.Clients.All.SendAsync("event", data); // 全部客户端
_hubContext.Clients.Client(connId).SendAsync("event", data); // 单个连接
_hubContext.Clients.Clients(connIds).SendAsync("event", data); // 多个连接
_hubContext.Clients.Group("tenant_xxx").SendAsync("event", data); // 租户分组
6.2 使用 IMessageNotifier(推荐,按用户推送)
ZR.ServiceCore/Services/MessageNotifier.cs 封装了"按 userId + 租户"筛选在线连接并推送 receiveMessage 的逻辑,业务代码直接注入即可,无需关心连接细节。
public class MyService
{
private readonly IMessageNotifier _notifier;
public MyService(IMessageNotifier notifier) => _notifier = notifier;
public async Task PushToUser(long userId, object msg)
{
// 自动按 userId 找到其全部在线连接推送(多租户按 TenantId 隔离)
await _notifier.NotifyUserAsync(userId, msg);
}
}
MessageNotifier推送异常会被捕获并记录日志,不影响主流程消息落库。
6.3 强退在线用户
SysUserOnlineController 提供单个/批量强退接口,底层通过 IHubContext 向指定连接推送 forceUser:
// 单个强退
await HubContext.Clients.Client(dto.ConnnectionId)
.SendAsync(HubsConstant.ForceUser, new { dto.Reason, dto.Time });
7. 多租户隔离
- 多租户模式下,连接建立时按
TenantId加入分组tenant_{tenantId}(GetTenantGroup)。 - 在线人数广播(
onlineNum)仅推送到同租户分组;跨租户互不可见。 - 消息推送(
sendMessage、NotifyUserAsync)按TenantId过滤目标连接,确保租户间数据隔离。 - 非多租户模式(
App.IsTenantEnabled() == false)下,所有连接视为同一租户,广播使用Clients.All。
8. 配置项说明
| 配置 | 位置 | 说明 |
|---|---|---|
VUE_APP_SOCKET_API | 前端 .env | SignalR 连接地址,须与后端端点对应。同域部署填 /msgHub;跨域填完整地址 https://域名/msgHub |
singleLogin | 后端 appsettings.json | 单点登录开关。true 时新设备登录会踢掉该用户其他设备的连接 |
AddJsonProtocol | Program.cs | 启用驼峰命名序列化,前后端字段名保持一致 |
9. 前端监听事件注册
前端在 signalR.js 的 receiveMsg(connection) 中统一注册服务端事件监听。若需新增服务端推送事件,需在此处补充 connection.on('事件名', handler),否则前端收不到该事件。
connection.on('onlineNum', (data) => {
store.dispatch('socket/changeOnlineNum', data);
});
connection.on('moreNotice', (data) => {
if (data.code == 200) store.dispatch('socket/getNoticeList', data.data);
});
connection.on('receiveNotice', (title, data) => {
Notification({ type: 'info', title, message: data, duration: 0 });
});
// 新增事件请在此注册 ...
10. 注意事项与已知限制
- 必须登录后连接:未携带有效 token 的连接会被
OnConnectedAsync忽略,不会计入在线用户。 - 离线消息未持久化:
sendMessage在对方离线时仅写日志(StoreOfflineMessageAsync标注 TODO),刷新/离线期间消息会丢失,后续版本计划落库。 - 在线用户存于内存:
MessageHub.OnlineClients为进程内静态字典,多实例部署(负载均衡)时在线用户统计与强退仅对当前节点生效,需配合 Redis 背板(AddStackExchangeRedis)或粘性会话。 - 跨域部署:前后端不同源时,除配置完整连接地址外,还需后端 CORS 允许 SignalR 协商请求(negotiate)。
- 事件名一致性:服务端
SendAsync的事件名、前端connection.on的事件名、HubsConstant常量三者必须一致,建议统一引用HubsConstant避免拼写错误。
11. 常见问题排查
| 现象 | 可能原因 | 处理 |
|---|---|---|
| 连接立即断开 | token 无效/过期 | 确认已登录且 getToken() 返回有效 JWT |
| 收不到通知 | 未注册对应 connection.on | 检查 signalR.js 的 receiveMsg 是否监听该事件 |
| 在线人数不对 | 多实例部署 / 连接未认证 | 检查是否单节点运行;确认连接已通过认证 |
| 跨域连接失败 | CORS 未放行 / 地址错误 | 配置完整 https://域名/msgHub 并放行 CORS |
| 聊天消息丢失 | 对方离线 | 当前版本离线消息不持久化(已知限制) |
文档依据项目源码(MessageHub.cs、signalR.js、Program.cs、SysNoticeController.cs、SysUserOnlineController.cs、MessageNotifier.cs)整理。
