2018 年 8 月

第 33 卷,第 8 期

领先技术 - 使用 ASP.NET Core SignalR 实现社交网络式通知

通过Dino Esposito |2018 年 8 月

Dino Esposito社交网络和移动操作系统进行弹出,气球样式通知非常受欢迎且最主流,但 Microsoft Windows 2000 可能是第一个软件广泛地使用它们。气球状通知,让可以通信而无需立即执行操作和注意到用户值得注意的事件-与传统的弹出窗口。这些气球样式通知的关键是将该消息传送实时从右到打开的窗口用户正在使用的底层基础结构。

在本文中,您将了解如何使用 ASP.NET Core SignalR 来生成弹出通知。本文提供了可跟踪已登录的用户,并为每个提供的机会来构建和维护的朋友的网络的示例应用程序。如在社交网络方案中,任何已登录的用户可能会添加或删除从好友列表在任何时间。在此情况下在示例应用程序中,已登录的用户接收上下文相关的通知。

应用程序用户进行身份验证

气球样式通知不是普通的广播的通知发送到任何人正在侦听 Web 套接字通道上的 SignalR 消息。相反,它们被发送到特定用户身份登录到应用程序。打开并聆听普通的 Web 套接字通道是解决此问题的好方法,但 ASP.NET Core SignalR 只需提供编程接口的详细摘要,并提供了对 Websocket 之外的备用网络协议的支持。

应用程序的构建的第一步添加一个层进行用户身份验证。用户提供一个规范的登录窗体,并提供自己的凭据。一旦正确识别为有效的用户的系统,她将收到身份验证 cookie 打包有很多的声明,如下所示:

var claims = new[]
{
  new Claim(ClaimTypes.Name, input.UserName),
  new Claim(ClaimTypes.Role, actualRole)
};
var identity = new ClaimsIdentity(claims,
  CookieAuthenticationDefaults.AuthenticationScheme);
await HttpContext.SignInAsync(
  CookieAuthenticationDefaults.AuthenticationScheme,
    new ClaimsPrincipal(identity));

示例代码演示如何创建即席 IPrincipal 对象在 ASP.NET Core 中围绕用户名称后已成功验证凭据。它的键需要注意,进行身份验证才能在 ASP.NET Core 中正常工作,与最新的 SignalR 库结合使用时应启用身份验证在 Configure 方法中的 startup 类很早 (并在任何情况下,早于完成SignalR 路由初始化)。我稍后将在此点在一段时间;同时,让我们回顾一下示例应用程序中的友元关系的基础结构。

在应用程序中定义友元

在示例应用程序,友元关系是系统的只是系统的两个用户之间的链接。请注意演示应用程序不会使用任何数据库以保留用户和关系。一些用户和友元关系进行了硬编码并重新加载应用程序重新启动时重置或当前视图。下面是表示友元关系的类:

public class FriendRelationship
{
  public FriendRelationship(string friend1, string friend2)
  {
    UserName1 = friend1;
    UserName2 = friend2;
  }
  public string UserName1 { get; set; }
  public string UserName2 { get; set; }
}

在用户登录,因为他已提供服务提供好友列表的索引页。通过页面的 UI,用户将可以同时添加新朋友和删除现有的 (请参阅图 1)。

在已登录用户的主页
图 1: 在已登录用户的主页

当用户键入的名称的新朋友时,HTML 窗体发布,并在内存中创建新的友元关系。如果类型化的名称与现有用户不匹配,新用户创建对象并将其添加到内存中列表中,如下所示:

[HttpPost]
public IActionResult Add(string friend)
{
  var currentUser = User.Identity.Name;
  UserRepository.AddFriend(currentUser, friend);
  // More code here
  ...
  return new EmptyResult();
}

如您可能会注意到,控制器方法将返回一个空的操作结果。假设,事实上,HTML 窗体发布通过 JavaScript 其内容。因此,请单击 JavaScript 处理程序附加到窗体的提交按钮以编程方式,如下所示:

<form class="form-horizontal" id="add-form" method="post"
      action="@Url.Action("add", "friend")">
      <!-- More input controls here -->
      <!-- SUBMIT button -->
  <button id="add-form-submit-button"
       class="btn btn-danger" type="button">
       SAVE  </button></form>

发布代码的 JavaScript 触发服务器端操作,并返回。朋友的新列表作为 JSON 数组或 HTML 字符串,可控制器方法返回,且在当前页面文档对象模型中的相同的 JavaScript 调用方代码集成。很好,但需要考虑的可能问题可能出在某些情况下一个小问题。

