2018 年 4 月

第 33 卷,第 4 期

领先技术 - 发现 ASP.NET Core SignalR

作者 Dino Esposito | 2018 年 4 月

Dino EspositoASP.NET SignalR 是几年前推出的工具,可供 ASP.NET 开发人员使用,以向应用程序添加实时功能。只要基于 ASP.NET 的应用程序必须接收来自服务器(从监视系统到游戏)的频繁异步更新,就属于典型的库用例。这些年来,我还使用它来刷新 CQRS 体系结构方案中的 UI,以及在 socialware 应用程序中实现与 Facebook 类似的通知系统。从更具技术性的角度来看,SignalR 是抽象层,生成依据为一部分可以在完全兼容的客户端和服务器之间建立实时连接的传输机制。客户端通常为 Web 浏览器,服务器通常为 Web 服务器,但两者都不仅限于此。

ASP.NET SignalR 属于 ASP.NET Core 2.1。虽然库的总体编程模型与经典 ASP.NET 的编程模型类似,但库本身实际上已经完全重写。尽管如此,只要开发人员适应各方面的变化,应该就可以快速熟练掌握新方案。本文将介绍如何在规范 Web 应用程序中使用新库来监视可能会很漫长的远程任务。

设置环境

可能需要以下多个 NuGet 包,才能使用库:Microsoft.AspNetCore.SignalR 和 Microsoft.AspNetCore.SignalR.Client。前一个包提供核心功能;后一个包是 .NET 客户端,且只有在生成 .NET 客户端应用程序时才需要。此示例将通过 Web 客户端来使用库,因此改为需要 SignalR NPM 包。本文稍后将详细介绍这一点。请注意,在基于 MVC 应用程序模型的 Web 应用程序的上下文中使用 SignalR 并不是一项强制性要求。可以直接通过 ASP.NET Core 控制台应用程序使用 SignalR 库服务,还可以在控制台应用程序中托管 SignalR 的服务器部分。

应用程序的启动类需要包含一些特定代码,这一点不足为奇。具体而言,将把 SignalR 服务添加到系统服务集合中,并将它配置为可供实际使用。图 1 展示了使用 SignalR 的启动类的典型状态。

图 1:SignalR ASP.NET Core 应用程序的启动类

public class Startup
{
  public void ConfigureServices(IServiceCollection services)
  {
    services.AddMvc();
    services.AddSignalR();
  }
  public void Configure(IApplicationBuilder app)
  {
    app.UseStaticFiles();
    app.UseDeveloperExceptionPage();
    // SignalR
    app.UseSignalR(routes =>
    {
      routes.MapHub<UpdaterHub>("/updaterDemo");
      routes.MapHub<ProgressHub>("/progressDemo");
    });
    app.UseMvcWithDefaultRoute();
  }
}

SignalR 服务配置包括一个或多个服务器路由的定义,这些路由绑定到服务器端环境中的一个或多个终结点。MapHub<T> 方法将请求 URL 中的指定名称链接到 Hub 类的实例中。Hub 类既是实现 SignalR 协议的核心所在,也是处理客户端调用的位置所在。为服务器端打算接受和处理的每组逻辑相关调用创建 Hub。SignalR 对话由双方之间交换的消息组成,一方对另一方调用方法的结果可能是没有响应,可能是收到一个或多个响应,也可能是仅收到错误通知。任何 ASP.NET Core SignalR 服务器实现都会公开一个或多个 Hub。在图 1**** 中,有两个 Hub 类(UpdaterHub 和 ProgressHub)绑定到唯一字符串,这些字符串在内部用于生成实际调用的 URL 目标。

Hub 类

SignalR 应用程序中的 Hub 类是普通的简单类,继承自 Hub 基类。此基类仅用于免去开发人员一次又一次编写相同样本代码的麻烦。基类只提供某基础结构,而不提供预定义行为。具体而言,它定义了图 2 中的成员。

图 2:Hub 基类的成员

成员 说明
客户端 公开当前由 Hub 托管的客户端列表的属性。
上下文 公开当前调用方上下文的属性,包括连接 ID 和用户声明(若有)等信息。
公开各客户端子集的属性,这些客户端可能已经以编程方式定义为完整客户端列表中的组。组通常创建用于向选定受众广播特定消息。
OnConnectedAsync 每当有新客户端连接到 Hub 时调用的虚拟方法。
OnDisconnectedAsync 每当有新客户端与 Hub 断开连接时调用的虚拟方法。

