You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何检测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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.09 16:13:15