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

Патрик Флетчер, Том ФицМАккен

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

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

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

Обзор

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

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

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

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

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

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

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

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

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 завершится первым. В следующих примерах кода показано, как это сделать, один с помощью кода, который работает в .NET 4.5, и один с помощью кода, работающего в .NET 4.

Пример асинхронного .NET 4.5

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

Пример асинхронного .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."));
}

Как правило, при вызове 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; }
    }
}

Затем в центре можно получить сведения о группе и пользователе из базы данных и вручную добавить пользователя в соответствующие группы. В этом примере не содержится код для отслеживания подключений пользователей. В этом примере ключевое await слово не применяется раньше Groups.Add , так как сообщение не сразу отправляется членам группы. Если вы хотите отправить сообщение всем членам группы сразу после добавления нового члена, необходимо применить 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 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());
    }
}