共用方式為


使用 SignalR 1.x 中的群組

作者: Patrick FletcherTom FitzMacken

警告

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

本主題描述如何將使用者新增至群組,並保存群組成員資格資訊。

概觀

SignalR 中的群組提供將訊息廣播至指定連線用戶端子集的方法。 群組可以有任意數目的用戶端,而用戶端可以是任意數目群組的成員。 您不需要明確建立群組。 實際上,當您第一次在對 Groups.Add 的呼叫中指定其名稱時,就會自動建立群組,當您從其成員資格中移除最後一個連線時,就會刪除該群組。 如需使用群組的簡介,請參閱 How to manage group membership from the Hub class in the Hubs API - Server Guide。

沒有 API 可取得群組成員資格清單或群組清單。 SignalR 會根據發佈/訂閱模型將訊息傳送給用戶端和群組,而且伺服器不會維護群組或群組成員資格的清單。 這有助於將延展性最大化,因為每當您將節點新增至 Web 服務器陣列時,SignalR 維護的任何狀態都必須傳播至新的節點。

當您使用 Groups.Add 方法將使用者新增至群組時,使用者會收到在目前連線期間導向至該群組的訊息,但該群組中的使用者成員資格不會保存在目前的連線之外。 如果您想要永久保留群組和群組成員資格的相關資訊,您必須將該資料儲存在資料庫或 Azure 資料表儲存體等存放庫中。 然後,每次使用者連線到您的應用程式時,您都會從使用者所屬的存放庫擷取,然後手動將該使用者新增至這些群組。

在暫時中斷後重新連線時,使用者會自動重新加入先前指派的群組。 自動重新加入群組僅適用于重新連線時,不適用於建立新連線時。 數位簽署的權杖會從用戶端傳遞,其中包含先前指派的群組清單。 如果您想要確認使用者是否屬於要求的群組,您可以覆寫預設行為。

這個主題包括下列各節:

新增和移除使用者

若要從群組新增或移除使用者,您可以呼叫 AddRemove 方法,並將使用者的連線識別碼和組名當做參數傳入。 連線結束時,您不需要手動從群組中移除使用者。

下列範例顯示 Groups.Add 中樞方法中使用的 和 Groups.Remove 方法。

public class ContosoChatHub : Hub
{
    public Task JoinRoom(string roomName)
    {
        return Groups.Add(Context.ConnectionId, roomName);
    }

    public Task LeaveRoom(string roomName)
    {
        return Groups.Remove(Context.ConnectionId, roomName);
    }
}

Groups.AddGroups.Remove 方法會以非同步方式執行。

如果您想要將用戶端新增至群組,並使用群組立即將訊息傳送至用戶端,您必須確定 Groups.Add 方法會先完成。 下列程式碼範例示範如何這麼做,一個是使用 .NET 4.5 中運作的程式碼,另一個是使用 .NET 4 中的程式碼。

非同步 .NET 4.5 範例

public async Task JoinRoom(string roomName)
{
    await Groups.Add(Context.ConnectionId, roomName);
    Clients.Group(roomName).addChatMessage(Context.User.Identity.Name + " joined.");
}

非同步 .NET 4 範例

public void JoinRoom(string roomName)
{
    (Groups.Add(Context.ConnectionId, roomName) as Task).ContinueWith(antecedent =>
      Clients.Group(roomName).addChatMessage(Context.User.Identity.Name + " joined."));
}

一般而言,您不應該在呼叫 Groups.Remove 方法時包含 await ,因為您嘗試移除的連接識別碼可能已無法使用。 在此情況下, TaskCanceledException 會在要求逾時之後擲回。如果您的應用程式必須在將訊息傳送至群組之前,先確定使用者已從群組中移除,您可以在 Groups.Remove 之前新增 await ,然後攔截可能擲回的 TaskCanceledException 例外狀況。

呼叫群組的成員

您可以將訊息傳送給群組的所有成員,或只傳送給群組的指定成員,如下列範例所示。

  • 指定群組中的所有已連線用戶端。

    Clients.Group(groupName).addChatMessage(name, message);
    
  • 指定群組中的所有已連線 用戶端,但指定的用戶端除外,由連線識別碼識別。

    Clients.Group(groupName, connectionId1, connectionId2).addChatMessage(name, message);
    
  • 指定群組中的所有已連線用戶端 ,但呼叫用戶端除外

    Clients.OthersInGroup(groupName).addChatMessage(name, message);
    

