驾一叶之扁舟 举匏樽以相属
寄蜉蝣于天地,渺沧海之一粟。哀吾生之须臾,羡长江之无穷。
挟飞仙以遨游,抱明月而长终。知不可乎骤得,托遗响于悲风。

从壹开始 [Admin] 之四 || NetCore + SignalR 实现日志消息推送

缘起

哈喽大家周一好呀,感觉好久没有写文章了,上周出差了一次,感觉还是比坐办公室好的多,平时在读一本书《时生》,感兴趣的可以看看😂......

这几天翻看 NetCore 相关知识扩展的时候,发现了久违的一个知识点 —— SignalR ,为啥说久违呢,因为去年的时候,我在公司的项目里就想用了,后来组员说他学学看,也没有了下文,我也就耽搁了,昨天突然看到这个了,想着正好看看吧,尽量落地到 NetCore 项目上,当时我很自信的以为这个技术很老了,应该用的人很多,可是天不遂人愿,在.Net MVC中使用的很多有六成,在NetCore 的小demo有两成,NetCore + Vue 一起使用的就是寥寥无几了,而且更多的是仿照官网的,好吧,我就简单写一个吧,希望对大家有所帮助,尽量将这个技术落地。

 

你一定很好奇,为啥要学 SignalR ,或者说它有啥作用,那我先说几个场景,你就知道了:

1、用户登录处理相关场景; //平时我们是ajax请求,等待后端处理,然后返回 true ,根据返回结果相应处理;

