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

文章目录


前言

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. 代码如下(示例):

    csharp 复制代码
    /// <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. 代码如下(示例):

    csharp 复制代码
    // 在内存中缓存组信息以提高性能
    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. 代码示例:

    csharp 复制代码
    /// <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. 代码示例:

    csharp 复制代码
     /// <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. 代码示例:

    csharp 复制代码
    // 创建新连接
    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 的部分消息发送功能,满足不同场景下的实时通信需求。

相关推荐
颜酱5 分钟前
# 02 | 搭骨架:用 LangGraph 编排 12 步工作流(思路)
前端·人工智能·后端
颜酱6 分钟前
02 | 搭骨架:用 LangGraph 编排 12 步工作流
前端·人工智能·后端
嘉伟桑1 小时前
Python调用电价API返回JSON示例:分时电价、现货电价和字段解析
后端
武子康1 小时前
Shippy:确定性工具、会话级 Sandbox 与 Live-Data Eval(4 类收敛 + 7 步实现方案 + 6 类评测指标)
前端·人工智能·后端
我叫黑大帅2 小时前
我为什么单一消费者的场景下,要用 Redis List 当消息队列?
redis·后端·面试
AskHarries2 小时前
文件上传系统
后端
止语Lab3 小时前
好的 DX 不等于少写代码——三种语言的摩擦力设计课
后端
吃饱了得干活3 小时前
别再手动解析 LLM 输出了!LangChain 四种结构化输出方案对比
后端·python·langchain
程序员天天困3 小时前
Arthas trace 命令怎么用?一行定位最慢那行代码
jvm·后端
Huiturn3 小时前
GPT 5.6 连续编码 10 小时,纯 Python 啃下 Word 二进制格式——doc2docx 实现拆解
后端