在 SignalR 中使用组
作者 :Patrick Fletcher, Tom FitzMacken
警告
本文档不适用于最新版本的 SignalR。 查看 ASP.NET Core SignalR。
本主题介绍如何将用户添加到组并保留组成员身份信息。
本主题中使用的软件版本
- Visual Studio 2013
- .NET 4.5
- SignalR 版本 2
本主题的早期版本
有关 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.Add
和 Groups.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());
}
}