ASP.NET Core 3.x 学习笔记(6)——SignalR

本文是ASP.NET Core 3.x中SignalR的学习笔记。介绍了实时Web概念,SignalR的底层技术如轮询、长轮询、SSE和WebSocket,还讲解了SignalR的概念,包括回落机制、RPC、Hub和横向负载等,最后说明了如何使用SignalR进行通信及相关代码实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本系列学习笔记均来源于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() 注册一个处理程序,当调用具有指定方法名称的集线器方法时将调用该处理程序。

  1. 建立 Service

    namespace ASPNETCore_Learnting_SignalR.Services
    {
        public class CountService
        {
            private int _count;
            public int GetLatestCount()
            {
                return _count++;
            }
        }
    }
    
    
  2. 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");
                });
            }
        }
    }
    
  3. 建立 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);
            }
        }
    }
    
  4. 建立控制器 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);
            }
        }
    }
    
  5. 启用客户端和服务器端通信。

    • 通过重写 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 形式。

      1. 使用 libman 导入使用 SignalR 的 js 依赖。

在这里插入图片描述

在这里插入图片描述

  1. 建立 wwwroot 目录,并将 js 依赖导入

在这里插入图片描述

  1. 建立 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"));
  }); ```

 




评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值