.NET Core中SignalR如何按客户端区分外部调用?
解决SignalR控制器根据用户名定向推送消息的问题
核心思路是维护用户名与ConnectionId的映射关系,因为外部服务仅提供用户名,我们需要通过这个映射找到对应客户端的连接ID,再实现定向推送。以下是具体实现步骤:
1. 在Hub中维护用户名-ConnectionId映射
Hub是处理客户端连接的核心,我们可以在这里统一管理用户的连接信息,用线程安全的ConcurrentDictionary存储映射:
public class ChatHub : Hub { // 全局存储用户名与连接ID的映射,多线程环境下安全 private static readonly ConcurrentDictionary<string, string> _userConnectionMap = new ConcurrentDictionary<string, string>(); // 客户端连接成功后,主动调用这个方法注册用户名 public async Task RegisterUser(string username) { // 先移除该用户名旧的连接(比如用户重新登录) if (_userConnectionMap.TryGetValue(username, out var oldConnId)) { _userConnectionMap.TryRemove(username, out _); } // 添加新的连接映射 _userConnectionMap.TryAdd(username, Context.ConnectionId); await Clients.Caller.SendAsync("RegisterSuccess"); } // 用户断开连接时,清除对应的映射 public override async Task OnDisconnectedAsync(Exception exception) { // 找到当前连接ID对应的用户名并移除 var targetUser = _userConnectionMap.FirstOrDefault(kv => kv.Value == Context.ConnectionId); if (!string.IsNullOrEmpty(targetUser.Key)) { _userConnectionMap.TryRemove(targetUser.Key, out _); } await base.OnDisconnectedAsync(exception); } // 提供给控制器的方法:通过用户名获取连接ID public static string GetConnectionIdByUsername(string username) { _userConnectionMap.TryGetValue(username, out var connId); return connId; } }
2. 客户端连接后注册用户名
页面加载完成并建立SignalR连接后,调用Hub的RegisterUser方法,将当前用户的用户名传递给服务端,完成映射注册:
// 初始化SignalR连接 const connection = new signalR.HubConnectionBuilder() .withUrl("/chatHub") .build(); // 连接成功后注册用户名 connection.start().then(() => { // 从页面获取用户名(比如登录后存在页面的隐藏元素或全局变量) const username = document.querySelector("#current-username").textContent; connection.invoke("RegisterUser", username) .catch(err => console.error(err.toString())); }).catch(err => console.error(err.toString()));
3. 控制器中根据用户名定向推送
在控制器中注入IHubContext<ChatHub>,通过Hub的静态方法获取目标用户的连接ID,再调用Clients.Client发送消息:
[ApiController] [Route("api/external-messages")] public class ExternalMessagesController : ControllerBase { private readonly IHubContext<ChatHub> _hubContext; public ExternalMessagesController(IHubContext<ChatHub> hubContext) { _hubContext = hubContext; } [HttpPost] public async Task<IActionResult> ReceiveMessage([FromBody] ExternalMessageDto messageDto) { // 根据用户名获取对应连接ID var targetConnId = ChatHub.GetConnectionIdByUsername(messageDto.Username); if (!string.IsNullOrEmpty(targetConnId)) { // 给指定客户端推送消息 await _hubContext.Clients.Client(targetConnId) .SendAsync("ReceiveExternalMessage", messageDto.Content); return Ok(); } // 用户未在线或未注册连接 return NotFound($"用户 {messageDto.Username} 当前未在线"); } } // 外部服务传递的消息模型 public class ExternalMessageDto { public string Username { get; set; } public string Content { get; set; } }
补充说明
如果你的服务是多服务器部署(负载均衡场景),内存中的ConcurrentDictionary无法跨服务器共享,此时需要用**分布式缓存(如Redis)**替代内存字典,确保所有服务器能获取到统一的用户名-连接ID映射。
内容的提问来源于stack exchange,提问作者Guram Mikiashvili
相关产品推荐
相关产品推荐

