zoukankan      html  css  js  c++  java
  • ASP.NET Core 中的实时框架 SingalR

    SignalR 是什么?

    ASP.NET Core SignalR 是一个开源的实时框架,它简化了向应用中添加实时 Web 功能的过程。
    实时 Web 功能是服务器端能够即时的将数据推送到客户端,而无需让服务器等待客户端请求后才返回数据。

    SignalR 主要适用于:

    • 从服务器获取数据并高频更新的应用。比如股票,GPS应用等。
    • 仪表板和监视应用。比如状态实时更新等。
    • 需要通知的应用。比如即时聊天工具,以及社交网络里面的通知等。
    • 协作应用。比如团体会议软件。

    SignalR 支持下面几种底层传输技术:

    • Web Socket 是不同于HTTP的另一种TCP协议。它是全双工的通信协议,浏览器和服务器之间可以相互通信。它会保持长连接状态只到被主动关闭。它支持文本和二进制的消息传输,也支持流媒体。其实正常的HTTP请求也是使用TCP Socket. Web Socket标准使用了握手机制把用于HTTP的Socket升级为使用WS协议的 WebSocket socket.
    • 服务器发送事件 (Server Sent Events) 服务器可以在任何时间把数据发送到浏览器,而浏览器则会监听进来的信息,并使用一个叫做EventSource的对象用来处理传过来的信息。这个连接一直保持开放,直到服务器主动关闭它。它是单向通信,只能发生文本信息,而且很多浏览器都有最大并发连接数的限制。
    • 长轮询(Long Polling) 客户端会定期的向服务器发送HTTP请求,如果服务器没有新数据的话,那么服务器会继续保持连接,直到有新的数据产生, 服务器才把新的数据返回给客户端。如果请求发出后一段时间内没有响应, 那么请求就会超时。这时,客户端会再次发出请求。

    SignalR 封装了这些底层传输技术,会从服务器和客户端支持的功能中自动选择最佳传输方法,让我们只关注业务问题而不是底层传输技术问题.

    可以只使用WebSocket,具体参考WebSockets support in ASP.NET Core

    在 ASP.NET Core 中使用 SignalR

    使用 SignalR 会涉及到服务端和客户端.

    • Hub 是SignalR服务端最关键的组件, 它作为通信中心, 接受从客户端发来的消息, 也能把消息发送给客户端. 它是服务器端的一个类, 自己创建的Hub类需要继承于基类Hub.
    • 客户端 微软目前官方支持JavaScript, .NET 和 Java客户端. 具体参考ASP.NET Core SignalR 支持的平台.

    做一个小例子演练一下:

    1. 创建一个空白的Web项目, 然后添加 Hub 类

      public class ChatHub : Hub
      {
          public override async Task OnConnectedAsync()
          {
              await Clients.All.SendAsync("ReceiveMessage", $"{Context.ConnectionId} joined");
          }
      
          public override async Task OnDisconnectedAsync(Exception ex)
          {
              await Clients.All.SendAsync("ReceiveMessage", $"{Context.ConnectionId} left");
          }
      
          public Task Send(string message)
          {
              return Clients.All.SendAsync("ReceiveMessage", $"{Context.ConnectionId}: {message}");
          }
      
          public Task SendAllExceptMe(string message)
          {
              return Clients.AllExcept(Context.ConnectionId).SendAsync("ReceiveMessage", $"{Context.ConnectionId}: {message}");
          }
      
          public Task SendToGroup(string groupName, string message)
          {
              return Clients.Group(groupName).SendAsync("ReceiveMessage", $"{Context.ConnectionId}@{groupName}: {message}");
          }
      
          public async Task JoinGroup(string groupName)
          {
              await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
      
              await Clients.Group(groupName).SendAsync("ReceiveMessage", $"{Context.ConnectionId} joined {groupName}");
          }
      
          public async Task LeaveGroup(string groupName)
          {
              await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName);
      
              await Clients.Group(groupName).SendAsync("ReceiveMessage", $"{Context.ConnectionId} left {groupName}");
          }
      
          public Task Echo(string message)
          {
              return Clients.Client(Context.ConnectionId).SendAsync("ReceiveMessage", $"{Context.ConnectionId}: {message}");
          }
      }
      
    2. 添加配置代码

      public class Startup
      {
          public void ConfigureServices(IServiceCollection services)
          {
              services.AddSignalR();
          }
      
          public void Configure(IApplicationBuilder app, IHostingEnvironment env)
          {
              app.UseStaticFiles();
              app.UseSignalR(routes =>
              {
                  routes.MapHub<ChatHub>("/chatHub");
              });
          }
      }
      
    3. 添加客户端
      在wwwroot目录下创建一个名为chat.html的Html静态文件,内容如下:

      <!DOCTYPE html>
      <html>
      <head>
          <meta charset="utf-8" />
          <title></title>
      </head>
      <body>
          <h1 id="head1"></h1>
          <div>
              <input type="button" id="connect" value="Connect" />
              <input type="button" id="disconnect" value="Disconnect" />
          </div>
      
      
          <h4>To Everybody</h4>
          <form class="form-inline">
              <div class="input-append">
                  <input type="text" id="message-text" placeholder="Type a message" />
                  <input type="button" id="broadcast" class="btn" value="Broadcast" />
                  <input type="button" id="broadcast-exceptme" class="btn" value="Broadcast (All Except Me)" />
              </div>
          </form>
      
          <h4>To Me</h4>
          <form class="form-inline">
              <div class="input-append">
                  <input type="text" id="me-message-text" placeholder="Type a message" />
                  <input type="button" id="sendtome" class="btn" value="Send to me" />
              </div>
          </form>
      
          <h4>Group</h4>
          <form class="form-inline">
              <div class="input-append">
                  <input type="text" id="group-text" placeholder="Type a group name" />
                  <input type="button" id="join-group" class="btn" value="Join Group" />
                  <input type="button" id="leave-group" class="btn" value="Leave Group" />
              </div>
          </form>
      
          <h4>Private Message</h4>
          <form class="form-inline">
              <div class="input-prepend input-append">
                  <input type="text" id="group-message-text" placeholder="Type a message" />
                  <input type="text" id="group-name" placeholder="Type the group name" />
      
                  <input type="button" id="sendgroupmsg" class="btn" value="Send to group" />
              </div>
          </form>
      
          <ul id="message-list"></ul>
      </body>
      </html>
      <script src="signalr.js"></script>
      <script>
          let connectButton = document.getElementById('connect');
          let disconnectButton = document.getElementById('disconnect');
          disconnectButton.disabled = true;
          var connection = new signalR.HubConnectionBuilder().withUrl("/chatHub").build();
      
          document.getElementById("connect").addEventListener("click", function (event) {
      
              connectButton.disabled = true;
              disconnectButton.disabled = false;
      
              connection.on('ReceiveMessage', msg => {
                  addLine(msg);
              });
      
              connection.onClosed = e => {
                  if (e) {
                      addLine('Connection closed with error: ' + e, 'red');
                  }
                  else {
                      addLine('Disconnected', 'green');
                  }
              }
      
              connection.start()
                  .then(() => {
                      addLine('Connected successfully', 'green');
                  })
                  .catch(err => {
                      addLine(err, 'red');
                  });
      
              event.preventDefault();
          });
      
          document.getElementById("disconnect").addEventListener("click", function (event) {
      
              connectButton.disabled = false;
              disconnectButton.disabled = true;
      
              connection.stop();
      
              event.preventDefault();
          });
      
          document.getElementById("broadcast").addEventListener("click", function (event) {
      
              var message = document.getElementById('message-text').value;
              connection.invoke("Send", message).catch(function (err) {
                  addLine(err, 'red');
              });
      
              event.preventDefault();
          });
      
          document.getElementById("broadcast-exceptme").addEventListener("click", function (event) {
      
              var message = document.getElementById('message-text').value;
              connection.invoke("SendAllExceptMe", message).catch(function (err) {
                  addLine(err, 'red');
              });
      
              event.preventDefault();
          });
      
          document.getElementById("sendtome").addEventListener("click", function (event) {
      
              var message = document.getElementById('me-message-text').value;
              connection.invoke("Echo", message).catch(function (err) {
                  addLine(err, 'red');
              });
      
              event.preventDefault();
          });
      
          document.getElementById("join-group").addEventListener("click", function (event) {
      
              var groupName = document.getElementById('group-text').value;
              connection.invoke("JoinGroup", groupName).catch(function (err) {
                  addLine(err, 'red');
              });
      
              event.preventDefault();
          });
      
          document.getElementById("leave-group").addEventListener("click", function (event) {
      
              var groupName = document.getElementById('group-text').value;
              connection.invoke("LeaveGroup", groupName).catch(function (err) {
                  addLine(err, 'red');
              });
      
              event.preventDefault();
          });
      
          document.getElementById("sendgroupmsg").addEventListener("click", function (event) {
              var groupName = document.getElementById('group-name').value;
              var message = document.getElementById('group-message-text').value;
              connection.invoke("SendToGroup", groupName, message).catch(function (err) {
                  addLine(err, 'red');
              });
      
              event.preventDefault();
          });
      
          function addLine(line, color) {
              var child = document.createElement('li');
              if (color) {
                  child.style.color = color;
              }
              child.innerText = line;
              document.getElementById('message-list').appendChild(child);
          }
      </script>
      
    4. 编译并运行 http://localhost:port/chat.html 测试.

    权限验证

    SignalR 可以采用 ASP.NET Core 配置好的认证和授权体系, 比如 Cookie 认证, Bearer token 认证, Authorize授权特性和 Policy 授权策略等.

    • Cookie 认证基本上不需要额外配置, 但仅限于浏览器客户端.
    • Bearer token 认证适用于所有客户端. 可以参考上篇文章 ASP.NET Core WebAPI中使用JWT Bearer认证和授权 进行Token的分发和验证. 在 SignalR 中使用的时候需要注意两点:
      • 在 WebAPI 中, bearer token 是通过 HTTP header 传输的, 但当 SignalR 使用 WebSockets 和 Server-Sent Events 传输协议的时候, 由于不支持 header, Token是通过 query string 传输的, 类似于ws://localhost:56202/chatHub?id=2fyJlq1T5vBOwAsITQaW8Q&access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9, 所以需要在服务端增加额外的配置如下:

        services.AddAuthentication(options =>
        {
            options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        
        }).AddJwtBearer(configureOptions =>
        {
            // Configure JWT Bearer Auth to expect our security key
        
            // We have to hook the OnMessageReceived event in order to
            // allow the JWT authentication handler to read the access
            // token from the query string when a WebSocket or 
            // Server-Sent Events request comes in.
            configureOptions.Events = new JwtBearerEvents
            {
                OnMessageReceived = context =>
                {
                    var accessToken = context.Request.Query["access_token"];
        
                    if (!string.IsNullOrEmpty(accessToken) && (context.HttpContext.Request.Path.StartsWithSegments("/chatHub")))
                    {
                        context.Token = accessToken;
                    }
                    return Task.CompletedTask;
                }
            };
        });
        

        同时, 给 Hub 添加 Authorize 特性.

        [Authorize]
        public class ChatHub: Hub
        {
        }
        
      • JS 客户端使用 accessTokenFactory 创建带 Token 的连接.

        this.connection = new signalR.HubConnectionBuilder()
            .withUrl("/chatHub", { accessTokenFactory: () => this.loginToken })
            .build();
        
      • 如果服务端认证通过, 可以使用 Context.User 获取用户信息, 它是一个 ClaimsPrinciple 对象.

    横向扩展

    Hub 服务器可以支持的 TCP 并发连接数是有限的. 同时由于 SignalR 连接是持久的, 甚至当客户端进入空闲状态时,SignalR 连接依然保持着打开状态。所以当连接数比较多时, 通过增加服务器来实现横向扩展是很有必要的.

    但相比于 WebAPI的单向通信(只存在客户端请求,服务端响应的情景), SignalR 中可能使用双向通信协议(客户端可以请求服务端的数据, 服务端也可以向客户端推送数据), 此时服务端水平扩展的时候, 一台服务器是不知道其他服务器上连接了哪些客户端. 当在一台服务器想要将消息发送到所有客户端时,消息只是发送到连接到该服务器的客户端. 为了能够把消息发送给所有服务器都连接的客户端, 微软提供了下面两种方案:

    • Azure SignalR 服务 是一个代理。当客户端启动连接到服务器时,会重定向连接到 Azure SignalR 服务。

      azure-signalr-service-multiple-connections

    • Redis 底板 当服务器想要将消息发送到所有客户端时,它将先发送到 Redis 底板, 然后使用 Redis 的发布订阅功能转发给其他所有服务器从而发送给所有客户端.

      redis-backplane

      添加 NuGet 包, ASP.NET Core 2.2 及更高版本中使用 Microsoft.AspNetCore.SignalR.StackExchangeRedis, 之前版本使用Microsoft.AspNetCore.SignalR.Redis.
      然后在Startup.ConfigureServices方法中, 添加 AddStackExchangeRedis services.AddSignalR().AddStackExchangeRedis("<your_Redis_connection_string>");

    源代码

    Github

    参考

  • 相关阅读:
    ES6 学习笔记(整理一遍阮一峰大神得入门文档,纯自己理解使用)
    怪异模式和标准模式
    计算机网络七层协议模型 “开放系统互联参考模型”,即著名的OSI/RM模型(Open System Interconnection/Reference Model)
    流行得前端构建工具比较,以及gulp配置
    谈谈刚接触sea.js框架得看法
    MAC终端安装grunt--javascript世界得构建工具
    js的数组与对象关系
    JavaScript中的setInterval用法
    每周一题:平方数之和((更新JS)
    每周一题:拿硬币(更新JS)
  • 原文地址:https://www.cnblogs.com/royzshare/p/10194326.html
Copyright © 2011-2022 走看看