ASP.NET Core SignalR - 部分客户端消息发送

发布于:2025-06-08 ⋅ 阅读:(21) ⋅ 点赞:(0)


前言

SignalR 提供了强大的消息发送机制,支持向特定用户、组或所有客户端广播消息。

一、消息发送的核心概念

1.客户端标识

  • 连接 ID (Context.ConnectionId):每次客户端连接时生成的唯一标识符,用于精确识别某个连接。
  • 用户标识 (Context.UserIdentifier):与身份验证关联的用户唯一标识(如数据库 ID),可关联多个连接(同一用户多设备登录)。

2.消息接收范围

  1. 所有客户端:Clients.All
  2. 特定用户:Clients.User(userId)
  3. 特定连接:Clients.Client(connectionId)
  4. 特定组:Clients.Group(groupName)
  5. 除发送者外的客户端:Clients.Others

二、向特定用户发送消息

管理员向指定用户发送私信,或用户之间一对一聊天。

  1. 代码如下(示例):
    /// <summary>
    /// 向特定用户发送消息 
    /// </summary>
    /// <param name="toUserName">接收者</param>
    /// <param name="content">发送的消息</param>
    /// <returns></returns>
    public async Task SendPrivateMsgAsync(string toUserName, string content)
    {
        try
        {
            var senderUserID = Context.UserIdentifier;
            var senderUser= await userManager.FindByIdAsync(senderUserID);
            var toUser = await userManager.FindByNameAsync(toUserName);
            await Clients.User(toUser.Id.ToString()).SendAsync("ReceivePrivateMsg", senderUser.UserName, content);
        }
        catch (Exception ex)
        {
    
            throw;
        }
        
    }
    
    1. 关键点
    • 用户标识:需通过身份验证系统获取(如 JWT 的 sub 声明)。
    • 多设备支持:同一用户的多个连接都会收到消息。

三、向组发送消息

聊天室、工作群组、通知订阅等。

  1. 代码如下(示例):

    // 在内存中缓存组信息以提高性能
    private static readonly ConcurrentDictionary<string, GroupInfo> _groups = new ConcurrentDictionary<string, GroupInfo>();
    /// <summary>
    /// 创建自定义组
    /// </summary>
    /// <param name="groupName"></param>
    /// <returns></returns>
    public async Task CreateGroup(string groupName)
    {
        long userId = Convert.ToInt64(Context.UserIdentifier);
        if (_groups.ContainsKey(groupName))
        {
            await Clients.Caller.SendAsync("GroupCreationFailed", "组已存在");
            return;
        }
    
        // 创建新组并保存到数据库
        var group = new Group
        {
            GroupName = groupName,
            CreatedAt = DateTime.UtcNow,
            CreatorId = userId
        };
    
        myDbContext.Groups.Add(group);
        await myDbContext.SaveChangesAsync();
    
        // 添加到内存缓存
        var groupInfo = new GroupInfo
        {
            GroupId = group.GroupId,
            GroupName = groupName,
            MemberIds = new HashSet<long> { userId }
        };
    
        _groups.TryAdd(groupName, groupInfo);
        // 创建者自动加入组            
        await AddUserToGroup(groupName, userId);
    
        await Clients.All.SendAsync("GroupCreated", groupName);
      
    }
    private async Task AddUserToGroup(string groupName, long userId)
    {
        try
        {
            var groupInfo = _groups[groupName];
    
            // 添加到数据库
            var groupMember = new GroupMember
            {
                GroupId = groupInfo.GroupId,
                UserId = userId,
                JoinedAt = DateTime.UtcNow
            };
    
            myDbContext.GroupMembers.Add(groupMember);
            await myDbContext.SaveChangesAsync();
        }
        catch (Exception)
        {
    
            throw;
        }
        
    }
    /// <summary>
    /// 加入自定义组
    /// </summary>
    /// <param name="groupName"></param>
    /// <returns></returns>
    public async Task JoinGroup(string groupName)
    {
        var userId = Convert.ToInt64(Context.UserIdentifier);
    
        if (!_groups.TryGetValue(groupName, out var groupInfo))
        {
            await Clients.Caller.SendAsync("JoinGroupFailed", "组不存在");
            return;
        }
    
        if (groupInfo.MemberIds.Contains(userId))
        {
            await Clients.Caller.SendAsync("JoinGroupFailed", "您已在该组中");
            return;
        }
    
        // 添加用户到组
        await AddUserToGroup(groupName, userId);
    
        // 更新内存缓存
        groupInfo.MemberIds.Add(userId);
    
        // 将用户加入 SignalR 组
        await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
    
        await Clients.Group(groupName).SendAsync("UserJoinedGroup", Context.User.Identity.Name, groupName);
        //try
        //{
        //    if (_groups.ContainsKey(groupName))
        //    {
        //        await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
        //        _groups[groupName].Add(Context.ConnectionId);
        //        await Clients.Group(groupName).SendAsync("UserJoinGroup", Context.UserIdentifier, groupName); ;
        //    }
        //}
        //catch (Exception ex)
        //{
    
        //    throw;
        //}
    
    }
    /// <summary>
    /// 用户离开自定义组
    /// </summary>
    /// <param name="groupName"></param>
    /// <returns></returns>
    public async Task LeaveGroup(string groupName)
    {
        var userId = Convert.ToInt64(Context.UserIdentifier);                
    
        if (!_groups.TryGetValue(groupName, out var groupInfo) ||
            !groupInfo.MemberIds.Contains(userId))
        {
            await Clients.Caller.SendAsync("LeaveGroupFailed", "您不在该组中");
            return;
        }
    
        // 从组中移除用户
        await RemoveUserFromGroup(groupName, userId);
    
        // 更新内存缓存
        groupInfo.MemberIds.Remove(userId);
    
        // 如果组为空,删除组
        if (groupInfo.MemberIds.Count == 0)
        {
            await DeleteGroup(groupName);
        }
        else
        {
            // 将用户移出 SignalR 组
            await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName);
            await Clients.Group(groupName).SendAsync("UserLeftGroup", Context.User.Identity.Name, groupName);
        }
        //if (_groups.ContainsKey(groupName) && _groups[groupName].Contains(Context.ConnectionId))
        //{
        //    await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName);
        //    _groups[groupName].Remove(Context.ConnectionId);
        //    await Clients.Group(groupName).SendAsync("UserLeaveGroup",Context.UserIdentifier, groupName); 
        //}
    }
    private async Task RemoveUserFromGroup(string groupName, long userId)
    {
        var groupInfo = _groups[groupName];
    
        // 从数据库移除
        var groupMember = await myDbContext.GroupMembers
            .FirstOrDefaultAsync(gm => gm.GroupId == groupInfo.GroupId && gm.UserId == userId);
    
        if (groupMember != null)
        {
            myDbContext.GroupMembers.Remove(groupMember);
            await myDbContext.SaveChangesAsync();
        }
    }
    
    private async Task DeleteGroup(string groupName)
    {
        if (_groups.TryRemove(groupName, out var groupInfo))
        {
            // 从数据库删除组
            var group = await myDbContext.Groups.FindAsync(groupInfo.GroupId);
            if (group != null)
            {
                myDbContext.Groups.Remove(group);
                await myDbContext.SaveChangesAsync();
            }
    
            await Clients.All.SendAsync("GroupDeleted", groupName);
        }
    }
    
  2. 关键点

  • 组管理:需手动维护用户与组的关系(如 JoinGroup 和 LeaveGroup)。
  • 持久化:组信息不持久化,服务器重启后需重新加入。

