如何区分Blazor Server用户真实离开与页面导航?
一、方案可行性
你的核心思路是可行的:页面硬导航(刷新、直接输入URL跳转)导致的Circuit重建通常在几秒内完成,而真实断开(关闭标签页、网络中断)不会有后续的新Circuit连接。通过设置5秒延迟,能有效过滤导航场景的误触发,避免把正常导航判定为用户离线。
二、需要注意的边缘情况与竞态条件
多标签页用户的状态误判
你的方案以单个CircuitId为跟踪单位,但一个用户可能打开多个标签页(对应多个独立Circuit)。当用户关闭其中一个标签时,旧Circuit的OnConnectionDownAsync触发并启动定时器;但用户其他标签的Circuit仍处于活跃状态。此时定时器触发后,因旧CircuitId不在_activeCircuits中,会错误标记用户为离线。Timer资源泄漏风险
如果用户频繁触发Circuit断开/连接,未正确Dispose的Timer会累积,导致内存泄漏。另外,Timer的回调执行时机可能和OnConnectionUpAsync的执行存在竞态:比如定时器回调已经开始执行,此时OnConnectionUpAsync尝试移除并Dispose Timer,可能引发线程安全问题。自动重连场景的冲突
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

