Bagikan melalui


Memetakan Pengguna SignalR ke Koneksi

oleh 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.

Patrick Fletcher membantu menulis topik ini.

Versi perangkat lunak yang digunakan dalam topik ini

Versi sebelumnya dari topik ini

Untuk informasi tentang versi SignalR yang lebih lama, lihat Versi Lama SignalR.

Pertanyaan dan komentar

Silakan tinggalkan umpan balik tentang bagaimana Anda menyukai tutorial ini dan apa yang dapat kami tingkatkan di komentar di bagian bawah halaman. Jika Anda memiliki pertanyaan yang tidak terkait langsung dengan tutorial, Anda dapat mempostingnya ke forum ASP.NET SignalR atau StackOverflow.com.

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:

Masing-masing implementasi ini ditampilkan dalam topik ini. Anda menggunakan OnConnectedmetode , 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
Penyedia UserID
Dalam memori
Grup pengguna tunggal
Permanen, eksternal

Penyedia IUserID

Fitur ini memungkinkan pengguna untuk menentukan apa yang didasarkan pada IRequest melalui antarmuka baru IUserIdProvider.

The IUserIdProvider

public interface IUserIdProvider
{
    string GetUserId(IRequest request);
}

Secara default, akan ada implementasi yang menggunakan pengguna IPrincipal.Identity.Name sebagai nama pengguna. Untuk mengubah ini, daftarkan implementasi IUserIdProvider Anda dengan host global saat aplikasi Anda dimulai:

GlobalHost.DependencyResolver.Register(typeof(IUserIdProvider), () => new MyIdProvider());

Dari dalam hub, Anda akan dapat mengirim pesan ke pengguna ini melalui API berikut:

Mengirim pesan ke pengguna tertentu

public class MyHub : Hub
{
    public void Send(string userId, string message)
    {
        Clients.User(userId).send(message);
    }
}

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 _connectionsvariabel .

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();
        }
    }
}

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(bool stopCalled)
        {
            using (var db = new UserContext())
            {
                var connection = db.Connections.Find(Context.ConnectionId);
                connection.Connected = false;
                db.SaveChanges();
            }
            return base.OnDisconnected(stopCalled);
        }
    }
}

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(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");
        }
    }
}