将 SignalR 用户映射到连接

作者 Tom FitzMacken

警告

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

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

帕特里克·弗莱彻帮助写了这个话题。

本主题中使用的软件版本

本主题的早期版本

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

问题和评论

请留下反馈,说明你对本教程的喜爱程度,以及我们可以在页面底部的评论中改进的内容。 如果你有与本教程不直接相关的问题,可以将其发布到 ASP.NET SignalR 论坛StackOverflow.com

简介

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

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

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

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

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

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

IUserID 提供程序

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

The 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);
    }
}

内存中存储

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