Memetakan Pengguna SignalR ke Koneksi di SignalR 1.x
oleh Patrick Fletcher, Tom FitzMacken
Peringatan
Dokumentasi ini bukan untuk versi terbaru SignalR. Lihatlah ASP.NET Core SignalR.
Topik ini menunjukkan cara menyimpan informasi tentang pengguna dan koneksi mereka.
Pengantar
Setiap klien yang terhubung ke hub meneruskan id koneksi unik. Anda dapat mengambil nilai ini di Context.ConnectionId
properti konteks hub. Jika aplikasi Anda perlu memetakan pengguna ke id koneksi dan mempertahankan pemetaan tersebut, Anda dapat menggunakan salah satu hal berikut:
- Penyimpanan dalam memori, seperti kamus
- Grup SignalR untuk setiap pengguna
- Penyimpanan permanen dan eksternal, seperti tabel database atau penyimpanan tabel Azure
Masing-masing implementasi ini ditampilkan dalam topik ini. Anda menggunakan OnConnected
metode , OnDisconnected
, dan OnReconnected
kelas Hub
untuk melacak status koneksi pengguna.
Pendekatan terbaik untuk aplikasi Anda bergantung pada:
- Jumlah server web yang menghosting aplikasi Anda.
- Apakah Anda perlu mendapatkan daftar pengguna yang saat ini tersambung.
- Apakah Anda perlu mempertahankan informasi grup dan pengguna saat aplikasi atau server dimulai ulang.
- Apakah latensi memanggil server eksternal adalah masalah.
Tabel berikut ini memperlihatkan pendekatan mana yang berfungsi untuk pertimbangan ini.
Pertimbangan | Lebih dari satu server | Mendapatkan daftar pengguna yang saat ini tersambung | Mempertahankan informasi setelah menghidupkan ulang | Performa optimal |
---|---|---|---|---|
Dalam memori | ||||
Grup pengguna tunggal | ||||
Permanen, eksternal |
Penyimpanan dalam memori
Contoh berikut menunjukkan cara mempertahankan koneksi dan informasi pengguna dalam kamus yang disimpan dalam memori. Kamus HashSet
menggunakan untuk menyimpan id koneksi. Kapan saja pengguna dapat memiliki lebih dari satu koneksi ke aplikasi SignalR. Misalnya, pengguna yang terhubung melalui beberapa perangkat atau lebih dari satu tab browser akan memiliki lebih dari satu id koneksi.
Jika aplikasi dimatikan, semua informasi hilang, tetapi akan diisi ulang saat pengguna membangun kembali koneksi mereka. Penyimpanan dalam memori tidak berfungsi jika lingkungan Anda menyertakan lebih dari satu server web karena setiap server akan memiliki kumpulan koneksi terpisah.
Contoh pertama menunjukkan kelas yang mengelola pemetaan pengguna ke koneksi. Kunci untuk HashSet akan menjadi nama pengguna.
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);
}
}
}
}
}
}
Contoh berikutnya menunjukkan cara menggunakan kelas pemetaan koneksi dari hub. Instans kelas disimpan dalam nama _connections
variabel .
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();
}
}
}
Grup pengguna tunggal
Anda dapat membuat grup untuk setiap pengguna, lalu mengirim pesan ke grup tersebut saat Anda hanya ingin menjangkau pengguna tersebut. Nama setiap grup adalah nama pengguna. Jika pengguna memiliki lebih dari satu koneksi, setiap id koneksi ditambahkan ke grup pengguna.
Anda tidak boleh menghapus pengguna secara manual dari grup saat pengguna terputus. Tindakan ini secara otomatis dilakukan oleh kerangka kerja SignalR.
Contoh berikut menunjukkan cara mengimplementasikan grup pengguna tunggal.
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();
}
}
}
Penyimpanan eksternal permanen
Topik ini memperlihatkan cara menggunakan database atau penyimpanan tabel Azure untuk menyimpan informasi koneksi. Pendekatan ini berfungsi ketika Anda memiliki beberapa server web karena setiap server web dapat berinteraksi dengan repositori data yang sama. Jika server web Anda berhenti berfungsi atau aplikasi dimulai ulang, OnDisconnected
metode tidak dipanggil. Oleh karena itu, ada kemungkinan bahwa repositori data Anda akan memiliki catatan untuk id koneksi yang tidak lagi valid. Untuk membersihkan catatan yatim piatu ini, Anda mungkin ingin membatalkan koneksi apa pun yang dibuat di luar jangka waktu yang relevan dengan aplikasi Anda. Contoh di bagian ini menyertakan nilai untuk pelacakan saat koneksi dibuat, tetapi tidak memperlihatkan cara membersihkan rekaman lama karena Anda mungkin ingin melakukannya sebagai proses latar belakang.
Database
Contoh berikut menunjukkan cara mempertahankan koneksi dan informasi pengguna dalam database. Anda dapat menggunakan teknologi akses data apa pun; namun, contoh di bawah ini menunjukkan cara menentukan model menggunakan Kerangka Kerja Entitas. Model entitas ini sesuai dengan tabel dan bidang database. Struktur data Anda dapat sangat bervariasi tergantung pada persyaratan aplikasi Anda.
Contoh pertama menunjukkan cara menentukan entitas pengguna yang dapat dikaitkan dengan banyak entitas koneksi.
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; }
}
}
Kemudian, dari hub, Anda dapat melacak status setiap koneksi dengan kode yang ditunjukkan di bawah ini.
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();
}
}
}
Penyimpanan tabel Azure
Contoh penyimpanan tabel Azure berikut ini mirip dengan contoh database. Ini tidak termasuk semua informasi yang Anda perlukan untuk mulai menggunakan Azure Table Storage Service. Untuk informasi, lihat Cara menggunakan penyimpanan Tabel dari .NET.
Contoh berikut menunjukkan entitas tabel untuk menyimpan informasi koneksi. Ini mempartisi data berdasarkan nama pengguna, dan mengidentifikasi setiap entitas dengan id koneksi, sehingga pengguna dapat memiliki beberapa koneksi kapan saja.
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;
}
}
}
Di hub, Anda melacak status koneksi setiap pengguna.
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");
}
}
}