在 SignalR 1.x 中将 SignalR 用户映射到连接

作者 :Patrick FletcherTom FitzMacken

警告

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

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

简介

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

本主题中介绍了其中每个实现。 使用 类的 OnConnectedHubOnDisconnectedOnReconnected 方法来跟踪用户连接状态。

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

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

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

注意事项 多个服务器 获取当前已连接用户的列表 重启后保留信息 最佳性能
内存中
单用户组
永久、外部

内存中存储

以下示例演示如何在内存中存储的字典中保留连接和用户信息。 字典使用 HashSet 来存储连接 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);
                    }
                }
            }
        }
    }
}

下一个示例演示如何从中心使用连接映射类。 类的实例存储在名为 的变量 _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()
        {
            string name = Context.User.Identity.Name;

            _connections.Remove(name, Context.ConnectionId);

            return base.OnDisconnected();
        }

        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()
        {
            using (var db = new UserContext())
            {
                var connection = db.Connections.Find(Context.ConnectionId);
                connection.Connected = false;
                db.SaveChanges();
            }
            return base.OnDisconnected();
        }
    }
}

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()
        {
            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();
        }

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