Zuordnen von SignalR-Benutzern zu Verbindungen in SignalR 1.x

von Patrick Fletcher, Tom FitzMacken

Warnung

Diese Dokumentation ist nicht für die neueste Version von SignalR vorgesehen. Sehen Sie sich ASP.NET Core SignalR an.

In diesem Thema wird gezeigt, wie Informationen zu Benutzern und deren Verbindungen beibehalten werden.

Einführung

Jeder Client, der eine Verbindung mit einem Hub herstellt, übergibt eine eindeutige Verbindungs-ID. Sie können diesen Wert in der Context.ConnectionId -Eigenschaft des Hubkontexts abrufen. Wenn Ihre Anwendung einen Benutzer der Verbindungs-ID zuordnen und diese Zuordnung beibehalten muss, können Sie eine der folgenden Optionen verwenden:

Jede dieser Implementierungen wird in diesem Thema gezeigt. Sie verwenden die OnConnectedMethoden , OnDisconnectedund OnReconnected der Hub -Klasse, um die Benutzerverbindung status nachzuverfolgen.

Der beste Ansatz für Ihre Anwendung hängt von folgenden Faktoren ab:

  • Die Anzahl der Webserver, die Ihre Anwendung hosten.
  • Gibt an, ob Sie eine Liste der aktuell verbundenen Benutzer abrufen müssen.
  • Gibt an, ob Sie Gruppen- und Benutzerinformationen beibehalten müssen, wenn die Anwendung oder der Server neu gestartet wird.
  • Gibt an, ob die Latenz beim Aufrufen eines externen Servers ein Problem ist.

Die folgende Tabelle zeigt, welcher Ansatz für diese Überlegungen funktioniert.

Aspekt Mehrere Server Abrufen einer Liste der aktuell verbundenen Benutzer Beibehalten von Informationen nach Neustarts Optimale Leistung
Im Arbeitsspeicher
Einzelbenutzergruppen
Permanent, extern

Im Arbeitsspeicher

Die folgenden Beispiele zeigen, wie Verbindungs- und Benutzerinformationen in einem Im Arbeitsspeicher gespeicherten Wörterbuch beibehalten werden. Das Wörterbuch verwendet eine HashSet , um die Verbindungs-ID zu speichern. Ein Benutzer kann jederzeit über mehrere Verbindungen mit der SignalR-Anwendung verfügen. Beispielsweise verfügt ein Benutzer, der über mehrere Geräte oder mehrere Browserregisterkarten verbunden ist, über mehr als eine Verbindungs-ID.

Wenn die Anwendung heruntergefahren wird, gehen alle Informationen verloren, aber sie werden erneut aufgefüllt, wenn die Benutzer ihre Verbindungen erneut herstellen. In-Memory-Speicher funktioniert nicht, wenn Ihre Umgebung mehr als einen Webserver enthält, da jeder Server über eine separate Sammlung von Verbindungen verfügt.

Das erste Beispiel zeigt eine Klasse, die die Zuordnung von Benutzern zu Verbindungen verwaltet. Der Schlüssel für das HashSet ist der Name des Benutzers.

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

Das nächste Beispiel zeigt, wie die Verbindungszuordnungsklasse von einem Hub verwendet wird. Die instance der -Klasse wird in einem Variablennamen _connectionsgespeichert.

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

Einzelbenutzergruppen

Sie können eine Gruppe für jeden Benutzer erstellen und dann eine Nachricht an diese Gruppe senden, wenn Sie nur diesen Benutzer erreichen möchten. Der Name jeder Gruppe ist der Name des Benutzers. Wenn ein Benutzer über mehrere Verbindungen verfügt, wird jede Verbindungs-ID der Gruppe des Benutzers hinzugefügt.

Sie sollten den Benutzer nicht manuell aus der Gruppe entfernen, wenn der Benutzer die Verbindung trennt. Diese Aktion wird automatisch vom SignalR-Framework ausgeführt.

Das folgende Beispiel zeigt, wie Sie Einzelbenutzergruppen implementieren.

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

Permanenter, externer Speicher

In diesem Thema wird gezeigt, wie Sie entweder eine Datenbank oder einen Azure-Tabellenspeicher zum Speichern von Verbindungsinformationen verwenden. Dieser Ansatz funktioniert, wenn Sie über mehrere Webserver verfügen, da jeder Webserver mit demselben Datenrepository interagieren kann. Wenn Ihre Webserver nicht mehr funktionieren oder die Anwendung neu gestartet wird, wird die OnDisconnected -Methode nicht aufgerufen. Daher ist es möglich, dass Ihr Datenrepository Datensätze für verbindungs-IDs enthält, die nicht mehr gültig sind. Um diese verwaisten Datensätze zu sauber, möchten Sie möglicherweise jede Verbindung für ungültig erklären, die außerhalb eines für Ihre Anwendung relevanten Zeitraums erstellt wurde. Die Beispiele in diesem Abschnitt enthalten einen Wert für die Nachverfolgung, wann die Verbindung erstellt wurde, zeigen jedoch nicht, wie alte Datensätze sauber werden, da Sie dies möglicherweise als Hintergrundprozess durchführen möchten.

Datenbank

Die folgenden Beispiele zeigen, wie Verbindungs- und Benutzerinformationen in einer Datenbank beibehalten werden. Sie können eine beliebige Datenzugriffstechnologie verwenden. Das folgende Beispiel zeigt jedoch, wie Modelle mithilfe von Entity Framework definiert werden. Diese Entitätsmodelle entsprechen Datenbanktabellen und -feldern. Ihre Datenstruktur kann je nach Den Anforderungen Ihrer Anwendung erheblich variieren.

Das erste Beispiel zeigt, wie eine Benutzerentität definiert wird, die vielen Verbindungsentitäten zugeordnet werden kann.

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

Anschließend können Sie über den Hub den Status jeder Verbindung mit dem unten gezeigten Code nachverfolgen.

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

Azure Table Storage

Das folgende Azure-Tabellenspeicherbeispiel ähnelt dem Datenbankbeispiel. Sie enthält nicht alle Informationen, die Sie für die ersten Schritte mit dem Azure Table Storage-Dienst benötigen. Weitere Informationen finden Sie unter Verwenden von Tabellenspeicher in .NET.

Das folgende Beispiel zeigt eine Tabellenentität zum Speichern von Verbindungsinformationen. Es partitioniert die Daten nach Benutzername und identifiziert jede Entität anhand der Verbindungs-ID, sodass ein Benutzer jederzeit über mehrere Verbindungen verfügen kann.

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

Im Hub verfolgen Sie die status der Verbindung jedes Benutzers.

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