mm_os
Version:
MM_OS服务端架构,用于快速构建应用程序,支持网站建设、小程序后台、AI应用、物联网(IOT/AIOT)、游戏服务端等多种场景。
823 lines (761 loc) • 19.8 kB
JavaScript
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;
}