假设用户保存多个连接到相同的服务器页。作为示例,用户可以在同一页上打开多个浏览器窗口,并使用这些页之一进行交互。在其中调用将返回直接响应的情况下 (是否 JSON 或 HTML),只从发出请求的页,该怎么办正在更新。任何其他打开的浏览器窗口保持静态的和不受影响。若要解决此问题,您可以利用 ASP.NET Core SignalR,可与相同的用户帐户相关的所有连接到广播更改的一项功能。

用户连接到广播

ASP.NET MVC 控制器类接收调用,以添加或删除好友集成了对 SignalR hub 上下文的引用。此代码演示如何执行的操作:

[Authorize]
public class FriendController : Controller
{
  private readonly IHubContext<FriendHub> _friendHubContext;
  public FriendController(IHubContext<FriendHub> friendHubContext)
  {
    _friendHubContext = friendHubContext;
  }
  // Methods here
  ...
}

像往常一样在 SignalR 编程中,友元中心是 startup 类中定义,如中所示图 2

图 2 定义中心

public void Configure(IApplicationBuilder app)
{
  // Enable security
  app.UseAuthentication();
  // Add MVC
  app.UseStaticFiles();
  app.UseMvcWithDefaultRoute();
  // SignalR (must go AFTER authentication)
  app.UseSignalR(routes =>
  {
    routes.MapHub<FriendHub>("/friendDemo");
  });
}

它的键的 UseSignalR 调用遵循 UseAuthentication 调用。这可确保当 SignalR 连接建立针对给定路由提供了有关已登录的用户和声明的信息。图 3提供了深入地了解处理窗体 post,当用户添加到列表的新朋友的代码。

图 3 处理窗体发布

[HttpPost]
public IActionResult Add(string friend)
{
  var currentUser = User.Identity.Name;
  UserRepository.AddFriend(currentUser, friend);
  // Broadcast changes to all user-related windows
  _friendHubContext.Clients.User(currentUser).SendAsync("refreshUI");
  // More code here  ...
  return new EmptyResult();
}

集线器上下文的客户端属性具有属性称为用户,采用用户 id。当在用户对象上调用,SendAsync 方法通知给同一用户名称下的所有当前连接的浏览器窗口将给定的消息。换而言之,SignalR 已分组自动与所有连接来自同一已经过身份验证用户的单个池的功能。鉴于这一点,SendAsync 调用从用户有权广播到与用户相关的所有 windows 消息,无论它们是从多个桌面浏览器、 应用程序内 Web 视图、 移动浏览器、 桌面客户端或其他任何内容。代码如下:

_friendHubContext.Clients.User(currentUser).SendAsync("refreshUI");

RefreshUI 将消息发送到同一用户名称下的所有连接可确保所有打开的窗口是同步到添加友元函数的方式。在示例源代码,您将看到相同的操作发生时当前登录的用户从其列表中删除一个朋友。

配置用户客户端代理

在 SignalR 中,用户对象具有与 HTTP 上下文关联的用户对象没有任何关系。尽管相同的属性名,SignalR 用户对象是一个客户端代理和声明的容器。尚未,SignalR 特定于用户的客户端代理和表示已经过身份验证的用户的对象之间存在细微关系。您可能已经注意到,用户客户端代理将需要一个字符串参数。在中的代码图 3,字符串参数是当前登录的用户的名称。只是一个字符串标识符,不过,且可以是任何内容将其配置。

默认情况下,识别用户客户端代理的用户 ID 是 NameIdentifier 声明的值。如果已经过身份验证的用户的声明列表不包括 NameIdentifier,就无法在广播会起作用。因此有两个选项:一个创建身份验证 cookie 时添加 NameIdentifier 声明,另一个是编写您自己 SignalR 用户 ID 提供程序。若要添加 NameIdentifier 声明,需要在登录过程中的以下代码:

var claims = new[]
{
  new Claim(ClaimTypes.Name, input.UserName),
  new Claim(ClaimTypes.NameIdentifier, input.UserName),
  new Claim(ClaimTypes.Role, actualRole),
};

分配给 NameIdentifier 声明的值并不重要,只要它是唯一的每个用户。在内部,SignalR 使用的 IUserIdProvider 组件以匹配到的连接组的用户 ID 植根于当前登录的用户,如下所示:

public interface IUserIdProvider
{
  string GetUserId(HubConnectionContext connection);
}

IUserIdProvider 接口 DI 基础结构中具有默认的实现。类是 DefaultUserIdProvider,因此进行编码,如下所示:

public class DefaultUserIdProvider : IUserIdProvider
{
  public string GetUserId(HubConnectionContext connection)
  {
    var first = connection.User.FindFirst(ClaimTypes.NameIdentifier);
    if (first == null)
      return  null;
    return first.Value;
  }
}

