Azure SignalR服务端超时配置与连接状态管理问题咨询
针对你的Azure SignalR问题,分点解答如下:
1. 关于OnDisconnectedAsync的触发时机
是的,当客户端在指定时间内未响应时,OnDisconnectedAsync会被触发,但需要正确配置超时参数。你当前遇到离线后仍能收到Ping的问题,是因为默认超时设置不符合你的预期。
要让服务器在30秒内未收到客户端响应时判定连接丢失,需在ConfigureServices中配置SignalR的超时选项:
services.AddSignalR(options => { // 客户端超过30秒未响应则视为断开 options.ClientTimeoutInterval = TimeSpan.FromSeconds(30); // 服务器发送Ping的间隔(建议设为ClientTimeout的1/3到1/2,确保及时检测断开) options.KeepAliveInterval = TimeSpan.FromSeconds(10); }).AddAzureSignalR(Configuration.GetSection("Values")["AzureSignalRConnectionString"]);
当客户端离线超过ClientTimeoutInterval,服务器会标记连接为断开并触发OnDisconnectedAsync。Azure SignalR服务端也会自动清理长时间无响应的连接,上述配置能进一步加快断开逻辑的触发。
2. 页面刷新导致连接堆积的解决方案
页面刷新时旧连接未触发OnDisconnectedAsync,是因为客户端直接关闭页面,没有主动向服务器发送断开请求,导致服务器误认为连接仍存活。解决办法有两个:
- 客户端主动断开:在页面的
beforeunload事件中调用SignalR连接的stop()方法,确保服务器收到断开通知:window.addEventListener('beforeunload', () => { if (connection) { connection.stop(); } }); - 缩短超时时间:结合上面的
ClientTimeoutInterval配置,让服务器更快回收僵尸连接,避免连接数达到免费层上限。
3. 更好的Hub状态维护方式
你当前使用的static Dictionary存在几个问题:线程不安全(多连接并发操作会引发异常)、多服务器部署时状态无法共享、服务器重启后状态丢失。推荐以下优化方案:
方案一:单实例部署用ConcurrentDictionary
替换static Dictionary为线程安全的ConcurrentDictionary,避免并发冲突:
// 替换原来的静态字典,确保线程安全 private static readonly ConcurrentDictionary<string, UserInMall> ConnectedUsers = new ConcurrentDictionary<string, UserInMall>(); public override async Task OnConnectedAsync() { try { var userId = Context.User.Claims.FirstOrDefault(c => c.Type == "id").Value; var mallCodename = Context.User.Claims.FirstOrDefault(c => c.Type == "mcn").Value; // 用AddOrUpdate处理并发场景:若用户已存在则更新ConnectionId(支持同一用户多设备登录) ConnectedUsers.AddOrUpdate(userId, _ => new UserInMall { ConnectionId = Context.ConnectionId, MallCodename = mallCodename }, (_, existing) => { existing.ConnectionId = Context.ConnectionId; return existing; }); var offers = await _offerService.GetUserOffersById(userId); await Clients.Client(Context.ConnectionId).SendAsync("updateOffers", offers); await base.OnConnectedAsync(); } catch (Exception e) { throw new HubException(e.Message); } } public override Task OnDisconnectedAsync(Exception exception) { var userId = Context.User.Claims.FirstOrDefault(c => c.Type == "id")?.Value; if (!string.IsNullOrEmpty(userId)) { ConnectedUsers.TryRemove(userId, out _); } return base.OnDisconnectedAsync(exception); }
方案二:多实例/分布式部署用缓存(如Azure Redis)
如果你的API需要横向扩展多实例,静态字典无法跨实例共享状态,此时需要用分布式缓存存储连接映射。例如使用Azure Redis Cache:
- 安装
StackExchange.RedisNuGet包 - 注入
IDistributedCache或Redis客户端 - 将
ConnectedUsers的操作替换为Redis的键值对操作,同时设置与ClientTimeoutInterval一致的过期时间,定期清理过期条目。
另外,SignalR本身支持通过UserIdentifier直接向指定用户发送消息,你可以在Hub配置中指定用户ID提供者,简化消息发送:
services.AddSignalR() .AddAzureSignalR(Configuration.GetSection("Values")["AzureSignalRConnectionString"]) .AddHubOptions<MallHub>(options => { // 设置用户ID为你的领域userId options.UserIdProvider = context => context.User.Claims.FirstOrDefault(c => c.Type == "id")?.Value; });
之后可以直接用Clients.User(userId).SendAsync("updateOffers", offers)发送消息,无需手动查找ConnectionId(如果不需要额外的MallCodename信息的话)。
内容的提问来源于stack exchange,提问作者Alienown

