Zuordnen von SignalR-Benutzern zu Verbindungen

von Tom FitzMacken

Warnung

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

In diesem Thema wird gezeigt, wie Sie Informationen zu Benutzern und deren Verbindungen aufbewahren.

Patrick Fletcher half bei der Erstellung dieses Themas.

In diesem Thema verwendete Softwareversionen

Frühere Versionen dieses Themas

Informationen zu früheren Versionen von SignalR finden Sie unter Ältere Versionen von SignalR.

Fragen und Kommentare

Bitte hinterlassen Sie Feedback dazu, wie Ihnen dieses Tutorial gefallen hat und was wir in den Kommentaren unten auf der Seite verbessern könnten. Wenn Sie Fragen haben, die nicht direkt mit dem Tutorial zusammenhängen, können Sie diese im ASP.NET SignalR-Forum oder StackOverflow.com posten.

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 Wartezeit beim Aufrufen eines externen Servers ein Problem ist.

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

Aspekt Mehrere Server Liste der aktuell verbundenen Benutzer abrufen Beibehalten von Informationen nach neustarten Optimale Leistung
Benutzer-ID-Anbieter
Im Arbeitsspeicher
Einzelbenutzergruppen
Permanent, extern

IUserID-Anbieter

Mit diesem Feature können Benutzer die userId basierend auf einer IRequest über eine neue Schnittstelle IUserIdProvider angeben.

Der IUserIdProvider

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

Standardmäßig gibt es eine Implementierung, die den Benutzernamen des IPrincipal.Identity.Name Benutzers verwendet. Um dies zu ändern, registrieren Sie Ihre Implementierung beim IUserIdProvider globalen Host, wenn Ihre Anwendung gestartet wird:

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

Von einem Hub aus können Sie Nachrichten über die folgende API an diese Benutzer senden:

Senden einer Nachricht an einen bestimmten Benutzer

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

Im Arbeitsspeicher

Die folgenden Beispiele zeigen, wie Sie Verbindungs- und Benutzerinformationen in einem Im Arbeitsspeicher gespeicherten Wörterbuch beibehalten. Das Wörterbuch verwendet einen 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 mehrere Verbindungs-ID.

Wenn die Anwendung heruntergefahren wird, gehen alle Informationen verloren, aber sie werden erneut aufgefüllt, wenn die Benutzer ihre Verbindungen erneut herstellen. Der 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. Der 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(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();
        }
    }
}

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 Benutzergruppe 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 Einzelbenutzergruppen implementiert werden.

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, können Sie jede Verbindung ungültig machen, die außerhalb eines für Ihre Anwendung relevanten Zeitrahmens erstellt wurde. Die Beispiele in diesem Abschnitt enthalten einen Wert für die Nachverfolgung, wenn die Verbindung erstellt wurde, zeigen jedoch nicht, wie alte Datensätze sauber werden, da Sie dies möglicherweise als Hintergrundprozess ausführen möchten.

Datenbank

Die folgenden Beispiele zeigen, wie Sie Verbindungs- und Benutzerinformationen in einer Datenbank beibehalten. Sie können jede 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(bool stopCalled)
        {
            using (var db = new UserContext())
            {
                var connection = db.Connections.Find(Context.ConnectionId);
                connection.Connected = false;
                db.SaveChanges();
            }
            return base.OnDisconnected(stopCalled);
        }
    }
}

Azure Table Storage

Das folgende Azure-Tabellenspeicherbeispiel ähnelt dem Datenbankbeispiel. Es enthält nicht alle Informationen, die Sie für die ersten Schritte mit Azure Table Storage Service benötigen. Weitere Informationen finden Sie unter Verwenden von Tabellenspeicher aus .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 einzelnen Benutzerverbindungen nach.

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