Share via


Mappage des utilisateurs SignalR aux connexions dans SignalR 1.x

par Patrick Fletcher, Tom FitzMacken

Avertissement

Cette documentation ne concerne pas la dernière version de SignalR. Consultez ASP.NET Core SignalR.

Cette rubrique montre comment conserver des informations sur les utilisateurs et leurs connexions.

Introduction

Chaque client qui se connecte à un hub passe un ID de connexion unique. Vous pouvez récupérer cette valeur dans la Context.ConnectionId propriété du contexte hub. Si votre application doit mapper un utilisateur à l’ID de connexion et conserver ce mappage, vous pouvez utiliser l’un des éléments suivants :

Chacune de ces implémentations est présentée dans cette rubrique. Vous utilisez les OnConnectedméthodes , OnDisconnectedet de OnReconnected la Hub classe pour suivre la connexion utilisateur status.

La meilleure approche pour votre application dépend des éléments suivants :

  • Nombre de serveurs web hébergeant votre application.
  • Si vous avez besoin d’obtenir la liste des utilisateurs actuellement connectés.
  • Indique si vous devez conserver les informations sur le groupe et l’utilisateur lors du redémarrage de l’application ou du serveur.
  • Si la latence d’appel d’un serveur externe est un problème.

Le tableau suivant indique l’approche qui convient à ces considérations.

Considération Plusieurs serveurs Obtenir la liste des utilisateurs actuellement connectés Conserver les informations après les redémarrages Performances optimales
En mémoire
Groupes mono-utilisateurs
Permanent, externe

Stockage en mémoire

Les exemples suivants montrent comment conserver les informations de connexion et d’utilisateur dans un dictionnaire stocké en mémoire. Le dictionnaire utilise un HashSet pour stocker l’ID de connexion. À tout moment, un utilisateur peut avoir plusieurs connexions à l’application SignalR. Par exemple, un utilisateur connecté via plusieurs appareils ou plusieurs onglets de navigateur aurait plusieurs ID de connexion.

Si l’application s’arrête, toutes les informations sont perdues, mais elles seront renseignées à mesure que les utilisateurs rétabliront leurs connexions. Le stockage en mémoire ne fonctionne pas si votre environnement inclut plusieurs serveurs web, car chaque serveur aurait une collection distincte de connexions.

Le premier exemple montre une classe qui gère le mappage des utilisateurs aux connexions. La clé du HashSet sera le nom de l’utilisateur.

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

L’exemple suivant montre comment utiliser la classe de mappage de connexion à partir d’un hub. Le instance de la classe est stocké dans un nom _connectionsde variable .

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

Groupes mono-utilisateurs

Vous pouvez créer un groupe pour chaque utilisateur, puis envoyer un message à ce groupe lorsque vous souhaitez atteindre cet utilisateur uniquement. Le nom de chaque groupe est le nom de l’utilisateur. Si un utilisateur a plusieurs connexions, chaque ID de connexion est ajouté au groupe de l’utilisateur.

Vous ne devez pas supprimer manuellement l’utilisateur du groupe lorsque l’utilisateur se déconnecte. Cette action est automatiquement effectuée par l’infrastructure SignalR.

L’exemple suivant montre comment implémenter des groupes à utilisateur unique.

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

Stockage externe permanent

Cette rubrique montre comment utiliser une base de données ou un stockage table Azure pour stocker les informations de connexion. Cette approche fonctionne lorsque vous avez plusieurs serveurs web, car chaque serveur web peut interagir avec le même référentiel de données. Si vos serveurs web cessent de fonctionner ou si l’application redémarre, la OnDisconnected méthode n’est pas appelée. Par conséquent, il est possible que votre référentiel de données ait des enregistrements pour les ID de connexion qui ne sont plus valides. Pour propre ces enregistrements orphelins, vous souhaiterez peut-être invalider toute connexion créée en dehors d’un délai pertinent pour votre application. Les exemples de cette section incluent une valeur pour le suivi de la création de la connexion, mais ne montrent pas comment propre d’anciens enregistrements, car vous pouvez le faire en tant que processus en arrière-plan.

Base de données

Les exemples suivants montrent comment conserver les informations de connexion et d’utilisateur dans une base de données. Vous pouvez utiliser n’importe quelle technologie d’accès aux données ; toutefois, l’exemple ci-dessous montre comment définir des modèles à l’aide d’Entity Framework. Ces modèles d’entité correspondent à des tables et des champs de base de données. Votre structure de données peut varier considérablement en fonction des exigences de votre application.

Le premier exemple montre comment définir une entité utilisateur qui peut être associée à de nombreuses entités de connexion.

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

Ensuite, à partir du hub, vous pouvez suivre l’état de chaque connexion avec le code ci-dessous.

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

Stockage de table Azure

L’exemple de stockage de table Azure suivant est similaire à l’exemple de base de données. Il n’inclut pas toutes les informations dont vous avez besoin pour commencer à utiliser Azure Table Storage Service. Pour plus d’informations, consultez Utilisation du stockage Table à partir de .NET.

L’exemple suivant montre une entité de table pour stocker les informations de connexion. Il partitionne les données par nom d’utilisateur et identifie chaque entité par l’ID de connexion, afin qu’un utilisateur puisse avoir plusieurs connexions à tout moment.

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

Dans le hub, vous suivez la status de la connexion de chaque utilisateur.

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