ASP.NET Core 3.x 入门(五)SignalR
此入门教程是记录下方参考资料视频的学习过程
开发工具:Visual Studio 2019
参考资料:https://www.bilibili.com/video/BV1c441167KQ
API文档:https://docs.microsoft.com/zh-cn/dotnet/api/?view=aspnetcore-3.1
目录
SignalR 原理
SignalR 用于做实时的 Web 应用
传统的HTTP请求:
浏览器 发送HTTP请求到 ASP.NET Core Web Server
Web 服务器处理请求并返回响应,并且在 payload里会包含请求的数据
什么是实时Web?
例如:网页版的即时通讯工具、网页直播、网页游戏、股票
原理:不需要从浏览器发起,Server 主动发送到 Client
SignalR “底层”技术
- SignalR 使用了三种“底层”技术来实现实时Web,它们分别是 Long Polling(长轮询),Server Sent Events 和 Web Socket
Polling
- Polling 是实现实时 Web 的一种笨方法,它就是通过定期的向服务器发送请求,来查看服务器的数据是否有变化
- 如果服务器数据没有变化,那么就返回 204 No Content;如果有变化就把最新的数据发送给客户端
- 这就是Polling,很简单,但是比较浪费资源
- SignalR 没有采用 Polling 这种技术
Long Polling
- Long Polling 和 Polling 有类似的地方,客户端都是发送数据请求到服务器,但是不同之处是:如果服务器没有新数据要发给客户端的话,那么服务器会继续保持连接,直到有新的数据产生,服务器才把新的数据返回给客户端
- 如果请求发出后一段时间内没有响应,那么请求就会超时。这时,客户端会再次发出请求
Server Sent Events (SSE)
- 使用 SSE 的话,Web 服务器可以在任何时间把数据发送到浏览器,可以称之为推送。而浏览器则会监听进来的信息,这些信息就像流数据一样,这个连接也会一直保持开放,直到服务器主动关闭它
- 浏览器会使用一个叫做 EventSource 的对象用来处理传过来的信息
优点:
- 使用简单,使用的还是 HTTP 协议
- 自动重连
缺点:
- 很多浏览器都有最大并发连接数的限制
- 只能发送文本信息
- 只能单向通信
Web Socket
- Web Socket 是不同于 HTTP 的另一个 TCP 协议。它使得浏览器和服务器之间的交互式通信变得可能。使用 WebSocket,消息可以从服务器发往客户端,也可以从客户端发往服务器,并且没有 HTTP 那样的延迟。信息流没有完成的时候,TCP Socket 通常是保持打开的状态
- 使用现代浏览器时,SignalR大部分情况下都会使用 Web Socket,这也是最有效的传输方式
- 全双工通信:客户端和服务器可以同时往对方发送消息
- 并且不受 SSE 的那个浏览器连接数限制(6个),大部分浏览器对 Web Socket 连接数的限制是 50 个
- 消息类型:可以是文本和二进制,web Socket 也支持流媒体(音频和视频)
- 其实正常的 HTTP 请求也使用了 TCP Socket。web Socket 标准使用了握手机制把用于 HTTP 的 Socket 升级为使用 WS 协议的 WebSocket socket
Web Socket 生命周期
所有的一切都发生在 TCP Socket 里面
首先,一个常规的 HTTP 请求,会要求服务器更新 Socket 并协商,这个就叫做 HTTP 握手
然后消息就可以在 Socket 里面来回传送,直到这个 Socket 被主动关闭
在主动关闭的时候,关闭的原因也会被统计
HTTP 握手
- 每一个 Web Socket 开始的时候都是一个简单的 HTTP Socket
- 客户端首先发送一个 GET 请求到服务器,来请求升级 Socket
- 如果服务器同意的话,这个 Socket 从这时候开始就变成了 Web Socket ,同意的状态码是101
消息类型
- Web Socket 的消息类型可以是文本,二进制。也包括控制类的消息:Ping/Pong,和关闭
- 每个消息由一个或多个 Frame 组成
SignalR
- SignalR 是一个 .NET Core/.NET Framework 的开源实时框架。SignalR可使用 Web Socket、Server Sent Events 和 Long Polling 作为底层传输方式
- SignalR基于这三种技术构建,抽象于它们之上,它让你更好的关注业务问题而不是底层传输技术问题
- SignalR 这个框架服务器端和客户端,服务器端支持 ASP.NET Core 和 ASP.NET ;而客户端除了支持浏览器里的 JavaScript 以外,也支持其它类型的客户端,例如控制台、WPF、桌面应用
SignalR 回落机制
- SignalR 使用的三种底层传输技术分别是 Web Socket、Server Sent Events 和 Long Polling
- 其中Web Socket 仅支持比较现代的浏览器,Web 服务器也不能太老
- 而 Server Sent Events 情况可能好一点,但是也存在同样的问题
- 所以 SignalR采用了回落机制,SignalR有能力去协商支持的传输类型
Web Socket -> Server Sent Events -> Long Polling ,根据浏览器是否支持而更换
一旦 SignalR 建立连接之后,就会开始发送 Keep Alive 这种消息,保持存活或者叫保持活跃,发送这个消息目的是来检查这个连接是否正常,如果有问题就会抛出异常
因为 SignalR 是基于三种技术之上的,所以我们使用 SignalR 的时候, 用法都是一样的
SignalR 默认采用这种回落机制来进行传输和连接,但也可以禁用这种回落机制,只采用其中一种传输方式也可以
RPC
- RPC(Remote Procedure Call)。它的优点就是可以像调用本地方法一样调用远程访问
- SignalR 采用 RPC 范式来进行客户端与服务器端之间的通信
- SignalR 利用底层传输来让服务器可以调用客户端的方法,反之亦然,这些方法可以带参数,参数也可以是复杂对象,SignalR 负责序列化和反序列化
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 小例子
新建项目
还是创建 ASP.NET Core 3.0 的空模板
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
//使用 控制器(Mvc也可以,根据需求决定)
services.AddControllers();
//使用 SignalR
services.AddSignalR();
//服务使用单例模式
services.AddSingleton<CountService>();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseStaticFiles();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
//使用 Controller
endpoints.MapControllers();
//路由,使用 Hub
endpoints.MapHub<CountHub>("/countHub");
});
}
新建一个服务,由于这是一个小例子,所以不使用接口,Services 目录下新建 CountService.cs
使用单例模式,已经在 Startup 类中配置好了
public class CountService
{
private int _count;
public int GetLatestCount()
{
return this._count++;
}
}
在项目目录下新建 Hubs 文件夹,Hubs 目录下新建一个类 CountHub
,继承自 Hub
public class CountHub : Hub
{
private readonly CountService _countService;
public CountHub(CountService countService)
{
this._countService = countService;
}
public async Task GetLatestCount()
{
int count;
do
{
count = this._countService.GetLatestCount();
Thread.Sleep(1000);
await this.Clients.All.SendAsync("ReceiveUpdate", count);
} while (count < 10);
await this.Clients.All.SendAsync("Finished");
}
}
控制器,新建一个 CountController.cs
[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 方法,传递一个 random 参数
await this._countHub.Clients.All.SendAsync("someFunc", new { random = "abcd" });
return Accepted(1);
}
}
Hub 类中有一个 Context ,是 HubCallerContext 类,其中有一个 ConnectionId ,这个 Id 是客户端的唯一标识,我们可以通过这个获取客户端的信息,就可以调用客户端的方法
CountHub 中添加,实际运行时,需要注释掉以下代码
public override async Task OnConnectedAsync()
{
var connectionId = this.Context.ConnectionId;
//获取客户端
var client = this.Clients.Client(connectionId);
//调用客户端上的 someFunc 方法
await client.SendAsync("someFunc", new { });
//除了指定的客户端,调用其它客户端上的 someFunc 方法,并传递参数
//await this.Clients.AllExcept(connectionId).SendAsync("someFunc", new { });
//给客户端分组
//await this.Groups.AddToGroupAsync(connectionId, "MyGroup");
//从分组中移除客户端
//await this.Groups.RemoveFromGroupAsync(connectionId, "MyGroup");
//指定分组中的所有客户端调用 someFunc 方法
//await this.Clients.Groups("MyGroup").SendAsync("someFunc");
}
安装 SignalR
右键项目 -》 Add -》 Client Side Library
provider:unpkg
Library:@aspnet/signalr@next
选择 dist/browser/signalr.js 即可,放到 wwwroot 里
wwwroot 目录下新建 index.html 和 index.js
index.html
<!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 type="text/javascript" src="/lib/aspnet/signalr/dist/browser/signalr.js"></script>
<script type="text/javascript" src="index.js"></script>
</body>
</html>
index.js
let connection = null;
setupConnection = () => {
//使用 HubConnectionBuilder 构建 connection
//withUrl() 中的路由地址要与 Startup 中的一致
//也可以在 withUrl() 中 指定传输类型 Web Socket 、Server Sent Events 、Long Polling,使用 signalR.HttpTransportType. 去引用
connection = new signalR.HubConnectionBuilder()
.withUrl("/counthub")
.build();
//定义三个事件,对应后端 Hub 或者 Controller 里的代码
connection.on("ReceiveUpdate", (update) => {
const resultDiv = document.getElementById("result");
resultDiv.innerHTML = update;
});
connection.on("someFunc", function (obj) {
const resultDiv = document.getElementById("result");
resultDiv.innerHTML = "Someone called parameters:" + obj.random;
});
//事件不区分大小写
connection.on("finished", function () {
//连接完成,停止连接
connection.stop();
const resultDiv = document.getElementById("result");
resultDiv.innerHTML = "Finished";
console.log("Finished");
});
//定义完事件,开始连接
connection.start()
.catch(err => console.error(err.toString()));
};
setupConnection();
document.getElementById("submit").addEventListener("click", e => {
e.preventDefault();
//Fetch API
//控制器只有一个 Post 方法
fetch("/api/count",
{
method: "POST",
headers: {
'content-type': 'application/json'
}
})
.then(response => response.text())
.then(id => connection.invoke("GetLatestCount", id));//这里可以调用到后端 Hub 的 GetLatestCount 方法
});
效果就是不同的客户端会显示相同的内容,开多个网页测试
如果要使用 Message Pack 需要去 NuGet 安装,再去 Startup 里添加
services.AddSignalR().AddMessagePackProtocol();
执行顺序,调试可以看出来
- index.js
fetch()
- CountController
await this._countHub.Clients.All.SendAsync("someFunc", new { random = "abcd" });
- index.js
then(id => connection.invoke("GetLatestCount", id))
- CountHub
GetLatestCount()
- GetLatestCount()
await this.Clients.All.SendAsync("ReceiveUpdate", count);
- index.js
ReceiveUpdate
事件 - GetLatestCount()
await this.Clients.All.SendAsync("Finished");
- index.js
finished
事件
直白的说,就是前后端的方法可以通信了,互相调用
ASP.NET Core 3.x 入门(五)SingnalR 结束
我觉得这个例子不是很好,就潦草的带过了
我觉得比较好的例子:https://github.com/aspnet/SignalR-samples
要看出效果需要开多个浏览器页面