记录下MQQT的使用

10 阅读6分钟

二次封装mqqt

import mqtt, { IClientOptions } from 'mqtt';
//这些都是抽离出来的方法---可以忽略
import { getClientId } from "@/utils/idGenerator"
import time from "@/utils/time"
import { getSSLFile } from "@/utils/getFiles"
import { cloneDeep,isObject } from 'lodash-es'
import { jsonStringify } from '@/utils/parseNested'
import { increment} from '@/hooks/web/useSequence';
import { mqttLogger } from '@/utils/mqtt-logger';

const setMQTT5Properties = option => {
    if (option === undefined) {
        return undefined
    }
    const properties = cloneDeep(option)
    return Object.fromEntries(
        Object.entries(properties).filter(([_, v]) => v !== null && v !== undefined)
    )
}

const setWillMQTT5Properties = option => {
    if (option === undefined) {
        return undefined
    }
    const properties = cloneDeep(option)
    return Object.fromEntries(
        Object.entries(properties).filter(([_, v]) => v !== null && v !== undefined)
    )
}

const getClientOptions = record => {
    const mqttVersionDict = {
        "3.1.1": 4,
        "5.0": 5
    }
    const {
        clientId,
        username,
        password,
        keepalive,
        clean,
        connectTimeout,
        ssl,
        certType,
        mqttVersion,
        reconnect,
        reconnectPeriod, // reconnectPeriod = 0 在客户端中禁用自动重新连接
        will,
        rejectUnauthorized,
        ALPNProtocols,
        clientIdWithTime
    } = record
    const protocolVersion = mqttVersionDict[mqttVersion]
    const options:IClientOptions = {
        clientId,
        keepalive,
        clean,
        reconnectPeriod: reconnect ? reconnectPeriod : 0,
        protocolVersion
    }
    options.connectTimeout = time.convertSecondsToMs(connectTimeout)
    // 将时间戳附加到MQTT客户端id
    if (clientIdWithTime) {
        const clickIconTime = Date.parse(new Date().toString())
        options.clientId = `${options.clientId}_${clickIconTime}`
    }
    // Auth
    if (username !== "") {
        options.username = username
    }
    if (password !== "") {
        options.password = password
    }
    // MQTT Version
    if (protocolVersion === 5 && record.properties) {
        const properties = setMQTT5Properties(record.properties)
        if (properties && Object.keys(properties).length > 0) {
            options.properties = properties
        }
    }
    // else if (protocolVersion === 3) {
    //   options.protocolId = 'MQIsdp'
    // }
    // SSL
    if (ssl) {
        options.rejectUnauthorized =
            rejectUnauthorized === undefined ? true : rejectUnauthorized
        if (ALPNProtocols) {
            options.ALPNProtocols = ALPNProtocols.replace(/[\[\] ]/g, "").split(",")
        }
        if (certType === "self") {
            const sslRes = getSSLFile({
                ca: record.ca,
                cert: record.cert,
                key: record.key
            })
            if (sslRes) {
                options.ca = sslRes.ca
                options.cert = sslRes.cert
                options.key = sslRes.key
            }
        }
    }
    // Will Message
    if (will) {
        const {
            lastWillTopic: topic,
            lastWillPayload: payload,
            lastWillQos: qos,
            lastWillRetain: retain
        } = will
        if (topic) {
            options.will = { topic, payload, qos: qos, retain }
            if (protocolVersion === 5) {
                const { properties } = will
                if (properties) {
                    const willProperties = setWillMQTT5Properties(properties)
                    if (willProperties && Object.keys(willProperties).length > 0) {
                        options.will.properties = willProperties
                    }
                }
            }
        }
    }
    // 自动重新订阅,仅在重新连接时有效
    options.resubscribe =  true
    return options
}

const getUrl = record => {
    const { host, port, path='' } = record
    const protocol = getMQTTProtocol(record)
    let url = `${protocol}://${host}:${port}`
    if (protocol === "ws" || protocol === "wss") {
        url = `${url}${path.startsWith("/") ? "" : "/"}${path}`
    }
    return url
}

const shouldKeep = (item) => !mqttLogger.skipTopics.some(exclude =>{
    // 如果  item  是对象的话 取除 key 进行判断
    if (isObject(item)) {
        return Object.keys(item).some(key => /^(?:[^/]*\/){7,}/.test(item[key]) && item[key].includes(exclude))
    }
    return item && item.includes(exclude)
})

// 添加日志消息监听处理
export function setupMqttMessageLogging(mqttService) {
    mqttService.on('message', (topic: string, message: Buffer) => {
      // 避免循环记录日志服务自己的消息
        if (shouldKeep(topic)) {
            mqttLogger.logMqttOperation('message', topic, {
                messageSize: message.length,
                messageContent: message.toString()
            });
        }
    });
}


