Uso dei gruppi in SignalR 1.x

di Patrick Fletcher, Tom FitzMacken

Avviso

Questa documentazione non è per la versione più recente di SignalR. Esaminare ASP.NET Core SignalR.

Questo argomento descrive come aggiungere utenti ai gruppi e rendere persistenti le informazioni sull'appartenenza ai gruppi.

Panoramica

I gruppi in SignalR forniscono un metodo per la trasmissione di messaggi ai subset specificati di client connessi. Un gruppo può avere un numero qualsiasi di client e un client può essere membro di un numero qualsiasi di gruppi. Non è necessario creare in modo esplicito i gruppi. In effetti, un gruppo viene creato automaticamente la prima volta che si specifica il nome in una chiamata a Groups.Add e viene eliminato quando si rimuove l'ultima connessione dall'appartenenza. Per un'introduzione all'uso dei gruppi, vedere How to manage group membership from the Hub class in the Hub API - Server Guide .For an introduction to using groups, see How to manage group membership from the Hub class in the Hubs API - Server Guide.

Non esiste alcuna API per ottenere un elenco di appartenenze a gruppi o un elenco di gruppi. SignalR invia messaggi a client e gruppi basati su un modello pub/sub e il server non gestisce elenchi di gruppi o appartenenze a gruppi. Ciò consente di ottimizzare la scalabilità, perché ogni volta che si aggiunge un nodo a una Web farm, qualsiasi stato gestito da SignalR deve essere propagato al nuovo nodo.

Quando si aggiunge un utente a un gruppo usando il Groups.Add metodo , l'utente riceve messaggi indirizzati a tale gruppo per la durata della connessione corrente, ma l'appartenenza dell'utente a tale gruppo non viene mantenuta oltre la connessione corrente. Se si vogliono conservare in modo permanente le informazioni sui gruppi e sull'appartenenza a gruppi, è necessario archiviarli in un repository, ad esempio un database o un archivio tabelle di Azure. Ogni volta che un utente si connette all'applicazione, si recupera dal repository a cui appartiene l'utente e si aggiunge manualmente l'utente a tali gruppi.

Quando si riconnette dopo un'interruzione temporanea, l'utente esegue automaticamente il join dei gruppi assegnati in precedenza. La riconnessione automatica di un gruppo si applica solo quando si riconnette, non quando si stabilisce una nuova connessione. Un token con firma digitale viene passato dal client che contiene l'elenco di gruppi assegnati in precedenza. Per verificare se l'utente appartiene ai gruppi richiesti, è possibile eseguire l'override del comportamento predefinito.

Questo argomento include le sezioni seguenti:

Aggiunta e rimozione di utenti

Per aggiungere o rimuovere utenti da un gruppo, chiamare i metodi Add o Remove e passare l'ID connessione dell'utente e il nome del gruppo come parametri. Non è necessario rimuovere manualmente un utente da un gruppo al termine della connessione.

Nell'esempio seguente vengono illustrati i Groups.Add metodi e Groups.Remove usati nei metodi 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);
    }
}

I Groups.Add metodi e Groups.Remove vengono eseguiti in modo asincrono.

Se si vuole aggiungere un client a un gruppo e inviare immediatamente un messaggio al client usando il gruppo, è necessario assicurarsi che il metodo Groups.Add venga completato per primo. Gli esempi di codice seguenti illustrano come eseguire questa operazione, uno usando il codice che funziona in .NET 4.5 e uno usando il codice che funziona in .NET 4.

Esempio asincrono di .NET 4.5

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

Esempio di .NET 4 asincrono

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

In generale, non è consigliabile includere await quando si chiama il Groups.Remove metodo perché l'ID connessione che si sta tentando di rimuovere potrebbe non essere più disponibile. In tal caso, TaskCanceledException viene generata dopo il timeout della richiesta. Se l'applicazione deve assicurarsi che l'utente sia stato rimosso dal gruppo prima di inviare un messaggio al gruppo, è possibile aggiungere await prima di Groups.Remove e quindi intercettare l'eccezione TaskCanceledException che potrebbe essere generata.

