新文章 网摘 文章 随笔 日记

将 SignalR 用户映射到连接

通过汤姆菲茨麦肯

警告

本文档不适用于 SignalR 的最新版本。看看ASP.NET Core SignalR

本主题说明如何保留有关用户及其连接的信息。

帕特里克·弗莱彻 (Patrick Fletcher) 帮助撰写了此主题。

本主题中使用的软件版本

本主题的先前版本

有关 SignalR 早期版本的信息,请参阅SignalR 旧版本

问题和评论

请在页面底部的评论中留下关于您喜欢本教程的反馈以及我们可以改进的地方。如果您有与本教程没有直接关系的问题,可以将它们发布到ASP.NET SignalR 论坛StackOverflow.com

介绍

每个连接到集线器的客户端都会传递一个唯一的连接 ID。您可以Context.ConnectionId在集线器上下文属性中检索此值如果您的应用程序需要将用户映射到连接 ID 并保留该映射,您可以使用以下方法之一:

本主题中显示了这些实现中的每一个。您可以使用类的OnConnectedOnDisconnectedOnReconnected方法Hub来跟踪用户连接状态。

适用于您的应用程序的最佳方法取决于:

  • 托管您的应用程序的 Web 服务器的数量。
  • 是否需要获取当前连接用户的列表。
  • 是否需要在应用程序或服务器重新启动时保留组和用户信息。
  • 调用外部服务器的延迟是否是一个问题。

下表显示了哪种方法适用于这些注意事项。

 不止一台服务器获取当前连接的用户列表重启后保留信息最佳性能
用户 ID 提供者    
在记忆中    
单用户组    
永久的,外部的  

 

IUserID 提供者

此功能允许用户通过新接口 IUserIdProvider 指定基于 IRequest 的 userId。

IUserIdProvider

public interface IUserIdProvider
{
    string GetUserId(IRequest request);
}

默认情况下,会有一个使用用户的IPrincipal.Identity.Name作为用户名的实现。要更改此设置,请IUserIdProvider在应用程序启动时向全局主机注册您的实现

GlobalHost.DependencyResolver.Register(typeof(IUserIdProvider), () => new MyIdProvider());

在中心内,您将能够通过以下 API 向这些用户发送消息:

向特定用户发送消息

public class MyHub : Hub
{
    public void Send(string userId, string message)
    {
        Clients.User(userId).send(message);
    }
}

 

内存存储

以下示例显示如何在存储在内存中的字典中保留连接和用户信息。字典使用 aHashSet来存储连接 ID。在任何时候,用户都可以有多个到 SignalR 应用程序的连接。例如,通过多个设备或多个浏览器选项卡连接的用户将拥有多个连接 ID。

如果应用程序关闭,所有信息都会丢失,但会在用户重新建立连接时重新填充。如果您的环境包含多个 Web 服务器,则内存中存储将不起作用,因为每个服务器都有一个单独的连接集合。

第一个示例显示了一个管理用户到连接映射的类。HashSet 的键是用户名。

using System.Collections.Generic;
using System.Linq;

namespace BasicChat
{
    public class ConnectionMapping<T>
    {
        private readonly Dictionary<T, HashSet<string>> _connections =
            new Dictionary<T, HashSet<string>>();

        public int Count
        {
            get
            {
                return _connections.Count;
            }
        }

        public void Add(T key, string connectionId)
        {
            lock (_connections)
            {
                HashSet<string> connections;
                if (!_connections.TryGetValue(key, out connections))
                {
                    connections = new HashSet<string>();
                    _connections.Add(key, connections);
                }

                lock (connections)
                {
                    connections.Add(connectionId);
                }
            }
        }

        public IEnumerable<string> GetConnections(T key)
        {
            HashSet<string> connections;
            if (_connections.TryGetValue(key, out connections))
            {
                return connections;
            }

            return Enumerable.Empty<string>();
        }

        public void Remove(T key, string connectionId)
        {
            lock (_connections)
            {
                HashSet<string> connections;
                if (!_connections.TryGetValue(key, out connections))
                {
                    return;
                }

                lock (connections)
                {
                    connections.Remove(connectionId);

                    if (connections.Count == 0)
                    {
                        _connections.Remove(key);
                    }
                }
            }
        }
    }
}

下一个示例说明如何使用集线器中的连接映射类。类的实例存储在变量 name 中_connections

