一、前言
作为前后端分离项目,前后端交互是一个非常重要的功能。目前主流框架都是通过Socket
实现,本系统自然也是实现了基于Signalr
的前后端交互,并在此基础上实现了基于MQTT
的前后端交互功能,MQTT
相比socket
业务场景更多更灵活,在物联网方向有着非常多的应用。在工业物联网方向,mqtt也是应用非常广泛,最为.neter来说学习mqtt
很有必要。
二、基于Signalr
系统默认是用的Signalr
做前后端通信,关于Signalr
使用文档可以去看Furion
的文档:https://furion.baiqian.ltd/docs/signalr。
2.1 后端部分
首先需要在启动时注册signalr
服务和集线器。
新建一个集线器
,在类名上加上MapHub
特性,这样我们前端就能通过signalr
连接到后端。
2.2 前端部分
前端的signalr基于"@microsoft/signalr": "^7.0.0"
可以在package.json中查看,关于signalr
的连接也是非常简单,我封装在了utils
文件夹下的signalr.js
中
import { Modal } from 'ant-design-vue'
import sysConfig from '@/config/index'
import tool from '@/utils/tool'
import * as signalR from '@microsoft/signalr'
import * as signalrMessage from './mqtt/message'
//使用signalr
export default function useSignalr() {
const userInfo = tool.data.get('USER_INFO') //用户信息
let socketUrl = '/hubs/simple' //socket地址
if (sysConfig.VITE_PROXY === 'false') {
socketUrl = sysConfig.API_URL + socketUrl //判断是否要走代理模式,走了的话发布之后直接nginx代理
}
//开始
const startSignalr = () => {
//初始化连接
const connection = init()
// 启动连接
connection.start().then(() => {
console.log('启动连接')
})
}
//初始化
const init = () => {
console.log('初始化SignalR对象')
// SignalR对象
const connection = new signalR.HubConnectionBuilder()
.withUrl(socketUrl, {
accessTokenFactory: () => tool.data.get(sysConfig.ACCESS_TOEKN_KEY)
})
.withAutomaticReconnect({
nextRetryDelayInMilliseconds: () => {
return 5000 // 每5秒重连一次
}
}) //自动重新连接
.configureLogging(signalR.LogLevel.Information)
.build()
connection.keepAliveIntervalInMilliseconds = 15 * 1000 // 心跳检测15s
// connection.serverTimeoutInMilliseconds = 30 * 60 * 1000 // 超时时间30m
// 断开连接
connection.onclose(async () => {
console.log('断开连接')
})
//断线重新
connection.onreconnected(() => {
console.log('断线重新连接成功')
})
//消息处理
receiveMsg(connection)
return connection
}
//接收消息处理
const receiveMsg = (connection) => {
// 接收登出
connection.on('LoginOut', (data) => {
signalrMessage.loginOut(data)
})
}
//页面销毁
onUnmounted(() => {})
return { startSignalr }
}
使用也是很简单,只需要在需要连接的页面引用usesignalr
,在本系统中,我们需要全局监听,所以我在layout
文件夹下的index.vue
中启用signalr
并封装一个方法用来连接signalr
。
import useSignalr from '@/utils/signalr'
//连接signalr
connectSignalr() {
const { startSignalr } = useSignalr()
startSignalr()
}
然后在页面created
的最后连接signalr就行
F12
查看控制台输出,登录系统之后,应该会提示连接singlar成功。
三、基于Mqtt
如果使用mqtt
则需要一个mqtt broker
来进行数据中转,前端和后端都是通过客户端的方式去连接服务端,然后再通过发布/订阅
的方式进行数据交互。这里mqtt broker推荐使用emqx
来搭建。
下载地址:https://www.emqx.com/zh/try?product=broker
后端mqtt客户端基于我自己封装的SimpleMQTT
组件,gitee地址:https://gitee.com/zxzyjs/SimpleMQTT.git
前端基于"mqtt": "^4.3.7"
,可在package.json中查看。
3.1 MQTT配置
既然登录系统需要用户名/密码登录,那么连接mqtt服务器也应该需要账号密码才行,然而如果将账号密码信息写在前端配置文件中是不安全的,别有用心的人可能会盗取我们的用户名和密码。而且账号密码写死在前端也显得不那么灵活,如果账号密码修改了需要重新打包上传发布。基于以上两种情况,本系统将mqtt配置改为可配置化,用户可以在系统运维
->系统配置
->MQTT配置
中配置域名和账号密码。
3.2 后端部分
首先需要在SimpleAdmin.Web.Core
项目的配置文件中的WebSettings
打开mqtt
配置。
配置账号密码
系统会自动注册mqtt
服务,连接mqttbroker
因为我们mqtt的连接信息都是存储在后端,前端想要连接就得通过接口获取连接信息,所以我们需要写一个接口来返回连接信息和订阅的主题。
3.3 前端部分
首先需要在配置文件中设置 VITE_MQTT = true
关于mqtt的封装可以在utils
下的mqtt
文件夹中找到。
mqtt.js
import * as mqtt from 'mqtt/dist/mqtt.min.js'
//mqtt客户端对象
export const mqttClient = ref()
//初始化操作
export const init = (url, options) => {
mqttClient.value = mqtt.connect(url, options) //连接mqtt
//报错
mqttClient.value.on('error', (error) => {
console.log('mqtt连接报错')
console.log(error)
})
//重连
mqttClient.value.on('reconnect', (error) => {
console.log('mqtt重连')
console.log(error)
})
}
//接收消息
export const link = (callback) => {
mqttClient.value.on('connect', callback)
}
//订阅
export const subscribes = (topics) => {
if (Array.isArray(topics)) {
//订阅频道
topics.forEach((topic) => {
subscribe(topic)
})
}
}
//订阅
export const subscribe = (topic) => {
mqttClient.value.subscribe(topic, (error) => {
if (!error) {
console.log(topic, '订阅成功')
} else {
console.log(topic, '订阅失败')
}
})
}
//取消订阅
export const unSubscribes = (topic) => {
mqttClient.value.unsubscribe(topic, (error) => {
if (!error) {
console.log(topic, '取消订阅成功')
} else {
console.log(topic, '取消订阅失败')
}
})
}
//接收消息
export const getMessage = (callback) => {
mqttClient.value.on('message', callback)
}
//关闭连接
export const close = () => {
console.log('关闭连接')
mqttClient.value.end()
mqttClient.value = null
}
usemqtt.js
import { mqttClient, init, link, getMessage, close, subscribes } from '@/utils/mqtt/mqtt'
import mqttapi from '@/api/auth/mqttApi'
import * as mqttMessage from './message'
//使用mqtt
export default function useMqtt() {
let options = {
clientId: '',
username: '',
password: '',
clean: true,
keepalive: 60,
connectTimeout: 3000
}
//连接mqtt并订阅频道
const startMqtt = () => {
mqttapi.getParameter().then((res) => {
options.clientId = res.clientId
options.username = res.userName
options.password = res.password
console.log(options)
//mqtt初始化
init(res.url, options)
//连接成功
link(() => {
console.log('mqtt连接成功')
console.log(res.topics)
subscribes(res.topics)
//接收消息
receivceMessage()
})
})
}
//接收消息
const receivceMessage = () => {
getMessage((topic, message) => {
console.log(`收到主题${topic}的消息`)
const msg = JSON.parse(message.toString())
console.log(`消息为:${message}`)
// mqttMessage.loginOut(message)
const msgType = msg.MsgType
if (msgType === 'LoginOut') {
var clientIds = msg.Data.ClientIds
clientIds.forEach((clientId) => {
if (clientId == options.clientId) {
mqttMessage.loginOut(msg.Data.Message)
}
})
}
})
}
//页面销毁
onUnmounted(() => {
if (mqttClient.value) {
close()
}
})
return { startMqtt }
}
在需要启用mqtt的页面引入usemqtt
并封装成方法
import useMqtt from '@/utils/mqtt/usemqtt'
//连接mqtt
connectMqtt() {
const { startMqtt } = useMqtt()
startMqtt()
},
这样在系统启动时就会启用mqtt
而不是singalr
F12
查看控制台输出,登录系统之后已经成功连上mqtt服务器并订阅了Topic
四、在线用户
通过即时通讯我们可以判断当前用户是否在线,原理非常简单,用户登录系统后,无论哪种方式后台都会收到当前token
连接了,然后把当前连接的客户端ID
存储到该token
信息中的客户端id
列表中,当用户关闭了浏览器或者网络断开了连接,则会将断开的客户端id
从token
中的客户端id
列表中删除。在前端会话管理中只需要判断当前token的客户端id列表是否有数据就行了,如果有就是在线,如果没有就是离线。
4.1 Signalr方式
集线器里重写OnDisconnectedAsync
和OnConnectedAsync
方法
收到连接或断开的请求后更新redis
4.2 mqtt方式
对应signalr
的OnConnectedAsync
和OnDisconnectedAsync
,mqtt
叫做上线
和下线
,设备上线代表连接到服务器,设备下线代表断开服务器,通过emqx
我们可以订阅上下线主题获取设备的上下线信息。所以我们需要启动一个客户端后台去订阅上下线事件,并且不能像iis那样会被回收,所以我们可以通过建立workerservice
项目来后台运行。对应的SimpleAdmin.Background
后台服务层。
MqttWorker
中监听设备上下线主题,然后根据客户端id去更新redis
就行了,原理和signalr
一样。