如您所见,DefaultUserIdProvider 类使用 NameIdentifier 声明的值组特定于用户的连接 Id。若要指示在用户的名称,但不是一定提供通过其用户标识系统中的唯一标识符,是指名称声明。NameIdentifier 声明,而被旨在保留唯一的值,无论是一个 GUID、 字符串或整数。如果您切换到用户而不是 NameIdentifer,请确保分配给名称的任何值是唯一的每个用户。

来自具有匹配名称标识符的帐户的所有连接将组合在一起,并将自动收到通知时使用用户客户端代理。若要切换到使用规范的名称声明,需要自定义 IUserIdProvider,按如下所示:

public class MyUserIdProvider : IUserIdProvider
{
  public string GetUserId(HubConnectionContext connection)
  {
    return connection.User.Identity.Name;
  }
}

不用说,此组件必须在启动阶段注册使用 DI 基础结构中。下面是要包含在 startup 类的 ConfigureServices 方法中的代码:

services.AddSignalR();
services.AddSingleton(typeof(IUserIdProvider), typeof(MyUserIdProvider));

此时,所有内容设置为具有相同的状态和视图上同步的所有匹配用户 windows。如何气球样式通知?

最后的工作

添加和删除朋友会导致刷新通知发送到索引页查看当前用户的需求。如果给定的用户具有两个浏览器窗口打开同一个应用程序 (索引和另一页) 的不同页上,她将刷新仅接收通知的索引页。但是,添加和删除的朋友也会导致将添加和删除通知发送到已添加或从友元关系列表中删除的用户。例如,如果用户 Dino 决定从他的朋友列表中删除用户 Mary,用户 Mary 还收到删除通知。理想情况下,删除 (或添加) 通知应到达而查看的页的是否不考虑用户或任何其他索引。

若要实现此目的,有两个选项:

  • 用于设置连接移动到布局页,然后基于该布局的所有页面由继承的单个 SignalR hub。
  • 使用两个不同的中心 — 一个用于添加或删除好友后, 刷新 UI,一个通知添加或删除用户。

如果您决定继续采用不同的中心,添加/删除通知中心必须设置所有页中想要显示的通知,很可能布局页有应用程序中。

示例应用程序使用单个中心完全在布局页面中设置。请注意在客户端,这意味着在顶部的布局正文和 Razor 的 RenderBody 部分之前更好地放置 SignalR 初始化代码内全局共享引用的当前连接的 JavaScript 对象。

让我们看一下在控制器中的 Add 方法的最后一个代码。此方法就是中的窗体图 1最终将发布。方法会在后端保存友元关系进行任何必要更改,然后发出两个 SignalR 消息 — 一个用于以可视方式刷新的当前用户执行该操作,第二行通知添加 (或删除) 用户的好友列表。如果她当前已连接到应用程序和从页中配置为接收和处理这些特定的通知,实际上会通知用户。图 4演示了这一点。

图 4 最终添加方法代码

public IActionResult Add(string addedFriend)
{
  var currentUser = User.Identity.Name;
  // Save changes to the backend
  UserRepository.AddFriend(currentUser, addedFriend);
  // Refresh the calling page to reflect changes
  _friendHubContext.Clients.User(currentUser).SendAsync("refreshUI");
  // Notify the added user (if connected)
  _friendHubContext.Clients.User(addedFriend).SendAsync("added", currentUser);
  return new EmptyResult();
}

在布局页中,一些 JavaScript 代码显示气球样式通知 (或你想要的任何类型的 UI 调整)。在示例应用程序,通知采用最多 10 秒,通知栏上显示一条消息的形式,如下面的代码中所示:

friendConnection.on("added", (user) => {
  $("#notifications").html("ADDED by <b>" + user + "</b>");
  window.setTimeout(function() {
            $("#notifications").html("NOTIFICATIONS");
    },
    10000);
});

结果如图 5 所示。

跨用户通知
图 5 跨用户通知

有关 SignalR 组

在本文中,我将介绍所选通知发送到相关的一组用户的 SignalR 支持。SignalR 提供了几种方法 — 用户客户端代理和组。细微的区别是:用户客户端代理隐式生成多个组的名称由用户 ID 和成员都是相同的应用程序用户打开的所有连接。组是以编程方式将连接追加到逻辑组的更多常规机制。以编程方式设置连接和组的名称。

气球样式通知可能在这两种方法中实现,但此特定方案用户客户端代理是最简单的解决方案。源代码可从bit.ly/2HVyLp5


Dino Esposito 在他 25 年的职业生涯中撰写了超过 20 本书籍和 1,000 篇文章。Esposito 不仅是舞台剧《事业中断》的作者,还是 BaxEnergy 的数字策略分析师,正忙于编写有助于建设环保世界的软件。可以在 Twitter 上关注他 (@despos)。


在 MSDN 杂志论坛讨论这篇文章