从壹开始 [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后台展示出来吧,以后也试试强制登录的功能,请看:

动图:

image

一、什么是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 层,当然可以自定义任意层。

<pre style="color: rgb(0, 0, 0); font-family: "Courier New"; font-size: 12px; margin: 5px 8px; padding: 5px;">//请看清,还有一个 Net 版本的,但是也能用,还是用 core 版本的吧
Install-Package Microsoft.AspNetCore.SignalR</pre>

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

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

<pre style="color: rgb(0, 0, 0); font-family: "Courier New"; font-size: 12px; margin: 5px 8px; padding: 5px;"> 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 ,来接收
}
}</pre>

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

旁白:

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

image

3、配置服务 与 中间件

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

<pre style="color: rgb(0, 0, 0); font-family: "Courier New"; font-size: 12px; margin: 5px 8px; padding: 5px;">//这个配置就太简单了,不细说了,大家一看就知道往哪里放
services.AddSignalR();

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

4、跨域

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

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

image

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

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

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

1、安装库依赖包

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

<pre style="color: rgb(0, 0, 0); font-family: "Courier New"; font-size: 12px; margin: 5px 8px; padding: 5px;">//直接在项目中执行
npm install @aspnet/signalr</pre>

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

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

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

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

image

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

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

直接上代码:

<pre style="color: rgb(0, 0, 0); font-family: "Courier New"; font-size: 12px; margin: 5px 8px; padding: 5px;"> created: function () { //1、首先我们实例化一个连接器
this.connection = new signalR.HubConnectionBuilder() //然后配置通道路由
.withUrl('/api/chatHub') //日志信息
.configureLogging(signalR.LogLevel.Information) //创建
.build(); },</pre>

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

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

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

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

<pre style="color: rgb(0, 0, 0); font-family: "Courier New"; font-size: 12px; margin: 5px 8px; padding: 5px;">withUrl('/axxxxx/chatHub')</pre>

image

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

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

<pre style="color: rgb(0, 0, 0); font-family: "Courier New"; font-size: 12px; margin: 5px 8px; padding: 5px;">withUrl('http://localhost:8081/axxxxx/chatHub')</pre>

image

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

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

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

3、服务器Nginx代理

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

image

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

image

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

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

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

<pre style="color: rgb(0, 0, 0); font-family: "Courier New"; font-size: 12px; margin: 5px 8px; padding: 5px;"> // 开始通讯,并成功呼叫服务器
thisvue.connection.start().then(() => {
thisvue.connection.invoke('GetLatestCount', 1).catch(function (err) { return console.error(err);
});
});</pre>

image

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

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

<pre style="color: rgb(0, 0, 0); font-family: "Courier New"; font-size: 12px; margin: 5px 8px; padding: 5px;">mounted() {
thisVue.connection.on('ReceiveUpdate', function (update) {
console.info('update success!')
thisVue.tableData = update;//将返回的数据,实时的赋值给当前页面的 data 中; })
},</pre>

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

image

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

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

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

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

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

<pre style="color: rgb(0, 0, 0); font-family: "Courier New"; font-size: 12px; margin: 5px 8px; padding: 5px;"> private readonly IHubContext<ChatHub> _hubContext; public GlobalExceptionsFilter(IHostingEnvironment env, ILoggerHelper loggerHelper,
IHubContext<ChatHub> hubContext)
{
_env = env;
_loggerHelper = loggerHelper;
_hubContext = hubContext;
}</pre>

然后直接使用:

<pre style="color: rgb(0, 0, 0); font-family: "Courier New"; font-size: 12px; margin: 5px 8px; padding: 5px;">//采用log4net 进行错误日志记录
_loggerHelper.Error(json.Message, WriteLog(json.Message, context.Exception));

_hubContext.Clients.All.SendAsync("ReceiveUpdate", LogLock.GetLogData()).Wait();</pre>

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

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

四、模拟登录

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

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

image

五、结语

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

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

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

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

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

5、如何关闭连接?

六、Github && Gitee

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

   Vue [https://github.com/anjoy8/Blog.Admin](https://github.com/anjoy8/Blog.Admin)

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,084评论 1 32
  • ORA-00001: 违反唯一约束条件 (.) 错误说明:当在唯一索引所对应的列上键入重复值时,会触发此异常。 O...
    我想起个好名字阅读 5,169评论 0 9
  • 计算机网络概述 网络编程的实质就是两个(或多个)设备(例如计算机)之间的数据传输。 按照计算机网络的定义,通过一定...
    蛋炒饭_By阅读 1,209评论 0 10
  • 简介 日志组件,作为程序员使用频率最高的组件,给程序员开发调试程序提供了必要的信息。ASP.NET Core中内置...
    Lamond_Lu阅读 2,114评论 0 1
  • 有时候,我踏过一座小桥,看它们给你的影子佩戴青玉和罗兰紫,月光已升高了三千多尺,我才往回走,可我走了很远,花香还远...
    杏丹雪姣阅读 158评论 1 2