將群組成員資格儲存在資料庫中

下列範例示範如何在資料庫中保留群組和使用者資訊。 您可以使用任何資料存取技術;不過,下列範例示範如何使用 Entity Framework 定義模型。 這些實體模型會對應至資料庫資料表和欄位。 您的資料結構可能會根據應用程式的需求而有很大的差異。 此範例包含一 ConversationRoom 個名為 的類別,對應用程式而言是獨一無二的,可讓使用者加入不同主題的對話,例如運動或菜地。 此範例也包含連線的 類別。 追蹤群組成員資格不需要連接類別,但經常是追蹤使用者的健全解決方案的一部分。

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Data.Entity;

namespace GroupsExample
{
    public class UserContext : DbContext
    {
        public DbSet<User> Users { get; set; }
        public DbSet<Connection> Connections { get; set; }
        public DbSet<ConversationRoom> Rooms { get; set; }
    }

    public class User
    {
        [Key]
        public string UserName { get; set; }
        public ICollection<Connection> Connections { get; set; }
        public virtual ICollection<ConversationRoom> Rooms { get; set; } 
    }

    public class Connection
    {
        public string ConnectionID { get; set; }
        public string UserAgent { get; set; }
        public bool Connected { get; set; }
    }

    public class ConversationRoom
    {
        [Key]
        public string RoomName { get; set; }
        public virtual ICollection<User> Users { get; set; }
    }
}

然後,在中樞中,您可以從資料庫擷取群組和使用者資訊,並手動將使用者新增至適當的群組。 此範例不包含用來追蹤使用者連線的程式碼。 在此範例中 await ,不會在 之前 Groups.Add 套用 關鍵字,因為不會立即將訊息傳送給群組的成員。 如果您想要在新增成員之後立即將訊息傳送給群組的所有成員,您會想要套用 await 關鍵字,以確保非同步作業已完成。

using Microsoft.AspNet.SignalR;
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Threading.Tasks;

namespace GroupsExample
{
    [Authorize]
    public class ChatHub : Hub
    {
        public override Task OnConnected()
        {
            using (var db = new UserContext())
            {
                // Retrieve user.
                var user = db.Users
                    .Include(u => u.Rooms)
                    .SingleOrDefault(u => u.UserName == Context.User.Identity.Name);

                // If user does not exist in database, must add.
                if (user == null)
                {
                    user = new User()
                    {
                        UserName = Context.User.Identity.Name
                    };
                    db.Users.Add(user);
                    db.SaveChanges();
                }
                else
                {
                    // Add to each assigned group.
                    foreach (var item in user.Rooms)
                    {
                        Groups.Add(Context.ConnectionId, item.RoomName);
                    }
                }
            }
            return base.OnConnected();
        }

        public void AddToRoom(string roomName)
        {
            using (var db = new UserContext())
            {
                // Retrieve room.
                var room = db.Rooms.Find(roomName);

                if (room != null)
                {
                    var user = new User() { UserName = Context.User.Identity.Name};
                    db.Users.Attach(user);

                    room.Users.Add(user);
                    db.SaveChanges();
                    Groups.Add(Context.ConnectionId, roomName);
                }
            }
        }

        public void RemoveFromRoom(string roomName)
        {
            using (var db = new UserContext())
            {
                // Retrieve room.
                var room = db.Rooms.Find(roomName);
                if (room != null)
                {
                    var user = new User() { UserName = Context.User.Identity.Name };
                    db.Users.Attach(user);

                    room.Users.Remove(user);
                    db.SaveChanges();
                    
                    Groups.Remove(Context.ConnectionId, roomName);
                }
            }
        }
    }
}

將群組成員資格儲存在 Azure 資料表儲存體中

使用 Azure 資料表儲存體來儲存群組和使用者資訊類似于使用資料庫。 下列範例顯示儲存使用者名稱和組名的資料表實體。

using Microsoft.WindowsAzure.Storage.Table;
using System;

namespace GroupsExample
{
    public class UserGroupEntity : TableEntity
    {
        public UserGroupEntity() { }

        public UserGroupEntity(string userName, string groupName)
        {
            this.PartitionKey = userName;
            this.RowKey = groupName;
        }
    }
}

