ASP.Net Core 3.1 使用实时应用SignalR入门 ASP.NET Core SignalR 简介 实时Web简述 "底层"技术 Long Polling Server Sent Events (SSE) Web Socket

参考文章:微软官方文档:https://docs.microsoft.com/zh-cn/aspnet/core/signalr/introduction?view=aspnetcore-3.1https://www.cnblogs.com/cgzl/p/9509207.html 系列

什么是 SignalR?

实时 web 功能使服务器端代码可以立即将内容推送到客户端。

适用于 SignalR :

  • 示例包括游戏、社交网络、投票、拍卖、地图和 GPS 应用。
  • 示例包括公司仪表板、即时销售更新或旅行警报。
  • 协作应用的示例包括白板应用和团队会议软件。
  • 社交网络、电子邮件、聊天、游戏、旅行警报和很多其他应用都需使用通知。

Rpc 通过服务器端 .NET Core 代码从客户端调用 JavaScript 函数。

下面是的某些功能 SignalR ASP.NET Core:

  • 自动处理连接管理。
  • 例如,聊天室。
  • 向特定客户端或客户端组发送消息。
  • 可缩放以处理不断增加的流量。

传输

SignalR 支持以下用于按正常回退) (处理实时通信的技术:

SignalR 自动选择服务器和客户端功能内的最佳传输方法。

中心

SignalR 使用 集线器 在客户端和服务器之间进行通信。

XHR 级别 2 ,才能提供 MessagePack 协议支持。

当客户端找到匹配项时,它将调用方法并向其传递反序列化的参数数据。

下面介绍SignalR所用到的技术和原理

实时Web简述

大家都见过和用过实时Web, 例如网页版的即时通讯工具, 网页直播, 网页游戏, 还有股票仪表板等等.

传统的Web应用是这样工作的:

ASP.Net Core 3.1 使用实时应用SignalR入门
ASP.NET Core SignalR 简介
实时Web简述
"底层"技术
Long Polling
Server Sent Events (SSE)
Web Socket

浏览器发送HTTP请求到ASP.NET Core Web服务器, 如果一切顺利的话, Web服务器会处理请求并返回响应, 在Payload里面会包含所请求的数据.

但是这种工作方式对实时Web是不灵的. 实时Web需要服务器可以主动发送消息给客户端(可以是浏览器):

ASP.Net Core 3.1 使用实时应用SignalR入门
ASP.NET Core SignalR 简介
实时Web简述
"底层"技术
Long Polling
Server Sent Events (SSE)
Web Socket

Web服务器可以主动通知客户端数据的变化, 例如收到了新的对话消息.

"底层"技术

而SignalR使用了三种"底层"技术来实现实时Web, 它们分别是Long Polling, Server Sent Events 和 Websocket.

首先, 得知道什么是Ajax. 这个就不介绍了.

Long Polling

Polling

介绍Long Polling之前, 首先介绍一下Polling.

Polling是实现实时Web的一种笨方法, 它就是通过定期的向服务器发送请求, 来查看服务器的数据是否有变化.

如果服务器数据没有变化, 那么就返回204 No Content; 如果有变化就把最新的数据发送给客户端:

ASP.Net Core 3.1 使用实时应用SignalR入门
ASP.NET Core SignalR 简介
实时Web简述
"底层"技术
Long Polling
Server Sent Events (SSE)
Web Socket

下面是Polling的一个实现, 非常简单:

1、创建一个web MVC程序,在新创建的程序新增一个控制器命名为:DemoTestController 代码如下

ASP.Net Core 3.1 使用实时应用SignalR入门
ASP.NET Core SignalR 简介
实时Web简述
"底层"技术
Long Polling
Server Sent Events (SSE)
Web SocketASP.Net Core 3.1 使用实时应用SignalR入门
ASP.NET Core SignalR 简介
实时Web简述
"底层"技术
Long Polling
Server Sent Events (SSE)
Web Socket
 1 using Microsoft.AspNetCore.Http;
 2 using Microsoft.AspNetCore.Mvc;
 3 using Microsoft.AspNetCore.SignalR;
 4 using Newtonsoft.Json;
 5 using System;
 6 using System.Collections.Generic;
 7 using System.Linq;
 8 using System.Net.WebSockets;
 9 using System.Text;
10 using System.Threading;
11 using System.Threading.Tasks;
12 
13 namespace SignaIRDemoTwo.Controllers
14 {
15     public class DemoTestController : Controller
16     {
17        
18         #region Polling
19 
20         /// <summary>
21         ///  Polling
22         /// </summary>
23         /// <returns></returns>
24         public IActionResult PollingTest()
25         {
26             return View();
27         }
28 
29 
30         /// <summary>
31         ///  Polling
32         /// </summary>
33         /// <param name="count"></param>
34         /// <returns></returns>
35         public IActionResult PollingTest_GetCount(int id)
36         {
37             var count = GetLastedCount();
38             if (id > 10)
39             {
40                 return Ok(new { id, count, finished = true });
41 
42             }
43             if (id > 6)
44             {
45                 return Ok(new { id, count });
46             }
47             return NotFound();
48         }
49 
50         #endregion
51 
52         private static int _count;
53         public int GetLastedCount()
54         {
55             _count++;
56             return _count;
57         }
58 
59     }
60 }
View Code

就看这个Controller的PollingTest_GetCount方法即可. 用到了GetLastedCount方法, 就是做了一个全局的Count, 它的GetLatestCount会返回最新的Count.

Controller里面的代码意思是: 如果Count > 6 就返回一个对象, 里面包含count的值和传进来的id; 如果 count > 10, 还要返回一个finished标志.

PollingTest页面的前端代码:

ASP.Net Core 3.1 使用实时应用SignalR入门
ASP.NET Core SignalR 简介
实时Web简述
"底层"技术
Long Polling
Server Sent Events (SSE)
Web SocketASP.Net Core 3.1 使用实时应用SignalR入门
ASP.NET Core SignalR 简介
实时Web简述
"底层"技术
Long Polling
Server Sent Events (SSE)
Web Socket
 1 <div>
 2     <button id="btnStart" type="button">开始Polling</button>
 3     <span id="result" style="color:red;font-weight:bolder;">
 4 
 5     </span>
 6 </div>
 7 <script src="~/lib/jquery/dist/jquery.js"></script>
 8 <script>
 9     var intervalId;
10     function poll(id) {
11         fetch('/DemoTest/PollingTest_GetCount?id='+id)
12             .then(function (response) {
13                 if (response.status === 200) {
14                     return response.json().then(j => {
15                         const resultDiv = document.getElementById("result");
16                         resultDiv.innerHTML = j.count;
17                         if (j.finished) {
18                             clearInterval(intervalId);
19                             resultDiv.innerHTML = j.count + ",已结束";
20                         }
21 
22                     })
23                 }
24                 ;
25             });
26     }
27 
28 
29     $(function () {
30 
31         $("#btnStart").click(function () {
32             intervalId = setInterval(poll, 3000, 8)
33         });
34     });
35 </script>
View Code

也是非常的简单, 点击按钮后定时发送请求, 如果有结果就显示最新count值; 如果有finished标志, 就显示最新值和已结束.

注意这里使用的是fetch API.

运行项目, count > 6的时候:

ASP.Net Core 3.1 使用实时应用SignalR入门
ASP.NET Core SignalR 简介
实时Web简述
"底层"技术
Long Polling
Server Sent Events (SSE)
Web Socket

count > 10的时候结束:

ASP.Net Core 3.1 使用实时应用SignalR入门
ASP.NET Core SignalR 简介
实时Web简述
"底层"技术
Long Polling
Server Sent Events (SSE)
Web Socket

这就是Polling, 很简单, 但是比较浪费资源.

SignalR没有采用Polling这种技术.

Long Polling

Long Polling和Polling有类似的地方, 客户端都是发送请求到服务器. 但是不同之处是: 如果服务器没有新数据要发给客户端的话, 那么服务器会继续保持连接, 直到有新的数据产生, 服务器才把新的数据返回给客户端.

如果请求发出后一段时间内没有响应, 那么请求就会超时. 这时, 客户端会再次发出请求.

ASP.Net Core 3.1 使用实时应用SignalR入门
ASP.NET Core SignalR 简介
实时Web简述
"底层"技术
Long Polling
Server Sent Events (SSE)
Web Socket

ASP.Net Core 3.1 使用实时应用SignalR入门
ASP.NET Core SignalR 简介
实时Web简述
"底层"技术
Long Polling
Server Sent Events (SSE)
Web Socket

例子, 在DemoTestController的代码中新增一个名称为LongPollingTest的 Action:

ASP.Net Core 3.1 使用实时应用SignalR入门
ASP.NET Core SignalR 简介
实时Web简述
"底层"技术
Long Polling
Server Sent Events (SSE)
Web SocketASP.Net Core 3.1 使用实时应用SignalR入门
ASP.NET Core SignalR 简介
实时Web简述
"底层"技术
Long Polling
Server Sent Events (SSE)
Web Socket
 1  #region Long Polling 
 2 
 3         public IActionResult LongPollingTest()
 4         {
 5             return View();
 6         }
 7 
 8         public IActionResult LongPollingTest_GetCount(int id)
 9         {
10             int count = GetLastedCount();
11             Thread.Sleep(1000);
12             if (count > 5)
13             {
14                 return Ok(new { id, count, finished = true });
15             }
16             else
17             {
18                 return Ok(new { id, count, finished = false });
19             }
20         }
21 
22         #endregion
View Code

LongPollingTest_GetCount方法是前端点击按钮后请求的方法。

改动的目的就是在符合要求的数据出现之前, 保持连接开放.

LongPollingTest页面前端也有一些改动:

ASP.Net Core 3.1 使用实时应用SignalR入门
ASP.NET Core SignalR 简介
实时Web简述
"底层"技术
Long Polling
Server Sent Events (SSE)
Web SocketASP.Net Core 3.1 使用实时应用SignalR入门
ASP.NET Core SignalR 简介
实时Web简述
"底层"技术
Long Polling
Server Sent Events (SSE)
Web Socket
 1 <div>
 2     <button id="btnStart" type="button">开始Polling</button>
 3     <span id="result" style="color:red;font-weight:bolder;">
 4 
 5     </span>
 6 </div>
 7 <script src="~/lib/jquery/dist/jquery.js"></script>
 8 <script>
 9     pollwithTimeout = (url, options, timeout = 9000) => {
10         return Promise.race([fetch(url, options), new Promise((_, reject) =>
11             setTimeout(() => reject(new Error('timeout')), timeout)
12         )]);
13     };
14     //pollWithTimeout方法使用了race, 如果请求后超过9秒没有响应, 那么就返回超时错误.
15 
16     //poll里面, 如果请求返回的结果是200, 那么就更新UI.但是如果没有finished标志, 就继续发出请求.
17     function poll(id) {
18         pollwithTimeout('/DemoTest/LongPollingTest_GetCount?id='+id)
19             .then( response => {
20                 if (response.status === 200) {
21                     return response.json().then(j => {
22                         const resultDiv = document.getElementById("result");
23                         resultDiv.innerHTML = j.count;
24                         if (!j.finished) {
25                             poll(id);
26                         }
27 
28                     })
29                 }
30                 ;
31             }).catch(response=>poll(id));
32     };
33 
34 
35     $(function () {
36 
37         $("#btnStart").click(function () {
38             poll(123);
39         });
40     });
41 </script>
View Code

pollWithTimeout方法使用了race, 如果请求后超过9秒没有响应, 那么就返回超时错误.

poll里面, 如果请求返回的结果是200, 那么就更新UI. 但是如果没有finished标志, 就继续发出请求.

运行后分别出现数字:1 2 3 4 5 6 在数字6那里停止请求

可以看到只有一个请求, 请求的时间很长, 标识连接开放了很长时间.

这里需要注意的一点是, 服务器的超时时长和浏览器的超时时长可能不一样.

前边介绍的Polling和Long Polling都是HTTP请求, 这其实并不是很适合.

下面介绍稍微一个好点的技术: 

Server Sent Events (SSE)

使用SSE的话, Web服务器可以在任何时间把数据发送到浏览器, 可以称之为推送. 而浏览器则会监听进来的信息, 这些信息就像流数据一样, 这个连接也会一直保持开放, 直到服务器主动关闭它.

浏览器会使用一个叫做EventSource的对象用来处理传过来的信息.

 ASP.Net Core 3.1 使用实时应用SignalR入门
ASP.NET Core SignalR 简介
实时Web简述
"底层"技术
Long Polling
Server Sent Events (SSE)
Web Socket

 例子,在该控制器继续新增一个名为SSETest 的action 这和之前的代码有很多地方不同, 用到了Reponse,C#代码如下:

ASP.Net Core 3.1 使用实时应用SignalR入门
ASP.NET Core SignalR 简介
实时Web简述
"底层"技术
Long Polling
Server Sent Events (SSE)
Web SocketASP.Net Core 3.1 使用实时应用SignalR入门
ASP.NET Core SignalR 简介
实时Web简述
"底层"技术
Long Polling
Server Sent Events (SSE)
Web Socket
 1  #region Server Sent Events (SSE)
 2 
 3         public IActionResult SSETest()
 4         {
 5             return View();
 6         }
 7 
 8 
 9         public async void SSEGet(int id)
10         {
11             Response.ContentType = "text/event-stream";
12             int count;
13             do 
14             {
15                 count = GetLastedCount();
16                 Thread.Sleep(1000);
17                 if (count % 3 == 0)
18                 {
19                     //注意SSE返回数据的只能是字符串, 而且以data:开头, 后边要跟着换行符号, 否则EventSource会失败.
20                     await HttpContext.Response.WriteAsync($"data:{count}

");
21                     await HttpContext.Response.Body.FlushAsync();
22                 }
23 
24             } while(count<10);
25 
26             Response.Body.Close();
27         }
28 
29         #endregion
View Code

注意SSE返回数据的只能是字符串, 而且以data:开头, 后边要跟着换行符号, 否则EventSource会失败.

页面客户端:

ASP.Net Core 3.1 使用实时应用SignalR入门
ASP.NET Core SignalR 简介
实时Web简述
"底层"技术
Long Polling
Server Sent Events (SSE)
Web SocketASP.Net Core 3.1 使用实时应用SignalR入门
ASP.NET Core SignalR 简介
实时Web简述
"底层"技术
Long Polling
Server Sent Events (SSE)
Web Socket
 1 <div class="text-center">
 2     <h1 class="display-4">Test</h1>
 3     <div>
 4         <button id="btn" type="submit">开始Polling</button>
 5         结果<span id="result" style="color:red;font-weight:bolder"></span>
 6     </div>
 7 
 8 </div>
 9 <script>
10 
11     listen = (id) => {
12         const eventSource = new EventSource('/DemoTest/SSEGet?id=' + id);
13         eventSource.onmessage = (event) => {
14             const resultDiv = document.getElementById("result");
15             console.log(event.data);
16             resultDiv.innerHTML = event.data;
17         };
18         eventSource.onerror = function (e) {
19             console.log("EventSource failed",e);
20         }
21     };
22     document.getElementById("btn").addEventListener("click", e => {
23         e.preventDefault();
24         listen(123);//id 123
25     });
26 
27 </script>
View Code

这个就很简单了, 使用EventSource的onmessage事件. 前一个请求等到响应回来后, 会再发出一个请求.

运行:

ASP.Net Core 3.1 使用实时应用SignalR入门
ASP.NET Core SignalR 简介
实时Web简述
"底层"技术
Long Polling
Server Sent Events (SSE)
Web Socket

这个EventSource要比Polling和Long Polling好很多.

它有以下优点: 使用简单(HTTP), 自动重连, 虽然不支持老浏览器但是很容易polyfill.

而缺点是: 很多浏览器都有最大并发连接数的限制, 只能发送文本信息, 单向通信.

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的生命周期是这样的:

ASP.Net Core 3.1 使用实时应用SignalR入门
ASP.NET Core SignalR 简介
实时Web简述
"底层"技术
Long Polling
Server Sent Events (SSE)
Web Socket

所有的一切都发生在TCP Socket里面, 首先一个常规的HTTP请求会要求服务器更新Socket并协商, 这个叫做HTTP握手. 然后消息就可以在Socket里来回传送, 直到这个Socket被主动关闭. 在主动关闭的时候, 关闭的原因也会被通信.

HTTP 握手

每一个Web Socket开始的时候都是一个简单的HTTP Socket.

客户端首先发送一个GET请求到服务器, 来请求升级Socket. 

如果服务器同意的话, 这个Socket从这时开始就变成了Web Socket.

ASP.Net Core 3.1 使用实时应用SignalR入门
ASP.NET Core SignalR 简介
实时Web简述
"底层"技术
Long Polling
Server Sent Events (SSE)
Web Socket

这个请求的示例如下:

ASP.Net Core 3.1 使用实时应用SignalR入门
ASP.NET Core SignalR 简介
实时Web简述
"底层"技术
Long Polling
Server Sent Events (SSE)
Web Socket

第一行表明这就是一个HTTP GET请求.

Upgrade 这个Header表示请求升级socket到Web Socket.

Sec-WebSocket-Key, 也很重要, 它用于防止缓存问题, 具体请查看官方文档.

服务器理解并同意请求以后, 它的响应如下:

ASP.Net Core 3.1 使用实时应用SignalR入门
ASP.NET Core SignalR 简介
实时Web简述
"底层"技术
Long Polling
Server Sent Events (SSE)
Web Socket

返回101状态码, 表示切换协议.

如果返回的不是101, 那么浏览器就会知道服务器没有处理WebSocket的能力.

此外Header里面还有Upgrade: websocket.

Sec-WebSocket-Accept是配合着Sec-WebSocket-Key来运作的, 具体请查阅官方文档.

消息类型

Web Socket的消息类型可以是文本, 二进制. 也包括控制类的消息: Ping/Pong, 和关闭.

每个消息由一个或多个Frame组成:

ASP.Net Core 3.1 使用实时应用SignalR入门
ASP.NET Core SignalR 简介
实时Web简述
"底层"技术
Long Polling
Server Sent Events (SSE)
Web Socket

所有的Frame都是二进制的. 所以文本的话, 就会首先转化成二进制.

Frame 有若干个Header bits.

有的可以表示这个Frame是否是消息的最后一个Frame;

有的可以表示消息的类型.

有的可以表示消息是否被掩蔽了. 客户端到服务器的消息被掩蔽了, 为了防止缓存投毒(使用恶意数据替换缓存).

有的可以设置payload的长度, payload会占据frame剩下的地方.

实际上用的时候, 你基本不会观察到frame, 它是在后台处理的, 你能看到的就是完整的消息.

但是在浏览器调试的时候, 你看到的是frame挨个传递进来而不是整个消息.

看下例子:

首先ASP.NET Core项目里已经内置了WebSocket, 但是需要配置和使用这个中间件, 在Startup:

ASP.Net Core 3.1 使用实时应用SignalR入门
ASP.NET Core SignalR 简介
实时Web简述
"底层"技术
Long Polling
Server Sent Events (SSE)
Web Socket

这里我们设置了每隔120秒就ping一下. 还设置用于接收和解析frame的缓存大小. 其实这两个值都是默认的值.

修改后的Controller:

ASP.Net Core 3.1 使用实时应用SignalR入门
ASP.NET Core SignalR 简介
实时Web简述
"底层"技术
Long Polling
Server Sent Events (SSE)
Web SocketASP.Net Core 3.1 使用实时应用SignalR入门
ASP.NET Core SignalR 简介
实时Web简述
"底层"技术
Long Polling
Server Sent Events (SSE)
Web Socket
 1   #region WebSocket
 2 
 3         public IActionResult WebSocketTest()
 4         {
 5             return View();
 6         }
 7 
 8 
 9         public async void WebSocketGet(int id)
10         {
11             if (HttpContext.WebSockets.IsWebSocketRequest)
12             {
13                 var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
14                 await SendEvents(webSocket, id);
15                 await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Done", CancellationToken.None);
16             }
17             else
18             {
19                 HttpContext.Response.StatusCode = 400;
20             }
21         }
22 
23 
24         private async Task SendEvents(WebSocket webSocket, int id)
25         {
26             int count;
27             do
28             {
29                 count = GetLastedCount();
30                 Thread.Sleep(1000);
31                 if (count % 3 != 0) continue;
32                 var obj = new { id, count };
33                 var jsonStr = JsonConvert.SerializeObject(obj);
34                 await webSocket.SendAsync(buffer: new ArraySegment<byte>(array:Encoding.UTF8.GetBytes(jsonStr),offset:0,count: jsonStr.Length),messageType:WebSocketMessageType.Text,endOfMessage:true,cancellationToken: CancellationToken.None);
35             } while (count < 10);
36         }
37 
38 
39         #endregion
View Code

这里需要注入HttpContextAccessor. 然后判断请求是否是WebSocket请求, 如果是的话, 客户端会收到回复, 这时Socket就升级完成了. 升级完返回一个webSocket对象, 然后我把events通过它发送出去. 随后我关闭了webSocket, 并指明了原因NormalClosure.

然后看看SendEvents方法:

ASP.Net Core 3.1 使用实时应用SignalR入门
ASP.NET Core SignalR 简介
实时Web简述
"底层"技术
Long Polling
Server Sent Events (SSE)
Web Socket

这里的重点就是webSocket对象的SendAsync方法. 我需要把数据转化成buffer进行传送. 数据类型是Text. 具体参数请查看文档.

看一下页面客户端:

ASP.Net Core 3.1 使用实时应用SignalR入门
ASP.NET Core SignalR 简介
实时Web简述
"底层"技术
Long Polling
Server Sent Events (SSE)
Web SocketASP.Net Core 3.1 使用实时应用SignalR入门
ASP.NET Core SignalR 简介
实时Web简述
"底层"技术
Long Polling
Server Sent Events (SSE)
Web Socket
 1 <div class="text-center">
 2     <h1 class="display-4">Test</h1>
 3     <div>
 4         <button id="btn" type="submit">开始Polling</button>
 5         结果<span id="result" style="color:red;font-weight:bolder"></span>
 6     </div>
 7 
 8 </div>
 9 <script>
10 
11 
12     listen = (id) => {
13         const socket = new WebSocket('ws://localhost:52136/DemoTest/WebSocketGet?id=' + id);
14         socket.onmessage = event => {
15             var resultDiv = document.getElementById("result");
16             console.log(event.data);
17             resultDiv.innerHTML = JSON.parse(event.data).count;
18         }
19     };
20     document.getElementById("btn").addEventListener("click", e => {
21         e.preventDefault();
22         listen(123);//id 123
23     });
24 
25 </script>
View Code

也很简单, 这里有一个WebSocket对象, 注意这里的url开头是ws而不是http, 还有一个wss, 就先当与http里的https.

然后eventhandler和SSE的差不多. 返回的json数据需要先parse, 然后再使用.

 介绍到这里下面开始真正的写SignalR的运用吧!

 建立一个TestHub, 继承于Hub代码如下:

ASP.Net Core 3.1 使用实时应用SignalR入门
ASP.NET Core SignalR 简介
实时Web简述
"底层"技术
Long Polling
Server Sent Events (SSE)
Web SocketASP.Net Core 3.1 使用实时应用SignalR入门
ASP.NET Core SignalR 简介
实时Web简述
"底层"技术
Long Polling
Server Sent Events (SSE)
Web Socket
 1 using Microsoft.AspNetCore.Authorization;
 2 using Microsoft.AspNetCore.SignalR;
 3 using System;
 4 using System.Collections.Generic;
 5 using System.Linq;
 6 using System.Threading;
 7 using System.Threading.Tasks;
 8 
 9 namespace SignaIRDemoTwo
10 {
11     //[Authorize]
12     public class TestHub: Hub
13     {
14         private static int count;
15 
16         private int GetLastCount()
17         {
18            return count++;
19         }
20 
21         public async Task GetLastedCount(string random)
22         {
23             int count;
24             do
25             {
26                 count = GetLastCount();
27                 Thread.Sleep(1000);
28                 await Clients.All.SendAsync("ReceiveMessage", count);
29             } while (count < 10);
30             Thread.Sleep(2000);
31             await Clients.All.SendAsync("Finished");
32         }
33 
34         //从Hub的Context属性, 可以获得用户的信息.
35         public override async Task OnConnectedAsync()
36         {
37             //var userName = Context.User.Identity.Name;
38             //return base.OnConnectedAsync();
39             //ConnectionId就是连接到Hub的这个客户端的唯一标识.
40             var connectionId = Context.ConnectionId;
41             await Clients.Clients(connectionId).SendAsync("someFunc", new { });
42             //await Clients.AllExcept(connectionId).SendAsync("someFunc");
43             //await Groups.AddToGroupAsync(connectionId, "MyGroup");
44             //await Groups.RemoveFromGroupAsync(connectionId, "MyGroup");
45             //await Clients.Group("MyGroup").SendAsync("someFunc");
46         }
47     }
48 }
View Code

Context

从Hub的Context属性, 我们可以获得用户的信息.

我们在CountHub里override父类的一个方法OnConnectedAsync():

如果有新的连接建立了, 这个方法就会被执行.

在Hub类里, 我们可以访问到Context属性. 从Context属性那, 我们可以获得一个常用的属性叫做ConnectionId. 这个ConnectionId就是连接到Hub的这个客户端的唯一标识.

使用ConnectionId, 我们就可以取得这个客户端, 并调用其方法, 如图中的Clients.Client(connectionId).xxx.

Hub的Clients属性表示客户端, 它有若干个方法可以选择客户端, 刚才的Client(connectionId)就是使用connectionId找到这一个客户端. 而AllExcept(connectionId)就是除了这个connectionId的客户端之外的所有客户端. 更多方法请查看文档.

SignalR还有Group分组的概念, 而且操作简单, 这里用到的是Hub的Groups属性. 向一个Group名添加第一个connectionId的时候, 分组就被建立. 移除分组内最后一个客户端的时候, 分组就被删除了. 使用Clients.Group("组名")可以调用组内客户端的方法.

配置SignalR

在Startup里注册SignalR:

 1      public void ConfigureServices(IServiceCollection services)
 2         {
 3             services.AddSignalR();
 4             //Asp.Net Core 3.1 修改Razor视图即时刷新的配置步骤
 5             //2. 安装package,Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation。
 6             //3.Startup的ConfigureServices里注册服务:services.AddMvc().AddRazorRuntimeCompilation();
 7 
 8             services.AddControllersWithViews().AddRazorRuntimeCompilation();
 9            
10         }

下面的方法3.1中以弃用了

然后在管道里使用SignalR, 使用app.UseSignalR():  

 推荐下面的红色加粗字体的内容

 1 // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
 2         public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
 3         {
 4             if (env.IsDevelopment())
 5             {
 6                 app.UseDeveloperExceptionPage();
 7             }
 8             else
 9             {
10                 app.UseExceptionHandler("/Home/Error");
11             }
12             //配置和使用这个WebSocket中间件
13             var webSocketOptions = new WebSocketOptions()
14             {
15                 KeepAliveInterval = TimeSpan.FromSeconds(120),
16                 ReceiveBufferSize = 4 * 1024
17             };
18             app.UseWebSockets(webSocketOptions);
19             app.UseStaticFiles();
20             //app.UseSignalR(routes => routes.MapHub<TestHub>("/DemoTest"));
21             app.UseRouting();
22 
23             app.UseAuthorization();
24 
25             app.UseEndpoints(endpoints =>
26             {
27                 endpoints.MapControllerRoute(
28                     name: "default",
29                     pattern: "{controller=Home}/{action=Index}/{id?}");
30                 endpoints.MapHub<TestHub>("/DemoTest");
31             });
32         }

使用Hub

首先在之前创建DemoTestController, 并注入IHubContext<CountHub>:

 public class DemoTestController : Controller
    {
        private readonly IHubContext<TestHub> _testHub;
        public DemoTestController(IHubContext<TestHub> testHub) 
        {
            _testHub = testHub;
        }
}

接下来我们就可以使用IHubContext<CountHub>这个对象与客户端进行实时通信了.

下面建立一个SignalRPost Action, 客户端点击按钮之后来到这个Action, 在这里我们使用hub为所有的客户端发送一个消息:

c#代码如下

ASP.Net Core 3.1 使用实时应用SignalR入门
ASP.NET Core SignalR 简介
实时Web简述
"底层"技术
Long Polling
Server Sent Events (SSE)
Web SocketASP.Net Core 3.1 使用实时应用SignalR入门
ASP.NET Core SignalR 简介
实时Web简述
"底层"技术
Long Polling
Server Sent Events (SSE)
Web Socket
 1 #region SignalR
 2 
 3         public IActionResult SignalRTest()
 4         {
 5 
 6             return View();
 7         }
 8 
 9         [HttpPost]
10         public async Task<IActionResult> SignalRPost()
11         {
12             await _testHub.Clients.All.SendAsync("someFunc", new { random = "abcd" });
13 
14             Thread.Sleep(2000);
15             return Accepted(1); //202: 请求已被接受并处理,但还没有处理完成
16         }
17 
18         #endregion
View Code

这里, 我调用了所有客户端上的someFunc这个方法, 参数是一个对象.

但是使用这种IHubContext<Hub>的注入方式, 我们无法在它那取得Caller(调用该方法的客户端)这个属性.

添加 SignalR 客户端库

unpkg 是一个内容分发网络 (CDN),可分发在 npm(即 Node.js 包管理器)中找到的任何内容。

  • 在“解决方案资源管理器”中,右键单击项目,然后选择“添加”>“客户端库” 。

  • 在“添加客户端库”对话框中,对于“提供程序”,选择“unpkg”。

  • 对于“库”,输入 @microsoft/signalr@latest

  • 选择“选择特定文件”,展开“dist/browser”文件夹,然后选择“signalr.js”和“signalr.min.js”。

  • 将“目标位置”设置为 wwwroot/js/signalr/,然后选择“安装”。

    ASP.Net Core 3.1 使用实时应用SignalR入门
ASP.NET Core SignalR 简介
实时Web简述
"底层"技术
Long Polling
Server Sent Events (SSE)
Web Socket

    LibMan 创建 wwwroot/js/signalr 文件夹并将所选文件复制到该文件夹。

在 SignalRTest action里添加对应的客户端页面

代码如下

ASP.Net Core 3.1 使用实时应用SignalR入门
ASP.NET Core SignalR 简介
实时Web简述
"底层"技术
Long Polling
Server Sent Events (SSE)
Web SocketASP.Net Core 3.1 使用实时应用SignalR入门
ASP.NET Core SignalR 简介
实时Web简述
"底层"技术
Long Polling
Server Sent Events (SSE)
Web Socket
 1 <button id="submit">提交</button>
 2 <div id="result" style="color:green;font-weight:bold;"></div>
 3 <script src="~/lib/microsoft/signalr/dist/browser/signalr.js"></script>
 4 
 5 <script>
 6         var connection = new signalR.HubConnectionBuilder().withUrl("/DemoTest").build();
 7 
 8     connection.on("someFunc", function (obj) {
 9         var resultDiv = document.getElementById("result");
10         resultDiv.innerHTML = "Someone called this ,parameters: " + obj.random;
11     });
12     connection.on("ReceiveMessage", function (update) {
13         var resultDiv = document.getElementById("result");
14         resultDiv.innerHTML = update;
15     });
16     connection.on("Finished", function () {
17         connection.stop();
18         var resultDiv = document.getElementById("result");
19         resultDiv.innerHTML = "Finished";
20     });
21     //connection.start().catch(err => console.error(err.toString()));
22     connection.start().then(function () {
23     }).catch(function (err) {
24         return console.error(err.toString());
25     });
26 
27 
28 
29     document.getElementById("submit").addEventListener("click", e => {
30         e.preventDefault();
31         fetch("/DemoTest/SignalRPost", { method: "post" })
32             .then(Response => Response.text())
33             .then(id => connection.invoke("GetLastedCount", id));//id 是1
34 
35         //connection.invoke("GetLastCountTest", "sss").catch(function (err) {
36         //    return console.error(err.toString());
37         //});
38 
39     })
40 </script>
View Code

先运行一下看看效果:

ASP.Net Core 3.1 使用实时应用SignalR入门
ASP.NET Core SignalR 简介
实时Web简述
"底层"技术
Long Polling
Server Sent Events (SSE)
Web Socket

可以看到使用Clients.All, 所有的客户端的方法都会被调用.

刚打开页面的时候, 我们就尝试建立连接, 从F12可以看到一个叫做negotiate的请求被发送了:

ASP.Net Core 3.1 使用实时应用SignalR入门
ASP.NET Core SignalR 简介
实时Web简述
"底层"技术
Long Polling
Server Sent Events (SSE)
Web Socket

这个请求的body如下:

ASP.Net Core 3.1 使用实时应用SignalR入门
ASP.NET Core SignalR 简介
实时Web简述
"底层"技术
Long Polling
Server Sent Events (SSE)
Web Socket

可以看到客户端选择了一个connectionId,  里面还有浏览器支持的传输方式.

服务器的响应:

ASP.Net Core 3.1 使用实时应用SignalR入门
ASP.NET Core SignalR 简介
实时Web简述
"底层"技术
Long Polling
Server Sent Events (SSE)
Web Socket

响应也包含着connectionId, 以及服务器支持的传输方式. 这里三种都支持. 由于我没有指定传输方式, 所以SignalR选择了最好的方式: websocket.

而在我点击按钮后, Web Socket连接才被初始化:

ASP.Net Core 3.1 使用实时应用SignalR入门
ASP.NET Core SignalR 简介
实时Web简述
"底层"技术
Long Polling
Server Sent Events (SSE)
Web Socket

如果需要手动指定传输方式, 请在withUrl()方法的第二个参数指定传输方式: 

ASP.Net Core 3.1 使用实时应用SignalR入门
ASP.NET Core SignalR 简介
实时Web简述
"底层"技术
Long Polling
Server Sent Events (SSE)
Web Socket

其它类型的客户端

.NET 客户端可以安装 Microsoft.AspNetCore.SignalR.Client 这个包来支持SignalR.

具体用法请查看官方文档, 语法和js的差不多.

完整代码链接 :https://github.com/hudean/SignaIRDemo.git