一、前言
作爲前後端分離項目,前後端交互是一個非常重要的功能。目前主流框架都是通過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
一樣。