最简单的 Hub 类如下所示:

public class ProgressHub : Hub
{
}

有趣的是,如果在 ASP.NET Core MVC 应用程序上下文中从控制器方法内使用它,就会直接采用 Hub 形式。几乎所有的 ASP.NET Core SignalR 示例(包括聊天示例)往往都会在客户端和 Hub 之间进行直接绑定和双向绑定,无需控制器提供任何形式调解。在这种情况下,Hub 采用的形式将会更有形一点:

public class SampleChat : Hub
{     
  // Invoked from outside the hub
  public void Say(string message)
  {
    // Invoke method on listening client(s)
    return Clients.All.InvokeAsync("Said", message);
  }
}

与数十篇博客文章中换汤不换药的规范 SignalR 聊天示例不同,本文中的示例其实并没有在客户端和服务器之间建立双向对话。虽然连接是从客户端建立的,但在此之后,客户端就不会发送其他任何请求。相反,服务器会监视任务进度,并在适当时将数据推送回客户端。也就是说,只有当用例要求客户端直接调用公共方法时,Hub 类才必须像上面的代码一样使用这些方法。如果有点复杂难懂,下面的示例足以阐明这一点。

监视远程任务

它的具体情形是这样的:ASP.NET Core 应用程序为用户提供了某 HTML 接口,以方便用户触发可能会很漫长的远程任务(如创建报告)。因此,作为开发人员,需要显示进度栏,以持续反馈进度(见图 3)。

使用 SignalR 监视远程任务的进度
图 3:使用 SignalR 监视远程任务的进度

可以猜到,在此示例中,客户端和服务器都在同一个 ASP.NET Core 项目的上下文中设置 SignalR 实时会话。在此开发阶段中,MVC 项目功能齐全,它使用图 1 中的启动代码进行了扩展。接下来,将设置客户端框架。需要在与 SignalR 终结点交互的所有 Razor(或纯 HTML)视图中完成此设置。

若要在 Web 浏览器中与 SignalR 终结点进行通信,首先要添加对 SignalR JavaScript 客户端库的引用:

<script src="~/scripts/signalr.min.js">
</script>

可以通过多种方式获取此 JavaScript 文件。最值得推荐的方法是,使用几乎所有开发计算机上都有的 Node.js 包管理器 (NPM) 工具(特别是在 Visual Studio 2017 版本推出后)。通过 NPM,查找并安装名为 @aspnet/signalr 的 ASP.NET Core SignalR 客户端。它会将许多 JavaScript 文件复制到磁盘,但其中只有一个文件才是大多数情况唯一需要的。不管怎样,这就是简单地链接 JavaScript 文件,还可以通过其他许多方式来获取此文件,包括从旧版 ASP.NET Core SignalR 项目中复制它。然而,NPM 是团队提供的唯一受支持的脚本获取方式。另请注意,ASP.NET Core SignalR 不再依赖 jQuery。

在客户端应用程序中,还需要另一段更具体的 JavaScript 代码。特别是,需要如下代码:

var progressConnection =
  new signalR.HubConnection("/progressDemo");
progressConnection.start();

与 SignalR Hub 建立的连接与指定路径匹配。更确切地说,以参数形式传递到 HubConnection 的名称,应该是映射到启动类中路由的名称之一。在内部,HubConnection 对象准备了串联当前服务器 URL 和给定名称生成的 URL 字符串。只有当此 URL 与已配置的路由之一匹配时,才会处理它。另请注意,如果客户端和服务器不是相同的 Web 应用程序,那么必须向 HubConnection 传递托管 SignalR Hub 的 ASP.NET Core 应用程序的完整 URL,外加 Hub名称。

然后,必须通过 start 方法打开 JavaScript Hub 连接对象。可以使用 JavaScript 承诺(特别是 then 方法)或 TypeScript 中的 async/await 执行后续操作(如初始化某用户界面)。SignalR 连接由字符串 ID 唯一标识。

请务必注意,如果传输连接或服务器失败,ASP.NET Core SignalR 就不再支持自动重新连接。在旧版中,如果发生服务器故障,客户端会尝试根据计划算法重新建立连接。如果成功,它会使用相同的 ID 重新打开连接。在 SignalR Core 中,如果连接中断,客户端只能通过 start 方法再次启动连接,这就会生成连接 ID 不同的其他连接实例。

