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

如何区分Blazor Server用户真实离开与页面导航?

Blazor Server 区分用户导航与真实断开连接的方案分析

一、方案可行性

你的核心思路是可行的:页面硬导航(刷新、直接输入URL跳转)导致的Circuit重建通常在几秒内完成,而真实断开(关闭标签页、网络中断)不会有后续的新Circuit连接。通过设置5秒延迟,能有效过滤导航场景的误触发,避免把正常导航判定为用户离线。

二、需要注意的边缘情况与竞态条件

  1. 多标签页用户的状态误判
    你的方案以单个CircuitId为跟踪单位,但一个用户可能打开多个标签页(对应多个独立Circuit)。当用户关闭其中一个标签时,旧Circuit的OnConnectionDownAsync触发并启动定时器;但用户其他标签的Circuit仍处于活跃状态。此时定时器触发后,因旧CircuitId不在_activeCircuits中,会错误标记用户为离线。

  2. Timer资源泄漏风险
    如果用户频繁触发Circuit断开/连接,未正确Dispose的Timer会累积,导致内存泄漏。另外,Timer的回调执行时机可能和OnConnectionUpAsync的执行存在竞态:比如定时器回调已经开始执行,此时OnConnectionUpAsync尝试移除并Dispose Timer,可能引发线程安全问题。

  3. 自动重连场景的冲突
    Blazor Server内置自动重连机制:当网络短暂中断时,客户端会尝试复用原有Circuit重连,而非创建新Circuit。此时OnConnectionDownAsync触发后,若重连成功,不会触发OnConnectionUpAsync(因为Circuit未重建),你的定时器会错误执行离线逻辑。

三、优化方案与更优跟踪模式

1. 基于用户的活跃Circuit计数

将跟踪单位从CircuitId改为用户ID,统计每个用户的活跃Circuit数量,避免多标签页场景的误判:

private readonly ConcurrentDictionary<string, int> _userActiveCircuits = new();
private readonly ConcurrentDictionary<string, CancellationTokenSource> _disconnectCts = new();

public override Task OnConnectionDownAsync(Circuit circuit, CancellationToken cancellationToken)
{
    var userId = circuit.User.Identity?.Name;
    if (string.IsNullOrEmpty(userId)) return Task.CompletedTask;

    // 减少用户活跃Circuit计数
    if (_userActiveCircuits.TryGetValue(userId, out var count))
    {
        count = Math.Max(count - 1, 0);
        _userActiveCircuits[userId] = count;
    }

    // 仅当计数为0时启动离线延迟
    if (count == 0)
    {
        var cts = new CancellationTokenSource();
        if (_disconnectCts.TryAdd(userId, cts))
        {
            _ = Task.Delay(TimeSpan.FromSeconds(5), cts.Token)
                .ContinueWith(async _ =>
                {
                    if (_userActiveCircuits.TryGetValue(userId, out var currentCount) && currentCount == 0)
                    {
                        _userTracker.MarkUserAsOffline(circuit.User);
                    }
                    _disconnectCts.TryRemove(userId, out _);
                }, TaskScheduler.Default);
        }
    }

    return Task.CompletedTask;
}

public override Task OnConnectionUpAsync(Circuit circuit, CancellationToken cancellationToken)
{
    var userId = circuit.User.Identity?.Name;
    if (string.IsNullOrEmpty(userId)) return Task.CompletedTask;

    // 增加用户活跃Circuit计数
    _userActiveCircuits.AddOrUpdate(userId, 1, (_, count) => count + 1);

    // 取消用户的离线延迟任务
    if (_disconnectCts.TryRemove(userId, out var cts))
    {
        cts.Cancel();
        cts.Dispose();
    }

    _userTracker.MarkUserAsOnline(circuit.User);

    return Task.CompletedTask;
}

2. 用CancellationTokenSource替代Timer

相比Timer,CancellationTokenSource更可控,能避免回调执行时的竞态问题,且资源清理更明确。

3. 结合Blazor自动重连事件

通过监听客户端的重连事件(在_Host.cshtml中配置),可以更精准判断是临时断开还是永久离线:

<script src="_framework/blazor.server.js" 
        autostart="false">
</script>
<script>
    Blazor.start({
        reconnectionHandler: {
            onReconnecting: () => {
                // 通知服务器用户正在重连
            },
            onReconnected: () => {
                // 通知服务器重连成功
            },
            onConnectionFailed: () => {
                // 通知服务器重连失败,标记用户离线
            }
        }
    });
</script>

服务器端可通过SignalR Hub接收这些客户端事件,补充CircuitHandler的判断逻辑。

4. 定时清理过期状态

定期扫描_userActiveCircuits,清理长时间无活跃Circuit的用户状态,避免内存占用过高。


内容的提问来源于stack exchange,提问作者derpascal

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 03:15:21