Mapeamento de usuários do SignalR para conexões no SignalR 1.x

por Patrick Fletcher, Tom FitzMacken

Aviso

Esta documentação não é para a versão mais recente do SignalR. Dê uma olhada em ASP.NET Core SignalR.

Este tópico mostra como reter informações sobre usuários e suas conexões.

Introdução

Cada cliente que se conecta a um hub passa uma ID de conexão exclusiva. Você pode recuperar esse valor na Context.ConnectionId propriedade do contexto do hub. Se o aplicativo precisar mapear um usuário para a ID de conexão e persistir esse mapeamento, você poderá usar um dos seguintes:

Cada uma dessas implementações é mostrada neste tópico. Você usa os OnConnectedmétodos , OnDisconnectede OnReconnected da Hub classe para acompanhar a conexão do usuário status.

A melhor abordagem para seu aplicativo depende de:

  • O número de servidores Web que hospedam seu aplicativo.
  • Se você precisa obter uma lista dos usuários conectados no momento.
  • Se você precisa persistir informações de grupo e usuário quando o aplicativo ou servidor é reiniciado.
  • Se a latência de chamar um servidor externo é um problema.

A tabela a seguir mostra qual abordagem funciona para essas considerações.

Consideração Mais de um servidor Obter lista de usuários conectados no momento Manter informações após reinicializações Desempenho ideal
Na memória
Grupos de usuário único
Permanente, externo

Armazenamento na memória

Os exemplos a seguir mostram como reter informações de conexão e de usuário em um dicionário armazenado na memória. O dicionário usa um HashSet para armazenar a ID da conexão. A qualquer momento, um usuário pode ter mais de uma conexão com o aplicativo SignalR. Por exemplo, um usuário conectado por meio de vários dispositivos ou mais de uma guia do navegador teria mais de uma ID de conexão.

Se o aplicativo for desligado, todas as informações serão perdidas, mas serão preenchidas novamente à medida que os usuários restabelecerem suas conexões. O armazenamento na memória não funcionará se o ambiente incluir mais de um servidor Web porque cada servidor teria uma coleção separada de conexões.

O primeiro exemplo mostra uma classe que gerencia o mapeamento de usuários para conexões. A chave para o HashSet será o nome do usuário.

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

O exemplo a seguir mostra como usar a classe de mapeamento de conexão de um hub. A instância da classe é armazenada em um nome _connectionsde variável .

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

Grupos de usuário único

Você pode criar um grupo para cada usuário e, em seguida, enviar uma mensagem para esse grupo quando quiser acessar apenas esse usuário. O nome de cada grupo é o nome do usuário. Se um usuário tiver mais de uma conexão, cada ID de conexão será adicionada ao grupo do usuário.

Você não deve remover manualmente o usuário do grupo quando o usuário se desconectar. Essa ação é executada automaticamente pela estrutura do SignalR.

O exemplo a seguir mostra como implementar grupos de usuário único.

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

Armazenamento permanente e externo

Este tópico mostra como usar um banco de dados ou um armazenamento de tabelas do Azure para armazenar informações de conexão. Essa abordagem funciona quando você tem vários servidores Web porque cada servidor Web pode interagir com o mesmo repositório de dados. Se os servidores Web pararem de funcionar ou o aplicativo for reiniciado, o OnDisconnected método não será chamado. Portanto, é possível que o repositório de dados tenha registros para ids de conexão que não são mais válidos. Para limpo esses registros órfãos, convém invalidar qualquer conexão que tenha sido criada fora de um período de tempo relevante para seu aplicativo. Os exemplos nesta seção incluem um valor para acompanhamento quando a conexão foi criada, mas não mostram como limpo registros antigos porque talvez você queira fazer isso como processo em segundo plano.

Banco de dados

Os exemplos a seguir mostram como reter informações de conexão e de usuário em um banco de dados. Você pode usar qualquer tecnologia de acesso a dados; no entanto, o exemplo a seguir mostra como definir modelos usando o Entity Framework. Esses modelos de entidade correspondem a tabelas e campos de banco de dados. Sua estrutura de dados pode variar consideravelmente dependendo dos requisitos do aplicativo.

O primeiro exemplo mostra como definir uma entidade de usuário que pode ser associada a muitas entidades de conexão.

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

Em seguida, no hub, você pode acompanhar o estado de cada conexão com o código mostrado abaixo.

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

Armazenamento de tabelas do Azure

O exemplo de armazenamento de tabela do Azure a seguir é semelhante ao exemplo de banco de dados. Ele não inclui todas as informações necessárias para começar a usar o Serviço de Armazenamento de Tabelas do Azure. Para obter informações, consulte Como usar o armazenamento de tabelas do .NET.

O exemplo a seguir mostra uma entidade de tabela para armazenar informações de conexão. Ele particiona os dados por nome de usuário e identifica cada entidade pela ID de conexão, para que um usuário possa ter várias conexões a qualquer momento.

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

No hub, você controla o status da conexão de cada usuário.

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