Mapping degli utenti di SignalR alle connessioni 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 illustra come conservare informazioni sugli utenti e sulle relative connessioni.

Introduzione

Ogni client che si connette a un hub passa un ID di connessione univoco. È possibile recuperare questo valore nella proprietà del contesto dell'hub Context.ConnectionId . Se l'applicazione deve eseguire il mapping di un utente all'ID connessione e rendere persistente il mapping, è possibile usare uno dei seguenti elementi:

Ognuna di queste implementazioni viene illustrata in questo argomento. Usare i OnConnectedmetodi , OnDisconnectede OnReconnected della Hub classe per tenere traccia dello stato della connessione utente.

L'approccio migliore per l'applicazione dipende da:

  • Numero di server Web che ospitano l'applicazione.
  • Indica se è necessario ottenere un elenco degli utenti attualmente connessi.
  • Se è necessario mantenere le informazioni sul gruppo e sull'utente quando l'applicazione o il server viene riavviato.
  • Se la latenza di chiamata a un server esterno è un problema.

Nella tabella seguente viene illustrato quale approccio funziona per queste considerazioni.

Considerazioni Più di un server Ottenere l'elenco degli utenti attualmente connessi Rendere persistenti le informazioni dopo il riavvio Prestazioni ottimali
In memoria
Gruppi di utenti singoli
Permanente, esterno

Archiviazione in memoria

Negli esempi seguenti viene illustrato come conservare le informazioni di connessione e utente in un dizionario archiviato in memoria. Il dizionario usa un HashSet oggetto per archiviare l'ID connessione. In qualsiasi momento un utente potrebbe avere più connessioni all'applicazione SignalR. Ad esempio, un utente connesso tramite più dispositivi o più schede del browser avrà più id di connessione.

Se l'applicazione viene arrestata, tutte le informazioni vengono perse, ma verranno ricompilato come ricompilato dagli utenti. L'archiviazione in memoria non funziona se l'ambiente include più server Web perché ogni server avrà una raccolta separata di connessioni.

Il primo esempio mostra una classe che gestisce il mapping degli utenti alle connessioni. La chiave per HashSet sarà il nome dell'utente.

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

Nell'esempio successivo viene illustrato come usare la classe di mapping di connessione da un hub. L'istanza della classe viene archiviata in un nome _connectionsdi variabile .

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

Gruppi di utenti singoli

È possibile creare un gruppo per ogni utente e quindi inviare un messaggio a tale gruppo quando si vuole raggiungere solo l'utente. Il nome di ogni gruppo è il nome dell'utente. Se un utente ha più connessioni, ogni ID connessione viene aggiunto al gruppo dell'utente.

Non è consigliabile rimuovere manualmente l'utente dal gruppo quando l'utente si disconnette. Questa azione viene eseguita automaticamente dal framework SignalR.

Nell'esempio seguente viene illustrato come implementare gruppi di utenti singoli.

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

Archiviazione permanente ed esterna

Questo argomento illustra come usare un database o un'archiviazione tabelle di Azure per archiviare le informazioni di connessione. Questo approccio funziona quando sono presenti più server Web perché ogni server Web può interagire con lo stesso repository di dati. Se i server Web non funzionano o l'applicazione viene riavviato, il OnDisconnected metodo non viene chiamato. Pertanto, è possibile che il repository dati disponga di record per gli ID di connessione che non sono più validi. Per pulire questi record orfani, è possibile invalidare qualsiasi connessione creata all'esterno di un intervallo di tempo pertinente all'applicazione. Gli esempi in questa sezione includono un valore per il rilevamento quando è stata creata la connessione, ma non mostrano come pulire i record precedenti perché è possibile eseguire questa operazione come processo in background.

Database

Negli esempi seguenti viene illustrato come conservare le informazioni di connessione e utente in un database. È possibile usare qualsiasi tecnologia di accesso ai dati; L'esempio seguente illustra tuttavia come definire 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.

Il primo esempio illustra come definire un'entità utente che può essere associata a molte entità di connessione.

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

Quindi, dall'hub è possibile tenere traccia dello stato di ogni connessione con il codice illustrato di seguito.

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

Archiviazione tabelle di Azure

L'esempio di archiviazione tabelle di Azure seguente è simile all'esempio di database. Non include tutte le informazioni necessarie per iniziare a usare il servizio di archiviazione tabelle di Azure. Per informazioni, vedere Come usare l'archiviazione tabelle da .NET.

Nell'esempio seguente viene illustrata un'entità di tabella per l'archiviazione delle informazioni di connessione. Partiziona i dati in base al nome utente e identifica ogni entità dall'ID connessione, in modo che un utente possa avere più connessioni in qualsiasi 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;
        }
    }
}

Nell'hub si tiene traccia dello stato della connessione di ogni utente.

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