共用方式為


將 SignalR 使用者對應至 SignalR 1.x 的連線

作者: Patrick FletcherTom FitzMacken

警告

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

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

簡介

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

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

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

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

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

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

記憶體內部儲存體

下列範例示範如何在儲存在記憶體中的字典中保留連線和使用者資訊。 字典會使用 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()
        {
            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();
        }
    }
}

單一使用者群組

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

當使用者中斷連線時,您不應該手動從群組中移除使用者。 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 資料表儲存體來儲存連線資訊。 當您有多個網頁伺服器時,此方法會運作,因為每個網頁伺服器都可以與相同的資料存放庫互動。 如果您的網頁伺服器停止運作或應用程式重新開機, 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()
        {
            using (var db = new UserContext())
            {
                var connection = db.Connections.Find(Context.ConnectionId);
                connection.Connected = false;
                db.SaveChanges();
            }
            return base.OnDisconnected();
        }
    }
}

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