Utilisation des groupes dans SignalR 1.x

par Patrick Fletcher, Tom FitzMacken

Avertissement

Cette documentation ne concerne pas la dernière version de SignalR. Jetez un coup d’œil à ASP.NET Core SignalR.

Cette rubrique explique comment ajouter des utilisateurs à des groupes et conserver les informations d’appartenance aux groupes.

Vue d’ensemble

Les groupes dans SignalR fournissent une méthode pour diffuser des messages vers des sous-ensembles spécifiés de clients connectés. Un groupe peut avoir n’importe quel nombre de clients, et un client peut être membre d’un nombre quelconque de groupes. Vous n’avez pas besoin de créer explicitement des groupes. En effet, un groupe est créé automatiquement la première fois que vous spécifiez son nom dans un appel à Groups.Add, et il est supprimé lorsque vous supprimez la dernière connexion de son appartenance. Pour une présentation de l’utilisation des groupes, consultez Comment gérer l’appartenance à un groupe à partir de la classe Hub dans l’API Hubs - Guide du serveur.

Il n’existe aucune API permettant d’obtenir une liste d’appartenances à un groupe ou une liste de groupes. SignalR envoie des messages aux clients et aux groupes en fonction d’un modèle pub/sub, et le serveur ne gère pas de listes de groupes ou d’appartenances à un groupe. Cela permet d’optimiser la scalabilité, car chaque fois que vous ajoutez un nœud à une batterie de serveurs web, tout état que SignalR gère doit être propagé au nouveau nœud.

Lorsque vous ajoutez un utilisateur à un groupe à l’aide de la Groups.Add méthode , l’utilisateur reçoit des messages dirigés vers ce groupe pendant la durée de la connexion actuelle, mais l’appartenance de l’utilisateur à ce groupe n’est pas conservée au-delà de la connexion actuelle. Si vous souhaitez conserver définitivement des informations sur les groupes et l’appartenance à un groupe, vous devez stocker ces données dans un référentiel tel qu’une base de données ou un stockage de tables Azure. Ensuite, chaque fois qu’un utilisateur se connecte à votre application, vous récupérez à partir du référentiel les groupes auxquels appartient l’utilisateur et vous ajoutez manuellement cet utilisateur à ces groupes.

Lors de la reconnexion après une interruption temporaire, l’utilisateur rejoint automatiquement les groupes précédemment affectés. La réintégration automatique d’un groupe s’applique uniquement lors de la reconnexion, et non lors de l’établissement d’une nouvelle connexion. Un jeton signé numériquement est transmis à partir du client qui contient la liste des groupes précédemment affectés. Si vous souhaitez vérifier si l’utilisateur appartient aux groupes demandés, vous pouvez remplacer le comportement par défaut.

Cette rubrique contient les sections suivantes :

Ajout et suppression d’utilisateurs

Pour ajouter ou supprimer des utilisateurs d’un groupe, vous appelez les méthodes Add ou Remove et transmettez l’ID de connexion de l’utilisateur et le nom du groupe en tant que paramètres. Vous n’avez pas besoin de supprimer manuellement un utilisateur d’un groupe à la fin de la connexion.

L’exemple suivant montre les méthodes et Groups.Remove utilisées dans les Groups.Add méthodes Hub.

public class ContosoChatHub : Hub
{
    public Task JoinRoom(string roomName)
    {
        return Groups.Add(Context.ConnectionId, roomName);
    }

    public Task LeaveRoom(string roomName)
    {
        return Groups.Remove(Context.ConnectionId, roomName);
    }
}

Les Groups.Add méthodes et s’exécutent Groups.Remove de manière asynchrone.

Si vous souhaitez ajouter un client à un groupe et envoyer immédiatement un message au client à l’aide du groupe, vous devez vous assurer que la méthode Groups.Add se termine en premier. Les exemples de code suivants montrent comment procéder, l’un en utilisant du code qui fonctionne dans .NET 4.5 et l’autre en utilisant du code qui fonctionne dans .NET 4.

Exemple .NET 4.5 asynchrone

public async Task JoinRoom(string roomName)
{
    await Groups.Add(Context.ConnectionId, roomName);
    Clients.Group(roomName).addChatMessage(Context.User.Identity.Name + " joined.");
}

Exemple .NET 4 asynchrone

public void JoinRoom(string roomName)
{
    (Groups.Add(Context.ConnectionId, roomName) as Task).ContinueWith(antecedent =>
      Clients.Group(roomName).addChatMessage(Context.User.Identity.Name + " joined."));
}

En général, vous ne devez pas inclure await lors de l’appel de la Groups.Remove méthode, car l’ID de connexion que vous essayez de supprimer n’est peut-être plus disponible. Dans ce cas, TaskCanceledException est levée une fois la demande expirée. Si votre application doit vous assurer que l’utilisateur a été supprimé du groupe avant d’envoyer un message au groupe, vous pouvez ajouter await avant Groups.Remove, puis intercepter l’exception TaskCanceledException qui peut être levée.