export const createClient = record => {
    const defaultRecord = getDefaultRecord();
    const options = getClientOptions({...defaultRecord,...record})
    const url = getUrl(record)
    const curConnectClient:any = mqtt.connect(url, options)
    const subscribedTopics = new Set()
    
    // 保存所有原始方法引用
    const original_connect = curConnectClient.connect;
    const original_subscribe = curConnectClient.subscribe;
    const original_publish = curConnectClient.publish;
    const original_disconnect = curConnectClient.disconnect;
    const original_publishAsync = curConnectClient.publishAsync;

    // 扩展connect方法
    curConnectClient.connect = function(brokerUrl: string, options = {}) {
        mqttLogger.logMqttOperation('connect', undefined, { brokerUrl, options });
        return original_connect.call(this, brokerUrl, options);
    };

    // 扩展subscribe方法 - 合并两次重写的功能
    curConnectClient.subscribe = function(topic, options, ...args) {
        // 记录日志功能
        const topicStr = Array.isArray(topic) ? topic.join(',') : topic;
        mqttLogger.logMqttOperation('subscribe', topicStr, { options });
        
        // 订阅管理功能
        if (!Array.isArray(topic) && !subscribedTopics.has(topic)) {
            subscribedTopics.add(topic);
        } else if (Array.isArray(topic)) {
            topic.forEach(t => subscribedTopics.add(t));
        }
        shouldKeep(topic) && window.log.warn(`订阅主题 ${topic}, 消息内容: ${JSON.stringify(options)}`,options);
        // console.log(topic, options, ...args,'subscribe')
        return original_subscribe.call(this, topic, options, ...args);
    };

    // 扩展publish方法
    curConnectClient.publish = function(topic, message, options) {
        // 避免循环记录日志服务自己的消息
        if (shouldKeep(topic)) {
            mqttLogger.logMqttOperation('publish', topic, { 
                messageSize: message.length,
                messageContent: message.toString(),
                options 
            });
        }
        return original_publish.call(this, topic, message, options);
    };

    // 扩展publishAsync方法
    curConnectClient.publishAsync = function(topic, message, ...args) {
        // 添加消息ID和时间戳
        if (isObject(message)) {
            const msgObj = message as any;
            msgObj.msgId = msgObj.msgId || increment();
            msgObj.sendTime = time.getNowDate('YYYY-MM-DDTHH:mm:ss');
            message = jsonStringify(message);
        }
        // 添加日志功能 (类似publish方法)
        if (shouldKeep(topic)) {
            mqttLogger.logMqttOperation('publishAsync', topic, { 
                messageSize: message.length,
                messageContent: message.toString(),
            });
        }
        return original_publishAsync.call(this, topic, message, ...args);
    };

    // 扩展disconnect方法
    curConnectClient.disconnect = function() {
        mqttLogger.logMqttOperation('disconnect');
        return original_disconnect.call(this);
    };

    // 添加检查订阅状态的方法
    curConnectClient.isSubscribed = function(topic) {
        return subscribedTopics.has(topic);
    };

    // 添加获取 topic 的方法
    curConnectClient.getTopic = function(topicName) {
        // 自动订阅
        this.subscribe(topicName);
        return topicName;
    };

    setupMqttMessageLogging(curConnectClient);
    console.log('MQTT日志记录已初始化');

    return {
        curConnectClient,
        connectUrl: url
    };
}

// 防止旧数据丢失协议字段
export const getMQTTProtocol = data => {
    const { protocol, ssl } = data
    if (!protocol) {
        return ssl ? "wss" : "ws"
    }
    return protocol
}

