Mapping degli utenti di SignalR alle connessioni

di Tom FitzMacken

Avviso

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

In questo argomento viene illustrato come conservare informazioni sugli utenti e sulle relative connessioni.

Patrick Fletcher ha contribuito a scrivere questo argomento.

Versioni software usate in questo argomento

Versioni precedenti di questo argomento

Per informazioni sulle versioni precedenti di SignalR, vedere Versioni precedenti di SignalR.

Domande e commenti

Lasciare commenti e suggerimenti su come è piaciuta questa esercitazione e cosa è possibile migliorare nei commenti nella parte inferiore della pagina. Se si hanno domande che non sono direttamente correlate all'esercitazione, è possibile pubblicarle nel forum di ASP.NET SignalR o StackOverflow.com.

Introduzione

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

Ognuna di queste implementazioni è 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.
  • Indica se è necessario rendere persistenti le informazioni sui gruppi e sugli utenti al riavvio dell'applicazione o del server.
  • Indica se la latenza di chiamata a un server esterno è un problema.

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

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

Provider IUserID

Questa funzionalità consente agli utenti di specificare il valore userId basato su un IRequest tramite una nuova interfaccia IUserIdProvider.

The IUserIdProvider

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

Per impostazione predefinita, sarà disponibile un'implementazione che usa l'utente IPrincipal.Identity.Name come nome utente. Per modificare questo problema, registrare l'implementazione di IUserIdProvider con l'host globale all'avvio dell'applicazione:

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

Dall'interno di un hub sarà possibile inviare messaggi a questi utenti tramite l'API seguente:

Invio di un messaggio a un utente specifico

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

Archiviazione in memoria

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

Se l'applicazione viene arrestata, tutte le informazioni andranno perse, ma verranno ricompilato quando gli utenti stabiliscono nuovamente le connessioni. L'archiviazione in memoria non funziona se l'ambiente include più di un server Web perché ogni server dispone di 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);
                    }
                }
            }
        }
    }
}

L'esempio seguente illustra come usare la classe di mapping della 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(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();
        }
    }
}

Gruppi di utenti singoli

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

Non 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 archivio tabelle di Azure per archiviare le informazioni di connessione. Questo approccio funziona quando si dispone di più server Web perché ogni server Web può interagire con lo stesso repository di dati. Se i server Web smette di funzionare o l'applicazione viene riavviata, il OnDisconnected metodo non viene chiamato. È quindi possibile che il repository di dati disponga di record per gli ID di connessione che non sono più validi. Per pulire questi record orfani, è possibile invalidare qualsiasi connessione creata al di fuori di un intervallo di tempo rilevante per l'applicazione. Gli esempi in questa sezione includono un valore per il rilevamento durante la creazione della connessione, ma non illustrano come pulire i record precedenti perché è possibile eseguire questa operazione come processo in background.

Database

Negli esempi seguenti viene illustrato come mantenere le informazioni di connessione e utente 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.

Nel primo esempio viene illustrato 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(bool stopCalled)
        {
            using (var db = new UserContext())
            {
                var connection = db.Connections.Find(Context.ConnectionId);
                connection.Connected = false;
                db.SaveChanges();
            }
            return base.OnDisconnected(stopCalled);
        }
    }
}

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 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à in base all'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(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");
        }
    }
}