using System.Threading.Tasks;
using Microsoft.AspNet.SignalR;

namespace BasicChat
{
    [Authorize]
    public class ChatHub : Hub
    {
        private readonly static ConnectionMapping<string> _connections = 
            new ConnectionMapping<string>();

        public void SendChatMessage(string who, string message)
        {
            string name = Context.User.Identity.Name;

            foreach (var connectionId in _connections.GetConnections(who))
            {
                Clients.Client(connectionId).addChatMessage(name + ": " + message);
            }
        }

        public override Task OnConnected()
        {
            string name = Context.User.Identity.Name;

            _connections.Add(name, Context.ConnectionId);

            return base.OnConnected();
        }

        public override Task OnDisconnected(bool stopCalled)
        {
            string name = Context.User.Identity.Name;

            _connections.Remove(name, Context.ConnectionId);

            return base.OnDisconnected(stopCalled);
        }

        public override Task OnReconnected()
        {
            string name = Context.User.Identity.Name;

            if (!_connections.GetConnections(name).Contains(Context.ConnectionId))
            {
                _connections.Add(name, Context.ConnectionId);
            }

            return base.OnReconnected();
        }
    }
}

 

单用户组

您可以为每个用户创建一个组,然后在您只想联系该用户时向该组发送消息。每个组的名称是用户的名称。如果用户有多个连接,则每个连接 ID 都会添加到用户的组中。

当用户断开连接时,您不应手动从组中删除用户。此操作由 SignalR 框架自动执行。

以下示例显示了如何实现单用户组。

using Microsoft.AspNet.SignalR;
using System;
using System.Threading.Tasks;

namespace BasicChat
{
    [Authorize]
    public class ChatHub : Hub
    {
        public void SendChatMessage(string who, string message)
        {
            string name = Context.User.Identity.Name;

            Clients.Group(who).addChatMessage(name + ": " + message);
        }

        public override Task OnConnected()
        {
            string name = Context.User.Identity.Name;

            Groups.Add(Context.ConnectionId, name);

            return base.OnConnected();
        }
    }
}

 

永久的外部存储

本主题演示如何使用数据库或 Azure 表存储来存储连接信息。当您有多个 Web 服务器时,这种方法很有效,因为每个 Web 服务器都可以与同一个数据存储库进行交互。如果您的 Web 服务器停止工作或应用程序重新启动,OnDisconnected则不会调用方法。因此,您的数据存储库可能会包含不再有效的连接 ID 记录。要清理这些孤立的记录,您可能希望使在与您的应用程序相关的时间范围之外创建的任何连接无效。本节中的示例包括一个用于在创建连接时进行跟踪的值,但不显示如何清理旧记录,因为您可能希望将其作为后台进程执行。

数据库

以下示例显示如何在数据库中保留连接和用户信息。您可以使用任何数据访问技术;但是,下面的示例显示了如何使用 Entity Framework 定义模型。这些实体模型对应于数据库表和字段。根据您的应用程序的要求,您的数据结构可能会有很大差异。

第一个示例显示如何定义可以与许多连接实体关联的用户实体。

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Data.Entity;

namespace MapUsersSample
{
    public class UserContext : DbContext
    {
        public DbSet<User> Users { get; set; }
        public DbSet<Connection> Connections { get; set; }
    }

    public class User
    {
        [Key]
        public string UserName { get; set; }
        public ICollection<Connection> Connections { get; set; }
    }

    public class Connection
    {
        public string ConnectionID { get; set; }
        public string UserAgent { get; set; }
        public bool Connected { get; set; }
    }
}

然后,从集线器,您可以使用下面显示的代码跟踪每个连接的状态。

using System;
using System.Data.Entity;
using System.Linq;
using System.Threading.Tasks;
using System.Collections.Concurrent;
using Microsoft.AspNet.SignalR;

namespace MapUsersSample
{
    [Authorize]
    public class ChatHub : Hub
    {
        public void SendChatMessage(string who, string message)
        {
            var name = Context.User.Identity.Name;
            using (var db = new UserContext())
            {
                var user = db.Users.Find(who);
                if (user == null)
                {
                    Clients.Caller.showErrorMessage("Could not find that user.");
                }
                else
                {
                    db.Entry(user)
                        .Collection(u => u.Connections)
                        .Query()
                        .Where(c => c.Connected == true)
                        .Load();

                    if (user.Connections == null)
                    {
                        Clients.Caller.showErrorMessage("The user is no longer connected.");
                    }
                    else
                    {
                        foreach (var connection in user.Connections)
                        {
                            Clients.Client(connection.ConnectionID)
                                .addChatMessage(name + ": " + message);
                        }
                    }
                }
            }
        }

