Arbeiten mit Gruppen 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 beschrieben, wie Benutzer zu Gruppen hinzugefügt und Informationen zur Gruppenmitgliedschaft beibehalten werden.

Überblick

Gruppen in SignalR stellen eine Methode zum Senden von Nachrichten an bestimmte Teilmengen verbundener Clients bereit. Eine Gruppe kann über eine beliebige Anzahl von Clients verfügen, und ein Client kann Mitglied einer beliebigen Anzahl von Gruppen sein. Sie müssen keine Gruppen explizit erstellen. Tatsächlich wird eine Gruppe automatisch erstellt, wenn Sie ihren Namen zum ersten Mal in einem Aufruf von Groups.Add angeben, und sie wird gelöscht, wenn Sie die letzte Verbindung aus ihrer Mitgliedschaft entfernen. Eine Einführung in die Verwendung von Gruppen finden Sie unter Verwalten der Gruppenmitgliedschaft über die Hub-Klasse im Hubs-API – Serverhandbuch.

Es gibt keine API zum Abrufen einer Gruppenmitgliedschaftsliste oder einer Liste von Gruppen. SignalR sendet Nachrichten an Clients und Gruppen basierend auf einem Pub/Sub-Modell, und der Server verwaltet keine Listen von Gruppen oder Gruppenmitgliedschaften. Dies trägt zur Maximierung der Skalierbarkeit bei, denn wenn Sie einer Webfarm einen Knoten hinzufügen, muss jeder Zustand, den SignalR verwaltet, an den neuen Knoten weitergegeben werden.

Wenn Sie einer Gruppe mithilfe der Groups.Add -Methode einen Benutzer hinzufügen, empfängt der Benutzer nachrichten, die für die Dauer der aktuellen Verbindung an diese Gruppe gerichtet sind, aber die Mitgliedschaft des Benutzers in dieser Gruppe wird nicht über die aktuelle Verbindung hinaus beibehalten. Wenn Sie Informationen zu Gruppen und Gruppenmitgliedschaften dauerhaft aufbewahren möchten, müssen Sie diese Daten in einem Repository wie einer Datenbank oder einem Azure-Tabellenspeicher speichern. Jedes Mal, wenn ein Benutzer eine Verbindung mit Ihrer Anwendung herstellt, rufen Sie dann aus dem Repository ab, zu welchen Gruppen der Benutzer gehört, und fügen diesen Benutzer manuell zu diesen Gruppen hinzu.

Wenn die Verbindung nach einer vorübergehenden Unterbrechung wiederhergestellt wird, wird der Benutzer den zuvor zugewiesenen Gruppen automatisch erneut beitreten. Das automatische erneute Beitreten zu einer Gruppe gilt nur, wenn die Verbindung wiederhergestellt wird, nicht beim Herstellen einer neuen Verbindung. Ein digital signiertes Token wird vom Client übergeben, der die Liste der zuvor zugewiesenen Gruppen enthält. Wenn Sie überprüfen möchten, ob der Benutzer zu den angeforderten Gruppen gehört, können Sie das Standardverhalten überschreiben.

Dieses Thema enthält die folgenden Abschnitte:

Hinzufügen und Entfernen von Benutzern

Zum Hinzufügen oder Entfernen von Benutzern aus einer Gruppe rufen Sie die Add- oder Remove-Methoden auf und übergeben die Verbindungs-ID und den Gruppennamen des Benutzers als Parameter. Sie müssen einen Benutzer nicht manuell aus einer Gruppe entfernen, wenn die Verbindung endet.

Im folgenden Beispiel werden die Methoden und Groups.Remove gezeigt, die Groups.Add in den Hub-Methoden verwendet werden.

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

Die Groups.Add Methoden und Groups.Remove werden asynchron ausgeführt.

Wenn Sie einer Gruppe einen Client hinzufügen und mithilfe der Gruppe sofort eine Nachricht an den Client senden möchten, müssen Sie sicherstellen, dass die Groups.Add-Methode zuerst abgeschlossen wird. In den folgenden Codebeispielen wird gezeigt, wie dies funktioniert, wobei eines mithilfe von Code verwendet wird, der in .NET 4.5 funktioniert, und eines mithilfe von Code, der in .NET 4 funktioniert.

Asynchrones .NET 4.5-Beispiel

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

Asynchrones .NET 4-Beispiel

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

Im Allgemeinen sollten Sie beim Aufrufen der Groups.Remove -Methode nicht einschließenawait, da die Verbindungs-ID, die Sie entfernen möchten, möglicherweise nicht mehr verfügbar ist. In diesem Fall wird ausgelöst, TaskCanceledException nachdem das Anforderungs-Zeitüberschreitungsout aufgetreten ist. Wenn Ihre Anwendung sicherstellen muss, dass der Benutzer aus der Gruppe entfernt wurde, bevor sie eine Nachricht an die Gruppe sendet, können Sie vor Groups.Remove hinzufügen await und dann die TaskCanceledException möglicherweise ausgelöste Ausnahme abfangen.

