如何检测SignalR连接是否主动关闭?适配服务端v2.4.1与客户端v1.1.4
解决SignalR主动断开与网络断开的区分问题,及刷新/关闭页面的识别
首先得先理清你当前的版本情况:你服务端用的是传统ASP.NET SignalR(Microsoft.AspNet.SignalR.Core 2.4.1),但客户端用的是ASP.NET Core SignalR的早期客户端(@aspnet/signalr ^1.1.4),这种跨版本搭配可能会导致OnDisconnectedAsync的Exception参数始终为空——这是因为两个版本的断开通知逻辑不一样。不过没关系,我们可以用客户端主动标记+服务端状态追踪的方案来解决你的问题。
一、区分主动断开与网络异常断开
因为服务端的异常参数不可靠,最稳妥的方式是让客户端在主动断开前给服务端发一个“预告”,服务端记录这个状态后,在断开回调里判断。
1. 客户端代码(JS)
在主动调用断开、页面刷新/关闭前,先给服务端发送一个标记请求:
const connection = new signalR.HubConnectionBuilder() .withUrl("/yourHubPath") .build(); // 主动调用stop时的标记 async function manualStop() { // 先告诉服务端我要主动断开 await connection.invoke("MarkAsActiveDisconnect"); await connection.stop(); } // 页面刷新/关闭时的标记(处理浏览器原生操作) window.addEventListener("beforeunload", async (event) => { try { // 用keepalive确保请求能在页面卸载时发送完成 await connection.invoke("MarkAsActiveDisconnect", { keepalive: true }); } catch (err) { console.error("发送主动断开标记失败:", err); } });
2. 服务端代码(C#)
用线程安全的字典存储主动断开的连接ID,在断开回调里校验状态:
// 存储主动断开的连接ID,确保多线程安全 private static readonly ConcurrentDictionary<string, bool> _activeDisconnectFlags = new ConcurrentDictionary<string, bool>(); // 接收客户端的主动断开标记 public async Task MarkAsActiveDisconnect() { _activeDisconnectFlags.TryAdd(Context.ConnectionId, true); } public override async Task OnDisconnectedAsync(Exception exception) { var user = users.SingleOrDefault(u => u.ConnectionId == Context.ConnectionId); if (user == null) { await base.OnDisconnectedAsync(exception); return; } // 检查是否是主动断开:如果能从字典移除成功,说明是主动标记过的 bool isActiveDisconnect = _activeDisconnectFlags.TryRemove(Context.ConnectionId, out _); if (isActiveDisconnect) { // 主动断开:执行移除和注销逻辑 users.Remove(user); ParticipantController.Logout(user.UserId); } // 否则是网络异常断开:可以根据业务逻辑选择保留用户一段时间,或者不处理 await base.OnDisconnectedAsync(exception); }
二、区分刷新页面与关闭标签页/窗口
浏览器的beforeunload事件在刷新、关闭标签页、关闭窗口时都会触发,没有原生API直接区分,但我们可以通过服务端追踪用户后续连接状态来间接判断:
实现思路
当收到主动断开标记后,延迟几秒(比如5秒)检查该用户是否有新的连接建立:
- 如果有新连接,说明是刷新页面(用户断开后马上重新连接)
- 如果没有新连接,说明是关闭标签页/窗口(用户没有再建立连接)
修改后的服务端代码
// 存储用户的最新连接信息:键为UserId,值为(连接ID, 连接时间) private static readonly ConcurrentDictionary<string, (string ConnectionId, DateTime ConnectTime)> _latestUserConnections = new ConcurrentDictionary<string, (string, DateTime)>(); // 连接建立时更新用户的最新连接信息 public override async Task OnConnectedAsync() { var user = /* 从Context获取当前用户,比如Context.User.Identity.Name */; if (user != null) { _latestUserConnections.AddOrUpdate( user.UserId, (Context.ConnectionId, DateTime.Now), (key, oldValue) => (Context.ConnectionId, DateTime.Now) ); } await base.OnConnectedAsync(); } public override async Task OnDisconnectedAsync(Exception exception) { var user = users.SingleOrDefault(u => u.ConnectionId == Context.ConnectionId); if (user == null) { await base.OnDisconnectedAsync(exception); return; } bool isActiveDisconnect = _activeDisconnectFlags.TryRemove(Context.ConnectionId, out _); if (isActiveDisconnect) { // 延迟5秒检查是否有新连接 await Task.Delay(5000); // 检查该用户的最新连接是否还是当前断开的这个ID if (_latestUserConnections.TryGetValue(user.UserId, out var connectionInfo) && connectionInfo.ConnectionId == Context.ConnectionId) { // 没有新连接,说明是关闭标签页/窗口,执行注销 users.Remove(user); ParticipantController.Logout(user.UserId); } // 否则是刷新页面,不执行注销逻辑 } await base.OnDisconnectedAsync(exception); }
额外提醒:版本兼容性问题
你当前的服务端是传统ASP.NET SignalR,客户端是ASP.NET Core SignalR客户端,两者属于不同的实现,长期使用可能会有隐藏的兼容性问题。建议尽量版本匹配:
- 如果保留传统SignalR服务端,客户端改用
jquery.signalR(对应2.4.1版本) - 如果升级到ASP.NET Core SignalR,服务端改用
Microsoft.AspNetCore.SignalR,客户端改用@microsoft/signalr最新版本
内容的提问来源于stack exchange,提问作者srzsanti
相关产品推荐
相关产品推荐

