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

目录

ASP.NET Core 3.x 入门(一)创建项目 和 部署

ASP.NET Core 3.x 入门(二)建立 Controller ,使用 Tag Helper

ASP.NET Core 3.x 入门(三)View Component

ASP.NET Core 3.x 入门(四)Razor Page

ASP.NET Core 3.x 入门(五)SignalR

ASP.NET Core 3.x 入门(六)Blazor

ASP.NET Core 3.x 入门(七)Web API

ASP.NET Core 3.x 入门(八)gRPC - Protocol Buffer

ASP.NET Core 3.x 入门(九)gRPC in ASP.NET Core 3.x

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();

执行顺序,调试可以看出来

  1. index.js fetch()
  2. CountController await this._countHub.Clients.All.SendAsync("someFunc", new { random = "abcd" });
  3. index.js then(id => connection.invoke("GetLatestCount", id))
  4. CountHub GetLatestCount()
  5. GetLatestCount() await this.Clients.All.SendAsync("ReceiveUpdate", count);
  6. index.js ReceiveUpdate 事件
  7. GetLatestCount() await this.Clients.All.SendAsync("Finished");
  8. index.js finished 事件

直白的说,就是前后端的方法可以通信了,互相调用

ASP.NET Core 3.x 入门(五)SingnalR 结束

我觉得这个例子不是很好,就潦草的带过了

我觉得比较好的例子:https://github.com/aspnet/SignalR-samples
要看出效果需要开多个浏览器页面

posted @ 2021-05-13 20:08  .NET好耶  阅读(573)  评论(0编辑  收藏  举报