        public override Task OnConnected()
        {
            var name = Context.User.Identity.Name;
            using (var db = new UserContext())
            {
                var user = db.Users
                    .Include(u => u.Connections)
                    .SingleOrDefault(u => u.UserName == name);
                
                if (user == null)
                {
                    user = new User
                    {
                        UserName = name,
                        Connections = new List<Connection>()
                    };
                    db.Users.Add(user);
                }

                user.Connections.Add(new Connection
                {
                    ConnectionID = Context.ConnectionId,
                    UserAgent = Context.Request.Headers["User-Agent"],
                    Connected = true
                });
                db.SaveChanges();
            }
            return base.OnConnected();
        }

        public override Task OnDisconnected(bool stopCalled)
        {
            using (var db = new UserContext())
            {
                var connection = db.Connections.Find(Context.ConnectionId);
                connection.Connected = false;
                db.SaveChanges();
            }
            return base.OnDisconnected(stopCalled);
        }
    }
}

 

Azure 表存储

以下 Azure 表存储示例类似于数据库示例。它不包括开始使用 Azure 表存储服务所需的所有信息。有关信息,请参阅如何从 .NET 使用表存储

以下示例显示了用于存储连接信息的表实体。它通过用户名对数据进行分区,并通过连接 id 标识每个实体,因此一个用户可以随时有多个连接。

using Microsoft.WindowsAzure.Storage.Table;
using System;

namespace MapUsersSample
{
    public class ConnectionEntity : TableEntity
    {
        public ConnectionEntity() { }        

        public ConnectionEntity(string userName, string connectionID)
        {
            this.PartitionKey = userName;
            this.RowKey = connectionID;
        }
    }
}

在集线器中,您可以跟踪每个用户的连接状态。

using Microsoft.AspNet.SignalR;
using Microsoft.WindowsAzure;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Table;
using System;
using System.Linq;
using System.Threading.Tasks;

namespace MapUsersSample
{
    public class ChatHub : Hub
    {
        public void SendChatMessage(string who, string message)
        {
            var name = Context.User.Identity.Name;
            
            var table = GetConnectionTable();

            var query = new TableQuery<ConnectionEntity>()
                .Where(TableQuery.GenerateFilterCondition(
                "PartitionKey", 
                QueryComparisons.Equal, 
                who));

            var queryResult = table.ExecuteQuery(query).ToList();
            if (queryResult.Count == 0)
            {
                Clients.Caller.showErrorMessage("The user is no longer connected.");
            }
            else
            {
                foreach (var entity in queryResult)
                {
                    Clients.Client(entity.RowKey).addChatMessage(name + ": " + message);
                }
            }
        }

        public override Task OnConnected()
        {
            var name = Context.User.Identity.Name;
            var table = GetConnectionTable();
            table.CreateIfNotExists();

            var entity = new ConnectionEntity(
                name.ToLower(), 
                Context.ConnectionId);
            var insertOperation = TableOperation.InsertOrReplace(entity);
            table.Execute(insertOperation);
            
            return base.OnConnected();
        }

        public override Task OnDisconnected(bool stopCalled)
        {
            var name = Context.User.Identity.Name;
            var table = GetConnectionTable();

            var deleteOperation = TableOperation.Delete(
                new ConnectionEntity(name, Context.ConnectionId) { ETag = "*" });
            table.Execute(deleteOperation);

            return base.OnDisconnected(stopCalled);
        }

        private CloudTable GetConnectionTable()
        {
            var storageAccount =
                CloudStorageAccount.Parse(
                CloudConfigurationManager.GetSetting("StorageConnectionString"));
            var tableClient = storageAccount.CreateCloudTableClient();
            return tableClient.GetTableReference("connection");
        }
    }
}


https://docs.microsoft.com/en-us/aspnet/signalr/overview/guide-to-the-api/mapping-users-to-connections
posted @ 2021-09-17 09:27  岭南春  阅读(406)  评论(0)    收藏  举报