SignalR:针对部分客户端的消息推送

上节我们使用Clients.All.SendAsync向连接到当前集线器的所有客户端推送消息,而在很多业务场景中,我们一般都只是向部分客户端推送消息。

在我们进行客户端筛选的时候,有3个筛选参数:ConnectionId、组和用户ID。ConnectionId是SignalR为每个连接分配的唯一标识,我们可以通过集线器的Context属性中的ConnectionId属性获取当前连接的ConnectionId;每个组有唯一的名字,对于连接到同一个集线器中的客户端,我们可以把它们分组;用户ID是登录用户的ID,它对应的是类型为ClaimTypes.NameIdentifier的Claim的值,如果使用用户ID进行筛选,我们需要在客户端登录的时候设定类型为ClaimTypes.NameIdentifier的Claim。

Hub类

Hub类的Groups属性为IGroupManager类型,它可以用于对组成员进行管理,IGroupManager类包含如下所示的方法。

//将connectionId的连接添加到名字为groupName的组中
Task AddToGroupAsync(string connectionId, string groupName, 
    CancellationToken cancellationToken = default);
//将connectionId的连接从名字为groupName的组中删除
Task RemoveFromGroupAsync(string connectionId, string groupName, 
    CancellationToken cancellationToken = default);

我们在把连接加入组中的时候,如果指定名字的组不存在,SignalR会自动创建组。因为连接和组的关系是通过ConnectionId建立的,所以客户端重连之后,我们就需要把连接重新加入组。

Hub类的Clients属性为IHubCallerClients类型,它可以用来对连接到当前集线器的客户端进行筛选。IHubCallerClients类包含如下所示的成员。

//获取当前连接的客户端
T Caller { get; }
//获取除了当前连接外的所有客户端
T Others { get; }
//获取名字为groupName组中除了当前连接外的其他客户端
T OthersInGroup(string groupName);

//获取所有连接的客户端
T All { get; }
//获取除了excludedConnectionIds的客户端
T AllExcept(IReadOnlyList<string> excludedConnectionIds);
//获取connectionId客户端
T Client(string connectionId);
//获取包含在connectionIds中的客户端
T Clients(IReadOnlyList<string> connectionIds);
//获取组名groupName中的客户端
T Group(string groupName);
//获取组名groupName中的客户端,除了在excludedConnectionIds中
T GroupExcept(string groupName, IReadOnlyList<string> excludedConnectionIds);
//获取组名包含在groupNames中的客户端
T Groups(IReadOnlyList<string> groupNames);
//获取用户id的客户端
T User(string userId);
//获取用户id包含在userIds中的客户端
T Users(IReadOnlyList<string> userIds);

这些成员的属性值、返回值都是IClientProxy类型的,我们可以通过IClientProxy向筛选到的客户端发送消息。IClientProxy类型中只定义了一个用来向客户端发送消息的SendCoreAsync方法,我们调用的SendAsync方法是用来简化SendCoreAsync调用的扩展方法。基于性能、准确度等的考虑,我们并不能获得筛选到的每一个客户端的信息,只能向筛选到的客户端推送消息。

发送私聊消息

下面我们来为之前编写的Web聊天室增加“发送私聊消息”的功能。

第1步:
在ChatRoomHub中增加一个发送私聊消息的SendPrivateMessage方法。

public async Task<string> SendPrivateMessage(string destUserName, string message)
{
    User? user = UserManager.FindByName(destUserName);
    if (user == null)
    {
        return "目标用户不存在";
    }
    string userId = user.Id.ToString();
    string srcUserName = this.Context.User!.FindFirst(ClaimTypes.Name)!.Value;
    string time = DateTime.Now.ToShortTimeString();
    await this.Clients.User(userId)
        .SendAsync("ReceivePrivateMessage", srcUserName, time, message);
    return "ok";
}

需要注意的是,SignalR不会对消息进行持久化,因此即使目标用户当前不在线,SendAsync方法的调用也不会出错,而且用户上线后也不会收到离线期间的消息。同样的道理也适用于分组发送消息,用户在上线后才能加入一个分组,因此用户也无法收到离线期间该组内的消息。

如果我们的系统需要实现接收离线期间的消息的功能,就需要再自行额外开发消息的持久化功能,比如服务器端在向客户端发送消息的同时,也要把消息保存到数据库中;在用户上线时,程序要先到数据库中查询历史消息。

第2步:
在前端页面增加私聊功能的界面和代码。

公屏:<input type="text" v-model="state.userMessage" 
     v-on:keypress="txtMsgOnkeypress" />
<div>
  私聊给<input type="text" v-model="state.privateMsg.destUserName"/>
  说<input type="text" v-model="state.privateMsg.message"
     v-on:keypress="txtPrivateMsgOnkeypress"/>
</div>
const txtPrivateMsgOnkeypress = async function (e) {
    if (e.keyCode != 13) return;
    const destUserName = state.privateMsg.destUserName;
    const msg = state.privateMsg.message;
    try {
        const ret = await connection.invoke("SendPrivateMessage",
            destUserName, msg);
        if (ret != "ok") { alert(ret);};
    } catch (err) {
        alert(err);
        return;
    }
    state.privateMsg.message = "";
};

第3步:
网页端监听服务器端发送的ReceivePrivateMessage消息,把收到的私聊消息添加到聊天消息界面中。

connection.on('ReceivePrivateMessage', (srcUser,time,msg) => {
  state.messages.push(srcUser+"在"+time+"发来私信:"+msg);
});

最后:
运行结果如下。
image

聊天室功能可以私聊,可以把所有的用户都放到一个聊天室中,如果我们想实现多个聊天室的效果,就可以把用户放入不同的分组中,这样每个分组就是一个聊天室了。

posted @ 2022-09-29 21:13  一纸年华  阅读(1165)  评论(0编辑  收藏  举报