export const getDefaultRecord = () => {
    return {
        //**生成唯一标识**:为每个客户端实例创建一个唯一的 `clientId` 和时间戳
        clientId: getClientId(),
        createAt: time.getNowDate(),// 记录配置创建时间
        updateAt: time.getNowDate(),// 记录配置最后更新时间
        name: "",
        // 启动“干净会话”,连接断开后,代理会清除该客户端的所有会话信息(订阅、未确认消息等)
        clean: true,
        //根据环境变量(`import.meta.env.VITE_IS_ONLINE_ENV`)自动选择 WebSocket 协议(`ws` 或 `wss`)和默认端口
        protocol: import.meta.env.VITE_IS_ONLINE_ENV === "true" ? "wss" : "ws",
        // MQTT代理地址,支持环境变量覆盖
        host: import.meta.env.VITE_IS_DEFAULT_HOST ?? "broker.emqx.io",
        // 心跳间隔(秒),客户端每5秒发送一次心跳包以维持连接
        keepalive: 5,
        connectTimeout: 5, // 连接超时改为5秒
        reconnect: true,
        // 重连间隔(毫秒),连接断开后每4秒尝试重连一次,默认的是1s
        reconnectPeriod: 4000, 
        username: "",
        password: "",
        path: "/mqtt",// WebSocket的路径,与代理配置匹配
        port: import.meta.env.VITE_IS_ONLINE_ENV === "true" ? 8084 : 8083,
        ssl: false,// 是否启用SSL(对于wss,通常由协议本身处理)
        certType: "",
        rejectUnauthorized: true,
        ALPNProtocols: "",
        ca: "",// CA证书
        cert: "",// 客户端证书
        key: "",// 客户端私钥
        mqttVersion: "5.0",// 指定使用的MQTT协议版本
        subscriptions: [],// 用于在UI或内存中存储当前订阅的主题列表
        messages: [],// 用于存储接收到的消息历史
        pushProps: {},// 可能用于存储推送通知的配置
        unreadMessageCount: 0,// 未读消息计数器
        // 客户端的连接状态,可用于UI绑定
        client: {
            connected: false
        },
        //#### **MQTT 5.0 高级特性**
        will: {
            lastWillTopic: "",
            lastWillPayload: "",
            lastWillQos: 0,
            lastWillRetain: false,
            properties: {
                payloadFormatIndicator: undefined,
                willDelayInterval: undefined,
                messageExpiryInterval: undefined,
                contentType: "",
                responseTopic: "",
                correlationData: undefined,
                userProperties: undefined
            }
        },
        properties: {
            sessionExpiryInterval: undefined,
            receiveMaximum: undefined,
            maximumPacketSize: undefined,
            topicAliasMaximum: undefined,
            requestResponseInformation: undefined,
            requestProblemInformation: undefined,
            userProperties: undefined,
            authenticationMethod: undefined,
            authenticationData: undefined
        },
        clientIdWithTime: false
    }
}


封装可以直接调用mqttHandler

import { ref, shallowRef, onMounted } from 'vue'
import { createClient } from '@/utils/mqttUtils'
import { MqttMessageHandler, MqttMessageHandlerType } from '@/utils/mqtt-message-handler'
import { mqttNotificationManager } from '@/components/MqttNotification/manager'

// 单例模式,确保全应用只有一个MQTT实例
const clientInstances = ref<any>(null)
const clientInitData = ref({
  connected: false
})

// 连接状态
const isConnected = ref(false)

// MQTT消息处理器单例
const mqttHandlerInstance = ref<MqttMessageHandlerType | null>(null)

// 创建连接的单例实现,确保全局只执行一次
let connectionPromise: Promise<any> | null = null;

// 统一的MQTT状态通知管理
const updateMqttNotification = (title: string, message: string, type: 'success' | 'warning' | 'info' | 'error', duration?: number) => {
  // 如果是错误消息,提供重连回调
  const onReconnect = type === 'error' ? () => {
    console.log('用户点击重连按钮')
    // 获取当前最新的 useMqttClient 实例并调用强制重连
    const { forceReconnect } = useMqttClient()
    forceReconnect()
  } : undefined

  mqttNotificationManager.update(title, message, type, onReconnect)

  // 如果是成功消息,设置自动关闭
  if (type === 'success' && duration) {
    setTimeout(() => {
      mqttNotificationManager.destroy()
    }, duration)
  }
}

// 默认连接配置
const defaultConnectionInfo = {
  protocol: window.config.VITE_MQTT_PROTOCOL,
  host: window.config.VITE_MQTT_URL,
  port: window.config.VITE_MQTT_PORT,
  path: window.config.VITE_MQTT_PATH,
  username: window.config.VITE_MQTT_USERNAME,
  password: window.config.VITE_MQTT_PASSWORD,
  clean: false,
  properties: {
    sessionExpiryInterval: 0,
    requestResponseInformation: true,
    requestProblemInformation: true
  }
}

/**
 * MQTT客户端Hook
 * @param connectionParams 可选的连接参数,覆盖默认设置
 * @returns MQTT客户端实例和相关状态
 */
