Asignar usuarios de SignalR a las conexiones

por Tom FitzMacken

Advertencia

Esta documentación no se aplica a la última versión de SignalR. Eche un vistazo a SignalR de ASP.NET Core.

Este tema muestra cómo conservar la información sobre los usuarios y sus conexiones.

Patrick Fletcher ayudó a escribir este tema.

Versiones de software empleadas en este tema

Versiones anteriores de este tema

Para obtener información sobre versiones anteriores de SignalR, consulte Versiones anteriores de SignalR.

Preguntas y comentarios

Deje sus comentarios sobre este tutorial y sobre lo que podríamos mejorar en los comentarios en la parte inferior de la página. Si tiene alguna pregunta que no esté directamente relacionadas con el tutorial, puede publicarla en el foro de ASP.NET SignalR o en StackOverflow.com.

Introducción

Cada cliente que se conecta a un centro de conectividad pasa un identificador de conexión único. Puede recuperar este valor en la propiedad Context.ConnectionId del contexto del centro de conectividad. Si su aplicación necesita asignar un usuario al id. de conexión y conservar esa asignación, puede usar uno de los siguientes:

Cada una de estas implementaciones se muestra en este tema. Use los métodos OnConnected, OnDisconnected y OnReconnected de la clase Hub para hacer un seguimiento del estado de la conexión del usuario.

El mejor enfoque para su aplicación depende de:

  • El número de servidores web que hospedan su aplicación.
  • Si necesita obtener una lista de los usuarios conectados actualmente.
  • Si necesita conservar la información sobre grupos y usuarios cuando se reinicie la aplicación o el servidor.
  • Si la latencia de llamar a un servidor externo es un problema.

La siguiente tabla muestra qué enfoque funciona para estas consideraciones.

Consideración Más de un servidor Obtener la lista de usuarios conectados actualmente Conservar la información tras los reinicios Rendimiento óptimo
Proveedor UserID
En memoria
Grupos de usuarios únicos
Permanente, externo

Proveedor IUserID

Esta característica permite a los usuarios especificar qué userId se basa en un IRequest a través de una nueva interfaz IUserIdProvider:

El IUserIdProvider

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

De forma predeterminada, habrá una implementación que use el IPrincipal.Identity.Name del usuario como nombre de usuario. Para cambiar esto, registre la implementación de IUserIdProvider con el host global cuando se inicie la aplicación:

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

Desde un centro de conectividad, podrá enviar mensajes a estos usuarios a través de la siguiente API:

Envío de un mensaje a un usuario específico

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

Almacenamiento en memoria

Los siguientes ejemplos muestran cómo conservar la información sobre la conexión y el usuario en un diccionario que se almacena en la memoria. El diccionario usa un HashSet para almacenar el identificador de conexión. En cualquier momento, un usuario puede tener más de una conexión con la aplicación de SignalR. Por ejemplo, un usuario que esté conectado a través de varios dispositivos o más de una pestaña del explorador tendría más de un id. de conexión.

Si la aplicación se cierra, toda la información se pierde, pero volverá a rellenarse a medida que los usuarios restablezcan sus conexiones. El almacenamiento en memoria no funciona si su entorno incluye más de un servidor web porque cada servidor tendría una colección separada de conexiones.

El primer ejemplo muestra una clase que administra la asignación de usuarios a conexiones. La clave del HashSet será el nombre del usuario.

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

El siguiente ejemplo muestra cómo usar la clase de asignación de conexiones de un centro de conectividad. La instancia de la clase se almacena en un nombre de variable _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();
        }
    }
}

Grupos de usuarios únicos

Puede crear un grupo para cada usuario y después enviar un mensaje a ese grupo cuando quiera llegar solo a ese usuario. El nombre de cada grupo es el nombre del usuario. Si un usuario tiene más de una conexión, cada id. de conexión se agrega al grupo del usuario.

No debe quitar manualmente el usuario del grupo cuando el usuario se desconecte. Esta acción la realiza automáticamente el marco SignalR.

El siguiente ejemplo muestra cómo implementar grupos de usuarios únicos.

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

Almacenamiento externo permanente

Este tema muestra cómo usar una base de datos o el almacenamiento de tablas de Azure para almacenar la información de conexión. Este enfoque funciona cuando se tienen varios servidores web porque cada servidor web puede interactuar con el mismo repositorio de datos. Si sus servidores web dejan de funcionar o la aplicación se reinicia, no se llama al método OnDisconnected. Por lo tanto, es posible que su repositorio de datos tenga registros de id. de conexión que ya no sean válidos. Para limpiar estos registros huérfanos, puede querer invalidar cualquier conexión que se haya creado fuera de un marco temporal relevante para su aplicación. Los ejemplos de esta sección incluyen un valor para hacer un seguimiento de cuándo se creó la conexión, pero no muestran cómo limpiar los registros antiguos porque es posible que quiera hacerlo como proceso en segundo plano.

Base de datos

Los siguientes ejemplos muestran cómo conservar la información de conexión y de usuario en una base de datos. Puede usar cualquier tecnología de acceso a datos; sin embargo, el ejemplo siguiente muestra cómo definir modelos usando Entity Framework. Estos modelos de entidad corresponden a tablas y campos de la base de datos. Su estructura de datos podría variar considerablemente en función de los requisitos de su aplicación.

El primer ejemplo muestra cómo definir una entidad de usuario que puede asociarse a muchas entidades de conexión.

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

Después, desde el centro de conectividad, podrá seguir el estado de cada conexión con el código que se muestra a continuación.

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

Almacenamiento de tablas de Azure

El siguiente ejemplo de almacenamiento de tablas de Azure es similar al ejemplo de la base de datos. No incluye toda la información necesaria para empezar a utilizar el servicio de Azure Table Storage. Para más información, consulte Uso del almacenamiento en tablas de .NET.

El siguiente ejemplo muestra una entidad de tabla para almacenar información de conexión. Divide los datos por nombre de usuario e identifica cada entidad por el id. de conexión, de modo que un usuario puede tener varias conexiones en cualquier 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;
        }
    }
}

En el centro de conectividad se realiza un seguimiento del estado de la conexión de cada usuario.

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