BitzOrcas 已为 Chat 实现 SignalR:ChatRealtimeHub 提供连接入口,SignalRChatRealtimeAdapter 按频道成员的 user id 广播消息与已读标记。实时层不是写入入口,也不是事实来源。
authorized REST command → repository save → realtime adapter → SignalR clientsclient reconnect ─────────────────────────→ REST history cursor关键路径图
下图展示了实时通信服务架构:从客户端 WebSocket 连接建立,到 Redis Backplane 跨节点广播,再到租户频道安全鉴权的流转全景。
当前边界
- Chat 生产组合可以注册 SignalR;缺失时
NullChatRealtimeAdapter保持 REST 可用。 - Documents 协作与白板仍允许 Null Hub,不能描述成已交付实时协作。
- Notifications 的 Inbox 是事实来源;是否配置实时提示取决于宿主适配器。
- SSE 当前没有生产端点,详见SSE。
Hub 类型本身没有客户端可调用方法,并标注 [Authorize];它是被动传输端点。配置 Chat:Realtime:Enabled=true 时注册 SignalR 与 scoped adapter,并将 Hub 映射到 Chat:Realtime:HubPath,默认 /hubs/chat。
{ "Chat": { "Realtime": { "Enabled": true, "HubPath": "/hubs/chat" } }}未启用时,不映射 Hub,Application 的 NullChatRealtimeAdapter 让 REST 主链继续工作。这是可选传输降级,不代表宣称 Chat Realtime 的 Edition 已满足就绪。
授权
连接认证只确认调用者身份。每次订阅或广播还要依据持久化 Membership 路由,不能让客户端声明任意 group。成员移除后应停止后续广播,历史读取仍由 REST 授权决定。
当前 adapter 并不使用 SignalR Group,而是从已加载的 channel.Memberships 取 distinct user id,然后调用 Clients.Users(recipients)。因此 IUserIdProvider 必须把 SignalR connection 的 user identifier 与 Membership.UserId 对齐;只配置 JWT 认证而未验证 user mapping,可能导致收不到或错投。
// 广播目标来自持久化聚合成员,不接受客户端提交的 group 名。var recipients = channel.Memberships .Select(member => member.UserId) .Where(userId => !string.IsNullOrWhiteSpace(userId)) .Distinct(StringComparer.Ordinal) .ToArray();
// SignalR 使用 User 路由;IUserIdProvider 是身份契约的一部分。await hubContext.Clients.Users(recipients) .SendAsync(ChatRealtimeMethods.MessageSent, payload, cancellationToken);payload 仍包含 tenant/channel 标识,但这些字段只是客户端校验与路由证据,不能替代服务端 recipients 选择。
事件契约
当前公开方法至少包括 MessageSent 和 ReadMarkerUpdated。消息 payload 包含 tenant、channel、message、sender、正文、mention user ids、attachment file ids 与 sent time;已读 payload 包含 user、message 和 read time。
正文通过实时通道发送,日志和 telemetry 不应序列化完整 payload。客户端必须按稳定 method name 与版本化 DTO 消费;字段新增可向后兼容,重命名/删除需要协议版本。
提交后投递的演进
理想链路是事务内保存事实与 Outbox,提交后由事件消费者调用实时 adapter。这样实时失败可以独立重试,数据库回滚不会产生 ghost notification。
Command transaction: aggregate + repository + outbox → commitPost-commit consumer: outbox event → realtime adapter → SignalRClient: realtime hint → REST cursor/query confirms fact即便迁移到 post-commit,SignalR 仍是至少一次提示。consumer 需要稳定 event/message id;客户端去重并从 REST 事实源恢复。
断线与扩展
SignalR 消息可能丢失、重复或乱序。payload 带稳定消息/事件 ID 和序列信息,客户端重连后通过历史游标补齐。多实例部署需要经过验证的 SignalR scale-out/backplane 组合,不能假设进程内连接表跨实例共享。
当前注册只调用 AddSignalR(),源码中没有 Redis/Azure SignalR scale-out 配置。多副本时连接只存在于落点实例,其他实例发出的 Clients.Users 不会自动覆盖全部连接。生产要显式接入 backplane/service 并验证同一用户多连接、多实例和断线重连。
慢客户端与大消息需要 MaximumReceiveMessageSize、keepalive、client timeout、transport fallback 和代理 WebSocket 配置。当前组合未在本页可验证出专属限制,采用方应由部署测试给出容量边界。
客户端恢复模式
// 实时事件只是提示;messageId 用于去重,REST cursor 补齐遗漏事实。connection.on("chat.message.sent", event => { if (seen.has(event.messageId)) return; seen.add(event.messageId); inbox.apply(event);});
// 重连后从最后确认的游标查询,而不是假设离线期间没有消息。connection.onreconnected(() => history.catchUp(lastCommittedCursor));GA 检查
- WebSocket 升级、Token 过期、成员移除和跨租户 group 有测试。
- 断线重连从持久化历史恢复。
- 多实例广播、背压和最大消息大小经过压测。
- 日志不记录 Token 和完整聊天正文。
- 宣称实时能力的 Edition 不允许 Null 适配器通过就绪检查。
还要故障注入“Save 后实时发送失败”“实时已发后事务回滚”“消息重复/乱序”“IUserIdProvider 不匹配”和“实例 A 连接、实例 B 广播”。当前直接 Handler 广播窗口没有消除前,不应以严格提交后事件语义对外承诺。
Chat 业务边界见聊天模块。