Udostępnij za pośrednictwem


Praca z grupami w usłudze SignalR 1.x

Autor: Patrick Fletcher, Tom FitzMacken

Ostrzeżenie

Ta dokumentacja nie jest przeznaczona dla najnowszej wersji usługi SignalR. Przyjrzyj się ASP.NET Core SignalR.

W tym temacie opisano sposób dodawania użytkowników do grup i utrwalania informacji o członkostwie w grupie.

Omówienie

Grupy w usłudze SignalR udostępniają metodę nadawania komunikatów określonym podzbiorom połączonych klientów. Grupa może mieć dowolną liczbę klientów, a klient może być członkiem dowolnej liczby grup. Nie musisz jawnie tworzyć grup. W efekcie grupa jest automatycznie tworzona po raz pierwszy po określeniu jej nazwy w wywołaniu grup.Dodaj i zostanie usunięta po usunięciu ostatniego połączenia z członkostwa w nim. Aby zapoznać się z wprowadzeniem do korzystania z grup, zobacz How to manage group membership from the Hub class in the Hubs API - Server Guide (Jak zarządzać członkostwem w grupie z klasy Hubs w interfejsie API usługi Hubs — Przewodnik po serwerze).

Brak interfejsu API do pobierania listy członkostwa w grupie ani listy grup. Usługa SignalR wysyła komunikaty do klientów i grup w oparciu o model pub/sub, a serwer nie przechowuje list grup ani członkostwa w grupach. Pomaga to zmaksymalizować skalowalność, ponieważ za każdym razem, gdy dodasz węzeł do farmy sieci Web, każdy stan, który utrzymuje usługa SignalR, musi być propagowany do nowego węzła.

Po dodaniu użytkownika do grupy przy użyciu Groups.Add metody użytkownik otrzymuje komunikaty skierowane do tej grupy przez czas trwania bieżącego połączenia, ale członkostwo użytkownika w tej grupie nie jest utrwalane poza bieżącym połączeniem. Jeśli chcesz trwale zachować informacje o grupach i członkostwie w grupach, musisz przechowywać te dane w repozytorium, takim jak baza danych lub usługa Azure Table Storage. Następnie za każdym razem, gdy użytkownik łączy się z aplikacją, pobiera się z repozytorium, do którego należy użytkownik, i ręcznie dodaj tego użytkownika do tych grup.

Podczas ponownego nawiązywania połączenia po tymczasowym zakłóceniach użytkownik automatycznie ponownie dołącza do wcześniej przypisanych grup. Automatyczne ponowne dołączanie grupy ma zastosowanie tylko podczas ponownego nawiązywania połączenia, a nie podczas nawiązywania nowego połączenia. Token podpisany cyfrowo jest przekazywany z klienta, który zawiera listę wcześniej przypisanych grup. Jeśli chcesz sprawdzić, czy użytkownik należy do żądanych grup, możesz zastąpić domyślne zachowanie.

Ten temat zawiera następujące sekcje:

Dodawanie i usuwanie użytkowników

Aby dodać lub usunąć użytkowników z grupy, należy wywołać metody Dodaj lub Usuń i przekazać identyfikator połączenia użytkownika i nazwę grupy jako parametry. Nie trzeba ręcznie usuwać użytkownika z grupy po zakończeniu połączenia.

W poniższym przykładzie przedstawiono Groups.Add metody i Groups.Remove używane w metodach centrum.

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

Metody Groups.Add i Groups.Remove są wykonywane asynchronicznie.

Jeśli chcesz dodać klienta do grupy i natychmiast wysłać komunikat do klienta przy użyciu grupy, musisz upewnić się, że metoda Groups.Add zakończy się najpierw. W poniższych przykładach kodu pokazano, jak to zrobić, korzystając z kodu, który działa na platformie .NET 4.5, a drugi przy użyciu kodu działającego na platformie .NET 4.

Przykład asynchroniczny platformy .NET 4.5

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

Przykład asynchroniczny platformy .NET 4

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

Ogólnie rzecz biorąc, nie należy uwzględniać await podczas wywoływania Groups.Remove metody, ponieważ identyfikator połączenia, który próbujesz usunąć, może nie być już dostępny. W takim przypadku TaskCanceledException jest zgłaszany po upływie limitu czasu żądania. Jeśli aplikacja musi upewnić się, że użytkownik został usunięty z grupy przed wysłaniem komunikatu do grupy, możesz dodać await przed grupą.Usuń, a następnie przechwycić TaskCanceledException wyjątek, który może zostać zgłoszony.

