使用 SignalR 集线器向特定连接用户广播消息

Posted

技术标签:

【中文标题】使用 SignalR 集线器向特定连接用户广播消息【英文标题】:Broadcast messages to specific connected users using SignalR hub 【发布时间】:2014-06-10 08:07:53 【问题描述】:

我的目标是当数据库发生插入/更新时立即通知连接的用户。

我有一个Hub

[Authorize]
public class NotificationsHub : Hub

    private readonly IWorker _worker;

    public NotificationsHub() : this(new WorkerFacade(new ExtremeDataRepository()))  

    public NotificationsHub(IWorker worker)
    
        _worker = worker;
    

    public override Task OnConnected()
    
        if (!HubUsersManager.ConnectedUsers.Any(p => p.Name.Equals(Context.User.Identity.Name)))
        
            HubUsersManager.ConnectedUsers.Add(new HubUserHandler
            
                Name = Context.User.Identity.Name,
                ConnectionId = Context.ConnectionId
            );
        
        return base.OnConnected();
    

    //public override Task OnDisconnected()
    //
    //    HubUserHandler hubUserHandler =
    //        HubUsersManager.ConnectedUsers.FirstOrDefault(p => p.Name == Context.User.Identity.Name);

    //    if (hubUserHandler != null)
    //        HubUsersManager.ConnectedUsers.Remove(hubUserHandler);

    //    return base.OnDisconnected();
    //

    public async Task<ICollection<NotificationMessage>> GetAllPendingNotifications(string userId)
    
        return await _worker.GetAllPendingNotifications(userId);
    

    public void UpdateNotificationMessagesToAllClients(ICollection<NotificationMessage> notificationMessages)
    
        Clients.All.updateNotificationMessages(notificationMessages);
    

如您所见,我从已连接用户的手动映射中删除了 OnDisconnected,以便稍后使用用户名和 ConnectionId 供我的 Entity Framework MemberShip 用户广播消息,因为每次客户端离开时都会调用 OnDisconnected集线器,而不是实际断开连接时,例如关闭浏览器。

SignalR的初始化工作完美,我测试了它,GetAllPendingNotifications在连接开始时被调用,我调用了SignalR javascript代码的函数,见下面的代码。

// A simple templating method for replacing placeholders enclosed in curly braces.
if (!String.prototype.supplant) 
    String.prototype.supplant = function (o) 
        return this.replace(/([^]*)/g,
            function (a, b) 
                var r = o[b];
                return typeof r === 'string' || typeof r === 'number' ? r : a;
            
        );
    ;


if (typeof String.prototype.endsWith !== 'function') 
    String.prototype.endsWith = function(suffix) 
        return this.indexOf(suffix, this.length - suffix.length) !== -1;
    ;


$(function () 

    // Reference the auto-generated proxy for the hub.
    var notificationsHub = $.connection.notificationsHub;
    var userName, userId;

    // Create a function that the hub can call back to display messages.
    notificationsHub.client.updateNotificationMessages = updateNotificationsToPage;

    function updateNotificationsToPage(notificationMessages) 
        alert('Im in updating notifications');
        if (notificationMessages.count > 0) 
            $.each(function(index) 
                alert($(this).Message);
            );
        
    ;

    function init() 
        notificationsHub.server.getAllPendingNotifications(userId).done(updateNotificationsToPage);
    

    function userDetailsRetrieved(data) 
        userName = data.UserName;
        userId = data.Id;
        // Start the connection.
        $.connection.hub.start().done(init);
    

    function getUserDetails() 
        var baseURL = document.baseURI,
            url;
        if (!baseURL.endsWith('Home/GetUserDetails')) 
            if (baseURL.endsWith('/'))
                url = baseURL + 'Home/GetUserDetails';
            else if (baseURL.endsWith('/Home') ||
                baseURL.endsWith('/Home/'))
                url = baseURL + '/GetUserDetails';
            else 
                url = baseURL + '/Home/GetUserDetails';
            
         else
            url = baseURL;

        $.ajax(
            url: url, success: userDetailsRetrieved, type: 'POST', error: function () 
            console.log(arguments);
        , dataType: 'json' );
    

    getUserDetails();
);

我知道映射可能不是必需的,我可以重写实现以将其映射到我的实际ApplicationUser GUID Id 以避免它,但现在它似乎可以达到目的,如果我先做可能会改变它它工作。