Appel de membres d’un groupe

Vous pouvez envoyer des messages à tous les membres d’un groupe ou uniquement aux membres spécifiés du groupe, comme illustré dans les exemples suivants.

  • Tous les clients connectés dans un groupe spécifié.

    Clients.Group(groupName).addChatMessage(name, message);
    
  • Tous les clients connectés dans un groupe spécifié, à l’exception des clients spécifiés, identifiés par l’ID de connexion.

    Clients.Group(groupName, connectionId1, connectionId2).addChatMessage(name, message);
    
  • Tous les clients connectés dans un groupe spécifié , à l’exception du client appelant.

    Clients.OthersInGroup(groupName).addChatMessage(name, message);
    

Stockage de l’appartenance à un groupe dans une base de données

Les exemples suivants montrent comment conserver les informations de groupe 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 aux tables et aux champs de base de données. Votre structure de données peut varier considérablement en fonction des exigences de votre application. Cet exemple inclut une classe nommée ConversationRoom qui serait propre à une application qui permet aux utilisateurs de participer à des conversations sur différents sujets, tels que le sport ou le jardinage. Cet exemple inclut également une classe pour les connexions. La classe de connexion n’est pas absolument nécessaire pour le suivi de l’appartenance aux groupes, mais elle fait souvent partie d’une solution robuste pour le suivi des utilisateurs.

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Data.Entity;

namespace GroupsExample
{
    public class UserContext : DbContext
    {
        public DbSet<User> Users { get; set; }
        public DbSet<Connection> Connections { get; set; }
        public DbSet<ConversationRoom> Rooms { get; set; }
    }

    public class User
    {
        [Key]
        public string UserName { get; set; }
        public ICollection<Connection> Connections { get; set; }
        public virtual ICollection<ConversationRoom> Rooms { get; set; } 
    }

    public class Connection
    {
        public string ConnectionID { get; set; }
        public string UserAgent { get; set; }
        public bool Connected { get; set; }
    }

    public class ConversationRoom
    {
        [Key]
        public string RoomName { get; set; }
        public virtual ICollection<User> Users { get; set; }
    }
}

Ensuite, dans le hub, vous pouvez récupérer les informations de groupe et d’utilisateur de la base de données et ajouter manuellement l’utilisateur aux groupes appropriés. L’exemple n’inclut pas de code pour le suivi des connexions utilisateur. Dans cet exemple, le await mot clé n’est pas appliqué avantGroups.Add, car un message n’est pas immédiatement envoyé aux membres du groupe. Si vous souhaitez envoyer un message à tous les membres du groupe immédiatement après l’ajout du nouveau membre, vous devez appliquer le await mot clé pour vous assurer que l’opération asynchrone est terminée.

using Microsoft.AspNet.SignalR;
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Threading.Tasks;

namespace GroupsExample
{
    [Authorize]
    public class ChatHub : Hub
    {
        public override Task OnConnected()
        {
            using (var db = new UserContext())
            {
                // Retrieve user.
                var user = db.Users
                    .Include(u => u.Rooms)
                    .SingleOrDefault(u => u.UserName == Context.User.Identity.Name);

                // If user does not exist in database, must add.
                if (user == null)
                {
                    user = new User()
                    {
                        UserName = Context.User.Identity.Name
                    };
                    db.Users.Add(user);
                    db.SaveChanges();
                }
                else
                {
                    // Add to each assigned group.
                    foreach (var item in user.Rooms)
                    {
                        Groups.Add(Context.ConnectionId, item.RoomName);
                    }
                }
            }
            return base.OnConnected();
        }

        public void AddToRoom(string roomName)
        {
            using (var db = new UserContext())
            {
                // Retrieve room.
                var room = db.Rooms.Find(roomName);

                if (room != null)
                {
                    var user = new User() { UserName = Context.User.Identity.Name};
                    db.Users.Attach(user);

                    room.Users.Add(user);
                    db.SaveChanges();
                    Groups.Add(Context.ConnectionId, roomName);
                }
            }
        }

        public void RemoveFromRoom(string roomName)
        {
            using (var db = new UserContext())
            {
                // Retrieve room.
                var room = db.Rooms.Find(roomName);
                if (room != null)
                {
                    var user = new User() { UserName = Context.User.Identity.Name };
                    db.Users.Attach(user);

                    room.Users.Remove(user);
                    db.SaveChanges();
                    
                    Groups.Remove(Context.ConnectionId, roomName);
                }
            }
        }
    }
}

Stockage de l’appartenance à un groupe dans le stockage table Azure

L’utilisation du stockage table Azure pour stocker des informations sur les groupes et les utilisateurs est similaire à l’utilisation d’une base de données. L’exemple suivant montre une entité de table qui stocke le nom d’utilisateur et le nom du groupe.

