SignalR 实现站内实时通知:从轮询到长连接

Words: 1073Read Time: 3 minLast edited: 2026-8-4

从轮询的痛点说起

我做的后台系统需要站内通知功能:审核结果、订单状态变更这类消息要实时推给在线用户。第一版图省事用的轮询——前端每 10 秒打一次接口查未读消息。
功能是能跑,但问题很快暴露:几十个人在线,服务器每 10 秒就收到一波请求,九成九的响应都是「没有新消息」,纯粹空转;用户体验也不好,消息延迟最多 10 秒,急脾气的同事直接在群里喊「我的单子到底过了没」。于是下决心换长连接方案,选了 SignalR——自家技术栈,不用白不用。

SignalR 的传输协商

SignalR 最省心的地方是传输层自动协商,不用自己操心兼容性。它会按优先级依次尝试:
  1. WebSocket:首选,全双工、开销最小
  1. Server-Sent Events:WebSocket 不可用时的降级(比如某些代理掐了 WS)
  1. 长轮询(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 横向扩展的经典问题,官方给了两个解法:
  1. Redis backplaneAddSignalR().AddStackExchangeRedis(连接串),所有实例通过 Redis 的发布订阅互通,任何实例发起的推送都能广播到正确的连接上。
  1. Azure SignalR Service:把连接管理整个托管出去,应用只负责发消息,省事但要花钱,国内项目还得考虑连通性。
我自建了 Redis,一行配置解决问题。另外记得配负载均衡的会话保持(sticky session),SignalR 协商阶段的多个请求要落到同一台机器,Nginx 里用 ip_hash 或者 cookie 都行。
回头看,从轮询换到 SignalR 没有想象中复杂,官方库把传输协商、重连这些脏活都干了。真正的门槛不在写代码,在于部署形态变复杂之后,那些单机时代根本不存在的坑开始一个个冒头。
Loading...
© 2024 - 2026 ihuadz
中文