在 SignalR 1.x 中使用组

作者 :Patrick FletcherTom FitzMacken

警告

本文档不适用于最新版本的 SignalR。 查看 ASP.NET Core SignalR

本主题介绍如何将用户添加到组并保留组成员身份信息。

概述

SignalR 中的组提供一种方法,用于将消息广播到连接的客户端的指定子集。 一个组可以有任意数量的客户端,一个客户端可以是任意数量的组的成员。 无需显式创建组。 实际上,当您在对 Groups.Add 的调用中首次指定其名称时,将自动创建组,并且当您从其成员身份中删除最后一个连接时,该组将被删除。 有关使用组的简介,请参阅中心 API - 服务器指南中的 如何从中心类管理组成员身份

没有用于获取组成员身份列表或组列表的 API。 SignalR 基于发布/订阅模型将消息发送到客户端和组,服务器不维护组或组成员身份的列表。 这有助于最大化可伸缩性,因为每当向 Web 场添加节点时,SignalR 维护的任何状态都必须传播到新节点。

使用 Groups.Add 方法将用户添加到组时,用户会收到在当前连接期间定向到该组的消息,但该用户在该组中的成员身份不会保留到当前连接之外。 如果要永久保留有关组和组成员身份的信息,必须将该数据存储在存储库(如数据库或 Azure 表存储)中。 然后,每次用户连接到应用程序时,你都会从该用户所属的组的存储库中检索,然后手动将该用户添加到这些组。

在临时中断后重新连接时,用户会自动重新加入以前分配的组。 自动重新加入组仅在重新连接时适用,而不适用于建立新连接时。 数字签名的令牌是从包含以前分配的组列表的客户端传递的。 如果要验证用户是否属于请求的组,可以替代默认行为。

本主题包含下列部分:

添加和删除用户

若要在组中添加或删除用户,请调用“添加或删除”方法,并将用户的连接 ID 和组名称作为参数传入。 连接结束时,无需手动从组中删除用户。

以下示例演示 Hub 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 ,因为尝试删除的连接 ID 可能不再可用。 在这种情况下, TaskCanceledException 在请求超时后引发。如果应用程序必须确保在向组发送消息之前已从组中删除用户,则可以在 Groups.Remove 之前添加 await ,然后捕获可能引发的 TaskCanceledException 异常。

呼叫组成员

可以向组的所有成员或仅向组的指定成员发送邮件,如以下示例所示。

  • 指定组中所有连接的客户端。

    Clients.Group(groupName).addChatMessage(name, message);
    
  • 指定组中的所有已连接客户端 (指定客户端除外),由连接 ID 标识。

    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关键字 (keyword) ,因为不会立即向组成员发送消息。 如果要在添加新成员后立即向组的所有成员发送消息,需要应用await关键字 (keyword) 以确保异步操作已完成。

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