// let logId = '';
export function useMqttClient(connectionParams = {}) {
  let reconnectCount = 0
  let reconnectTimeout: NodeJS.Timeout | null = null
  let isManualClose = false // 是否为手动关闭

  // 合并连接参数
  const connectionInfo = {
    ...defaultConnectionInfo,
    ...connectionParams
  }

  // 创建连接
  const createConnection = () => {
    console.count('createConnection called')
    // 如果已经有实例了,直接返回
    if (clientInstances.value && clientInitData.value.connected) {
      return { client: clientInstances, mqttHandler: mqttHandlerInstance.value }
    }

    // 如果连接已经在创建中,等待该连接创建完成
    if (connectionPromise) {
      return connectionPromise;
    }

    // 创建新的连接
    connectionPromise = new Promise((resolve) => {
      // logId = addLog(`MQTT连接中...`)
      console.log('连接参数', connectionInfo)

      const { curConnectClient } = createClient(connectionInfo)

      // 统一处理消息
      const mqttHandler = MqttMessageHandler.getInstance(curConnectClient)
      mqttHandlerInstance.value = mqttHandler

      // 连接失败
      curConnectClient.on('error', (err) => {
        console.log(err, '连接失败')
        clientInitData.value.connected = false
        isConnected.value = false

        // 重置连接Promise以允许重新连接
        connectionPromise = null
      })

      //重新连接
      curConnectClient.on('reconnect', () => {
        console.log('reconnect attempt:', reconnectCount + 1)
        reconnectCount++
        connectionPromise = null

        updateMqttNotification('网络连接', `正在重新连接...`, 'warning')
      })

      // 连接成功
      curConnectClient.on('connect', (connack) => {
        // updateLog(logId,{
        //   type: 'success',
        //   message: `MQTT连接成功`
        // })

        updateMqttNotification('网络连接', 'MQTT连接成功', 'success', 3000)

        clientInitData.value.connected = true
        isConnected.value = true
        reconnectCount = 0 // 连接成功后重置重连计数
        clientInstances.value = curConnectClient

        // 清除连接超时定时器
        if (reconnectTimeout) {
          clearTimeout(reconnectTimeout)
          reconnectTimeout = null
        }
      })

      // 设置连接超时检查
      reconnectTimeout = setTimeout(() => {
        if (!curConnectClient.connected && !isConnected.value) {
          console.log('连接超时,当前重连次数:', reconnectCount)
          console.log('连接建立中,请等待...')
        }
      }, 10000) // 10秒超时

      // 保存实例
      clientInstances.value = curConnectClient
      resolve({ client: clientInstances, mqttHandler })
    });

    return connectionPromise;
  }

  // 关闭连接
  const closeConnection = () => {
    isManualClose = true // 标记为手动关闭

    if (clientInstances.value) {
      clientInstances.value.end(true) // 强制关闭连接
      clientInitData.value.connected = false
      isConnected.value = false
      clientInstances.value = null

      // 关闭通知
      mqttNotificationManager.destroy()

      // 清除超时定时器
      if (reconnectTimeout) {
        clearTimeout(reconnectTimeout)
        reconnectTimeout = null
      }

      // 重置连接Promise以允许重新连接
      connectionPromise = null
      reconnectCount = 0 // 重置重连计数
      mqttHandlerInstance.value?.destroy() // 销毁消息处理器实例
      mqttHandlerInstance.value = null // 清理消息处理器实例
    }

    setTimeout(() => {
      isManualClose = false // 重置手动关闭标记
    }, 1000)
  }

  // 组件挂载时自动初始化连接
  onMounted(() => {
    if (!mqttHandlerInstance.value && !connectionPromise) {
      initConnection()
    }
  })

  // 需要立即创建连接并获取初始mqttHandler
  const initConnection = () => {
    console.count('initConnection called')
    if (!connectionPromise) {
      createConnection()
    }
    return mqttHandlerInstance.value
  }

  // 初始化 mqttHandler 如果需要
  if (!mqttHandlerInstance.value && !connectionPromise) {
    initConnection()
  }

  // 强制重连方法
  const forceReconnect = () => {
    console.log('强制重连中...')

    // 先关闭现有连接
    if (clientInstances.value) {
      clientInstances.value.end(true)
      clientInstances.value = null
    }

    // 清理状态
    clientInitData.value.connected = false
    isConnected.value = false
    connectionPromise = null
    reconnectCount = 0

    // 清除超时定时器
    if (reconnectTimeout) {
      clearTimeout(reconnectTimeout)
      reconnectTimeout = null
    }

    // 短暂延迟后重新创建连接
    setTimeout(() => {
      createConnection()
    }, 1000)
  }

  return {
    clientInstances,
    isConnected,
    createConnection,
    closeConnection,
    forceReconnect,
    clientInitData,
    mqttHandler: mqttHandlerInstance.value
  }
}

调用

import { useMqttClient } from '@/hooks/web/useMqttClient'
const { mqttHandler} = useMqttClient()

//调用
//getPsamAuthConfig.topic mqqt主题
mqttHandler?.onMessage(`${getPsamAuthConfig.topic}`, (message) => {
  if(message.code !== 0){
    console.log('PSAM卡授权失败', message)
    logStore.addLog(`PSAM卡授权失败`, 'error')
  }else{
    console.log('PSAM卡授权成功', message)
    logStore.addLog(`PSAM卡授权成功`, 'success')
  }
})