2、后端(c#)强制用户退出登录(js); //这个后端还真没有做过,没啥思路

3、用户支付订单,等待成功后跳转; //我以前用的是 ajax 轮询,额。。。

4、给用户发消息,或网页内简单的聊天;

5、秒杀用户名单,在页面实时进行滑动展示;

大家从这几个栗子中,可以看到一个共性:

就是想用后端来操作前端,也就是说可以通过 服务端代码,来控制前端 js 事件,来实现响应式的实时场景过程,用户完全不用做任何操作,或者做少量的操作就能实现更多的效果。

大家可以先自己用自己平时的想法和经验来实现上边的场景,当然并不是一定使用 SignalR ,Scoket 现在也是很火,本文就是简单说说 SignalR 的基础用法,想了想,怎么才能让这个技术落地,最后决定将全部的操作日志通过 SignalR 的形式在Admin后台展示出来吧,以后也试试强制登录的功能,请看:

动图:

 

 

 

一、什么是SignalR?

1、基本概念

本文重点说如何使用,但是为了文章的完整性,还是粘贴了一些概念讲解,参考《ASP.NET Core SignalR 简介》,更多的知识点请自行研究吧,网上这种概念类文章很多:

 

ASP.NET Core SignalR 是一个开源代码库,它简化了向应用添加实时 Web 功能的过程。 实时 Web 功能使服务器端代码能够即时将内容推送到客户端。

SignalR的可使用Web Socket, Server Sent Events 和 Long Polling作为底层传输方式。

SignalR 的适用对象:

  • 需要来自服务器的高频率更新的应用。 例如:游戏、社交网络、投票、拍卖、地图和 GPS 应用。
  • 仪表板和监视应用。 示例包括公司仪表板、销售状态即时更新或行程警示。
  • 协作应用。 协作应用的示例包括白板应用和团队会议软件。
  • 需要通知的应用。 社交网络、电子邮件、聊天、游戏、行程警示以及许多其他应用都使用通知。

SignalR 提供了一个用于创建服务器到客户端 《远程过程调用(RPC的 API。 RPC 通过服务器端 .NET Core 代码调用客户端上的 JavaScript 函数。

以下是 ASP.NET Core SignalR 的一些功能:

  • 自动管理连接。
  • 同时向所有连接的客户端发送消息。 例如,聊天室。
  • 将消息发送到特定的客户端或客户端组。
  • 扩展以处理增加的流量。

 

 

2、支持平台

 

服务端:ASP.NET Core SignalR 适用于 ASP.NET Core 支持的任何服务器平台。

JS 客户端:需要支持NodeJS 8+、或者常见主流浏览器都支持。

Java 客户端:支持Java 8或更高版本。

Net 客户端:可以在 ASP.NET Core 支持的任何平台上运行。 例如, Xamarin 开发人员可以使用 SignalR用于构建 Android 应用程序使用 Xamarin.Android 8.4.0.1 或更高版本和 iOS 应用程序使用 Xamarin.iOS 11.14.0.4 或更高版本。如果服务器运行 IIS,Websocket 传输要求安装 IIS 8.0 或更高版本在 Windows Server 2012 或更高版本。 其他传输在所有平台上都受支持。
 

3、回落机制

参考文章《SignalR简介及使用

SignalR使用的三种底层传输技术分别是Web Socket, Server Sent Events 和 Long Polling;

其中Web Socket仅支持比较现代的浏览器,Web服务器也不能太老;

而Server Sent Events 情况可能好一点, 但是也存在同样的问题。

 

Web Socket是最好的最有效的传输方式, 如果浏览器或Web服务器不支持它的话, 就会降级使用SSE, 实在不行就用Long Polling;

 

一旦建立连接, SignalR就会开始发送keep alive消息, 来检查连接是否还正常, 如果有问题, 就会抛出异常;

因为SignalR是抽象于三种传输方式的上层, 所以无论底层采用的哪种方式, SignalR的用法都是一样的;

 

SignalR默认采用这种回落机制来进行传输和连接。但是也可以禁用回落机制, 只采用其中一种传输方式。

 

4、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也可以扩展使用其它协议。

 

好啦,复杂而又枯燥的概念说完了,接下来咱们开始动手写代码了!(额概念还是要看的😊)

 

二、搭建 SignalR 服务中心

既然要实现实时交互,肯定得有服务端,那我们就直接在 Blog.Core 项目上,进行处理吧

1、引用 SignalR 包

为了以后好拓展,我就把 SignalR 中心,也可以是通讯管道定义到了 Common 层,当然可以自定义任意层。

//请看清,还有一个 Net 版本的,但是也能用,还是用 core 版本的吧
Install-Package Microsoft.AspNetCore.SignalR

 

 

2、声明 Hub 管道——集线器

在 Blog.Core.Common 层,新建一个 Hubs 文件夹,然后添加一个 ChatHub 类:

 public class ChatHub : Hub
 {
     public async Task SendMessage(string user, string message)
     {
         await Clients.All.SendAsync("ReceiveMessage", user, message);
     }

     //定于一个通讯管道,用来管理我们和客户端的连接
     //1、客户端调用 GetLatestCount,就像订阅
     public async Task GetLatestCount(string random)
     {
         //2、服务端主动向客户端发送数据,名字千万不能错
         await Clients.All.SendAsync("ReceiveUpdate", LogLock.GetLogData());
         
         //3、客户端再通过 ReceiveUpdate ,来接收

     }
 }

基本的概念已经在上边了,大家结合之前的概念,应该能看懂,看不懂也没事儿,等看了下边的 Vue 代码,就理解了。

旁白:

这里说一下,上文中的GetLogData,这个方法,是直接把我们之前的日志从log文件给提取出来了,包括AOP日志,异常日志,Sql日志,因为格式规则变了,如果你本地已经存在之前的错误日志了,请删了文件重新生成,否则格式不正确会报错。

 

 

3、配置服务 与 中间件

还是老规则,在netcore 中,基本只要涉及到Http请求相关的,一定要配置中间件,任何需要在宿主中用的服务,都需要注入:

//这个配置就太简单了,不细说了,大家一看就知道往哪里放
services.AddSignalR();


 app.UseMvc();

//我这个放到了 Mvc 管道下边,注意顺序
 app.UseSignalR(routes =>
 {
     //这里要说下,为啥地址要写 /api/xxx 
     //因为我前后端分离了,而且使用的是代理模式,所以如果你不用/api/xxx的这个规则的话,会出现跨域问题,毕竟这个不是我的controller的路由,而且自己定义的路由
     routes.MapHub<ChatHub>("/api/chatHub");
 });

 

4、跨域

这一块大家肯定都已经配置好了,要不然不会前后端分离的,至于用前端 proxy 代理,还是后端 CORS 配置,看自己喜好吧,我更喜欢前者。不过和群里使用过的小伙伴讨论,发现后端配置跨域,也就是 Cors 可能更方便些,我用 Nginx 代理,会有一丢丢的小问题,不过无伤大雅,如果你遇到了什么错误,请联系我,我也踩了很多坑了。

 

这个时候,我们的后端通道就打通了,如何验证呢,我们在路由器直接输入上边的自定义路由地址即可:

 

 因为每次请求需要一个 ID 号的,直接访问肯定不行,那这个 ID 我们怎么来拿呢,别着急,下文会说到,重点来了。

 

三、在 Vue 中配置客户端连接

 服务端可以了,那就改配置客户端了,其实客户端特别简单,就好像我们使用一个js库插件一样,比如大家一定用过地图api库 ,直接引用js,然后new map对象即可使用了,没错,SignalR和它一毛一样。

1、安装库依赖包

 现在 vue 也和 core 很像了,用一个东西,都需要安装包,配置服务,再调用这三部曲了。

//直接在项目中执行
npm install @aspnet/signalr

 

我为了更好的让大家理解这个通讯的过程,每个标题后边,都破折号了我对这个过程的理解,大家一看就懂了。

 

2、添加引用——买个手机

在Admin 项目里,我增加了一个展示日志的页面,大家自己看看就都懂了,然后之前是需要每次刷新的,但是这次改造成可以自推送的。

上边也说到了,这个 SignalR 我们只需要像 map 地图那样,引用就行了,很简单:

 

网上很坑的是,很多教程里竟然要在 main.js 中引用,最后导致还出现了依赖 Jquery 的各种bug,大家如果无聊可以试试。

 

3、开始连接到中心——连上网络

 直接上代码:

 created: function () {
     //1、首先我们实例化一个连接器
     this.connection = new signalR.HubConnectionBuilder()
         //然后配置通道路由
         .withUrl('/api/chatHub')
         //日志信息
         .configureLogging(signalR.LogLevel.Information)
         //创建
         .build();
 },

 是不是很简单,只需要我们在页面初始化的时候,创建连接即可,只不过这里有一些小问题,

 

 咱们的项目中,已经配置好了跨域,并且所有接口也已经成功调用了,但是唯独 Hub 却不行,情况如下:

还记得上边咱们在 startup.cs 中配置的 hub 路由么,如果我们不使用 /api/chatHub 会是怎么样呢?这里我也简单把错误的给写出来,留作参考:

1、相对路径,没用代理规则:

withUrl('/axxxxx/chatHub')

 

是因为我们用的相对路径,而且也没与代理,系统会认为我们访问的是一个页面路由,所以 404;

 

2、绝对路径,没用代理规则

withUrl('http://localhost:8081/axxxxx/chatHub')

 

这样就会出现代理的问题。

当然!如果你使用的是后端 CORS 机制跨域,不会这个问题的,其他的各种情况自己把握就好。

是不是我们这么配置好了就没事了呢,别着急,还有要给bug,

 

3、服务器Nginx代理

如果你在服务器里用的是 Nginx 做代理的话,可能会遇到这个问题:

大家可以看看,这个错误,和上边两个都不一样,是已经连上了,但是去不能开启数据 的交互 transport !神奇,简单的看了看开源的 websockets 上提的 issues,是这么解决的《wss: Error during WebSocket handshake: Unexpected response code: 200 #979

 

好啦!这次应该没啥问题了,继续往下走。

 

4、客户端调用集线器——建立连接,呼叫对方

那我们现在已经连接成功了,剩下的就是调用集线器了,也就是上边我们定义的 Hub 通道,用来接收日志:

 // 开始通讯,并成功呼叫服务器
 thisvue.connection.start().then(() => {
     thisvue.connection.invoke('GetLatestCount', 1).catch(function (err) {
         return console.error(err);
     });
 });

 

 

5、从集线器调用客户端方法——接收回应

 上边我们是从客户端去订阅了一个 通道 连接,也就是说,我需要这个约定,那约定成功后,就需要接收来自服务器的通讯返回结果了,

mounted() {
    thisVue.connection.on('ReceiveUpdate', function (update) {
        console.info('update success!')
        thisVue.tableData = update;//将返回的数据,实时的赋值给当前页面的 data 中;
    })
},

这个时候我们刷新页面,已经能看到消息了,然后我们在看看接口请求:

是不是很熟悉!没错,这个就是我们上边说到的那个 ID ,不记得的往上看,这个是自动生成的,而且不随着消息推送而变化,只有每次请求重新连接的时候,才会变化。 

好啦,这样我们就成功了在页面上展示出了我们的数据,BUT!别慌,好像还没有完成,因为我们现在仅仅是展示了出来,还没有实现推送啊!别着急,既然一次能显示,那多次也能显示。

 

6、每次更新日志,推送到客户端——实时短信

这个就很简单了,我们只需要在每次日志产生的时候,来推送出来即可,举个全局异常的栗子吧:

先注入我们的通道上下文:

 private readonly IHubContext<ChatHub> _hubContext;

 public GlobalExceptionsFilter(IHostingEnvironment env, ILoggerHelper loggerHelper,
IHubContext<ChatHub> hubContext)
 {
     _env = env;
     _loggerHelper = loggerHelper;
     _hubContext = hubContext;
 }

然后直接使用:

//采用log4net 进行错误日志记录
_loggerHelper.Error(json.Message, WriteLog(json.Message, context.Exception));

_hubContext.Clients.All.SendAsync("ReceiveUpdate", LogLock.GetLogData()).Wait();

 

这样,每次我们操作的时候,就会触发生成日志的功能,同时再触发推送功能,就这样,我们把消息及时的推送了出去,达到了目的,实现了文章开头的功能。

如果想中断连接,只需要页面关闭的时候,执行 connection.stop() 即可。

 

7、对不同对象进行推送

话不多说,直接看代码即可:

 /// <summary>
 /// 全员推送
 /// </summary>
 /// <returns></returns>
 [HttpPost]
 public async Task<IActionResult> PushMessageAsync([FromBody]object data)
 {
     await _hubContext.Clients.All.SendAsync(MessageDefault.ReceiveMessage, data);
     return Ok();
 }

 /// <summary>
 /// 对某人推送
 /// </summary>
 /// <returns></returns>
 [HttpPost]
 public async Task<IActionResult> PushAnyOneAsync([FromBody]MessagePushDTO model)
 {
     if (model == null)
     {
         return Forbid();
     }
     var user = SignalRMessageGroups.UserGroups.FirstOrDefault(m => m.UserId == model.UserId && m.GroupName == model.GroupName);
     if (user != null)
     {
         await _hubContext.Clients.Client(user.ConnectionId).SendAsync(MessageDefault.ReceiveAnyOne, model.MsgJson);
     }
     return Ok();
 }

 /// <summary>
 /// 对某组进行推送
 /// </summary>
 /// <returns></returns>
 [HttpPost]
 public async Task<IActionResult> PushGroupAsync([FromBody]MessageGroupPushDTO model)
 {
     if (model == null)
     {
         return Forbid();
     }
     var list = SignalRMessageGroups.UserGroups.Where(m => m.GroupName == model.GroupName);
     foreach (var item in list)
     {
         await _hubContext.Clients.Client(item.ConnectionId).SendAsync(model.GroupName, model.MsgJson);
     }
     return Ok();
 }

 

8、SignalR 断点续连

当我们使用 SignalR 的时候,或多或少的都会遇到过断线了,需要重连,一般我们是通过 try catch 的方法,但是这样不能,因为这个不是异常,所以 catch 无法捕捉到,官方给我们已经提供了一个方法了:

官网地址:https://docs.microsoft.com/zh-cn/aspnet/core/signalr/javascript-client?view=aspnetcore-3.1

 

 

 

 

 

四、模拟登录

 在文章开头,我说了几个场景,其他的不好实现,先来个模拟登录吧,就是把用户名密码传到后台,然后后台将结果推送回来。

具体的流程就不说了,和上边的是一样的,只是很简单的一个动作,接收下数据即可,

 

 

五、结语

今天很简单的实现了两个小功能,一个是模拟登录,一个是实时推送消息,大家学会了么,这里有几个问题,大家可以思考思考:

1、SignalR到底能在平时开发中,使用哪些地方?

2、服务中心是如何将消息发出去的?

3、客户端是如何来订阅某一个通道集线器的?

4、SignalR的底层原理是什么?

5、如何关闭连接?

 

六、Github && Gitee

 

NetCore https://github.com/anjoy8/Blog.Core

       Vue https://github.com/anjoy8/Blog.Admin

 

 ------♥------♥------♥----------

posted @ 2019-04-29 10:42  老张的哲学  阅读(...)  评论(... 编辑 收藏
作者:老张的哲学
好好学习,天天向上
返回顶部小火箭
好友榜:
如果愿意,把你的博客地址放这里
jianshu.com/u/老张
SqlSugar codeisbug.com
钰玺 studenty.cn