客户端回调 API

需要的另一段基本 JavaScript 代码是,Hub 回调的 JavaScript,用于刷新接口并在客户端上反映服务器上的进度。虽然这段代码在 ASP.NET Core SignalR 中的编写方式与旧版中的略有不同,但意向是完全相同的。此示例中有以下三个方法能够从服务器回调:initProgressBar、updateProgressBar 和 clearProgressBar。不用说,可以使用任意名称和签名。以下是实现示例:

progressConnection.on("initProgressBar", () => {
  setProgress(0);
  $("#notification").show();
});
progressConnection.on("updateProgressBar", (perc) => {
  setProgress(perc);
});
progressConnection.on("clearProgressBar", () => {
  setProgress(100);
  $("#notification").hide();
});

例如,如果从服务器回调 initProgressBar 方法,帮助程序 setProgress JavaScript 函数就会配置并显示进度栏(此演示使用的是启动进度栏组件)。请注意,代码中使用了 jQuery 库,但仅用于更新 UI。如前所述,客户端 SignalR Core 库不再是 jQuery 插件。也就是说,如果 UI 是基于 Angular 等,可能根本无需使用 jQuery。

服务器端事件

缺少的解决方案部分是,决定何时调用客户端函数。主要有以下两种方案。一种是在客户端通过 Web API 或控制器终结点调用服务器操作时调用。另一种是在客户端直接调用 Hub 时调用。最后,只需决定在哪里为回调客户端的任务编写代码。

在规范聊天示例中,这一切都发生在 Hub 中:执行所需的全部逻辑,并将消息分派给相应连接。监视远程任务是另一回事。它假设后台正在运行某业务流程,以通过某种方式通知进度。从技术角度来讲,可能会在 Hub 中编码此流程,并从中建立与客户端 UI 之间的对话。也可以让控制器 (API) 触发此流程,Hub 仅用于将事件传递给客户端。比此示例更为实际的做法是,在低于控制器级别的层中编码此流程。

总而言之,可以定义 Hub 类,并随时可将它用于决定何时以及是否调用客户端函数。有趣的地方在于,需要什么才能将 Hub 实例注入控制器或其他业务类。此演示在控制器中注入 Hub,但也会对其他更深级别的类执行完全相同的操作。示例 TaskController 是通过 JavaScript 直接从客户端调用,以触发进度栏将显示其进度的远程任务:

public class TaskController : Controller
{
  private readonly IHubContext<ProgressHub> _progressHubContext;
  public TaskController(IHubContext<ProgressHub> progressHubContext)
  {
    _progressHubContext = progressHubContext;
  }
  public IActionResult Lengthy()
  {
    // Perform the task and call back
  }
}

通过 IHubContext<THub> 接口在控制器或其他任何类中注入 Hub。IHubContext 接口封装 Hub 实例,但无法直接访问它。从中可以将消息分派回 UI,但无法访问连接 ID(举个例子)。假设远程任务是在 Lengthy 方法中执行,并需要在其中更新客户端进度栏:

progressHubContext
  .Clients
  .Client(connId)
  .InvokeAsync("updateProgressBar", 45);

连接 ID 可以从 Hub 类中进行检索,但无法像此示例一样从通用 Hub 上下文中进行检索。因此,最简单的方法是,让客户端方法在启动远程任务时就传递连接字符串:

public void Lengthy([Bind(Prefix="id")] string connId) { … }

最后,控制器类接收 SignalR 连接 ID,注入有 Hub 上下文,并使用通过非类型化通用 API 调用的上下文方法(InvokeAsync 方法)执行操作。这样一来,Hub 类就无需包含任何方法!如果觉得这很奇怪,请参阅 bit.ly/2DWd8SV 中的代码。

总结

本文介绍了如何在 Web 应用程序上下文中使用 ASP.NET Core SignalR 监视远程任务。Hub 几乎是空的,因为所有通知逻辑都被内置到控制器中,并使用通过 DI 注入的 Hub 上下文进行编排。这只是 ASP.NET Core SignalR 漫长旅程的起点。接下来,我将深入研究基础结构,并探索类型化 Hub。


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

衷心感谢以下 Microsoft 专家对本文的审阅:Andrew Stanton-Nurse


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