SignalR Hub中Clients调用触发System.ObjectDisposedException异常排查
首先,我们来拆解你遇到的System.ObjectDisposedException异常原因,然后一步步修复并完善你的ChatHub实现。
一、异常核心原因分析
你的代码里有几个关键问题会导致这个异常,以及在线状态管理的逻辑漏洞:
线程不安全的在线用户列表
你使用了静态List<string> online_users来管理在线用户,但List<T>不是线程安全的集合。当多个用户同时连接/断开时,会出现竞态条件:比如用户已经离线,但列表里还残留着用户名,导致你判断用户在线(check3 = online_users.Contains(recieverId)),但实际该用户的所有连接已经断开,此时调用Clients.Group(recieverId).RecievedMessage就会尝试操作已释放的连接对象,触发ObjectDisposedException。异步操作未等待(await)
- 在
OnConnectedAsync和OnDisconnectedAsync中,Groups.AddToGroupAsync和Groups.RemoveFromGroupAsync都是异步方法,但你没有使用await等待操作完成。这会导致组的状态更新不及时,比如用户还没加入组就尝试发消息,或者用户已经断开但组里还残留连接,后续操作时就会访问已释放的资源。 fetchPendingMessages中调用SendMessageAsync时没有await,会导致消息发送逻辑未完成就继续执行,可能引发资源释放问题。
- 在
错误的方法返回类型
OnConnectedAsync的返回类型被错误定义为Task<Task>,正确的重写应该是Task。这个错误会导致异步执行顺序混乱,Hub的初始化逻辑可能还没完成就处理后续请求。冗余的组管理
其实SignalR已经内置了基于用户身份的连接管理,你不需要手动给每个用户创建同名组。使用Clients.User(recieverId)可以直接向该用户的所有在线连接发送消息,比手动维护组更可靠。
二、修复后的完整ChatHub实现
下面是修复并优化后的代码,解决了异常问题,同时完善了消息推送逻辑:
using IdentityCore.DbLayer.Entity; using IdentityCore.Interface; using IdentityCore.Services.Contracts; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.SignalR; using System.Collections.Concurrent; using System.Diagnostics; namespace IdentityCore.Hubs { [Authorize] public class ChatHub : Hub<IChatHub> { // 使用线程安全的集合存储用户的连接ID集合(支持多设备登录) private static readonly ConcurrentDictionary<string, HashSet<string>> _userConnections = new(); private readonly IUserContract _userContract; public ChatHub(IUserContract userContract) { _userContract = userContract; } // 修复返回类型为Task,并正确await所有异步操作 public override async Task OnConnectedAsync() { var userName = Context.User.Identity.Name; var connectionId = Context.ConnectionId; // 线程安全地添加用户连接 _userConnections.AddOrUpdate( userName, _ => new HashSet<string> { connectionId }, (_, existingConnections) => { lock (existingConnections) { existingConnections.Add(connectionId); return existingConnections; } }); // 拉取离线消息 await FetchPendingMessages(); await base.OnConnectedAsync(); } public override async Task OnDisconnectedAsync(Exception? exception) { var userName = Context.User.Identity.Name; var connectionId = Context.ConnectionId; // 线程安全地移除用户连接 if (_userConnections.TryGetValue(userName, out var connections)) { lock (connections) { connections.Remove(connectionId); if (connections.Count == 0) { _userConnections.TryRemove(userName, out _); } } } await base.OnDisconnectedAsync(exception); } public void Greetings() { Clients.Caller.DisplayGreeting($"You are now connected with connectionId:- {Context.ConnectionId}"); } public async Task FetchPendingMessages() { var userName = Context.User.Identity.Name; var pendingMessages = await _userContract.GetTempMessages(userName); foreach (var message in pendingMessages) { // 等待消息发送完成,避免资源泄漏 await SendMessageAsync(message.RecieverId, message.SenderId, message.SenderUsername, message.Message); // 可选:发送完成后从数据库删除离线消息 await _userContract.RemoveTempMessage(message.Id); } } public async Task SendMessageAsync(string recieverId, string senderId, string senderUsername, string message) { // 验证发送方和接收方存在 var receiverExists = await _userContract.CheckIfUserExists(recieverId); var senderExists = await _userContract.CheckIfUserExists(senderId); if (!receiverExists || !senderExists) { // 可选:通知发送方用户不存在 await Clients.Caller.DisplayError("Target user does not exist."); return; } // 判断接收方是否在线(通过连接集合是否存在) var isReceiverOnline = _userConnections.ContainsKey(recieverId); if (isReceiverOnline) { // 使用SignalR内置的用户发送方法,自动推送到该用户所有在线连接 await Clients.User(recieverId).RecievedMessage(senderId, senderUsername, message); } else { // 离线用户存储消息到数据库 await _userContract.AddTempMessage(recieverId, senderId, senderUsername, message); } } } }
三、关键优化点说明
线程安全的连接管理
使用ConcurrentDictionary<string, HashSet<string>>存储每个用户的所有连接ID,确保多线程环境下的安全操作,同时支持用户多设备在线的场景。移除冗余的组操作
利用SignalR的Clients.User(userId)方法,直接向目标用户的所有在线连接发送消息,无需手动维护组,减少出错概率。正确处理异步操作
所有异步方法都添加了await,确保操作顺序正确,避免资源未初始化或已释放的问题。完善离线消息处理
在拉取离线消息后,建议删除已推送的消息(避免重复推送),并添加了用户不存在的错误处理。
四、额外注意事项
- 确保你的
IChatHub接口中正确定义了RecievedMessage、DisplayGreeting、DisplayError等客户端方法,客户端代码要实现这些方法来接收消息。 - 如果你的用户ID和用户名不是同一个值,要注意在
Context.User中获取正确的用户ID(比如通过Context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value),而不是Identity.Name。 - 生产环境中,建议使用分布式缓存(如Redis)来存储用户连接信息,避免应用重启后丢失在线状态。
内容的提问来源于stack exchange,提问作者Smarpan Sharma

