SignalR 实现站内实时通知:从轮询到长连接
Words: 1073Read Time: 3 minLast edited: 2026-8-4
从轮询的痛点说起
我做的后台系统需要站内通知功能:审核结果、订单状态变更这类消息要实时推给在线用户。第一版图省事用的轮询——前端每 10 秒打一次接口查未读消息。
功能是能跑,但问题很快暴露:几十个人在线,服务器每 10 秒就收到一波请求,九成九的响应都是「没有新消息」,纯粹空转;用户体验也不好,消息延迟最多 10 秒,急脾气的同事直接在群里喊「我的单子到底过了没」。于是下决心换长连接方案,选了 SignalR——自家技术栈,不用白不用。
SignalR 的传输协商
SignalR 最省心的地方是传输层自动协商,不用自己操心兼容性。它会按优先级依次尝试:
- WebSocket:首选,全双工、开销最小
- Server-Sent Events:WebSocket 不可用时的降级(比如某些代理掐了 WS)
- 长轮询(Long Polling):最后的兜底,兼容性最好但开销最大
连接建立时客户端和服务端握手,谈出一个双方都支持的传输方式,之后网络波动还能配合自动重连恢复。我原来担心公司内网代理会拦 WebSocket,实测下来 SignalR 自己降级到了长轮询,功能无损,只是性能差点——这正是它存在的意义。
最小实现:服务端 Hub
服务端定义一个 Hub,按用户分组管理连接:
注册和路由映射(Program.cs):
业务代码里推送消息,注入
IHubContext<NotificationHub> 即可:JS 客户端接入
前端引
@microsoft/signalr 的浏览器包,连接代码没几行:两个注意点:
withAutomaticReconnect() 一定要加,网络抖动是常态,不加的话断线就哑巴了;另外生产环境别学我用 query string 传 userId 做身份标识,太容易被伪造,正经做法是走认证流程后在 Hub 里从 Context.User 取用户标识,这里只是演示最小闭环。横向扩展别忘了 backplane
单实例跑起来很美好,直到我把应用部署成两个实例做负载均衡,诡异的事发生了:消息有时收得到有时收不到。原因很快想明白——连接只存在于某一台服务器的内存里。用户的连接在 A 机器上,而触发推送的接口请求落在 B 机器,B 根本不知道这个连接的存在。
这是 SignalR 横向扩展的经典问题,官方给了两个解法:
- Redis backplane:
AddSignalR().AddStackExchangeRedis(连接串),所有实例通过 Redis 的发布订阅互通,任何实例发起的推送都能广播到正确的连接上。
- Azure SignalR Service:把连接管理整个托管出去,应用只负责发消息,省事但要花钱,国内项目还得考虑连通性。
我自建了 Redis,一行配置解决问题。另外记得配负载均衡的会话保持(sticky session),SignalR 协商阶段的多个请求要落到同一台机器,Nginx 里用
ip_hash 或者 cookie 都行。回头看,从轮询换到 SignalR 没有想象中复杂,官方库把传输协商、重连这些脏活都干了。真正的门槛不在写代码,在于部署形态变复杂之后,那些单机时代根本不存在的坑开始一个个冒头。
Loading...