UNPKG

mm_os

Version:

MM_OS服务端架构,用于快速构建应用程序,支持网站建设、小程序后台、AI应用、物联网(IOT/AIOT)、游戏服务端等多种场景。

823 lines (761 loc) 19.8 kB
const Mqtt_client = require('mqtt'); const Manager = require('mm_machine').Manager; const Drive = require('./drive'); /** * Mqtt通讯类 * @augments {Manager} * @class */ class Mqtt extends Manager { /** * 配置参数 * @type {object} */ static config = { /** * 名称 * @type {string} */ name: '', /** * 标题 * @type {string} */ title: 'Mqtt通讯类', /** * 描述 * @type {string} */ description: '这是Mqtt通讯类管理器', /** * 检索文件名 * @type {string} */ filename: 'mqtt.json', /** * 模板目录 * @type {string} */ tpl_dir: __dirname, /** * 基础目录 * @type {string} */ base_dir: '../common/mqtt'.fullname(__dirname), /** * 自定义目录,加载项目自定义资源 * @type {string} */ dir: './app'.fullname(), /** * 搜索模式 dir按目录搜索 | file按文件名搜索 * @type {string} */ search_way: 'file', /** * 是否懒加载 * @type {boolean} */ lazy_load: true, /** * 模式 * 1.生产模式,改变文件不会重新加载 * 2.热更新模式,改变配置文件会重新加载配置,不重新加载脚本 * 3.热重载模式,改变配置文件都会加载配置和脚本 * 4.重载模式,执行完后重新加载脚本,避免变量污染 * 5.热更新+重载模式,改变配置文件重新加载配置和脚本,执行完后重新加载脚本 * @type {number} */ mode: 3, hostname: '127.0.0.1', port: '1883', protocol: 'mqtt', client_id: 'server', subscribe_qos: 2, publish_qos: 2, username: 'server', password: 'asd123', table: 'iot_device', clean: false, longtime: 15000, request_timeout: 3000, // 在线有效期时长判断 单位:毫秒,150000毫秒为2.5分钟 online_expires: 150000, // 在线有效期检测间隔 online_interval: 10000, // 连接超时 connect_timeout: 30000, // maxInflight: 20, // 重连间隔 reco_period: 6000 // 重连间隔1秒 }; /** * 构造函数 * @param {object} config 配置参数 * @param {object} parent 父级模块 */ constructor(config, parent) { super({ ...Mqtt.config, ...config }, parent); } } /** * 预置 */ Mqtt.prototype._preset = function () { this.dict = {}; // 定时器 this.timer = null; // mqtt客户端服务器 this.client = null; /** * message queue */ this.list_msg = [ /* { // information ID id: "", // request method method: "", // message parameters params: {}, // Callback func: function(res){} } */ ]; /** * 订阅集合 (主题 => 函数列表) */ this.dict_subscribe = {}; /** * method collection */ var _this = this; this.methods = { /** * 获取所有方法 * @returns {Array} 函数列表 */ getMethod: function () { return Object.keys(_this.methods); } }; this.retry_times = 0; this.connecting = false; this._createClient(); } /** * mqtt驱动类 * @type {Drive} */ Mqtt.prototype.Drive = Drive; /** * 主题匹配 * @param {string} topic 接收到的主题 * @param {string} top 匹配用的主题 * @returns {boolean} 返回匹配结果,正确返回true,错误返回false */ Mqtt.prototype.match = function (topic, top) { if (topic === top) { return true; } var str = '^' + top.replace('#', '.*').replace('+', '~~~').replace('~~~', '[a-zA-Z0-9_-]+').replace('$', '~~~') .replace('~~~', '\\$'); var s = str.substring(str.length - 1, str.length); if (s !== '#') { str += '$'; } var mh = new RegExp(str); return mh.test(topic); }; /** * 初始化 */ Mqtt.prototype.updateAfter = async function () { var _this = this; let infos = this.getInfos(); for (var i = 0; i < infos.length; i++) { var info = infos[i]; if (info.state === 1) { let mod = this.getMod(info.name); if (mod) { mod.send = function (topic, msg, qos = 0, retain = false) { if (topic) { _this.send(topic, msg, qos, retain); } }; await mod.do('init'); var topics = mod.config.topic; if (topics) { for (var n = 0; n < topics.length; n++) { var tc = topics[n]; var retain = mod.config.retain || false; var qos = mod.config.qos || 0; if (tc.indexOf('heartbeat') !== -1) { retain = false; qos = 0; } this.subscribe(tc, null, qos, retain); } } } } } }; /** * 重新连接 */ Mqtt.prototype.reconnect = function () { // $.sleep(this.config.interval); // this.retryTimes += 1; // if (this.retryTimes > 5) { // try { // this.client.end(); // this.init(); // } catch (error) { // this.$message.error(error.toString()); // } // } }; /** * 初始化 * @param {object} config 配置参数 */ Mqtt.prototype.init = function (config) { this.config = Object.assign(this.config, config); this.retry_times = 0; this.connecting = false; }; /** * 运行Mqtt * @returns {Promise} 连接结果 */ Mqtt.prototype.start = function () { setTimeout(() => { this.startTimer(); }, 2000); this.connecting = true; try { this._createClient(); if (this.client && this.client.on) { return this._setupClientEvents(); } } catch (error) { this.connecting = false; return this._rejectPromise(error); } return this._rejectPromise(); }; /** * 创建客户端 * @private */ Mqtt.prototype._createClient = function () { var cg = { ...this.config }; cg.clientId = cg.client_id; if (cg.host) { delete cg.hostname; delete cg.port; this.client = Mqtt_client.connect(cg.host, cg); } else { this.client = Mqtt_client.connect(cg); } }; /** * 设置客户端事件 * @returns {Promise} 连接结果 * @private */ Mqtt.prototype._setupClientEvents = function () { return new Promise((resolve, reject) => { this.client.on('connect', (packet, err) => { this.connecting = false; if (err) { resolve(null); reject(err); } else { this.client.on('message', async (topic, message) => { await this.receive(topic, message.toString()); }); resolve(packet); } }); this.client.on('reconnect', () => { this.reconnect(); }); this.client.on('error', (error) => { console.error('Connection failed', error); }); }); }; /** * 创建被拒绝的Promise * @param {Error} error 错误对象 * @returns {Promise} 被拒绝的Promise * @private */ Mqtt.prototype._rejectPromise = function (error) { return new Promise((resolve, reject) => { resolve(null); reject(error); }); }; /** * 监听事件 * @param {string} topic 事件名称 * @param {Function} func 回调函数 */ Mqtt.prototype.on = function (topic, func) { this.client.on(topic, func); }; /** * 接收消息 * @param {string} topic 订阅板块 * @param {object} msg 消息主体 */ Mqtt.prototype.message = async function (topic, msg) { if (this.dict_subscribe[topic]) { var list = this.dict_subscribe[topic]; try { for (var key in list) { list[key](msg); } } catch (err) { console.error(err); } } }; /** * 接收消息 * @param {string} push_topic 推送过来的主题 * @param {object} message 消息主体 * @returns {object} 解析后的消息主体 */ Mqtt.prototype.receive = async function (push_topic, message) { var msg = this._parseMessage(message, push_topic); if (msg === null) { return; } // 处理消息回复 this._handleResponse(msg); var ret = await this._processTopics(push_topic, msg); try { await this.message(ret.topic, msg); } catch (error) { this.log('error', 'mqtt处理程序错误', error); return; } return ret.result; }; /** * 解析消息 * @param {object} message 消息主体 * @param {string} push_topic 推送过来的主题 * @returns {object|null} 解析后的消息 * @private */ Mqtt.prototype._parseMessage = function (message, push_topic) { var msg = {}; if (message && (message.indexOf('[') === 0 || message.indexOf('{') === 0)) { try { msg = JSON.parse(message); } catch (error) { this.log('error', '消息结构体不对', push_topic, error); return null; } } return msg; }; /** * 处理主题 * @param {string} push_topic 推送过来的主题 * @param {object} msg 消息主体 * @returns {object} 处理结果和主题 * @private */ Mqtt.prototype._processTopics = async function (push_topic, msg) { var ret; var topic; var mods = this.getMods(); for (var k in mods) { var o = mods[k]; if (o.config.state !== 1) { continue; } var topics = o.config.topic; if (!topics) { continue; } for (var n = 0; n < topics.length; n++) { topic = topics[n]; if (this.match(push_topic, topic)) { ret = await o.call('handle', push_topic, msg, topic, n); } } } return { result: ret, topic }; }; /** * 订阅主题 * @param {string} topic 主题 * @param {Function} func 回调函数 * @param {number} qos 推送方式 0 * @param {boolean} retain 保持 * @returns {string} 主键 */ Mqtt.prototype.subscribe = function (topic, func, qos = null, retain = false) { if (!this.client) { return ''; } this.client.subscribe(topic, { // 订阅消息方式,0为保留 , 1为确认收到1次 qos: qos || this.config.subscribe_qos || 0, retain }); if (func) { if (!this.dict_subscribe[topic]) { this.dict_subscribe[topic] = {}; } var key = this.key_num + 1; this.dict_subscribe[topic][key] = func; return key; } return ''; }; /** * 取消订阅 * @param {string} topic 主题 * @param {string} key 主键 */ Mqtt.prototype.unsubscribe = function (topic, key) { this.client.unsubscribe(topic); if (this.dict_subscribe[topic]) { delete this.dict_subscribe[topic][key]; } }; /** * 结束客户端 */ Mqtt.prototype.end = function () { if (this.client) { this.client.end(); } if (this.timer) { clearInterval(this.timer); this.timer = null; } }; /** * 发布主题 * @param {string} topic 主题 * @param {object} message 消息对象 * @param {number} qos 推送方式 0 * @param {boolean} retain 保持 */ Mqtt.prototype.publish = function (topic, message, qos = null, retain = false) { this.client.publish(topic, message, { qos: qos || this.config.publish_qos || 0, retain }); }; /** * 主题推送 * @param {string} topic 主题 * @param {string} msg_obj 消息主体 * @param {number} qos 推送方式 0 * @param {boolean} retain 保持 */ Mqtt.prototype.send = function (topic, msg_obj, qos = null, retain = false) { this.publish(topic, JSON.stringify(msg_obj), qos, retain); }; /** * 生成ID * @param {string} clientId 客户端ID * @returns {string} ID */ Mqtt.prototype.genId = function (clientId) { return clientId + '_' + Date.parse(new Date()); }; /** * 请求 * @param {string} topic 主题 * @param {string} method 请求方法 * @param {object} params 请求参数 * @param {Function} func 回调函数 * @param {number} timeout 超时时间(毫秒) */ Mqtt.prototype.req = function (topic, method, params, func, timeout = 0) { var data = { id: this.genId(this.client.options.clientId), method, params }; if (func) { data.func = func; data.timestamp = Date.now(); data.timeout = timeout || this.config.request_timeout || 3000; this.list_msg.push(data); // 设置超时移除定时器 var _this = this; data.timer = setTimeout(() => { _this._delMsgById(data.id); }, data.timeout); } this.send(topic, data); }; /** * 同步请求 - 可及时取回消息 * @param {string} topic 订阅板块 * @param {string} method 请求方法 * @param {object} params 传递参数 * @param {number} timeout 超时时间 0为默认值 10000ms * @returns {object} 返回响应结果 */ Mqtt.prototype.reqAsync = function (topic, method, params, timeout = 0) { var _this = this; return new Promise((resolve, reject) => { _this.req(topic, method, params, (res) => { resolve(res); }, timeout); }); }; /** * 根据ID移除消息 * @param {string} msg_id 消息ID * @private */ Mqtt.prototype._delMsgById = function (msg_id) { for (var i = 0; i < this.list_msg.length; i++) { if (this.list_msg[i].id === msg_id) { // 清除定时器 if (this.list_msg[i].timer) { clearTimeout(this.list_msg[i].timer); } this.list_msg.splice(i, 1); break; } } }; /** * 处理消息回复 * @param {object} msg 回复消息 * @private */ Mqtt.prototype._handleResponse = function (msg) { if (!msg || !msg.id) { return; } for (var i = 0; i < this.list_msg.length; i++) { let o = this.list_msg[i]; if (o.id === msg.id && o.func) { try { // 执行回调函数 o.func(msg); } catch (error) { console.error('MQTT回调函数执行错误:', error); } // 从列表中移除已处理的消息 this._delMsgById(o.id); break; } } }; /** * 保存在线到数据库 * @param {Array} arr 客户端ID数组 * @param {number} online 在线情况 0为不在线,1为在线 */ Mqtt.prototype.saveOnline = async function (arr, online = 1) { if (!arr || !arr.length) { return; } var body = { online }; var db = $.admin.sql('sys').db(); db.table = this.config.table || 'iot_device'; db.size = 0; var query = {}; query.clientid_has = arr.join(','); await db.set(query, body); }; /** * 获取所有设备 * @returns {Array} 设备列表 */ Mqtt.prototype.getClients = async function () { var db = $.admin.sql('sys').db(); db.table = this.config.table || 'iot_device'; db.size = 0; return await db.get({}, '', 'clientid, online'); }; /** * 更新在线设备 */ Mqtt.prototype.updateOnline = async function () { var now = new Date().getTime(); var online_expires = this.config.online_expires; // 180 // 获取所有客户端 var drives = await this.getClients(); var { list_online, list_offline } = this._processDeviceStatus(drives, now, online_expires); // 处理未出现的设备 // this._processUnseenDevices(drives, list_offline, list_has); // 批量更新设备状态 await this._updateDeviceStatus(list_offline, list_online); }; /** * 处理设备状态 * @param {Array} drives 设备列表 * @param {number} now 当前时间戳 * @param {number} online_expires 在线过期时间 * @returns {object} 在线和离线设备列表 * @private */ Mqtt.prototype._processDeviceStatus = function (drives, now, online_expires) { var list_online = []; var list_offline = []; var list_has = []; var mods = this.getMods(); for (var k in mods) { var o = mods[k]; var dict = o.drives; for (var clientid in dict) { list_has.push(clientid); var drive = drives.getObj({ clientid }); var o = dict[clientid]; if (o.online === 1) { this._onlineDevice( o, drive, clientid, now, online_expires, list_online, list_offline, dict ); } else { this._offlineDevice(drive, clientid, list_offline); } } } return { list_online, list_offline, list_has }; }; /** * 处理在线设备 * @param {object} device 设备对象 * @param {object} drive 数据库中的设备信息 * @param {string} clientid 设备ID * @param {number} now 当前时间戳 * @param {number} online_expires 在线过期时间 * @param {Array} list_online 在线设备列表 * @param {Array} list_offline 离线设备列表 * @param {object} dict 设备字典 * @private */ /* eslint-disable no-param-reassign */ Mqtt.prototype._onlineDevice = function ( device, drive, clientid, now, online_expires, list_online, list_offline, dict ) { if (device.time_last) { var cha = now - device.time_last; if (cha > online_expires) { if (drive && drive.online) { list_offline.push(clientid); } // 这里需要修改设备状态,但由于参数修改限制,我们直接修改字典 // 注意:这里仍然会修改外部对象,但这是业务逻辑需要 device.online = 0; delete dict[clientid]; } else { if (drive && !drive.online) { list_online.push(clientid); } } } }; /* eslint-enable no-param-reassign */ /** * 处理离线设备 * @param {object} drive 数据库中的设备信息 * @param {string} clientid 设备ID * @param {Array} list_offline 离线设备列表 * @private */ Mqtt.prototype._offlineDevice = function (drive, clientid, list_offline) { if (drive && drive.online) { list_offline.push(clientid); } }; /** * 处理未出现的设备 * @param {Array} drives 设备列表 * @param {Array} list_offline 离线设备列表 * @param {Array} list_has 已处理的设备ID列表 * @private */ Mqtt.prototype._processUnseenDevices = function (drives, list_offline, list_has) { // 处理未出现的设备 for (var i = 0; i < drives.length; i++) { var { clientid, online } = drives[i]; if (online && list_has.indexOf(clientid) === -1) { list_offline.push(clientid); } } }; /** * 更新设备状态 * @param {Array} list_offline 离线设备列表 * @param {Array} list_online 在线设备列表 * @private */ Mqtt.prototype._updateDeviceStatus = async function (list_offline, list_online) { // 先将设备离线 var list_off = list_offline.to2D(30); for (var i = 0; i < list_off.length; i++) { await this.saveOnline(list_off[i], 0); } // 再将设备在线 var list_on = list_online.to2D(30); for (var i = 0; i < list_on.length; i++) { await this.saveOnline(list_on[i], 1); } }; /** * 开始计时器 */ Mqtt.prototype.startTimer = function () { if (!this.timer) { this.timer = setInterval(() => { try { this.updateOnline(); } catch (err) { console.error('更新在线设备失败', err); } }, this.config.online_interval); } }; exports.Mqtt = Mqtt; /** * 创建全局管理器 */ if (!$.pool.mqtt) { $.pool.mqtt = {}; } function mqttAdmin(scope, title) { var sc = scope || $.val.scope + ''; var obj = $.pool.mqtt[sc]; if (!obj) { $.pool.mqtt[sc] = new Mqtt({ name: sc, title: title }); obj = $.pool.mqtt[sc]; } return obj; } /** * mqtt管理器, 用于管理插件 * @param {string} scope 作用域 * @param {string} title 标题 * @returns {object} 返回一个缓存类 */ if ($.admin) { $.admin.mqtt = mqttAdmin; }