Aufrufen von Mitgliedern einer Gruppe

Sie können Nachrichten an alle Mitglieder einer Gruppe oder nur an bestimmte Mitglieder der Gruppe senden, wie in den folgenden Beispielen gezeigt.

  • Alle verbundenen Clients in einer angegebenen Gruppe.

    Clients.Group(groupName).addChatMessage(name, message);
    
  • Alle verbundenen Clients in einer angegebenen Gruppe mit Ausnahme der angegebenen Clients, die durch die Verbindungs-ID identifiziert werden.

    Clients.Group(groupName, connectionId1, connectionId2).addChatMessage(name, message);
    
  • Alle verbundenen Clients in einer angegebenen Gruppe mit Ausnahme des aufrufenden Clients.

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

Speichern der Gruppenmitgliedschaft in einer Datenbank

Die folgenden Beispiele zeigen, wie Gruppen- 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. Dieses Beispiel enthält eine Klasse namens ConversationRoom , die für eine Anwendung eindeutig wäre, mit der Benutzer an Unterhaltungen zu verschiedenen Themen teilnehmen können, z. B. Sport oder Gartenarbeit. Dieses Beispiel enthält auch eine Klasse für die Verbindungen. Die Verbindungsklasse ist für die Nachverfolgung der Gruppenmitgliedschaft nicht unbedingt erforderlich, ist aber häufig Teil einer robusten Lösung für die Nachverfolgung von Benutzern.

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

Anschließend können Sie im Hub die Gruppen- und Benutzerinformationen aus der Datenbank abrufen und den Benutzer manuell den entsprechenden Gruppen hinzufügen. Das Beispiel enthält keinen Code zum Nachverfolgen der Benutzerverbindungen. In diesem Beispiel wird die await Schlüsselwort (keyword) nicht zuvor Groups.Add angewendet, da eine Nachricht nicht sofort an Mitglieder der Gruppe gesendet wird. Wenn Sie unmittelbar nach dem Hinzufügen des neuen Mitglieds eine Nachricht an alle Mitglieder der Gruppe senden möchten, sollten Sie die await Schlüsselwort (keyword) anwenden, um sicherzustellen, dass der asynchrone Vorgang abgeschlossen ist.

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

Speichern der Gruppenmitgliedschaft in Azure Table Storage

Die Verwendung von Azure Table Storage zum Speichern von Gruppen- und Benutzerinformationen ähnelt der Verwendung einer Datenbank. Das folgende Beispiel zeigt eine Tabellenentität, in der der Benutzername und der Gruppenname gespeichert werden.

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

Im Hub rufen Sie die zugewiesenen Gruppen ab, wenn der Benutzer eine Verbindung herstellt.

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

Überprüfen der Gruppenmitgliedschaft beim erneuten Herstellen der Verbindung

Standardmäßig weist SignalR einen Benutzer automatisch den entsprechenden Gruppen zu, wenn die Verbindung nach einer vorübergehenden Unterbrechung wiederhergestellt wird, z. B. wenn eine Verbindung unterbrochen und erneut hergestellt wird, bevor das Verbindungstimen auftritt. Die Gruppeninformationen des Benutzers werden in einem Token übergeben, wenn die Verbindung wiederhergestellt wird, und dieses Token wird auf dem Server überprüft. Informationen zum Überprüfungsprozess für das erneute Beitreten von Benutzern zu Gruppen finden Sie unter Erneutes Beitreten von Gruppen beim wiederherstellen der Verbindung.

Im Allgemeinen sollten Sie das Standardverhalten des automatischen erneuten Beitretens von Gruppen bei der erneuten Verbindung verwenden. SignalR-Gruppen sind nicht als Sicherheitsmechanismus zum Einschränken des Zugriffs auf vertrauliche Daten vorgesehen. Wenn Ihre Anwendung jedoch die Gruppenmitgliedschaft eines Benutzers beim erneuten Herstellen der Verbindung überprüfen muss, können Sie das Standardverhalten überschreiben. Das Ändern des Standardverhaltens kann ihre Datenbank belasten, da die Gruppenmitgliedschaft eines Benutzers für jede erneute Verbindung abgerufen werden muss und nicht nur, wenn der Benutzer eine Verbindung herstellt.

Wenn Sie die Gruppenmitgliedschaft beim erneuten Herstellen der Verbindung überprüfen müssen, erstellen Sie ein neues Hubpipelinemodul, das eine Liste der zugewiesenen Gruppen zurückgibt, wie unten gezeigt.

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

Fügen Sie dann dieses Modul der Hubpipeline hinzu, wie unten hervorgehoben.

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