在中樞中,您會在使用者連線時擷取指派的群組。

using Microsoft.AspNet.SignalR;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.WindowsAzure.Storage.Table;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure;

namespace GroupsExample
{
    [Authorize]
    public class ChatHub : Hub
    {
        public override Task OnConnected()
        {
            string userName = Context.User.Identity.Name;

            var table = GetRoomTable();
            table.CreateIfNotExists();
            var query = new TableQuery<UserGroupEntity>()
                .Where(TableQuery.GenerateFilterCondition(
                "PartitionKey", QueryComparisons.Equal, userName));
            
            foreach (var entity in table.ExecuteQuery(query))
            {
                Groups.Add(Context.ConnectionId, entity.RowKey);
            }

            return base.OnConnected();
        }

        public Task AddToRoom(string roomName)
        {
            string userName = Context.User.Identity.Name;

            var table = GetRoomTable();

            var insertOperation = TableOperation.InsertOrReplace(
                new UserGroupEntity(userName, roomName));
            table.Execute(insertOperation);

            return Groups.Add(Context.ConnectionId, roomName);
        }

        public Task RemoveFromRoom(string roomName)
        {
            string userName = Context.User.Identity.Name;

            var table = GetRoomTable();

            var retrieveOperation = TableOperation.Retrieve<UserGroupEntity>(
                userName, roomName);
            var retrievedResult = table.Execute(retrieveOperation);

            var deleteEntity = (UserGroupEntity)retrievedResult.Result;

            if (deleteEntity != null)
            {
                var deleteOperation = TableOperation.Delete(deleteEntity);
                table.Execute(deleteOperation);
            }

            return Groups.Remove(Context.ConnectionId, roomName);
        }

       private CloudTable GetRoomTable()
        {
            var storageAccount =
                CloudStorageAccount.Parse(
                CloudConfigurationManager.GetSetting("StorageConnectionString"));
            var tableClient = storageAccount.CreateCloudTableClient();
            return tableClient.GetTableReference("room");
        }
    }
}

重新連線時確認群組成員資格

根據預設,SignalR 會在從暫時中斷重新連線時自動將使用者重新指派給適當的群組,例如在連線逾時之前卸載並重新建立連線。使用者群組資訊會在重新連線時傳入權杖,並在伺服器上驗證該權杖。 如需將使用者重新加入群組之驗證程式的相關資訊,請參閱 重新連線時重新加入群組

一般而言,您應該在重新連線時使用自動重新加入群組的預設行為。 SignalR 群組並非用來限制敏感性資料存取的安全性機制。 不過,如果您的應用程式必須在重新連線時再次檢查使用者的群組成員資格,您可以覆寫預設行為。 變更預設行為可能會增加資料庫的負擔,因為必須針對每次重新連線擷取使用者的群組成員資格,而不是當使用者連線時擷取。

如果您必須在重新連線時確認群組成員資格,請建立傳回指派群組清單的新中樞管線模組,如下所示。

using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hubs;
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;

namespace GroupsExample
{
    public class RejoingGroupPipelineModule : HubPipelineModule
    {
        public override Func<HubDescriptor, IRequest, IList<string>, IList<string>> 
            BuildRejoiningGroups(Func<HubDescriptor, IRequest, IList<string>, IList<string>> 
            rejoiningGroups)
        {
            rejoiningGroups = (hb, r, l) => 
            {
                List<string> assignedRooms = new List<string>();
                using (var db = new UserContext())
                {
                    var user = db.Users.Include(u => u.Rooms)
                        .Single(u => u.UserName == r.User.Identity.Name);
                    foreach (var item in user.Rooms)
                    {
                        assignedRooms.Add(item.RoomName);
                    }
                }
                return assignedRooms;
            };

            return rejoiningGroups;
        }
    }
}

然後,將該模組新增至中樞管線,如下所示。

public class Global : HttpApplication
{
    void Application_Start(object sender, EventArgs e)
    {
        // Code that runs on application startup
        BundleConfig.RegisterBundles(BundleTable.Bundles);
        AuthConfig.RegisterOpenAuth();
        RouteConfig.RegisterRoutes(RouteTable.Routes);
        RouteTable.Routes.MapHubs();
        GlobalHost.HubPipeline.AddModule(new RejoingGroupPipelineModule());
    }
}