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

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

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

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

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

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

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

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