Работа с группами в SignalR

Патрик Флетчер (Patrick Fletcher), Том ФитцМаккен (Tom FitzMacken)

Предупреждение

Эта документация не подходит для последней версии SignalR. Ознакомьтесь с ASP.NET Core SignalR.

В этом разделе описывается добавление пользователей в группы и сохранение сведений о членстве в группах.

Версии программного обеспечения, используемые в этом разделе

Предыдущие версии этого раздела

Сведения о более ранних версиях SignalR см. в разделе Старые версии SignalR.

Вопросы и комментарии

Оставьте отзыв о том, как вам понравилось это руководство и что мы могли бы улучшить в комментариях в нижней части страницы. Если у вас есть вопросы, которые не связаны непосредственно с руководством, вы можете опубликовать их на форуме ASP.NET SignalR или StackOverflow.com.

Общие сведения

Группы в SignalR предоставляют метод для трансляции сообщений в указанные подмножества подключенных клиентов. Группа может иметь любое количество клиентов, а клиент может быть членом любого числа групп. Вам не нужно явно создавать группы. Фактически группа создается автоматически при первом указании ее имени в вызове Groups.Add и удаляется при удалении последнего подключения из членства в ней. Общие сведения об использовании групп см. в статье Управление членством в группах из класса Hub в руководстве по api концентраторов — руководство по серверу.

Api для получения списка членства в группах или списка групп отсутствует. SignalR отправляет сообщения клиентам и группам на основе модели pub/sub, а сервер не поддерживает списки групп или членства в группах. Это помогает добиться максимальной масштабируемости, так как при каждом добавлении узла в веб-ферму любое состояние, поддерживаемое SignalR, должно распространяться на новый узел.

При добавлении пользователя в группу с помощью Groups.Add метода пользователь получает сообщения, направленные в эту группу в течение текущего соединения, но членство пользователя в этой группе не сохраняется за пределами текущего соединения. Если вы хотите постоянно хранить сведения о группах и членстве в группах, необходимо хранить эти данные в репозитории, например в базе данных или хранилище таблиц Azure. Затем каждый раз, когда пользователь подключается к приложению, вы извлекаете из репозитория группы, к которым принадлежит пользователь, и вручную добавляете этого пользователя в эти группы.

При повторном подключении после временного сбоя пользователь автоматически повторно присоединяется к ранее назначенным группам. Автоматическое повторное присоединение к группе применяется только при повторном подключении, но не при установке нового подключения. От клиента, содержащего список ранее назначенных групп, передается маркер с цифровой подписью. Если вы хотите проверить, принадлежит ли пользователь к запрошенным группам, можно переопределить поведение по умолчанию.

Этот раздел включает следующие подразделы:

Добавление и удаление пользователей

Чтобы добавить или удалить пользователей из группы, вызовите методы Add или Remove и передайте идентификатор подключения пользователя и имя группы в качестве параметров. Вам не нужно вручную удалять пользователя из группы после завершения подключения.

В следующем примере показаны методы Groups.Add и Groups.Remove , используемые в методах hub.

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

Методы Groups.Add и Groups.Remove выполняются асинхронно.

Если вы хотите добавить клиента в группу и немедленно отправить клиенту сообщение с помощью группы, необходимо убедиться, что метод Groups.Add будет завершен первым. В следующих примерах кода показано, как это сделать.

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

Как правило, не следует включать await при вызове Groups.Remove метода, так как идентификатор подключения, который вы пытаетесь удалить, может оказаться недоступным. В этом случае TaskCanceledException возникает по истечении времени ожидания запроса. Если приложение должно убедиться, что пользователь был удален из группы перед отправкой сообщения в группу, можно добавить await до Groups.Remove, а затем перехватить TaskCanceledException исключение, которое может быть вызвано.

Вызов участников группы

Вы можете отправлять сообщения всем участникам группы или только указанным членам группы, как показано в следующих примерах.

  • Все подключенные клиенты в указанной группе.

    Clients.Group(groupName).addChatMessage(name, message);
    
  • Все подключенные клиенты в указанной группе , кроме указанных клиентов, идентифицируемых по идентификатору подключения.

    Clients.Group(groupName, connectionId1, connectionId2).addChatMessage(name, message);
    
  • Все подключенные клиенты в указанной группе , кроме вызывающего клиента.

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

Хранение членства в группе в базе данных

В следующих примерах показано, как сохранить сведения о группах и пользователях в базе данных. Вы можете использовать любую технологию доступа к данным; Однако в приведенном ниже примере показано, как определить модели с помощью Entity Framework. Эти модели сущностей соответствуют таблицам и полям базы данных. Структура данных может значительно отличаться в зависимости от требований приложения. Этот пример включает класс с именем ConversationRoom , который будет уникальным для приложения, которое позволяет пользователям присоединяться к беседам на различные темы, такие как спорт или садоводство. В этом примере также содержится класс для подключений. Класс подключения не является абсолютно обязательным для отслеживания членства в группах, но часто является частью надежного решения для отслеживания пользователей.

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

Затем в центре можно получить сведения о группе и пользователе из базы данных и вручную добавить пользователя в соответствующие группы. В этом примере не содержится код для отслеживания подключений пользователей. В этом примере ключевое слово не применяется раньшеGroups.Add, await так как сообщение не отправляется сразу членам группы. Если вы хотите отправить сообщение всем участникам группы сразу после добавления нового члена, следует применить await ключевое слово, чтобы убедиться, что асинхронная операция завершена.

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

Хранение членства в группах в хранилище таблиц Azure

Использование хранилища таблиц Azure для хранения сведений о группах и пользователях аналогично использованию базы данных. В следующем примере показана сущность таблицы, в котором хранятся имя пользователя и имя группы.

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

В центре вы получите назначенные группы при подключении пользователя.

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

Проверка членства в группе при повторном подключении

По умолчанию SignalR автоматически повторно назначает пользователя соответствующим группам при повторном подключении из-за временного сбоя, например при разрыве и повторном подключении до истечения времени ожидания подключения. Сведения о группе пользователя передаются в маркер при повторном подключении, и этот маркер проверяется на сервере. Сведения о процессе проверки для повторного присоединения пользователей к группам см. в разделе Повторное присоединение к группам при повторном подключении.

Как правило, следует использовать поведение по умолчанию— автоматическое повторное присоединение к группам при повторном подключении. Группы SignalR не предназначены как механизм безопасности для ограничения доступа к конфиденциальным данным. Однако если приложение должно дважды проверка членство пользователя в группе при повторном подключении, можно переопределить поведение по умолчанию. Изменение поведения по умолчанию может создать нагрузку на базу данных, так как членство пользователя в группах должно быть извлечено для каждого повторного подключения, а не только при подключении пользователя.

Если необходимо проверить членство в группе при повторном подключении, создайте модуль конвейера концентратора, который возвращает список назначенных групп, как показано ниже.

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

Затем добавьте этот модуль в конвейер концентратора, как показано ниже.

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