SignalR实时聊天应用中如何为每个用户存储客户端数据?
我开发了一款聊天应用,用户登录后需要保存其ID。常规Web应用里会用JavaScript的localStorage存储,但多用户共用同一浏览器时,localStorage里的数据会被互相覆盖——比如ID为01的用户登录后用localStorage.setItem("userId")保存ID,之后ID为11的用户登录并保存,"userId"的值就会变成11。
我想知道有没有办法为每个用户存储对应的数据?目前想到的是服务器端存储:用户登录时把客户端的connectionId加入以userId命名的组,同时用字典以connectionId为键存储用户数据,代码如下:
public async Task Login(int userId) { var userIdStr = userId.ToString(); await Groups.AddToGroupAsync(Context.ConnectionId, userIdStr); _tempDb.connection[Context.ConnectionId] = userIdStr; } public async Task GetUserInfo() { if( _tempDb.connection.TryGetValue(Context.ConnectionId, out string userId)) { await Clients.Client(Context.ConnectionId).SendAsync("GetUserInfo", userId.ToString()); }else { Console.WriteLine("ConnectionId doesnt exist"); } }
但这样每次需要用户信息时都得调用这个方法,有没有更优的实现方式?
优化方案
1. 用SignalR自带的Context.Items存连接级数据
SignalR的HubCallerContext自带Items字典,专门存储当前连接的上下文数据,生命周期和连接绑定,比自己维护内存字典更省心:
public async Task Login(int userId) { var userIdStr = userId.ToString(); await Groups.AddToGroupAsync(Context.ConnectionId, userIdStr); // 直接把用户ID存入当前连接的上下文 Context.Items["UserId"] = userIdStr; } // 后续任何Hub方法里直接取,不用额外调用接口 public async Task SendMessage(string content) { if (Context.Items.TryGetValue("UserId", out var userIdObj) && userIdObj is string userId) { await Clients.All.SendAsync("ReceiveMessage", userId, content); } }
2. 连接建立时自动关联用户ID
如果用户登录状态是通过Cookie、JWT这类方式维护的,可以在连接建立阶段(重写OnConnectedAsync)自动解析用户ID并完成关联,不用等用户调用Login方法:
public override async Task OnConnectedAsync() { // 示例:从JWT Claims里取用户ID,实际根据你的认证逻辑调整 var userId = Context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value; if (!string.IsNullOrEmpty(userId)) { await Groups.AddToGroupAsync(Context.ConnectionId, userId); Context.Items["UserId"] = userId; } await base.OnConnectedAsync(); }
这样用户一连接就自动绑定ID,后续所有业务方法都能直接用,省掉手动登录的步骤。
3. 持久化存储替代内存字典
如果你的应用需要重启后保留连接和用户的关联(内存字典重启就清空),可以用Redis或数据库做持久化存储。比如用Redis的Hash结构存映射,同时设置过期时间匹配连接超时:
// 注入Redis客户端 private readonly IDatabase _redisDb; public async Task Login(int userId) { var userIdStr = userId.ToString(); await Groups.AddToGroupAsync(Context.ConnectionId, userIdStr); // 存入Redis并设置过期时间 await _redisDb.HashSetAsync("ConnectionUserMap", Context.ConnectionId, userIdStr); await _redisDb.KeyExpireAsync("ConnectionUserMap", TimeSpan.FromMinutes(30)); } // 获取时直接从Redis读 public async Task GetUserInfo() { var userId = await _redisDb.HashGetAsync("ConnectionUserMap", Context.ConnectionId); if (!userId.IsNullOrEmpty) { await Clients.Client(Context.ConnectionId).SendAsync("GetUserInfo", userId); } }
4. 客户端辅助方案(仅单标签页场景)
如果要在客户端存,sessionStorage比localStorage更合适——它的生命周期是当前标签页,关闭就清空,不会被同一浏览器的其他标签页用户覆盖。但同一标签页切换用户还是会有问题,只能作为辅助:
// 登录时存入sessionStorage sessionStorage.setItem("userId", "01"); // 获取时 const userId = sessionStorage.getItem("userId");
核心还是服务器端的关联更可靠,客户端存储容易被篡改或意外覆盖。
内容的提问来源于stack exchange,提问作者jpesa

