Trabalhar com grupos no SignalR 1.x

por Patrick Fletcher, Tom FitzMacken

Aviso

Esta documentação não é para a versão mais recente do SignalR. Dê uma olhada em ASP.NET Core SignalR.

Este tópico descreve como adicionar usuários a grupos e manter informações de associação de grupo.

Visão geral

Os grupos no SignalR fornecem um método para transmitir mensagens para subconjuntos especificados de clientes conectados. Um grupo pode ter qualquer número de clientes e um cliente pode ser membro de qualquer número de grupos. Você não precisa criar grupos explicitamente. Na verdade, um grupo é criado automaticamente na primeira vez que você especifica seu nome em uma chamada para Groups.Add e ele é excluído quando você remove a última conexão da associação nele. Para obter uma introdução ao uso de grupos, consulte How to manage group membership from the Hub class in the Hubs API - Server Guide.

Não há nenhuma API para obter uma lista de associação de grupo ou uma lista de grupos. O SignalR envia mensagens para clientes e grupos com base em um modelo pub/sub e o servidor não mantém listas de grupos ou associações de grupo. Isso ajuda a maximizar a escalabilidade, pois sempre que você adiciona um nó a um farm da Web, qualquer estado que o SignalR mantém precisa ser propagado para o novo nó.

Quando você adiciona um usuário a um grupo usando o Groups.Add método , o usuário recebe mensagens direcionadas a esse grupo durante a conexão atual, mas a associação do usuário nesse grupo não é mantida além da conexão atual. Se você quiser reter permanentemente informações sobre grupos e associação de grupo, deverá armazenar esses dados em um repositório, como um banco de dados ou armazenamento de tabelas do Azure. Em seguida, sempre que um usuário se conecta ao seu aplicativo, você recupera do repositório aos quais o usuário pertence e adiciona manualmente esse usuário a esses grupos.

Ao se reconectar após uma interrupção temporária, o usuário ingressa automaticamente nos grupos atribuídos anteriormente. Reencontrar automaticamente um grupo só se aplica ao se reconectar, não ao estabelecer uma nova conexão. Um token assinado digitalmente é passado do cliente que contém a lista de grupos atribuídos anteriormente. Se você quiser verificar se o usuário pertence aos grupos solicitados, poderá substituir o comportamento padrão.

Este tópico inclui as seções a seguir:

Adicionar e remover usuários

Para adicionar ou remover usuários de um grupo, você chama os métodos Adicionar ou Remover e passa a ID de conexão do usuário e o nome do grupo como parâmetros. Você não precisa remover manualmente um usuário de um grupo quando a conexão termina.

O exemplo a seguir mostra os Groups.Add métodos e Groups.Remove usados nos métodos 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);
    }
}

Os Groups.Add métodos e Groups.Remove são executados de forma assíncrona.

Se você quiser adicionar um cliente a um grupo e enviar imediatamente uma mensagem ao cliente usando o grupo, precisará garantir que o método Groups.Add seja concluído primeiro. Os exemplos de código a seguir mostram como fazer isso, um usando código que funciona no .NET 4.5 e outro usando código que funciona no .NET 4.

Exemplo do .NET 4.5 assíncrono

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

Exemplo assíncrono do .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."));
}

Em geral, você não deve incluir await ao chamar o Groups.Remove método porque a ID de conexão que você está tentando remover pode não estar mais disponível. Nesse caso, TaskCanceledException é gerado após o tempo limite da solicitação. Se o aplicativo precisar garantir que o usuário tenha sido removido do grupo antes de enviar uma mensagem para o grupo, você poderá adicionar await antes de Groups.Remove e, em seguida, capturar a TaskCanceledException exceção que pode ser gerada.

Chamando membros de um grupo

Você pode enviar mensagens para todos os membros de um grupo ou apenas membros especificados do grupo, conforme mostrado nos exemplos a seguir.

  • Todos os clientes conectados em um grupo especificado.

    Clients.Group(groupName).addChatMessage(name, message);
    
  • Todos os clientes conectados em um grupo especificado , exceto os clientes especificados, identificados pela ID de conexão.

    Clients.Group(groupName, connectionId1, connectionId2).addChatMessage(name, message);
    
  • Todos os clientes conectados em um grupo especificado , exceto o cliente de chamada.

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

Armazenando a associação de grupo em um banco de dados

Os exemplos a seguir mostram como reter informações de grupo e de usuário em um banco de dados. Você pode usar qualquer tecnologia de acesso a dados; no entanto, o exemplo a seguir mostra como definir modelos usando o Entity Framework. Esses modelos de entidade correspondem a tabelas e campos de banco de dados. Sua estrutura de dados pode variar consideravelmente dependendo dos requisitos do aplicativo. Este exemplo inclui uma classe chamada ConversationRoom que seria exclusiva para um aplicativo que permite que os usuários participem de conversas sobre diferentes assuntos, como esportes ou jardinagem. Este exemplo também inclui uma classe para as conexões. A classe de conexão não é absolutamente necessária para acompanhar a associação de grupo, mas frequentemente faz parte de uma solução robusta para rastrear usuários.

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

Em seguida, no hub, você pode recuperar o grupo e as informações do usuário do banco de dados e adicionar manualmente o usuário aos grupos apropriados. O exemplo não inclui código para acompanhar as conexões de usuário. Neste exemplo, o await palavra-chave não é aplicado antes Groups.Add porque uma mensagem não é enviada imediatamente aos membros do grupo. Se você quiser enviar uma mensagem a todos os membros do grupo imediatamente após adicionar o novo membro, convém aplicar o await palavra-chave para garantir que a operação assíncrona tenha sido concluída.

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

Armazenando a associação de grupo no armazenamento de tabelas do Azure

Usar o armazenamento de tabelas do Azure para armazenar informações de grupo e de usuário é semelhante ao uso de um banco de dados. O exemplo a seguir mostra uma entidade de tabela que armazena o nome de usuário e o nome de grupo.

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

No hub, você recupera os grupos atribuídos quando o usuário se conecta.

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

Verificando a associação de grupo ao se reconectar

Por padrão, o SignalR atribui automaticamente um usuário aos grupos apropriados ao se reconectar de uma interrupção temporária, como quando uma conexão é descartada e restabelecida antes do tempo limite da conexão. As informações de grupo do usuário são passadas em um token ao se reconectar e esse token é verificado no servidor. Para obter informações sobre o processo de verificação para reencontrar usuários em grupos, consulte Reencontrando grupos ao se reconectar.

Em geral, você deve usar o comportamento padrão de reencontrar automaticamente grupos na reconexão. Os grupos do SignalR não se destinam a um mecanismo de segurança para restringir o acesso a dados confidenciais. No entanto, se o aplicativo precisar marcar associação de grupo de um usuário ao se reconectar, você poderá substituir o comportamento padrão. Alterar o comportamento padrão pode adicionar uma carga ao banco de dados porque a associação de grupo de um usuário deve ser recuperada para cada reconexão em vez de apenas quando o usuário se conecta.

Se você precisar verificar a associação de grupo na reconexão, crie um novo módulo de pipeline de hub que retorna uma lista de grupos atribuídos, conforme mostrado abaixo.

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

Em seguida, adicione esse módulo ao pipeline do hub, conforme realçado abaixo.

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