.NET 8 SignalR SendAsync长时间连接后挂起,前端仍可接收消息
.NET 8 SignalR SendAsync 长时间连接后挂起问题排查与修复
可能的原因
- 连接目标调用错误:你代码中使用
Clients.User(<connection id>)是错误的——User方法是根据用户标识(如Claims中的用户ID)筛选连接,而非直接使用连接ID。误用会导致服务端无法定位到正确的连接,长时间后出现挂起。 - 长轮询连接会话不同步:LongPolling模式下客户端会定期发起新请求,若服务端未及时更新连接映射,使用旧连接ID发送消息时,服务端会持续等待已失效连接的响应,导致SendAsync挂起。此时前端仍能接收消息,说明新连接已建立,但服务端的连接ID映射未同步。
- 消息队列阻塞:长时间运行后,针对无效连接的消息堆积,SendAsync等待消息被处理,但旧连接无法确认消息,导致队列阻塞。
- 超时配置不合理:默认的SignalR长轮询超时设置可能过长,导致连接处于半开状态,服务端误判连接存活,持续等待客户端响应。
诊断方法(不使用取消令牌)
- 修正调用方式并验证:先将
Clients.User(<connection id>)改为Clients.Client(<connection id>),观察问题是否消失——这是最可能的直接修复点。 - 监控活跃连接状态:在Hub的
OnConnectedAsync/OnDisconnectedAsync中维护连接映射表,SendAsync前后打印当前活跃连接ID,确认使用的连接ID是否仍在活跃列表:private static readonly HashSet<string> _activeConnections = new HashSet<string>(); public override async Task OnConnectedAsync() { lock (_activeConnections) { _activeConnections.Add(Context.ConnectionId); } await base.OnConnectedAsync(); } public override async Task OnDisconnectedAsync(Exception exception) { lock (_activeConnections) { _activeConnections.Remove(Context.ConnectionId); } await base.OnDisconnectedAsync(exception); } // 在SendAsync前检查 if (!_activeConnections.Contains(targetConnectionId)) { // 记录日志:目标连接已失效 return; } await hub.Clients.Client(targetConnectionId).SendAsync(message, data); - 启用SignalR详细日志:在服务端
appsettings.json中配置日志级别,查看SendAsync执行时的细节:
日志会显示消息发送的目标连接、是否等待响应、连接状态等信息,帮助定位阻塞点。"Logging": { "LogLevel": { "Microsoft.AspNetCore.SignalR": "Debug", "Microsoft.AspNetCore.Http.Connections": "Debug" } } - 前端连接状态日志:在前端添加连接事件监听,记录重连、断开事件及新连接ID,对比服务端使用的ID是否匹配:
this.connectionWebService.onreconnecting((error) => { console.log(`Reconnecting: ${error}`); }); this.connectionWebService.onreconnected((connectionId) => { console.log(`Reconnected with new ID: ${connectionId}`); // 可将新连接ID同步到服务端 }); this.connectionWebService.onclose((error) => { console.log(`Connection closed: ${error}`); });
修复方案
- 使用正确的客户端调用方式:始终用
Clients.Client(connectionId)向指定连接ID发送消息,Clients.User(userId)用于向某用户的所有连接发送消息。 - 维护准确的连接映射:通过Hub的连接事件维护用户与活跃连接ID的映射,确保发送时使用的是当前有效的连接ID(参考诊断部分的映射表代码)。
- 调整超时配置:在服务端和前端同步调整SignalR超时参数,避免半开连接:
服务端(Program.cs):
前端:builder.Services.AddSignalR(options => { options.ClientTimeoutInterval = TimeSpan.FromMinutes(5); // 客户端无响应超时 options.KeepAliveInterval = TimeSpan.FromMinutes(2); // 服务端发送心跳间隔 });this.connectionWebService.serverTimeoutInMilliseconds = 300000; // 5分钟 this.connectionWebService.keepAliveIntervalInMilliseconds = 120000; // 2分钟 - 定期清理无效连接:通过定时任务或连接事件清理映射表中的无效连接ID,避免向已断开的连接发送消息。
内容的提问来源于stack exchange,提问作者Haim Rapoport
相关产品推荐
相关产品推荐

