Mapování uživatelů knihovny SignalR na připojení
, autor: 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.
Patrick Fletcher pomohl napsat toto téma.
Verze softwaru použité v tomto tématu
- Visual Studio 2013
- .NET 4.5
- SignalR verze 2
Předchozí verze tohoto tématu
Informace o starších verzích služby SignalR najdete v tématu Starší verze služby SignalR.
Dotazy a komentáře
V komentářích v dolní části stránky nám napište, jak se vám tento kurz líbil a co bychom mohli zlepšit. Pokud máte dotazy, které nesouvisejí přímo s kurzem, můžete je publikovat na fóru ASP.NET SignalR nebo StackOverflow.com.
Ú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í:
- Zprostředkovatel ID uživatele (SignalR 2)
- Ú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 |
---|---|---|---|---|
Zprostředkovatel UserID | ||||
V paměti | ||||
Skupiny pro jednoho uživatele | ||||
Trvalé, externí |
Zprostředkovatel IUserID
Tato funkce umožňuje uživatelům určit, co userId je založeno na IRequest prostřednictvím nového rozhraní IUserIdProvider.
The IUserIdProvider
public interface IUserIdProvider
{
string GetUserId(IRequest request);
}
Ve výchozím nastavení bude existovat implementace, která jako uživatelské jméno používá IPrincipal.Identity.Name
uživatelské jméno . Pokud to chcete změnit, zaregistrujte implementaci nástroje u globálního IUserIdProvider
hostitele při spuštění aplikace:
GlobalHost.DependencyResolver.Register(typeof(IUserIdProvider), () => new MyIdProvider());
Z centra budete moct těmto uživatelům posílat zprávy prostřednictvím následujícího rozhraní API:
Odeslání zprávy konkrétnímu uživateli
public class MyHub : Hub
{
public void Send(string userId, string message)
{
Clients.User(userId).send(message);
}
}
Ú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(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();
}
}
}
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(bool stopCalled)
{
using (var db = new UserContext())
{
var connection = db.Connections.Find(Context.ConnectionId);
connection.Connected = false;
db.SaveChanges();
}
return base.OnDisconnected(stopCalled);
}
}
}
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(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");
}
}
}