在 SignalR 中使用组

作者 :Patrick FletcherTom FitzMacken

警告

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

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

本主题中使用的软件版本

本主题的早期版本

有关 SignalR 早期版本的信息,请参阅 SignalR 旧版本

问题和评论

请留下反馈,说明你如何喜欢本教程,以及我们可以在页面底部的评论中改进的内容。 如果你有与本教程不直接相关的问题,可以将其发布到 ASP.NET SignalR 论坛StackOverflow.com

概述

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 方法首先完成。 以下代码示例演示如何执行此操作。

public async Task JoinRoom(string roomName)
{
    await Groups.Add(Context.ConnectionId, roomName);
    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 partial class Startup {
    public void Configuration(IAppBuilder app) {
        app.MapSignalR();
        GlobalHost.HubPipeline.AddModule(new RejoingGroupPipelineModule());
    }
}