using Microsoft.WindowsAzure.Storage.Table;
using System;

namespace GroupsExample
{
    public class UserGroupEntity : TableEntity
    {
        public UserGroupEntity() { }

        public UserGroupEntity(string userName, string groupName)
        {
            this.PartitionKey = userName;
            this.RowKey = groupName;
        }
    }
}

Dans le hub, vous récupérez les groupes attribués lorsque l’utilisateur se connecte.

using Microsoft.AspNet.SignalR;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.WindowsAzure.Storage.Table;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure;

namespace GroupsExample
{
    [Authorize]
    public class ChatHub : Hub
    {
        public override Task OnConnected()
        {
            string userName = Context.User.Identity.Name;

            var table = GetRoomTable();
            table.CreateIfNotExists();
            var query = new TableQuery<UserGroupEntity>()
                .Where(TableQuery.GenerateFilterCondition(
                "PartitionKey", QueryComparisons.Equal, userName));
            
            foreach (var entity in table.ExecuteQuery(query))
            {
                Groups.Add(Context.ConnectionId, entity.RowKey);
            }

            return base.OnConnected();
        }

        public Task AddToRoom(string roomName)
        {
            string userName = Context.User.Identity.Name;

            var table = GetRoomTable();

            var insertOperation = TableOperation.InsertOrReplace(
                new UserGroupEntity(userName, roomName));
            table.Execute(insertOperation);

            return Groups.Add(Context.ConnectionId, roomName);
        }

        public Task RemoveFromRoom(string roomName)
        {
            string userName = Context.User.Identity.Name;

            var table = GetRoomTable();

            var retrieveOperation = TableOperation.Retrieve<UserGroupEntity>(
                userName, roomName);
            var retrievedResult = table.Execute(retrieveOperation);

            var deleteEntity = (UserGroupEntity)retrievedResult.Result;

            if (deleteEntity != null)
            {
                var deleteOperation = TableOperation.Delete(deleteEntity);
                table.Execute(deleteOperation);
            }

            return Groups.Remove(Context.ConnectionId, roomName);
        }

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

Vérification de l’appartenance au groupe lors de la reconnexion

Par défaut, SignalR ré assigne automatiquement un utilisateur aux groupes appropriés lors d’une reconnexion après une interruption temporaire, par exemple lorsqu’une connexion est supprimée et rétablie avant l’expiration de la connexion. Les informations de groupe de l’utilisateur sont passées dans un jeton lors de la reconnexion, et ce jeton est vérifié sur le serveur. Pour plus d’informations sur le processus de vérification permettant de joindre des utilisateurs à des groupes, consultez Rejoindre des groupes lors de la reconnexion.

En général, vous devez utiliser le comportement par défaut qui consiste à rejoindre automatiquement des groupes lors de la reconnexion. Les groupes SignalR ne sont pas conçus comme un mécanisme de sécurité pour restreindre l’accès aux données sensibles. Toutefois, si votre application doit doublement case activée l’appartenance à un groupe d’un utilisateur lors de la reconnexion, vous pouvez remplacer le comportement par défaut. La modification du comportement par défaut peut ajouter une charge à votre base de données, car l’appartenance au groupe d’un utilisateur doit être récupérée pour chaque reconnexion plutôt que juste lorsque l’utilisateur se connecte.

Si vous devez vérifier l’appartenance au groupe lors de la reconnexion, créez un module de pipeline hub qui retourne une liste de groupes affectés, comme indiqué ci-dessous.

using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hubs;
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;

namespace GroupsExample
{
    public class RejoingGroupPipelineModule : HubPipelineModule
    {
        public override Func<HubDescriptor, IRequest, IList<string>, IList<string>> 
            BuildRejoiningGroups(Func<HubDescriptor, IRequest, IList<string>, IList<string>> 
            rejoiningGroups)
        {
            rejoiningGroups = (hb, r, l) => 
            {
                List<string> assignedRooms = new List<string>();
                using (var db = new UserContext())
                {
                    var user = db.Users.Include(u => u.Rooms)
                        .Single(u => u.UserName == r.User.Identity.Name);
                    foreach (var item in user.Rooms)
                    {
                        assignedRooms.Add(item.RoomName);
                    }
                }
                return assignedRooms;
            };

            return rejoiningGroups;
        }
    }
}

Ensuite, ajoutez ce module au pipeline hub, comme indiqué ci-dessous.

public class Global : HttpApplication
{
    void Application_Start(object sender, EventArgs e)
    {
        // Code that runs on application startup
        BundleConfig.RegisterBundles(BundleTable.Bundles);
        AuthConfig.RegisterOpenAuth();
        RouteConfig.RegisterRoutes(RouteTable.Routes);
        RouteTable.Routes.MapHubs();
        GlobalHost.HubPipeline.AddModule(new RejoingGroupPipelineModule());
    }
}