四、广播消息

系统公告、实时统计数据更新等。

  1. 代码示例:
    /// <summary>
    /// 向所有用户发送消息
    /// </summary>
    /// <param name="user"></param>
    /// <param name="content"></param>
    /// <returns></returns>
    [Authorize(Roles = "admin")]
    public async Task SendMessageAsync(string user, string content)
    {
        //var connectionId = this.Context.ConnectionId;
        //string msg = $"{connectionId},{DateTime.Now.ToString()}:{user}";
        await Clients.All.SendAsync("ReceiveMsg", user, content);
    }
     /// <summary>
     /// 向除发送者外的所有客户端发送消息
     /// </summary>
     /// <param name="sender"></param>
     /// <param name="content"></param>
     /// <returns></returns>
     public async Task SendOthersMsg(string sender, string content)
     {
         await Clients.Others.SendAsync("ReceiveMsg",sender, content);
     }
    

五、向角色发送消息

向管理员组发送系统警报,或向特定权限用户推送通知。

  1. 代码示例:
     /// <summary>
     /// 向管理员组AdminUsers发送消息
     /// </summary>
     /// <param name="sender"></param>
     /// <param name="content"></param>
     /// <returns></returns>
     public async Task SendAdminMsgAsync(string sender, string content)
     {
         await Clients.Group("AdminUsers").SendAsync("ReceiveAdminMsg", sender, content);
     }
    

六、客户端接收消息

JavaScript 客户端

  1. 代码示例:

    // 创建新连接
    state.connection = new signalR.HubConnectionBuilder()
         .withUrl(state.serverUrl, {
             accessTokenFactory: () => token,
             skipNegotiation: true,
             transport: signalR.HttpTransportType.WebSockets
         })
         .withAutomaticReconnect()
         .configureLogging(signalR.LogLevel.Information)
         .build();
     // 注册消息处理程序
    state.connection.on("ReceiveMsg", (user, message) => {
         state.messages.push({
             type: 'broadcast',
             sender: `${user}(广播消息)`,
             content: message,
             timestamp: new Date()
         });
     });
     
     state.connection.on("ReceivePrivateMsg", (sender, message) => {              
       if (!sender || !message) return;
         state.messages.push({
             type: 'private',
             sender: `${sender} (私信)`,
             content: message,
             timestamp: new Date()
         });
     });
     
     state.connection.on("ReceiveGroupMsg", (sender, group, message) => {
         state.messages.push({
             type: 'group',
             sender: `${sender} (${group})`,
             content: message,
             group: group,
             timestamp: new Date()
         });
     });
     ......
    // 启动连接
    await state.connection.start();
    

总结

通过以上方法,你可以灵活实现 SignalR 的部分消息发送功能,满足不同场景下的实时通信需求。


网站公告

今日签到

点亮在社区的每一天
去签到