Partilhar via


Trabalhar com grupos no SignalR

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.

Versões de software usadas neste tópico

Versões anteriores deste tópico

Para obter informações sobre versões anteriores do SignalR, consulte Versões mais antigas do SignalR.

Perguntas e comentários

Deixe comentários sobre como você gostou deste tutorial e o que poderíamos melhorar nos comentários na parte inferior da página. Se você tiver dúvidas que não estão diretamente relacionadas ao tutorial, poderá postá-las no fórum do ASP.NET SignalR ou StackOverflow.com.

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 é excluído quando você remove a última conexão da associação nele. Para obter uma introdução ao uso de grupos, consulte Como gerenciar a associação de grupo da classe Hub na API de Hubs – Guia do Servidor.

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 web farm, 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 para 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 um armazenamento de tabelas do Azure. Em seguida, sempre que um usuário se conecta ao seu aplicativo, você recupera do repositório a quais grupos 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. Reingressar automaticamente em 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:

Adicionando e removendo usuários

Para adicionar ou remover usuários de um grupo, chame os métodos Add ou Remove e passe 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.

public async Task JoinRoom(string roomName)
{
    await Groups.Add(Context.ConnectionId, roomName);
    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 é lançado 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 Groups.Removede 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 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 as informações do grupo e 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 reconectar

Por padrão, o SignalR reatribui 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 reingressar usuários em grupos, consulte Reingressando grupos ao se reconectar.

Em geral, você deve usar o comportamento padrão de reingressar automaticamente em grupos na reconexão. Os grupos do SignalR não se destinam como 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 retorne 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 partial class Startup {
    public void Configuration(IAppBuilder app) {
        app.MapSignalR();
        GlobalHost.HubPipeline.AddModule(new RejoingGroupPipelineModule());
    }
}