在很多业务场景中,我们一般都是向部分客户端推送消息,比如Web聊天室中的私聊、OA系统中向请假申请人推送审批结果。
在我们进行客户端筛选的时候,有3个筛选参数;
ConnectionId、组和用户ID。
ConnectionId是SignalR为每个连接分配的唯一标识,我们可以通过集线器的Context属性中的ConnectionId属性获取当前连接的ConnectionId;
每个组有唯一的名字,对于连接到同一个集线器的客户端,我们可以把它们分组;
用户ID是登录用户的ID,它对应的是类型为ClaimTypes.NameIdentifier的Claim的值,如果使用用户ID进行筛选,我们需要在客户端登录的时候设定类型为ClaimTypes.NameIdentifier的Claim。
Hub类的Groups属性为IGoupManager类型,它可以用于对组成员进行管理,IGroupManager类包含下图的方法:
![](https://ichiblog.cn/wp-content/uploads/2025/01/图片-11.png)
我们在把连接加入组中的时候,如果指定名字的组不存在,SignalR会自动创建组。
因为连接和组的关系是通过ConnectionId建立的,所以客户端重连之后,我们就需要把连接重新加入组。
Hub类的Clients属性为IHubCallerClients类型,它可以用来对连接到当前集线器的客户端进行筛选。
IHubCallerClients类包含如下图所示的成员:
![](https://ichiblog.cn/wp-content/uploads/2025/01/图片-12.png)
这些成员的属性值、返回值都是IClientProxy类型的,我们可以通过IClientProxy向筛选到的客户端发送消息。
IClientProxy类型中只定义了一个用来向客户端发送消息的SendCoreAsync方法,我们调用的SendAsync方法是用来简化SendCoreAsync调用的扩展方法。
基于性能、准确度等的考虑,我们并不能获得筛选到的每一个客户端的信息,只能向筛选到的客户端推送消息。
了解了在SignalR中对客户端进行筛选的方式后,下面我们来为之前编写的Web聊天室增加”发送私聊消息”的功能。
第1步:
我们在ChatRoomHub中增加一个发送私聊消息的SendPrivateMessage方法,如以下代码所示:
[Authorize]
public class ChatRoomHub(UserManager<User> _manager):Hub
{
public Task SendPublicMessage(string message)
{
//获取唯一标识
string connId = this.Context.ConnectionId;
string receivePublicMessage = $"{connId} {DateTime.Now}:{message}";
//发送消息
return Clients.All.SendAsync("ReceivePublicMessage", receivePublicMessage);
}
public async Task<string> SendPrivateMessage(string destUserName,string message)
{
//destUserName参数为私聊目标的用户名,message参数为私聊的信息
User? destUser = await _manager.FindByNameAsync(destUserName);
if (destUser == null) return "DestUserNotFround";
//获取用户名ID
string destUserId = destUser.Id!;
string srcUserName = this.Context.User!.FindFirst(ClaimTypes.Name)!.Value;
string time = DateTime.Now.ToShortTimeString();
//使用用户的ID来过滤目标客户端,向客户端发送ReceivePrivateMessage消息,
await this.Clients.User(destUserId).SendAsync("ReceivePrivateMessage",srcUserName,time,message);
return "OK";
}
}
在之前的做法中,我们把发送消息者的ConnectionId发送到聊天室中,这样所有聊天室中的用户都可以看到其他人的ConnectionId,这样是很不安全的,这样写只是一个简单演示而已。
基于安全考虑,在正式的项目中,ConnectionId不能被泄露给除了服务器和当前客户端之外的第三方。
因此,这里目标用户的参数没有选择用ConnectionId,而是使用用户名来代替。
在上面代码中,我们使用User方法根据用户名获取目标客户端,并且向客户端发送ReceivePrivateMessage消息;
这里我们没有在服务器端把当前用户名、当前时间、消息等拼接为一个字符串,而是将其以多个参数的形式发送到客户端,这样客户端可以决定展示这些信息的形式;
集线器中的方法是可以设置返回值的,SendPrivateMessage的返回值是Task<string>的类型。
需要注意的是,SingalR不会对消息进行持久化,因此即使目标用户当前不在线,上面代码的发送消息方法也不会报错,而且用户上线后也不会收到离线期间的消息。
同样的道理也适用于分组发送消息,用户在上线后才能加入一个分组,因此用户也无法收到离线期间该组内的消息。
如果我们的系统需要实现接收离线期间的消息的功能,就需要再自行额外开发消息的持久化功能。
比如服务器端在向客户端发送消息的同时,也要把消息保存到数据库中;
在用户上线时,程序要先到数据库中查询历史消息。
第2步:
在前端页面增加私聊功能的界面和代码,如下代码所示:
const ret = await connection.invoke("SendPrivateMessage",destUserName,msg);
第3步:
网页端监听服务器端发送的ReceivePrivateMessage消息,把收到的私聊消息添加到聊天消息界面中,如以下代码所示:
connection.on('ReceivePrivateMessage',(srcUser,time,msg)=>{
state.message.push(srcUser+"在"+time+"发来私信:"+msg);
});
运行结果如下图:
![](https://ichiblog.cn/wp-content/uploads/2025/01/1353252-20220929211302126-933870671.png)