Mapování uživatelů knihovny SignalR na připojení v SignalR 1.x
Patrick Fletcher, Tom FitzMacken
Upozornění
Tato dokumentace není určená pro nejnovější verzi služby SignalR. Podívejte se na ASP.NET Core SignalR.
Toto téma ukazuje, jak zachovat informace o uživatelích a jejich připojeních.
Úvod
Každý klient, který se připojuje k rozbočovači, předá jedinečné ID připojení. Tuto hodnotu můžete načíst ve Context.ConnectionId
vlastnosti kontextu centra. Pokud vaše aplikace potřebuje namapovat uživatele na ID připojení a toto mapování zachovat, můžete použít jednu z následujících možností:
- Úložiště v paměti, například slovník
- Skupina SignalR pro každého uživatele
- Trvalé externí úložiště, například databázová tabulka nebo úložiště tabulek Azure
Každá z těchto implementací je uvedena v tomto tématu. Ke sledování stavu připojení uživatele se používají OnConnected
metody Hub
, OnDisconnected
a OnReconnected
třídy .
Nejlepší přístup pro vaši aplikaci závisí na:
- Počet webových serverů hostujících vaši aplikaci.
- Jestli potřebujete získat seznam aktuálně připojených uživatelů.
- Jestli potřebujete zachovat informace o skupinách a uživatelích při restartování aplikace nebo serveru.
- Jestli je latence volání externího serveru problémem.
Následující tabulka ukazuje, který přístup je vhodný pro tyto aspekty.
Aspekty | Více než jeden server | Získání seznamu aktuálně připojených uživatelů | Zachovat informace po restartování | Optimální výkon |
---|---|---|---|---|
V paměti | ||||
Skupiny pro jednoho uživatele | ||||
Trvalé, externí |
Úložiště v paměti
Následující příklady ukazují, jak zachovat informace o připojení a uživateli ve slovníku, který je uložený v paměti. Slovník používá HashSet
k uložení ID připojení . Uživatel může mít kdykoli více než jedno připojení k aplikaci SignalR. Například uživatel, který je připojený prostřednictvím více zařízení nebo více než jedné karty prohlížeče, by měl více než jedno ID připojení.
Pokud se aplikace vypne, ztratí se všechny informace, ale budou znovu vyplněny, jakmile uživatelé znovu naváže připojení. Úložiště v paměti nefunguje, pokud vaše prostředí obsahuje více než jeden webový server, protože každý server má samostatnou kolekci připojení.
První příklad ukazuje třídu, která spravuje mapování uživatelů na připojení. Klíč pro HashSet bude jméno uživatele.
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);
}
}
}
}
}
}
Další příklad ukazuje, jak používat třídu mapování připojení z centra. Instance třídy je uložena v proměnné s názvem _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();
}
}
}
Skupiny pro jednoho uživatele
Můžete vytvořit skupinu pro každého uživatele a poté odeslat zprávu této skupině, pokud chcete kontaktovat pouze daného uživatele. Název každé skupiny je jméno uživatele. Pokud má uživatel více než jedno připojení, každé ID připojení se přidá do skupiny uživatele.
Uživatele byste neměli ručně odebírat ze skupiny, když se uživatel odpojí. Tuto akci automaticky provede architektura SignalR.
Následující příklad ukazuje, jak implementovat skupiny pro jednoho uživatele.
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();
}
}
}
Trvalé externí úložiště
Toto téma ukazuje, jak k ukládání informací o připojení použít databázi nebo úložiště tabulek Azure. Tento přístup funguje, když máte více webových serverů, protože každý webový server může pracovat se stejným úložištěm dat. Pokud webové servery přestanou fungovat nebo se aplikace restartuje, OnDisconnected
metoda se nevolá. Proto je možné, že vaše úložiště dat bude obsahovat záznamy pro ID připojení, která už nejsou platná. Pokud chcete tyto osamocené záznamy vyčistit, můžete chtít zrušit platnost připojení vytvořeného mimo časový rámec, který je relevantní pro vaši aplikaci. Příklady v této části zahrnují hodnotu pro sledování, kdy bylo připojení vytvořeno, ale neukazují, jak vyčistit staré záznamy, protože to můžete chtít udělat jako proces na pozadí.
Databáze
Následující příklady ukazují, jak zachovat informace o připojení a uživatelích v databázi. Můžete použít jakoukoli technologii přístupu k datům; Následující příklad však ukazuje, jak definovat modely pomocí Entity Frameworku. Tyto modely entit odpovídají databázovým tabulkám a polím. Vaše datová struktura se může výrazně lišit v závislosti na požadavcích vaší aplikace.
První příklad ukazuje, jak definovat entitu uživatele, která může být přidružena k mnoha entitami připojení.
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; }
}
}
Pak můžete z centra sledovat stav jednotlivých připojení pomocí kódu uvedeného níže.
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();
}
}
}
Azure Table Storage
Následující příklad úložiště tabulky Azure je podobný příkladu databáze. Neobsahuje všechny informace, které byste potřebovali, abyste mohli začít se službou Azure Table Storage. Informace najdete v tématu Použití služby Table Storage z .NET.
Následující příklad ukazuje entitu tabulky pro ukládání informací o připojení. Rozdělí data podle uživatelského jména a každou entitu identifikuje podle ID připojení, aby uživatel mohl mít kdykoli více připojení.
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;
}
}
}
V centru můžete sledovat stav připojení jednotlivých uživatelů.
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");
}
}
}