ASP.NET Core 3.x 学习笔记(6)——SignalR
本系列学习笔记均来源于B站UP主”软件工艺师“的学习视频,学习连接如下:
https://www.bilibili.com/video/BV1c441167KQ
ASP.NET Core 3.x 学习笔记(6)——SignalR
SignalR 使用来做实时 Web 应用的技术。
什么是实时 web
传统的HTTP请求:
实时的 Web 应用:不用从浏览器发起,Web 应用可以主动地通知客户端数据有变化。
SignalR “底层”技术
SiganlR 使用了三种“底层”技术来实现实时 web,它们分布式 Long Polling,Server Sent Events 和 Websocket。
Polling(轮询)
Polling 是实现实时 Web 的一种笨方法,他就是通过定期向服务器发送请求,来查看服务器的数据是否有变化。
如果服务器的数据没有变化,那么返回 204 No Content;如果有变化,就把最新的数据发送给客户端;
Polling 很简单,但是比较浪费资源,SignalR 没有采用 Polling 这种技术。
Long Polling
Long Polling 和 Polling 有类似的地方,客户端都是发送请求到服务器,但是不同之处是:如果服务器没有新数据要发给客户端的话,那么服务器会继续保持连接,直到有新数据产生,服务器才把新的数据返回给客户端。
如果请求发出一段时间内没有响应,那么请求就会超时。这时,客户端会再次发出请求。
Server Sent Events(SSE)
使用 SSE,Web 服务器可以在任何时间把数据发送到浏览器,可以称之为推送。而浏览器则会监听进来的信息,这些信息就像流数据一样,这个连接也会一直保持开放,直到服务器主动关闭他。
浏览器会使用一个叫做 EventSource 的对象用来处理传过来的信息。
优点:使用简单,依旧使用 HTTP 协议;支持自动重连
缺点:很多浏览器针对这种模式都有最大并发连接数的限制(6个);只能发送文本信息,且只能是单向通信。
Web Socket
Web Socket 是不同于 HTTP 的另一个 TCP 协议。它使得浏览器和服务器之间的交互式通信变得可能。使用 Web Socket,消息可以从服务器发往客户端,也可以从客户端发往服务器,并且没有 HTTP 那样的延迟。信息流没有完成的时候,TCP Socket 通常是保持打开的状态。
使用现代浏览器时,SignalR 大部分情况下都会使用 Web Socket,这也是最有效的传输方式。
SignalR 是全双工通信:客户端和服务器可以同时往对方发送消息。
并且 Web Socket 不受 SSE 的那个浏览器连接数限制(6个),大部分浏览器对 Web Socket 连接数的限制是 50 个。
Web Socket 传输的消息类型:可以是文本和二进制,也支持流媒体(音频和视频)
其实正常的 HTTP 请求也使用了 TCP Socket。Web Socket 标准使用了握手机制把用于 HTTP 的 Socket 升级为使用 WS 协议的 WebSocket socket。
Web Socket 的生命周期
- 首先,一个常规的 HTTP 请求会要求服务器更新 Socket 并协商(HTTP 握手)
- 然后消息就可以在 Socket 中来回传送,直到 Socket 主动关闭。在主动关闭的时候,关闭的原因也会被通信。
HTTP 握手
每一个 Web Socket 开始的时候都是一个简单的 HTTP Socket。客户端首先发送一个 GET 请求到服务器,来请求升级 Socket。如果服务器同意的话,这个 Socket 从这是开始就变成了 Web Socket。
下面是请求升级 Socket 的 GET 请求:
- Upgrade:webSocket 表示请求从 Socket 升级为 Web Socket
- Sec-WebSocket-Key:【重要】可以防止缓存问题
服务器接收并理解上面请求后,返回下面信息:
- HPPT/1.1 101:返回状态码 101,表示切换协议。如果返回的不是 101,浏览器就知道服务器没有处理 Web Socket 的能力
- Sec-WebSocket-Accept:与 Sec-WebSocket-Key 相对应
消息类型
Web Socket 的消息类型可以是文本、二进制,也包括控制类的消息:Ping/Pong、和关闭
每个消息类型由一个或多个 Frame 组成:
- Frame 是二进制的,所以如果消息发送的是文本,最终也是转换成二进制的
SignalR 概念学习
SiganlR 是一个 .NET Core/.NET Framework 的开源实时框架。SignalR 可用 Long Polling,Server Sent Events 和 Websocket 作为底层传输方式。
SiganlR 基于这三种技术构建,抽象于它们之上,它让你更好地关注业务问题,而不是底层传输技术问题。
SiganlR 这个框架分服务器和客户端,服务器端支持 ASP.NET Core 和 ASP.NET;而客户端除了支持浏览器里面的 JavaScript 以外,也支持其它类型的客户端,例如桌面应用。
SiganlR 回落机制
SignalR 使用的三种底层传输技术分布是 Web Socket,Server Sent Events 和 Long Polling。其中 Web Socket 仅支持比较现代的浏览器,Web 服务器也不能太老。而 Server Sent Events 情况好一些,但也存在同样的问题。==所以 SignalR 采用了回落机制。SignalR 有能力去协商支持的传输类型。==即若服务器或浏览器不支持高级别的 Web Socket,则 SignalR 可以协商逐步降级传输类型。
- 无论 SignalR 底层采用哪种底层传输技术,我们可以用相同的方式使用 SignalR。
- 而且我们可以针对 SignalR 中的三种传输技术进行启用和禁用,使其只采用其中一种传输方式。
- 一旦 SignalR 建立连接后,就会发送一个 KeepAlive 的消息,用于检查连接是否存在异常。
RPC
RPC(Remote Procedure Call 远程服务调用)。它的优点就是可以像调用本地方法一样调用远程服务。
SignalR 采用 RPC 范式来进行客户端与服务器之间的通信。
SignalR 采用底层传输来让服务器可以调用客户端的方法,反之亦然,这些方法可以带参数,参数也可以是复杂对象,SiganlR 负责序列化和反序列化。
Hub
Hub 是 SignalR 的一个组件,它运行在 ASP.NET Core 应用里。所以它是服务器端的一个类。
Hub 使用 RPC 接收从客户端发来的消息,也能把消息发送给客户端。所以它就是一个通信用的 Hub。
在 ASP.NET Core 中,自己创建的 Hub 类需要继承于基类的 Hub。
在 Hub 类里面,我们就可以调用所有客户端上的方法了。同样客户端也可以调用 Hub 类里面的方法。而且调用过程中,方法名都是不变的。
方法调用的时候可以传递复杂参数,SignalR 可以将参数序列化和反序列化。这些参数被序列化的格式叫做 Hub 协议,所以 Hub 协议就是一种用来序列化和反序列化的格式。
Hub 协议的默认协议是 JSON,还支持另外一个协议是 MessagePack。MessagePack 是二进制格式的,它比 JSON 更紧凑,而且处理起来更简单快速,因为它是二进制的。
此外,SignalR 也可以扩展使用其他协议。
横向负载
随着系统的运行,可能需要将系统进行横向扩展,即将系统运行在多个服务器上。这时负载均衡器会保证每个进来的请求,按照一定的逻辑分配到可能是不同的服务器上。
在使用 Web Socket 的时候,(使用负载均衡)没有什么问题。因为一旦 Web Socket 的连接建立,就像在浏览器和那个服务器之间打开了隧道一样,服务器是不会切换的。
但是如果使用 Long Polling 就可能有问题了,因为使用 Long Polling 的情况下,每次发送消息都是不同的请求,而每次请求可能会到达不同的服务器,不同的服务器可能不知道前一个服务器通信的内容,这就会造成问题。
针对这个问题,我们需要使用 Sticky Sessions(粘性规划)。
Sticky Sessions 貌似有很多种实现方式,但是主要是下面要介绍的这种方式:
作为第一次请求的响应的一部分,负载均衡器会在浏览器里面设置一个 Cookie,来表示使用过这个服务器。在后续的请求里,负载均衡器去读取 Cookie,然后把请求分配给同一个服务器。
使用 SignalR
如下代码中,简单地使用 SignalR 通信的核心在于:使用 SignalR 中心 Hub 中的 client.SendAsync 方法对客户端发送信息。SendAsync 方法中,第一个参数用于约定通信的方法名,其后的参数按照方法的参数要求进行传参。
取如下部分方法代码进行分析:
await _countHub.Clients.All.SendAsync("someFunc", new { Random = "abcd" });
setupConnection = () => {
connection = new signalR.HubConnectionBuilder()
.withUrl("/countHub")
.build();
connection.on("someFunc", function (obj) {
const resultDiv = document.getElementById("result");
retultDiv.innerHTML = "Some called, parameters: " + obj.random;
});
connection.start()
.catch(err => console.error(err.toString()));
};
后台服务器与客户端约定同一方法“someFunc”,该方法参数为一个对象。
在服务器端,对于当前 Hub 中所有客户端均发送一个消息,消息由客户端的 someFunc 方法接收,并传递一个对象作为参数。即表明该方法的形参仅为一个,且为一个对象。
在客户端,通过 signalR.HubConnectionBuilder() 建立通道进行接收请求;通过 connection.on() 注册一个处理程序,当调用具有指定方法名称的集线器方法时将调用该处理程序。
-
建立 Service
namespace ASPNETCore_Learnting_SignalR.Services { public class CountService { private int _count; public int GetLatestCount() { return _count++; } } }
-
Startup.cs 中添加 SignalR 相关配置
using ASPNETCore_Learnting_SignalR.Services; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; namespace ASPNETCore_Learnting_SignalR { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddControllers(); services.AddSignalR(); services.AddSingleton<CountService>(); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseStaticFiles(); app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); endpoints.MapHub<CountHub>("/countHub"); }); } } }
-
建立 CountHub.cs,继承 Hub。Hub 是 SignalR 的中心,用作处理客户端 - 服务器通信的高级管道。所有通信请求都从这里经过。
using ASPNETCore_Learnting_SignalR.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.SignalR; using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace ASPNETCore_Learnting_SignalR { //Hub 也支持身份认证 //[Authorize] //表示对对整个 CountHub 中的方法,都需要进行身份认证后才可以访问. public class CountHub: Hub { private readonly CountService _countService; public CountHub(CountService countService) { this._countService = countService; } public async Task GetLatestCount(string random) { //var user = Context.User.Identity.Name; //用于获得客户端用户的信息. int count; do { count = _countService.GetLatestCount(); Thread.Sleep(1000); await Clients.All.SendAsync("ReceiveUpdate", count); } while (count < 10); await Clients.All.SendAsync("Finished", count); } } }
-
建立控制器 HubController.cs
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SignalR; using System.Threading.Tasks; namespace ASPNETCore_Learnting_SignalR.Controllers { [Microsoft.AspNetCore.Components.Route("api/count")] public class CountController : Controller { private readonly IHubContext<CountHub> _countHub; public CountController(IHubContext<CountHub> countHub) { this._countHub = countHub; } [HttpPost] public async Task<IActionResult> Post() { //someFunc 是客户端的一个方法 //表示:调用客户端的 someFunc 方法,方法传递了一个对象参数 Random await _countHub.Clients.All.SendAsync("someFunc", new { Random = "abcd" }); return Accepted(1); } } }
-
启用客户端和服务器端通信。
-
通过重写 Hub(CountHub)的 OnConnectedAsync() 方法。关于处理通信:
- Hub 支持身份认证。使用 [Authorize] 即可,然后通过 Context.User.Identity.Name 获得客户端的用户信息
- 可对通信客户端进行分组处理
using ASPNETCore_Learnting_SignalR.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.SignalR; using System.Threading; using System.Threading.Tasks; namespace ASPNETCore_Learnting_SignalR { //Hub 也支持身份认证 [Authorize] //表示对对整个 CountHub 中的方法,都需要进行身份认证后才可以访问. public class CountHub: Hub { private readonly CountService _countService; public CountHub(CountService countService) { this._countService = countService; } public async Task GetLatestCount(string random) { var user = Context.User.Identity.Name; //用于获得客户端用户的信息. int count; do { count = _countService.GetLatestCount(); Thread.Sleep(1000); await Clients.All.SendAsync("ReceiveUpdate", count); } while (count < 10); await Clients.All.SendAsync("Finished", count); } public override async Task OnConnectedAsync() { var connectionId = Context.ConnectionId; var client = Clients.Client(connectionId); //获得客户端 await client.SendAsync("someFunc", new { }); //在 connectionId 连接的客户端调用 someFunc 方法,并传递 参数 new { } await Clients.AllExcept(connectionId).SendAsync("someFunc"); //在除 connectionId 连接的客户端外,其它的客户端调用 someFunc //对客户端进行分组操作 await Groups.AddToGroupAsync(connectionId, "MyGroup"); //将 connectionId 连接的客户端添加的 MyGroup 组 await Groups.RemoveFromGroupAsync(connectionId, "MyGroup"); //将 connectionId 连接的客户端从 MyGroup 组移除 await Clients.Groups("MyGroup").SendAsync("SomeFunc"); } } }
-
使用 SignalR 的 JavaScript 形式。
- 使用 libman 导入使用 SignalR 的 js 依赖。
-
- 建立 wwwroot 目录,并将 js 依赖导入
- 建立 index.js 文件,响应后台(服务器)发送过来的请求。
客户端由 new signalR.HubConnectionBuilder() 指定路由并进行接收请求信息。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>
<button id="submit">Submit</button>
<div id="result" style="color: green;font-weight:bold;font-size:24px;"></div>
<script src="/lib/aspnet/signalr/dist/browser/signalr.js"></script>
<script src="index.js"></script>
</body>
</html>
- 建立 index.js 文件,并编写通信代码。
import { signalR } from "./lib/aspnet/signalr/dist/browser/signalr";
let connection = null;
setupConnection = () => {
connection = new signalR.HubConnectionBuilder()
.withUrl("/countHub")
.build();
connection.on("ReceiveUpdate", (update) => {
const resultDiv = document.getElementById("result");
retultDiv.innerHTML = update;
});
connection.on("someFunc", function (obj) {
const resultDiv = document.getElementById("result");
retultDiv.innerHTML = "Some called, parameters: " + obj.random;
});
connection.on("finished", function (obj) {
connection.stop();
const resultDiv = document.getElementById("result");
retultDiv.innerHTML = "Finished";
});
connection.start()
.catch(err => console.error(err.toString()));
};
setupConnection();
document.getElementById("submit").addEventListener("click", e => {
e.preventDefault();
fetch("/api/count",
{
method: "POST",
headers: {
'content-type': 'application/json'
}
})
.then(response => response.text())
.then(id => connection.invoke("GetLatestCount"));
}); ```