您还可以看到IWorker 引用,如果客户端调用服务器方法,我使用它来获取一些数据。 此外,所有Controllers 都在使用IWorker 具体实现,并且具有Clients 属性,因此我可以立即将消息广播回连接的客户端。

private IHubConnectionContext _clients;
private IHubConnectionContext Clients

    get 
        return _clients ?? (_clients = GlobalHost.ConnectionManager.GetHubContext<NotificationsHub>().Clients);
    

正确填充。 在应用程序的某些点上,IWorker 正在调用Clients 属性。

foreach (HubUserHandler hubUserHandler in HubUsersManager.ConnectedUsers)
        
            ApplicationUser user = await GetApplicationUserWithName(hubUserHandler.Name);
            List<string> userRoles = await GetUserRoles(user);
            bool isInRole = userRoles.Any(p => p.Equals("ARole"));

            List<NotificationMessage> testList = new List<NotificationMessage>
            
                new NotificationMessage Message = "TEST MESSAGE!"
            ;

            if (isInRole)
                Clients.Client(hubUserHandler.ConnectionId).updateNotificationMessages(testList);
        

我的浏览器什么也没有,应该会弹出两个警报,一个是 Im in updating notifications,另一个是 TEST MESSAGE,就像您在上面的 Javascript 代码中看到的那样。

[编辑]

更改了代码以将消息发送到特定的Group,但我的客户端浏览器不再显示任何内容。

        public override Task OnConnected()
        
            if (_groupManager.Count == 0)
            
                ICollection<string> rolesCollection = _worker.GetAllRoles();
                foreach (string roleName in rolesCollection)
                
                    _groupManager.Add(roleName);
                
            

            foreach (string groupRole in _groupManager.Groups)
            
                if (Context.User.IsInRole(groupRole))
                
                    Groups.Add(Context.ConnectionId, groupRole);
                
            

            return base.OnConnected();
        

在我的工人阶级的某个地方:

    List<NotificationMessage> testList = new List<NotificationMessage>
        
            new NotificationMessage Message = "TEST MESSAGE!"
        ;

    Clients.Group("TestGroup").updateNotificationMessages(testList);

我想我错过了什么!!!

【问题讨论】:

【参考方案1】:

我的 2 美分:你看过Clients.User() 方法吗?它已经为您完成了所有ConnectionId 映射,并且您正在使用Context.User,因此它应该可以开箱即用。如果您需要更复杂的东西,您可以在IUserIdProvider 的自定义实现中编写自己的映射逻辑。至少您会消除一个级别的复杂性(但如果问题不是因为这个问题,则不确定它是否会解决您的特定问题)。只是一个想法。

【讨论】:

是的,我会将逻辑移至 IUserIdProvider,因为我使用了 ASP.NET 身份验证,这似乎更好。我将尝试 Clients.User() 空重载。 Clients.User() 对我的问题没有意义,因为我可能有多个连接的用户需要接收相同的消息。 嗯,我看不出这与您使用 ConnectionId 的方法有什么不同。不是循环访问“不安全”的连接 ID,而是循环访问用户 ID,这是一个不太需要解决的问题。另外,我建议您看看是否可以找到一种方法以某种方式对用户进行分组(例如使用角色)并利用 SignalR 的 Groups 支持,这样您只需一个电话即可同时击中所有用户。也许你可以在那个方向回顾你的逻辑。 是的,我同意您的评论,我将删除当前逻辑并构建新的逻辑,尝试使用来自 Hub 的 Groups 和 ConnectionId。有没有办法让所有当前连接到集线器客户端 ConnectionIds?我了解 IUserIdProvider 将让我将 ConnectionId 与我的数据库中的 User.Identity id 映射。当然,对于群组,我只需要说向群组发送消息,理论上所有连接到群组的客户端都会收到通知! AFAIK 没有办法获取所有当前的 ConnectionId,但是,即使这可能不是推荐的方法。您对 ConnectionId 的干扰越少越好,而且通常可以在不使用它们的情况下重组代码以解决问题。小组真的很棒。

以上是关于使用 SignalR 集线器向特定连接用户广播消息的主要内容,如果未能解决你的问题,请参考以下文章

从 SignalR 调用特定客户端

如何在 hubpipleline 之外使用 SignalR 集线器实例

SignalR 2.2 客户端未收到任何消息

在重新连接时检测丢失的消息/对消息的丢失操作

SignalR - 使用 (IUserIdProvider) *NEW 2.0.0* 向特定用户发送消息

使用一个连接发送并发 SignalR 消息