基于Blazor+Identity的在线用户列表实现方案咨询
Blazor + Identity 实现可靠在线用户列表的解决方案
一、优化SignalR的断开检测与主动通知
浏览器关闭时的unload/beforeunload事件存在被浏览器中断的可能,SignalR默认的心跳机制也可能延迟检测到断开,可通过以下方式优化:
强化服务器端超时配置
在Program.cs中调整SignalR Hub的超时与心跳参数,让服务器更快识别断开的客户端:builder.Services.AddSignalR() .AddHubOptions<OnlineUserHub>(options => { options.ClientTimeoutInterval = TimeSpan.FromSeconds(30); // 客户端30秒无响应则判定断开 options.KeepAliveInterval = TimeSpan.FromSeconds(10); // 每10秒发送心跳包 });客户端主动发送断开信号
利用navigator.sendBeacon(浏览器支持的可靠后台发送API)在页面卸载时通知服务器,避免异步操作被中断:- 在Blazor组件的
OnAfterRenderAsync中注册卸载监听:protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { var userId = (await _userManager.GetUserAsync(User))?.Id; await JS.InvokeVoidAsync("registerUnloadDisconnect", userId); } } - 在
wwwroot/js/site.js中添加JS逻辑:function registerUnloadDisconnect(userId) { window.addEventListener('beforeunload', () => { const data = new Blob([JSON.stringify({ userId: userId })], { type: 'application/json' }); navigator.sendBeacon('/api/online-users/disconnect', data); }); } - 后端新增API接口处理断开请求:
[ApiController] [Route("api/online-users")] public class OnlineUsersController : ControllerBase { private readonly UserManager<IdentityUser> _userManager; public OnlineUsersController(UserManager<IdentityUser> userManager) { _userManager = userManager; } [HttpPost("disconnect")] public async Task<IActionResult> Disconnect([FromBody] UserDisconnectRequest request) { var user = await _userManager.FindByIdAsync(request.UserId); if (user != null) { // 示例:更新用户最后活跃时间为超时阈值之前 user.LastActiveTime = DateTime.UtcNow.AddMinutes(-1); await _userManager.UpdateAsync(user); } return Ok(); } public class UserDisconnectRequest { public string UserId { get; set; } } }
- 在Blazor组件的
二、基于数据库的心跳与定时清理机制
通过数据库记录用户活跃状态,结合定时任务清理离线用户,是最可靠的兜底方案:
扩展用户状态存储
给IdentityUser添加LastActiveTime字段(或单独创建UserOnlineStatus表):public class ApplicationUser : IdentityUser { public DateTime LastActiveTime { get; set; } = DateTime.UtcNow; }客户端定时发送心跳
在Blazor组件中使用定时器,每隔10秒更新用户最后活跃时间:private Timer _heartbeatTimer; protected override async Task OnInitializedAsync() { await UpdateLastActive(); _heartbeatTimer = new Timer(async _ => await UpdateLastActive(), null, TimeSpan.Zero, TimeSpan.FromSeconds(10)); } private async Task UpdateLastActive() { var user = await _userManager.GetUserAsync(User); if (user != null) { user.LastActiveTime = DateTime.UtcNow; await _userManager.UpdateAsync(user); } } public void Dispose() { _heartbeatTimer?.Dispose(); }服务器端定时清理离线用户
创建后台服务,每隔1分钟扫描数据库,将超过30秒未活跃的用户标记为离线:public class OnlineUserCleanupService : BackgroundService { private readonly IServiceScopeFactory _scopeFactory; private readonly TimeSpan _offlineThreshold = TimeSpan.FromSeconds(30); public OnlineUserCleanupService(IServiceScopeFactory scopeFactory) { _scopeFactory = scopeFactory; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { using var scope = _scopeFactory.CreateScope(); var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>(); var offlineUsers = await userManager.Users .Where(u => DateTime.UtcNow - u.LastActiveTime > _offlineThreshold) .ToListAsync(stoppingToken); foreach (var user in offlineUsers) { // 可根据需求更新状态,比如添加IsOnline字段 // user.IsOnline = false; await userManager.UpdateAsync(user); } await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken); } } }在
Program.cs中注册后台服务:builder.Services.AddHostedService<OnlineUserCleanupService>();
三、Blazor Server专属:利用Circuit状态跟踪
Blazor Server的每个用户会话对应一个Circuit,可通过ICircuitHandler监听Circuit的开闭事件:
- 实现Circuit处理器:
在public class OnlineCircuitHandler : CircuitHandler { private readonly IServiceScopeFactory _scopeFactory; public OnlineCircuitHandler(IServiceScopeFactory scopeFactory) { _scopeFactory = scopeFactory; } public override async Task OnCircuitOpenedAsync(Circuit circuit, CancellationToken cancellationToken) { using var scope = _scopeFactory.CreateScope(); var httpContextAccessor = scope.ServiceProvider.GetRequiredService<IHttpContextAccessor>(); var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>(); var user = await userManager.GetUserAsync(httpContextAccessor.HttpContext.User); if (user != null) { user.LastActiveTime = DateTime.UtcNow; await userManager.UpdateAsync(user); } await base.OnCircuitOpenedAsync(circuit, cancellationToken); } public override async Task OnCircuitClosedAsync(Circuit circuit, CancellationToken cancellationToken) { using var scope = _scopeFactory.CreateScope(); var httpContextAccessor = scope.ServiceProvider.GetRequiredService<IHttpContextAccessor>(); var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>(); var user = await userManager.GetUserAsync(httpContextAccessor.HttpContext.User); if (user != null) { user.LastActiveTime = DateTime.UtcNow.AddMinutes(-1); await userManager.UpdateAsync(user); } await base.OnCircuitClosedAsync(circuit, cancellationToken); } }Program.cs中注册:builder.Services.AddScoped<CircuitHandler, OnlineCircuitHandler>();
方案组合建议
实际项目中推荐SignalR主动断开+数据库心跳+定时清理的组合方案:
- 用SignalR实现在线列表的实时推送
- 客户端通过心跳和主动断开保证状态及时更新
- 服务器端定时清理作为兜底,处理浏览器崩溃、网络异常等极端情况
内容的提问来源于stack exchange,提问作者Chardeux
相关产品推荐
相关产品推荐

