Mapeamento de usuários do SignalR para conexões
por 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 mostra como reter informações sobre usuários e suas conexões.
Patrick Fletcher ajudou a escrever este tópico.
Versões de software usadas neste tópico
- Visual Studio 2013
- .NET 4.5
- SignalR versão 2
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 perguntas que não estão diretamente relacionadas ao tutorial, poderá postá-las no fórum do ASP.NET SignalR ou StackOverflow.com.
Introdução
Cada cliente que se conecta a um hub passa uma ID de conexão exclusiva. Você pode recuperar esse valor na Context.ConnectionId
propriedade do contexto do hub. Se o aplicativo precisar mapear um usuário para a ID de conexão e persistir esse mapeamento, você poderá usar um dos seguintes:
- O provedor de ID de usuário (SignalR 2)
- Armazenamento na memória, como um dicionário
- Grupo SignalR para cada usuário
- Armazenamento externo permanente, como uma tabela de banco de dados ou armazenamento de tabelas do Azure
Cada uma dessas implementações é mostrada neste tópico. Você usa os OnConnected
métodos , OnDisconnected
e OnReconnected
da Hub
classe para acompanhar a conexão do usuário status.
A melhor abordagem para seu aplicativo depende de:
- O número de servidores Web que hospedam seu aplicativo.
- Se você precisa obter uma lista dos usuários conectados no momento.
- Se você precisa persistir informações de grupo e usuário quando o aplicativo ou servidor é reiniciado.
- Se a latência de chamar um servidor externo é um problema.
A tabela a seguir mostra qual abordagem funciona para essas considerações.
Consideração | Mais de um servidor | Obter lista de usuários conectados no momento | Manter informações após reinicializações | Desempenho ideal |
---|---|---|---|---|
Provedor UserID | ||||
Na memória | ||||
Grupos de usuário único | ||||
Permanente, externo |
Provedor IUserID
Esse recurso permite que os usuários especifiquem qual é a userId baseada em um IRequest por meio de uma nova interface IUserIdProvider.
O IUserIdProvider
public interface IUserIdProvider
{
string GetUserId(IRequest request);
}
Por padrão, haverá uma implementação que usa o do usuário como o nome de IPrincipal.Identity.Name
usuário. Para alterar isso, registre sua implementação de IUserIdProvider
com o host global quando seu aplicativo for iniciado:
GlobalHost.DependencyResolver.Register(typeof(IUserIdProvider), () => new MyIdProvider());
De dentro de um hub, você poderá enviar mensagens para esses usuários por meio da seguinte API:
Enviar uma mensagem para um usuário específico
public class MyHub : Hub
{
public void Send(string userId, string message)
{
Clients.User(userId).send(message);
}
}
Armazenamento na memória
Os exemplos a seguir mostram como reter informações de conexão e de usuário em um dicionário armazenado na memória. O dicionário usa um HashSet
para armazenar a ID da conexão. A qualquer momento, um usuário pode ter mais de uma conexão com o aplicativo SignalR. Por exemplo, um usuário conectado por meio de vários dispositivos ou mais de uma guia do navegador teria mais de uma ID de conexão.
Se o aplicativo for desligado, todas as informações serão perdidas, mas serão preenchidas novamente à medida que os usuários restabelecerem suas conexões. O armazenamento na memória não funcionará se o ambiente incluir mais de um servidor Web porque cada servidor teria uma coleção separada de conexões.
O primeiro exemplo mostra uma classe que gerencia o mapeamento de usuários para conexões. A chave para o HashSet será o nome do usuário.
using System.Collections.Generic;
using System.Linq;
namespace BasicChat
{
public class ConnectionMapping<T>
{
private readonly Dictionary<T, HashSet<string>> _connections =
new Dictionary<T, HashSet<string>>();
public int Count
{
get
{
return _connections.Count;
}
}
public void Add(T key, string connectionId)
{
lock (_connections)
{
HashSet<string> connections;
if (!_connections.TryGetValue(key, out connections))
{
connections = new HashSet<string>();
_connections.Add(key, connections);
}
lock (connections)
{
connections.Add(connectionId);
}
}
}
public IEnumerable<string> GetConnections(T key)
{
HashSet<string> connections;
if (_connections.TryGetValue(key, out connections))
{
return connections;
}
return Enumerable.Empty<string>();
}
public void Remove(T key, string connectionId)
{
lock (_connections)
{
HashSet<string> connections;
if (!_connections.TryGetValue(key, out connections))
{
return;
}
lock (connections)
{
connections.Remove(connectionId);
if (connections.Count == 0)
{
_connections.Remove(key);
}
}
}
}
}
}
O exemplo a seguir mostra como usar a classe de mapeamento de conexão de um hub. A instância da classe é armazenada em um nome _connections
de variável .
using System.Threading.Tasks;
using Microsoft.AspNet.SignalR;
namespace BasicChat
{
[Authorize]
public class ChatHub : Hub
{
private readonly static ConnectionMapping<string> _connections =
new ConnectionMapping<string>();
public void SendChatMessage(string who, string message)
{
string name = Context.User.Identity.Name;
foreach (var connectionId in _connections.GetConnections(who))
{
Clients.Client(connectionId).addChatMessage(name + ": " + message);
}
}
public override Task OnConnected()
{
string name = Context.User.Identity.Name;
_connections.Add(name, Context.ConnectionId);
return base.OnConnected();
}
public override Task OnDisconnected(bool stopCalled)
{
string name = Context.User.Identity.Name;
_connections.Remove(name, Context.ConnectionId);
return base.OnDisconnected(stopCalled);
}
public override Task OnReconnected()
{
string name = Context.User.Identity.Name;
if (!_connections.GetConnections(name).Contains(Context.ConnectionId))
{
_connections.Add(name, Context.ConnectionId);
}
return base.OnReconnected();
}
}
}
Grupos de usuário único
Você pode criar um grupo para cada usuário e, em seguida, enviar uma mensagem para esse grupo quando quiser acessar apenas esse usuário. O nome de cada grupo é o nome do usuário. Se um usuário tiver mais de uma conexão, cada ID de conexão será adicionada ao grupo do usuário.
Você não deve remover manualmente o usuário do grupo quando o usuário se desconectar. Essa ação é executada automaticamente pela estrutura do SignalR.
O exemplo a seguir mostra como implementar grupos de usuário único.
using Microsoft.AspNet.SignalR;
using System;
using System.Threading.Tasks;
namespace BasicChat
{
[Authorize]
public class ChatHub : Hub
{
public void SendChatMessage(string who, string message)
{
string name = Context.User.Identity.Name;
Clients.Group(who).addChatMessage(name + ": " + message);
}
public override Task OnConnected()
{
string name = Context.User.Identity.Name;
Groups.Add(Context.ConnectionId, name);
return base.OnConnected();
}
}
}
Armazenamento permanente e externo
Este tópico mostra como usar um banco de dados ou um armazenamento de tabelas do Azure para armazenar informações de conexão. Essa abordagem funciona quando você tem vários servidores Web porque cada servidor Web pode interagir com o mesmo repositório de dados. Se os servidores Web pararem de funcionar ou o aplicativo for reiniciado, o OnDisconnected
método não será chamado. Portanto, é possível que o repositório de dados tenha registros para ids de conexão que não são mais válidos. Para limpo esses registros órfãos, convém invalidar qualquer conexão que tenha sido criada fora de um período de tempo relevante para seu aplicativo. Os exemplos nesta seção incluem um valor para acompanhamento quando a conexão foi criada, mas não mostram como limpo registros antigos porque talvez você queira fazer isso como processo em segundo plano.
Banco de dados
Os exemplos a seguir mostram como reter informações de conexão 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.
O primeiro exemplo mostra como definir uma entidade de usuário que pode ser associada a muitas entidades de conexão.
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Data.Entity;
namespace MapUsersSample
{
public class UserContext : DbContext
{
public DbSet<User> Users { get; set; }
public DbSet<Connection> Connections { get; set; }
}
public class User
{
[Key]
public string UserName { get; set; }
public ICollection<Connection> Connections { get; set; }
}
public class Connection
{
public string ConnectionID { get; set; }
public string UserAgent { get; set; }
public bool Connected { get; set; }
}
}
Em seguida, no hub, você pode acompanhar o estado de cada conexão com o código mostrado abaixo.
using System;
using System.Data.Entity;
using System.Linq;
using System.Threading.Tasks;
using System.Collections.Concurrent;
using Microsoft.AspNet.SignalR;
namespace MapUsersSample
{
[Authorize]
public class ChatHub : Hub
{
public void SendChatMessage(string who, string message)
{
var name = Context.User.Identity.Name;
using (var db = new UserContext())
{
var user = db.Users.Find(who);
if (user == null)
{
Clients.Caller.showErrorMessage("Could not find that user.");
}
else
{
db.Entry(user)
.Collection(u => u.Connections)
.Query()
.Where(c => c.Connected == true)
.Load();
if (user.Connections == null)
{
Clients.Caller.showErrorMessage("The user is no longer connected.");
}
else
{
foreach (var connection in user.Connections)
{
Clients.Client(connection.ConnectionID)
.addChatMessage(name + ": " + message);
}
}
}
}
}
public override Task OnConnected()
{
var name = Context.User.Identity.Name;
using (var db = new UserContext())
{
var user = db.Users
.Include(u => u.Connections)
.SingleOrDefault(u => u.UserName == name);
if (user == null)
{
user = new User
{
UserName = name,
Connections = new List<Connection>()
};
db.Users.Add(user);
}
user.Connections.Add(new Connection
{
ConnectionID = Context.ConnectionId,
UserAgent = Context.Request.Headers["User-Agent"],
Connected = true
});
db.SaveChanges();
}
return base.OnConnected();
}
public override Task OnDisconnected(bool stopCalled)
{
using (var db = new UserContext())
{
var connection = db.Connections.Find(Context.ConnectionId);
connection.Connected = false;
db.SaveChanges();
}
return base.OnDisconnected(stopCalled);
}
}
}
Armazenamento de tabelas do Azure
O exemplo de armazenamento de tabela do Azure a seguir é semelhante ao exemplo de banco de dados. Ele não inclui todas as informações necessárias para começar a usar o Serviço de Armazenamento de Tabelas do Azure. Para obter informações, consulte Como usar o armazenamento de tabelas do .NET.
O exemplo a seguir mostra uma entidade de tabela para armazenar informações de conexão. Ele particiona os dados por nome de usuário e identifica cada entidade pela ID de conexão, para que um usuário possa ter várias conexões a qualquer momento.
using Microsoft.WindowsAzure.Storage.Table;
using System;
namespace MapUsersSample
{
public class ConnectionEntity : TableEntity
{
public ConnectionEntity() { }
public ConnectionEntity(string userName, string connectionID)
{
this.PartitionKey = userName;
this.RowKey = connectionID;
}
}
}
No hub, você controla o status da conexão de cada usuário.
using Microsoft.AspNet.SignalR;
using Microsoft.WindowsAzure;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Table;
using System;
using System.Linq;
using System.Threading.Tasks;
namespace MapUsersSample
{
public class ChatHub : Hub
{
public void SendChatMessage(string who, string message)
{
var name = Context.User.Identity.Name;
var table = GetConnectionTable();
var query = new TableQuery<ConnectionEntity>()
.Where(TableQuery.GenerateFilterCondition(
"PartitionKey",
QueryComparisons.Equal,
who));
var queryResult = table.ExecuteQuery(query).ToList();
if (queryResult.Count == 0)
{
Clients.Caller.showErrorMessage("The user is no longer connected.");
}
else
{
foreach (var entity in queryResult)
{
Clients.Client(entity.RowKey).addChatMessage(name + ": " + message);
}
}
}
public override Task OnConnected()
{
var name = Context.User.Identity.Name;
var table = GetConnectionTable();
table.CreateIfNotExists();
var entity = new ConnectionEntity(
name.ToLower(),
Context.ConnectionId);
var insertOperation = TableOperation.InsertOrReplace(entity);
table.Execute(insertOperation);
return base.OnConnected();
}
public override Task OnDisconnected(bool stopCalled)
{
var name = Context.User.Identity.Name;
var table = GetConnectionTable();
var deleteOperation = TableOperation.Delete(
new ConnectionEntity(name, Context.ConnectionId) { ETag = "*" });
table.Execute(deleteOperation);
return base.OnDisconnected(stopCalled);
}
private CloudTable GetConnectionTable()
{
var storageAccount =
CloudStorageAccount.Parse(
CloudConfigurationManager.GetSetting("StorageConnectionString"));
var tableClient = storageAccount.CreateCloudTableClient();
return tableClient.GetTableReference("connection");
}
}
}