Wywoływanie członków grupy

Komunikaty można wysyłać do wszystkich członków grupy lub tylko określonych członków grupy, jak pokazano w poniższych przykładach.

  • Wszyscy połączeni klienci w określonej grupie.

    Clients.Group(groupName).addChatMessage(name, message);
    
  • Wszyscy połączeni klienci w określonej grupie z wyjątkiem określonych klientów zidentyfikowanych przez identyfikator połączenia.

    Clients.Group(groupName, connectionId1, connectionId2).addChatMessage(name, message);
    
  • Wszyscy połączeni klienci w określonej grupie z wyjątkiem klienta wywołującego.

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

Przechowywanie członkostwa w grupie w bazie danych

W poniższych przykładach pokazano, jak zachować informacje o grupie i użytkowniku w bazie danych. Możesz użyć dowolnej technologii dostępu do danych; jednak w poniższym przykładzie pokazano, jak definiować modele przy użyciu programu Entity Framework. Te modele jednostek odpowiadają tabelom i polam bazy danych. Struktura danych może się znacznie różnić w zależności od wymagań aplikacji. Ten przykład zawiera klasę o nazwie ConversationRoom , która byłaby unikatowa dla aplikacji, która umożliwia użytkownikom dołączanie do rozmów o różnych tematach, takich jak sport lub ogrodowanie. Ten przykład zawiera również klasę dla połączeń. Klasa połączenia nie jest absolutnie wymagana do śledzenia członkostwa w grupie, ale jest często częścią niezawodnego rozwiązania do śledzenia użytkowników.

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

Następnie w centrum możesz pobrać informacje o grupie i użytkownikach z bazy danych i ręcznie dodać użytkownika do odpowiednich grup. Przykład nie zawiera kodu do śledzenia połączeń użytkowników. W tym przykładzie słowo kluczowe nie jest stosowane przedGroups.Add, await ponieważ komunikat nie jest natychmiast wysyłany do członków grupy. Jeśli chcesz wysłać komunikat do wszystkich członków grupy natychmiast po dodaniu nowego członka, należy zastosować await słowo kluczowe, aby upewnić się, że operacja asynchroniczna została ukończona.

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

Przechowywanie członkostwa w grupie w usłudze Azure Table Storage

Używanie usługi Azure Table Storage do przechowywania informacji o grupach i użytkownikach jest podobne do używania bazy danych. W poniższym przykładzie przedstawiono jednostkę tabeli, która przechowuje nazwę użytkownika i nazwę grupy.

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

W centrum pobierasz przypisane grupy po nawiązaniu połączenia przez użytkownika.

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

Weryfikowanie członkostwa w grupie podczas ponownego nawiązywania połączenia

Domyślnie usługa SignalR automatycznie ponownie przypisuje użytkownika do odpowiednich grup podczas ponownego nawiązywania połączenia z powodu tymczasowych zakłóceń, takich jak usunięcie połączenia i ponowne nawiązanie połączenia przed przekroczeniem limitu czasu połączenia. Informacje o grupie użytkownika są przekazywane w tokenie podczas ponownego nawiązywania połączenia, a token jest weryfikowany na serwerze. Aby uzyskać informacje na temat procesu weryfikacji ponownego dołączania użytkowników do grup, zobacz Ponowne dołączanie grup podczas ponownego nawiązywania połączenia.

Ogólnie rzecz biorąc, należy użyć domyślnego zachowania automatycznego ponownego dołączania grup podczas ponownego nawiązywania połączenia. Grupy signalR nie są przeznaczone jako mechanizm zabezpieczeń do ograniczania dostępu do poufnych danych. Jeśli jednak aplikacja musi dwukrotnie sprawdzić członkostwo w grupie użytkownika podczas ponownego nawiązywania połączenia, możesz zastąpić domyślne zachowanie. Zmiana domyślnego zachowania może zwiększyć obciążenie bazy danych, ponieważ członkostwo w grupie użytkownika musi zostać pobrane dla każdego ponownego połączenia, a nie tylko wtedy, gdy użytkownik nawiązuje połączenie.

Jeśli musisz zweryfikować członkostwo w grupie podczas ponownego nawiązywania połączenia, utwórz nowy moduł potoku koncentratora, który zwraca listę przypisanych grup, jak pokazano poniżej.

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

Następnie dodaj ten moduł do potoku koncentratora, jak wyróżniono poniżej.

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