Asignar usuarios de SignalR a las conexiones en SignalR 1.x
por Patrick Fletcher, Tom FitzMacken
Advertencia
Esta documentación no es para la última versión de SignalR. Eche un vistazo a ASP.NET Core SignalR.
Este tema muestra cómo conservar la información sobre los usuarios y sus conexiones.
Introducción
Cada cliente que se conecta a un hub pasa un id. de conexión único. Puede recuperar este valor en la propiedad Context.ConnectionId
del contexto del hub. Si su aplicación necesita asignar un usuario al id. de conexión y conservar esa asignación, puede usar uno de los siguientes:
- Almacenamiento en memoria, como un diccionario
- Grupo SignalR para cada usuario
- Almacenamiento externo y permanente, como una tabla de base de datos o el almacenamiento de tablas de Azure
Cada una de estas implementaciones se muestra en este tema. Use los métodos OnConnected
, OnDisconnected
y OnReconnected
de la clase Hub
para hacer un seguimiento del estado de la conexión del usuario.
El mejor enfoque para su aplicación depende de:
- El número de servidores web que hospedan su aplicación.
- Si necesita obtener una lista de los usuarios conectados actualmente.
- Si necesita conservar la información sobre grupos y usuarios cuando se reinicie la aplicación o el servidor.
- Si la latencia de llamar a un servidor externo es un problema.
La siguiente tabla muestra qué enfoque funciona para estas consideraciones.
Consideración | Más de un servidor | Obtener la lista de usuarios conectados actualmente | Conservar la información tras los reinicios | Rendimiento óptimo |
---|---|---|---|---|
En memoria | ||||
Grupos de usuarios únicos | ||||
Permanente, externo |
Almacenamiento en memoria
Los siguientes ejemplos muestran cómo conservar la información sobre la conexión y el usuario en un diccionario que se almacena en la memoria. El diccionario usa un HashSet
para almacenar el identificador de conexión. En cualquier momento, un usuario puede tener más de una conexión con la aplicación de SignalR. Por ejemplo, un usuario que esté conectado a través de varios dispositivos o más de una pestaña del explorador tendría más de un id. de conexión.
Si la aplicación se cierra, toda la información se pierde, pero volverá a rellenarse a medida que los usuarios restablezcan sus conexiones. El almacenamiento en memoria no funciona si su entorno incluye más de un servidor web porque cada servidor tendría una colección separada de conexiones.
El primer ejemplo muestra una clase que administra la asignación de usuarios a conexiones. La clave del HashSet será el nombre del usuario.
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);
}
}
}
}
}
}
El siguiente ejemplo muestra cómo usar la clase de asignación de conexiones de un hub. La instancia de la clase se almacena en un nombre de variable _connections
.
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()
{
string name = Context.User.Identity.Name;
_connections.Remove(name, Context.ConnectionId);
return base.OnDisconnected();
}
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 usuarios únicos
Puede crear un grupo para cada usuario y después enviar un mensaje a ese grupo cuando quiera llegar solo a ese usuario. El nombre de cada grupo es el nombre del usuario. Si un usuario tiene más de una conexión, cada id. de conexión se agrega al grupo del usuario.
No debe quitar manualmente el usuario del grupo cuando el usuario se desconecte. Esta acción la realiza automáticamente el marco SignalR.
El siguiente ejemplo muestra cómo implementar grupos de usuarios únicos.
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();
}
}
}
Almacenamiento externo permanente
Este tema muestra cómo usar una base de datos o el almacenamiento de tablas de Azure para almacenar la información de conexión. Este enfoque funciona cuando se tienen varios servidores web porque cada servidor web puede interactuar con el mismo repositorio de datos. Si sus servidores web dejan de funcionar o la aplicación se reinicia, no se llama al método OnDisconnected
. Por lo tanto, es posible que su repositorio de datos tenga registros de id. de conexión que ya no sean válidos. Para limpiar estos registros huérfanos, puede querer invalidar cualquier conexión que se haya creado fuera de un marco temporal relevante para su aplicación. Los ejemplos de esta sección incluyen un valor para hacer un seguimiento de cuándo se creó la conexión, pero no muestran cómo limpiar los registros antiguos porque es posible que quiera hacerlo como proceso en segundo plano.
Base de datos
Los siguientes ejemplos muestran cómo conservar la información de conexión y de usuario en una base de datos. Puede usar cualquier tecnología de acceso a datos; sin embargo, el ejemplo siguiente muestra cómo definir modelos usando Entity Framework. Estos modelos de entidad corresponden a tablas y campos de la base de datos. Su estructura de datos podría variar considerablemente en función de los requisitos de su aplicación.
El primer ejemplo muestra cómo definir una entidad de usuario que puede asociarse a muchas entidades de conexión.
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; }
}
}
Después, desde el hub, podrá seguir el estado de cada conexión con el código que se muestra a continuación.
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()
{
using (var db = new UserContext())
{
var connection = db.Connections.Find(Context.ConnectionId);
connection.Connected = false;
db.SaveChanges();
}
return base.OnDisconnected();
}
}
}
Almacenamiento de tablas de Azure
El siguiente ejemplo de almacenamiento de tablas de Azure es similar al ejemplo de la base de datos. No incluye toda la información necesaria para empezar a utilizar el servicio de Azure Table Storage. Para más información, consulte Cómo usar el almacenamiento en tablas de .NET.
El siguiente ejemplo muestra una entidad de tabla para almacenar información de conexión. Divide los datos por nombre de usuario e identifica cada entidad por el id. de conexión, de modo que un usuario puede tener varias conexiones en cualquier 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;
}
}
}
En el hub se realiza un seguimiento del estado de la conexión de cada usuario.
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()
{
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();
}
private CloudTable GetConnectionTable()
{
var storageAccount =
CloudStorageAccount.Parse(
CloudConfigurationManager.GetSetting("StorageConnectionString"));
var tableClient = storageAccount.CreateCloudTableClient();
return tableClient.GetTableReference("connection");
}
}
}