Praca z grupami w usłudze SignalR

Autor: Patrick Fletcher, Tom FitzMacken

Ostrzeżenie

Ta dokumentacja nie dotyczy 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 grupach.

Wersje oprogramowania używane w tym temacie

Poprzednie wersje tego tematu

Aby uzyskać informacje o wcześniejszych wersjach usługi SignalR, zobacz SignalR Starsze wersje.

Pytania i komentarze

Przekaż opinię na temat tego, jak ci się podobał ten samouczek i co możemy ulepszyć w komentarzach w dolnej części strony. Jeśli masz pytania, które nie są bezpośrednio związane z tym samouczkiem, możesz opublikować je na forum ASP.NET SignalR lub StackOverflow.com.

Omówienie

Grupy w usłudze SignalR udostępniają metodę emisji komunikatów do określonych podzestawów połączonych klientów. Grupa może mieć dowolną liczbę klientów, a klient może być członkiem dowolnej liczby grup. Nie trzeba jawnie tworzyć grup. W efekcie grupa jest tworzona automatycznie przy pierwszym określeniu jej nazwy w wywołaniu grupy.Dodaj i jest usuwana 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 poziomu klasy Hubs API — 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 na podstawie modelu pub/podrzędnego, a serwer nie obsługuje list grup ani członkostwa w grupach. Pomaga to zmaksymalizować skalowalność, ponieważ za każdym razem, gdy dodasz węzeł do farmy internetowej, do nowego węzła musi zostać rozpropagowany dowolny stan utrzymywany przez usługę SignalR.

Po dodaniu użytkownika do grupy przy użyciu Groups.Add metody użytkownik odbiera komunikaty kierowane 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łóceniu użytkownik automatycznie ponownie dołącza wcześniej przypisane grupy. 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ć.

public async Task JoinRoom(string roomName)
{
    await Groups.Add(Context.ConnectionId, roomName);
    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łynął limit 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 Groups.Remove, 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 do 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 identyfikowanych 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żytkownikach w bazie danych. Można 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 konwersacji na temat różnych tematów, takich jak sport lub ogrodowanie. Ten przykład obejmuje 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 służącego do śledzenia połączeń użytkowników. W tym przykładzie await słowo kluczowe nie jest stosowane wcześniej Groups.Add , 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 pokazano 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 po nawiązaniu połączenia użytkownik pobiera przypisane grupy.

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 po usunięciu połączenia i ponownym nawiązaniu połączenia przed upływem 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 usługi SignalR nie są przeznaczone jako mechanizm zabezpieczeń umożliwiający ograniczenie dostępu do poufnych danych. Jeśli jednak aplikacja musi dokładnie sprawdzić członkostwo użytkownika w grupie podczas ponownego nawiązywania połączenia, można zastąpić domyślne zachowanie. Zmiana domyślnego zachowania może zwiększać obciążenie bazy danych, ponieważ członkostwo użytkownika w grupie 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 przy ponownym połączeniu, 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 pokazano poniżej.

public partial class Startup {
    public void Configuration(IAppBuilder app) {
        app.MapSignalR();
        GlobalHost.HubPipeline.AddModule(new RejoingGroupPipelineModule());
    }
}