將 SignalR 使用者對應至連線

作者:Tom FitzMacken

警告

本檔不適用於最新版的 SignalR。 看看ASP.NET Core SignalR

本主題說明如何保留使用者及其連線的相關資訊。

Patrick Fletcher 協助撰寫本主題。

本主題中使用的軟體版本

本主題的舊版

如需舊版 SignalR 的相關資訊,請參閱 SignalR 舊版

問題和批註

請留下您喜歡本教學課程的意見反應,以及我們可以在頁面底部的批註中改善的內容。 如果您有與教學課程不直接相關的問題,您可以將問題張貼至 ASP.NET SignalR 論壇StackOverflow.com

簡介

每個連線到中樞的用戶端都會傳遞唯一的連線識別碼。您可以在中樞內容的 屬性中 Context.ConnectionId 擷取此值。 如果您的應用程式需要將使用者對應至連線識別碼並保存該對應,您可以使用下列其中一項:

本主題會顯示上述每個實作。 您可以使用 OnConnected 類別的 HubOnDisconnectedOnReconnected 方法來追蹤使用者線上狀態。

應用程式的最佳方法取決於:

  • 裝載應用程式的 Web 服務器數目。
  • 您是否需要取得目前連線的使用者清單。
  • 在應用程式或伺服器重新開機時,是否需要保存群組和使用者資訊。
  • 呼叫外部伺服器的延遲是否為問題。

下表顯示哪些方法適用于這些考慮。

考量 多部伺服器 取得目前連線的使用者清單 重新開機後保存資訊 最佳效能
UserID 提供者
記憶體內
單一使用者群組
永久、外部

IUserID 提供者

這項功能可讓使用者透過新的介面 IUserIdProvider 來指定 userId 以 IRequest 為基礎。

The IUserIdProvider

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

根據預設,會有一個實作會使用使用者的 IPrincipal.Identity.Name 作為使用者名稱。 若要變更此問題,請在應用程式啟動時,向全域主機註冊 的 IUserIdProvider 實作:

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

從中樞內,您將能夠透過下列 API 將訊息傳送給這些使用者:

將訊息傳送給特定使用者

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

記憶體內部儲存體

下列範例示範如何在儲存在記憶體中的字典中保留連線和使用者資訊。 字典會使用 來 HashSet 儲存連接識別碼。使用者隨時可以有多個與 SignalR 應用程式的連線。 例如,透過多個裝置或多個瀏覽器索引標籤連線的使用者會有一個以上的連線識別碼。

如果應用程式關閉,所有資訊都會遺失,但會在使用者重新建立其連線時重新填入。 如果您的環境包含多個網頁伺服器,記憶體內部儲存體無法運作,因為每部伺服器都有個別的連線集合。

第一個範例顯示一個類別,該類別會管理使用者與連線的對應。 HashSet 的索引鍵將會是使用者的名稱。

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

下一個範例示範如何使用中樞的連線對應類別。 類別的實例會儲存在變數名稱 _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();
        }
    }
}

單一使用者群組

您可以為每個使用者建立群組,然後在您想要只連線到該使用者時傳送訊息給該群組。 每個群組的名稱都是使用者的名稱。 如果使用者有多個連線,則會將每個連線識別碼新增至使用者的群組。

當使用者中斷連線時,您不應該手動從群組中移除使用者。 SignalR 架構會自動執行此動作。

下列範例示範如何實作單一使用者群組。

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

永久、外部儲存體

本主題說明如何使用資料庫或 Azure 資料表儲存體來儲存連線資訊。 當您有多個網頁伺服器時,此方法的運作方式是,因為每個網頁伺服器都可以與相同的資料存放庫互動。 如果您的 Web 服務器停止運作,或應用程式重新開機, OnDisconnected 則不會呼叫 方法。 因此,您的資料存放庫可能會有不再有效的連線識別碼記錄。 若要清除這些孤立的記錄,您可能想要使在與應用程式相關的時間範圍內建立的任何連線失效。 本節中的範例包含建立連線時追蹤的值,但不會顯示如何清除舊記錄,因為您可能會想要做為背景程式來執行。

資料庫

下列範例示範如何在資料庫中保留連線和使用者資訊。 您可以使用任何資料存取技術;不過,下列範例示範如何使用 Entity Framework 定義模型。 這些實體模型會對應至資料庫資料表和欄位。 您的資料結構可能會根據應用程式的需求而有所不同。

第一個範例示範如何定義可以與許多連接實體相關聯的使用者實體。

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

然後,您可以從中樞,使用如下所示的程式碼來追蹤每個連線的狀態。

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 表格儲存體

下列 Azure 資料表儲存體範例類似于資料庫範例。 它不包含開始使用 Azure 資料表儲存體服務所需的所有資訊。 如需詳細資訊,請參閱 如何從 .NET 使用資料表儲存體

下列範例顯示用來儲存連線資訊的資料表實體。 它會依使用者名稱分割資料,並依連線識別碼識別每個實體,讓使用者隨時可以有多個連線。

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

在中樞中,您會追蹤每個使用者連線的狀態。

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