从壹开始 [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或更高版本。
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 配置,看自己喜好吧。
我online项目统一使用的都是nginx,全部配置是这样的:http://apk.neters.club/.doc/guide/function-sheet.html#四、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
------♥------♥------♥----------