使用ABP SignalR重构消息服务(一)
使用ABP SignalR重构消息服务
最近协助蟹老板升级新框架,维护基础设施服务,目前已经稳了。
早上蟹老板看到我进入公司,马上就叫停我,说我为什么左脚先进公司,你这样会让我很难做耶,这样把我给你一次机会把现在的消息服务重构了,我就放过你这一次。(当时我都没有反应过来,蟹老板就准备和我讲需求了,我赶紧着小本子开始记需求)
背景
我们需要记录所有用户的在线状况(登录的设备存在多个设备同时登录)
、指定用户下线
、实时接收消息
技术你可以自由技术发挥,今天中午之前给我一个设计概要。(呜呜,天空是蔚蓝色、窗外还有千纸鹤)
技术点
- SignalR
SignalR 是一个开放源代码库,可用于简化向应用添加实时 Web 功能。 实时 Web 功能使服务器端代码能够将内容推送到客户端。 - Redis
Redis 是一个开源(BSD 许可)的内存数据结构存储,用作数据库、缓存和消息代理。 - Jwt
JSON Web Token (JWT) 是一个开放标准 ( RFC 7519 ),它定义了一种紧凑且自包含的方式,用于在各方之间以 JSON 对象的形式安全传输信息。此信息可以验证和信任,因为它是数字签名的。JWT 可以使用密钥(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对进行签名。
为什么使用SignalR
-
ASP.NET Core SignalR 的一些功能
- 自动处理连接管理。
- 同时向所有连接的客户端发送消息。 例如聊天室。
- 向特定客户端或客户端组发送消息。
- 对其进行缩放,以处理不断增加的流量。
-
SignalR支持如下的方式实现实时通信(SignalR会自动选择服务器和客户端能力范围内的最佳通信方式)
- WebSockets:是一种在单个TCP连接上进行全双工通信的协议,使得服务器和浏览器的通信更加简单,服务端可以主动发送信息。
- Server-Sent Events:SSE 与 WebSocket 作用相似,都是建立浏览器与服务器之间的通信渠道,然后服务器向浏览器推送信息。WebSocket是双向的,而SSE是单向的。
- Long Polling(长轮询) :和传统的轮询原理一样,只是服务端不会每次都返回响应信息,只有有数据或超时了才会返回,从而减少了请求次数。
-
SignalR核心
-
Hub 是一种高级管道,允许客户端和服务器相互调用方法。 SignalR 自动处理跨计算机边界的调度,并允许客户端调用服务器上的方法,反之亦然。 可以将强类型参数传递给方法,从而支持模型绑定。 SignalR 提供两种内置中心协议:基于 JSON 的文本协议和基于 SignalR 的二进制协议。 与 JSON 相比,MessagePack 通常会创建更小的消息。 旧版浏览器必须支持 XHR 级别 2 才能提供 MessagePack 协议支持。
-
中心通过发送包含客户端方法的名称和参数的消息来调用客户端代码。 作为方法参数发送的对象使用配置的协议进行反序列化。 客户端尝试将名称与客户端代码中的方法匹配。 当客户端找到匹配项时,它会调用该方法并将反序列化的参数数据传递给它。
-
Hubs(集线器)介绍
- Hub.Context
Hub 类具有一个 Context 属性,该属性包含具有连接相关信息的以下属性
属性 | 说明 |
---|---|
ConnectionId | 获取连接的唯一 ID(由 SignalR 分配)。 每个连接有一个连接 ID。 |
UserIdentifier | 获取用户标识符。 默认情况下,SignalR 使用与连接关联的 ClaimsPrincipal 中的 ClaimTypes.NameIdentifier 作为用户标识符。 |
User | 获取与当前用户关联的 ClaimsPrincipal。 |
Items | 获取可用于在此连接范围内共享数据的键/值集合。 数据可以存储在此集合中,会在不同的中心方法调用间为连接持久保存。 |
Features | 获取连接上可用的功能的集合。 目前,在大多数情况下不需要此集合,因此未对其进行详细记录。 |
ConnectionAborted | 获取一个 CancellationToken,它会在连接中止时发出通知。 |
Hub.Context还包含以下方法
方法 | 说明 |
---|---|
GetHttpContext | 返回连接的 HttpContext,如果连接不与 HTTP 请求关联,则返回 null。 对于 HTTP 连接,可以使用此方法获取 HTTP 标头和查询字符串等信息。 |
Abort | 中止连接。 |
- Hub.Clients
Hub 类具有一个 Clients 属性,该属性包含适用于服务器与客户端之间的通信的以下属性
属性 | 说明 |
---|---|
All | 对所有连接的客户端调用方法 |
Caller | 对调用了中心方法的客户端调用方法 |
Others | 对所有连接的客户端调用方法(调用了方法的客户端除外) |
Hub.Clients还包含以下方法
方法 | 说明 |
---|---|
AllExcept | 对所有连接的客户端调用方法(指定连接除外) |
Client | 对连接的一个特定客户端调用方法 |
Clients | 对连接的多个特定客户端调用方法 |
Group | 对指定组中的所有连接调用方法 |
GroupExcept | 对指定组中的所有连接调用方法(指定连接除外) |
Groups | 对多个连接组调用方法 |
OthersInGroup | 对一个连接组调用方法(不包括调用了中心方法的客户端) |
User | 对与一个特定用户关联的所有连接调用方法 |
Users | 对与多个指定用户关联的所有连接调用方法 |
设计思路
使用SignalR与客户端进行实时通讯、用户链接管理、JWt进行用户身份认证和鉴权、Redis保存用户链接信息
- 前端创建链接之后就会触发后端
OnConnectedAsync()
方法,这样我们就可以通过获取当前的连接IP信息和用户浏览器信息组成一个唯一设备标识。 - 我们创建一个Redis key数据类型为
Hashes
将用户Id当成key,然后将不同设备登录用户当成value存储。 - 反之当用户主动断开链接、或者关闭浏览器就会触发后端
OnDisconnectedAsync()
方法,就代表该设备的用户下线了。
前端设计
与服务端创建链接
前端使用@aspnet/signalr
与服务端进行握手通讯,用户登录成功建立一个Socket链接
// 创建链接
this.init.connection = new signalR.HubConnectionBuilder()
// IM_URL链接地址
.withUrl(IM_URL, {
// accessTokenFactory携带用户Token进行身份认证和鉴权
accessTokenFactory: () => this.token
}).build();
监听关闭事件
方式客户端发生意外断线,或者后端断开我们的链接,我们就可以监听关闭事件,给到用户一些提示
this.init.connection.onclose(function() {
console.log('connecition closed');
});
接收消息(画重点)
因为我自己写过一个IM的小应用,自己就也写过前端,所以这里我会给一些经验给到前端大佬。
思路是这样的:前端程序初始化Signalr
接收消息方法的时候带一个参数(类似委托的参数),这个委托是一个消息类型处理工厂。
App.Vue 文件中的代码
methods: {
// 接受用户信息进入消息总线
ReceiveUserMsg(data) {
....处理消息工厂代码.....
switch (switch_on)
{
case "消息类型" :
break;
}
}},
created() {
try {
// 初始化创建链接
this.$signalr.CreatorConnectServer();
// 初始化用户消息接收
this.$signalr.ReUserReceiveMessage(this.ReceiveUserMsg);
// 初始化链接关闭事件
this.$signalr.OnClose();
} catch (e) {
console.log("网络错误");
}}
signalr.js(自己专门封装的一个js)
// 接受信息
ReUserReceiveMessage(receiveUserMsg) {
this.init.connection.on("ReUserReceiveMessage", (result) => {
// 执行委托
receiveUserMsg(result);
console.log(result)
});
}
第一次,当它本可进取时,却故作谦卑;
第二次,当它空虚时,用爱欲来填充;
第三次,在困难和容易之间,它选择了容易;
第四次,它犯了错,却借由别人也会犯错来宽慰自己;
第五次,它自由软弱,却把它认为是生命的坚韧;
第六次,当它鄙夷一张丑恶的嘴脸时,却不知那正是自己面具中的一副;
第七次,它侧身于生活的污泥中虽不甘心,却又畏首畏尾。