如何实现微服务通用websocket
约 1828 字大约 6 分钟
2025-03-03
只要系统开始做实时推送,WebSocket 几乎迟早都会出现。
问题不是“要不要上 WebSocket”,而是当系统从单服务走向多服务之后,WebSocket 到底应该怎么组织。
如果每个业务模块都自己维护长连接、订阅表和推送逻辑,最终很容易出现三个问题:
- 连接资源被重复维护,成本高。
- 鉴权模型分散,逻辑越来越乱。
- 一次业务事件想推给多个维度的订阅者时,发布逻辑会越来越啰嗦。
联犀当前的做法,是把 WebSocket 统一沉到中台入口层,并升级出一套频道模型。
传统做法为什么容易失控
最常见的早期做法是:
- 前端连上某个 WebSocket 服务
- 发一条订阅消息
- 后端记录“这个用户订阅了什么”
- 业务方再手工匹配订阅关系进行推送
在单业务阶段,这种做法问题不大。
但到了多模块场景,很快就会碰到下面几个瓶颈。
1. 长连接和业务宿主绑死
如果每个业务服务都自己维护 WebSocket,那么:
- 扩缩容会受到长连接影响
- 连接不容易复用
- 业务服务要承担原本不属于自己的连接管理复杂度
2. 订阅键越来越不可读
很多系统会把订阅参数序列化后再做哈希,形成一个“订阅键”。
这种做法短期省事,但长期调试极其痛苦,因为你看到的只是一串 key,很难一眼判断它到底表示什么订阅范围。
3. 多维度发布越来越臃肿
当同一条事件既可能被“按设备订阅”的用户收到,也可能被“按项目订阅”或“按区域订阅”的用户收到时,业务代码往往会变成手工列举所有维度。
这不是功能做不到,而是发布逻辑会越来越像拼装脚本。
联犀当前的总体思路
联犀没有把 WebSocket 当成某个业务服务的附属能力,而是把它放到了统一宿主里。
统一接入
前端统一连接中台 WebSocket 入口。
业务服务不直接维护终端连接,而是通过消息总线发布事件,由统一宿主完成最终推送。
统一订阅模型
订阅不再围绕“某段自定义参数 + 哈希键”组织,而是围绕清晰的频道模型组织。
统一发布模型
业务方优先发布“主频道”,频道展开交给发布器完成,而不是每个业务逻辑都自己去列举所有派生维度。
新频道模型解决了什么问题
联犀当前的频道采用点分层级结构,例如:
device.prop.productA.deviceB.temperature
tenant.notify.tenantA
user.notice.12345这样做的好处很直接。
1. 频道本身可读
看一眼频道字符串,就能判断:
- 属于哪个命名空间
- 是什么事件类型
- 对应哪个业务维度
2. 支持通配符匹配
联犀当前的频道支持类似 NATS 的通配符语义,可以按层级匹配订阅。
这让“我关心某一类事件”不必再退化成一堆手工拼装的订阅键。
3. 频道和消息总线天然一致
因为频道本身是层级化的,所以很容易与消息总线主题、业务维度和权限命名空间对齐。
连接管理为什么要重做
仅仅有频道字符串还不够。
真正决定系统上限的,是连接和订阅匹配的内部结构。
联犀当前 WebSocket 核心主要由几部分组成:
Hub:连接与订阅中心管理器Conn:单连接封装SubTrie:订阅匹配树Publisher:消息发布器
Hub 的作用
Hub 负责统一管理:
- 连接注册与移除
- 单用户连接数限制
- 单连接订阅数量限制
- 鉴权分发
- 广播与消息下发
它本质上是“连接和订阅的运行时中心”,而不是某个业务模块的工具类。
SubTrie 的作用
如果频道支持通配符,就不能继续只靠普通 map 做精确匹配。
联犀当前用 Trie 树管理订阅关系,让频道匹配可以按段进行,而不是遍历所有订阅键。
这对高并发推送场景的意义很大:
频道层级越清晰,匹配成本越可控。
鉴权为什么不能继续写死
统一接入 WebSocket 后,下一个问题就是:
一个中心宿主怎么知道某个用户能不能订阅某个频道?
联犀当前没有把所有业务权限都塞进 WebSocket 宿主里,而是做了命名空间鉴权分发:
- 内置命名空间走默认鉴权
- 业务命名空间走 Hook 扩展或专门的鉴权提供者
这样做的价值在于:
- WebSocket 宿主不需要理解所有业务细节
- 设备、项目、区域等复杂权限仍然归业务模型自己负责
- 新业务接入时不必改动整个 WebSocket 基座
发布为什么要引入频道展开
如果一条设备属性上报消息既要推给:
- 订阅设备维度的用户
- 订阅项目维度的用户
- 订阅区域维度的用户
最笨的做法是业务方每次都自己手工发布 3 到 5 个频道。
这样时间一长,代码里就会充满重复的维度展开逻辑。
联犀当前的做法是引入 Publisher 和 ChannelExpander:
- 业务先发布一个主频道
- 展开器根据规则自动派生更多频道
- 最终统一批量广播
这样业务层关注的是“发生了什么事件”,而不是“这个事件要推几个频道”。
为什么批量广播也很重要
WebSocket 的压力不只来自连接数,还来自消息数量。
如果每条事件都立即逐连接逐频道发送,系统很容易在高峰期被推送风暴拖住。
联犀当前发布器会做批量聚合,再通过消息总线分发给各个 API 节点。
每个节点收到批量消息后,再由 Hub 匹配本地连接并推送。
这意味着:
- 业务服务不需要感知具体 WebSocket 节点
- 多节点部署时仍能保持统一推送语义
- 推送成本更多收敛在统一基础设施层
总结
联犀当前的 WebSocket 设计,本质上是在回答三个问题:
- 长连接由谁维护?
- 订阅关系如何表达?
- 一条业务事件如何优雅地推给多维度订阅者?
它的答案是:
- 连接统一收敛到中台宿主
- 订阅统一收敛到频道模型
- 推送统一收敛到发布器与展开器
这比“每个业务模块自己搞一套 WebSocket”复杂一些,但它解决的是系统长期演进时最贵的问题:
既保留统一接入,又不把所有业务细节硬编码进实时推送基座里。
更新日志
2026/5/18 10:48
查看所有更新日志
43ef3-docs(blog): 新增后端与架构系列技术博客 18 篇,更新原有 7 篇于d4fa0-doc: 更换皮肤到最新版于39365-doc: 完善文档于