Chiamata di membri di un gruppo

È possibile inviare messaggi a tutti i membri di un gruppo o solo ai membri specificati del gruppo, come illustrato negli esempi seguenti.

  • Tutti i client connessi in un gruppo specificato.

    Clients.Group(groupName).addChatMessage(name, message);
    
  • Tutti i client connessi in un gruppo specificato , ad eccezione dei client specificati, identificati dall'ID connessione.

    Clients.Group(groupName, connectionId1, connectionId2).addChatMessage(name, message);
    
  • Tutti i client connessi in un gruppo specificato , ad eccezione del client chiamante.

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

Archiviazione dell'appartenenza a un gruppo in un database

Negli esempi seguenti viene illustrato come conservare le informazioni sui gruppi e sugli utenti in un database. È possibile usare qualsiasi tecnologia di accesso ai dati; Tuttavia, l'esempio seguente illustra come definire i modelli usando Entity Framework. Questi modelli di entità corrispondono a tabelle e campi di database. La struttura dei dati può variare notevolmente a seconda dei requisiti dell'applicazione. Questo esempio include una classe denominata ConversationRoom che sarebbe univoca per un'applicazione che consente agli utenti di partecipare a conversazioni su argomenti diversi, ad esempio sport o giardino. Questo esempio include anche una classe per le connessioni. La classe di connessione non è assolutamente necessaria per tenere traccia dell'appartenenza al gruppo, ma fa spesso parte di una soluzione affidabile per tenere traccia degli utenti.

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

Nell'hub è quindi possibile recuperare le informazioni sul gruppo e sull'utente dal database e aggiungere manualmente l'utente ai gruppi appropriati. L'esempio non include il codice per tenere traccia delle connessioni utente. In questo esempio la await parola chiave non viene applicata prima Groups.Add perché un messaggio non viene inviato immediatamente ai membri del gruppo. Se si desidera inviare un messaggio a tutti i membri del gruppo immediatamente dopo l'aggiunta del nuovo membro, è necessario applicare la await parola chiave per assicurarsi che l'operazione asincrona sia stata completata.

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

Archiviazione dell'appartenenza ai gruppi nell'archiviazione tabelle di Azure

L'uso dell'archiviazione tabelle di Azure per archiviare le informazioni sui gruppi e sugli utenti è simile all'uso di un database. Nell'esempio seguente viene illustrata un'entità di tabella che archivia il nome utente e il nome del gruppo.

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

Nell'hub si recuperano i gruppi assegnati quando l'utente si connette.

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

Verifica dell'appartenenza al gruppo durante la riconnessione

Per impostazione predefinita, SignalR riassegnare automaticamente un utente ai gruppi appropriati durante la riconnessione da un'interruzione temporanea, ad esempio quando una connessione viene interrotta e riristabilita prima del timeout della connessione. Le informazioni sul gruppo dell'utente vengono passate in un token durante la riconnessione e tale token viene verificato nel server. Per informazioni sul processo di verifica per la riconnessione degli utenti ai gruppi, vedere Ricongiunzione dei gruppi durante la riconnessione.

In generale, è consigliabile usare il comportamento predefinito della riconnessione automatica dei gruppi. I gruppi SignalR non sono concepiti come meccanismo di sicurezza per limitare l'accesso ai dati sensibili. Tuttavia, se l'applicazione deve controllare due volte l'appartenenza a un gruppo di un utente durante la riconnessione, è possibile eseguire l'override del comportamento predefinito. La modifica del comportamento predefinito può aggiungere un carico al database perché l'appartenenza al gruppo di un utente deve essere recuperata per ogni riconnessione anziché solo quando l'utente si connette.

Se è necessario verificare l'appartenenza al gruppo per la riconnessione, creare un nuovo modulo della pipeline dell'hub che restituisce un elenco di gruppi assegnati, come illustrato di seguito.

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

Aggiungere quindi il modulo alla pipeline dell'hub, come evidenziato di seguito.

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