mm_mqtt_server 1.0.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -95
- package/example/demo.js +56 -0
- package/index.js +2 -4
- package/lib/mqtt_server.js +148 -204
- package/package.json +9 -6
- package/lib/message.js +0 -197
package/README.md
CHANGED
|
@@ -1,158 +1,121 @@
|
|
|
1
1
|
# mm_mqtt_server
|
|
2
2
|
|
|
3
|
-
MQTT
|
|
3
|
+
最薄的 MQTT 代理服务器入站适配器。继承 `mm_mod` 的 `Mod`,基于 [aedes](https://www.npmjs.com/package/aedes) 提供 Broker,把每条入站发布直接交给 `mm_event` 的事件管理器处理,不做路由表、不做插件、不加载脚本。
|
|
4
4
|
|
|
5
5
|
- 入口:`index.js`(CommonJS)
|
|
6
|
-
- 导出:`MqttServer
|
|
7
|
-
- 运行时依赖:`mm_corejs`、`aedes`
|
|
8
|
-
- 测试覆盖率:100%(`jest.config.js` 四项阈值)
|
|
6
|
+
- 导出:`MqttServer`、`admin`、`mqttServerAdmin`
|
|
7
|
+
- 运行时依赖:`mm_mod`、`mm_event`、`mm_corejs`、`aedes`
|
|
9
8
|
|
|
10
|
-
##
|
|
9
|
+
## 消息路径
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
- **JSON-RPC 2.0 插件**:`MessagePlugin` 挂载后接管 RPC 形态报文,应答发布到「请求主题 + 后缀」,支持通知回推、主动推送与服务器反向调用客户端方法。
|
|
17
|
-
- **选项透传**:连接级 `options`(user_id / device_id / agent_id 等)随每条入站消息透传给管道钩子与 RPC 处理函数,适配多用户 / 多设备 / 多智能体场景。
|
|
18
|
-
|
|
19
|
-
## 安装
|
|
20
|
-
|
|
21
|
-
```bash
|
|
22
|
-
npm install mm_mqtt_server
|
|
11
|
+
```
|
|
12
|
+
net → aedes → publish(topic, packet) → ctx = { client_id, topic, qos, retain, packet, client, broker }
|
|
13
|
+
→ eventManager.run(topic, params, ctx, options)
|
|
14
|
+
→ 结果形如 { topic, payload } 时由代理重发
|
|
23
15
|
```
|
|
24
16
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
- Node.js >= 22.5.0
|
|
28
|
-
- 模块规范:CommonJS
|
|
17
|
+
事件 `target` 即发布主题;`mm_event` 按字符串级 `*` 匹配(如 `sensor/*`、`*/temp`),不支持 `#`。`params` 为 `{ client_id, topic, payload, qos, retain }`,`payload` 已按 UTF-8 转为字符串。
|
|
29
18
|
|
|
30
19
|
## 快速开始
|
|
31
20
|
|
|
32
21
|
```js
|
|
33
22
|
'use strict';
|
|
34
23
|
|
|
35
|
-
const { MqttServer
|
|
24
|
+
const { MqttServer } = require('mm_mqtt_server');
|
|
36
25
|
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
services: new Map(),
|
|
40
|
-
getService(name) { return this.services.get(name); },
|
|
41
|
-
hasService(name) { return this.services.has(name); },
|
|
42
|
-
setService(name, instance) { this.services.set(name, instance); return this; },
|
|
43
|
-
getManager() { return undefined; },
|
|
44
|
-
hasManager() { return false; }
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const mqtt = new MqttServer({ name: 'mqtt_server', host: '127.0.0.1', port: 1883 }, server);
|
|
48
|
-
|
|
49
|
-
// 主题守卫:check 阶段产出即短路
|
|
50
|
-
mqtt.getHandle().on('check', (ctx, params) => {
|
|
51
|
-
if (params.topic.startsWith('blocked/')) return { dropped: true };
|
|
52
|
-
}, 100, 'guard');
|
|
26
|
+
const mqtt = new MqttServer({ name: 'mqtt', host: '127.0.0.1', port: 1883 });
|
|
27
|
+
const events = mqtt.getEventManager();
|
|
53
28
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
// 事件
|
|
60
|
-
mqtt.on('connection', (client) => mqtt.setClientOptions(client.id, { device_id: 'd1' }));
|
|
61
|
-
mqtt.on('message', (client_id, info) => console.log(client_id, info.topic, info.payload));
|
|
62
|
-
mqtt.on('subscribe', (client_id, topics) => console.log(client_id, topics));
|
|
63
|
-
mqtt.on('close', (client_id) => console.log('断开', client_id));
|
|
64
|
-
mqtt.on('error', (err) => console.error(err.message));
|
|
29
|
+
events.add({ name: 'echo', target: 'demo/echo', end: true }).main = async (params) => ({
|
|
30
|
+
topic: 'demo/echo/out',
|
|
31
|
+
payload: params.payload
|
|
32
|
+
});
|
|
65
33
|
|
|
66
34
|
await mqtt.init();
|
|
67
35
|
await mqtt.start();
|
|
68
36
|
|
|
69
|
-
|
|
70
|
-
await mqtt.run({}, { action: 'publish', topic: 'mm/test', payload: 'hello' });
|
|
71
|
-
await mqtt.run({}, { action: 'list_clients' });
|
|
72
|
-
|
|
73
|
-
await mqtt.stop();
|
|
74
|
-
await mqtt.dispose();
|
|
37
|
+
console.log(mqtt.getAddress());
|
|
75
38
|
```
|
|
76
39
|
|
|
77
|
-
##
|
|
78
|
-
|
|
79
|
-
顶层 `config` 字段即服务器设置,`init` 阶段统一校验。
|
|
40
|
+
## 服务器配置
|
|
80
41
|
|
|
81
42
|
| 字段 | 类型 | 默认值 | 说明 |
|
|
82
43
|
| --- | --- | --- | --- |
|
|
83
|
-
| `name` | string | `'
|
|
44
|
+
| `name` | string | `'os'` | 模块名称(必填) |
|
|
45
|
+
| `title` | string | `'MQTT代理服务器模块'` | 模块标题 |
|
|
46
|
+
| `description` | string | — | 模块描述 |
|
|
47
|
+
| `version` | string | `'2.0.0'` | 版本号 |
|
|
84
48
|
| `host` | string | `'0.0.0.0'` | 监听地址 |
|
|
85
|
-
| `port` |
|
|
49
|
+
| `port` | number | `1883` | 监听端口 |
|
|
86
50
|
| `allow_anonymous` | boolean | `true` | 未配置 `username` 时放行匿名连接 |
|
|
87
51
|
| `username` | string | `''` | 认证用户名,非空时启用口令认证 |
|
|
88
52
|
| `password` | string | `''` | 认证口令 |
|
|
53
|
+
| `options` | object | `{}` | 透传给 `Aedes.createBroker` 的额外选项(`authenticate` 固定由本模块提供) |
|
|
54
|
+
| `params` | object | `{ action }` | `main` 的参数约定 |
|
|
89
55
|
|
|
90
56
|
## API
|
|
91
57
|
|
|
92
|
-
### MqttServer
|
|
93
|
-
|
|
94
58
|
| 成员 | 说明 |
|
|
95
59
|
| --- | --- |
|
|
96
|
-
| `
|
|
60
|
+
| `init()` / `start()` / `stop()` / `dispose()` | `Mod` 生命周期模板方法 |
|
|
61
|
+
| `onInit()` / `onStart()` / `onStop()` / `onDispose()` | 生命周期钩子,子类可覆盖 |
|
|
62
|
+
| `getEventManager()` | 获取 mm_event 的 `mqtt` 作用域事件管理器(与其他实例共享),用于注册事件 |
|
|
63
|
+
| `setEventManager(manager)` | 注入外部事件管理器,需提供 `run()` |
|
|
64
|
+
| `getAddress()` | 实际监听地址,未启动为 `null` |
|
|
65
|
+
| `run(params, ctx, options)` | 主动操作入口,`params.action` 派发 `publish` / `close` / `list` |
|
|
97
66
|
| `publish(topic, payload, opts)` | 代理端发布消息(进入正常路由,订阅者可收到),返回是否成功 |
|
|
98
67
|
| `closeClient(client_id)` | 关闭指定客户端连接,返回是否发起 |
|
|
99
|
-
| `listClients(
|
|
68
|
+
| `listClients()` | 在线客户端列表 `{ id, address }` |
|
|
100
69
|
| `hasClient(client_id)` | 是否在线 |
|
|
101
|
-
| `setClientOptions(client_id, options)` |
|
|
102
|
-
| `use(plugin)` | 挂载插件(需实现 `bind(server)`) |
|
|
103
|
-
| `getHandle()` | 消息管道,注册 `before / check / main / render` 等钩子;产出 `{ topic, payload }` 时代理重发 |
|
|
104
|
-
| `getAddress()` | 实际监听地址,未启动为 `null` |
|
|
70
|
+
| `setClientOptions(client_id, options)` | 绑定连接级选项,随入站消息作为 `options` 透传(`null` 清空) |
|
|
105
71
|
|
|
106
|
-
|
|
72
|
+
## 请求上下文(ctx)
|
|
107
73
|
|
|
108
|
-
|
|
|
109
|
-
| --- | --- |
|
|
110
|
-
| `
|
|
111
|
-
| `
|
|
112
|
-
| `
|
|
113
|
-
| `
|
|
114
|
-
| `
|
|
115
|
-
| `
|
|
74
|
+
| 字段 | 说明 |
|
|
75
|
+
| --- | --- |
|
|
76
|
+
| `client_id` | 发布者客户端 ID |
|
|
77
|
+
| `topic` | 发布主题 |
|
|
78
|
+
| `qos` / `retain` | 发布报文的服务质量与保留标记 |
|
|
79
|
+
| `packet` | aedes 原始发布报文 |
|
|
80
|
+
| `client` | aedes 客户端实例,可直接调用其方法 |
|
|
81
|
+
| `broker` | aedes broker 实例,可自行发布或操作会话 |
|
|
116
82
|
|
|
117
|
-
|
|
83
|
+
## 事件处理函数
|
|
118
84
|
|
|
119
|
-
|
|
85
|
+
签名固定为 `(params, ctx, options)`:
|
|
120
86
|
|
|
121
|
-
```
|
|
122
|
-
|
|
87
|
+
```js
|
|
88
|
+
events.add({ name: 'relay', target: 'sensor/*', end: true }).main = async (params, ctx, options) => {
|
|
89
|
+
return { topic: 'archive/sensor', payload: params.payload };
|
|
90
|
+
};
|
|
123
91
|
```
|
|
124
92
|
|
|
125
|
-
|
|
93
|
+
返回 `{ topic, payload }` 时由代理重发该消息(可带 `qos` / `retain`);返回其他值或 `undefined` 时不重发。事件也可直接用 `ctx.broker.publish()` 或 `ctx.client` 自行处理。代理自身重发的报文 `client` 为空,不会再次进入事件,因此无回环。
|
|
126
94
|
|
|
127
|
-
|
|
95
|
+
事件抛出的异常由 `mm_event` 记入 `options.error`,本模块再通过 `error` 事件上报,不会中断 broker。
|
|
128
96
|
|
|
129
|
-
|
|
97
|
+
## 主动操作
|
|
130
98
|
|
|
131
99
|
```js
|
|
132
|
-
|
|
133
|
-
mqtt.
|
|
134
|
-
|
|
100
|
+
await mqtt.run({ action: 'publish', topic: 'sys/notice', payload: 'hi' });
|
|
101
|
+
await mqtt.run({ action: 'close', client_id: 'device_1' });
|
|
102
|
+
await mqtt.run({ action: 'list' });
|
|
135
103
|
```
|
|
136
104
|
|
|
137
|
-
|
|
138
|
-
| --- | --- |
|
|
139
|
-
| `addMethod(name, handler)` / `delMethod(name)` / `listMethods(query)` | 方法表管理,处理函数签名 `(params, extra, options)` |
|
|
140
|
-
| `push(client_id, data, options)` | 发布数据到 `options.topic`(缺省 `push_prefix/client_id`),客户端订阅即接收 |
|
|
141
|
-
| `call(client_id, method, params, options)` | 发布 JSON-RPC 请求到 `options.topic`(缺省 `call_prefix/client_id`)并等待客户端反馈,`options.timeout_ms` 缺省 30000 |
|
|
142
|
-
| `bind(server)` / `isBound()` | 挂载与状态查询 |
|
|
105
|
+
## 生命周期
|
|
143
106
|
|
|
144
|
-
|
|
107
|
+
`onStart` 创建 aedes broker 与 `net.Server` 并开始监听,绑定连接表与入站发布;`onStop` 断开全部客户端、关闭服务器与 broker。事件管理器取自 mm_event 的 `mqtt` 作用域,由全局 `Admin` 共享,停止时不清理。端口被占用时 `start()` 抛错,模块进入 `error` 状态,可 `dispose()` 释放。
|
|
145
108
|
|
|
146
109
|
## 示例
|
|
147
110
|
|
|
148
111
|
```bash
|
|
149
|
-
npm run demo #
|
|
112
|
+
npm run demo # 启动演示代理(127.0.0.1:1883),见 example/demo.js
|
|
150
113
|
```
|
|
151
114
|
|
|
152
115
|
## 测试
|
|
153
116
|
|
|
154
117
|
```bash
|
|
155
|
-
npm test #
|
|
118
|
+
npm test # node --test
|
|
156
119
|
```
|
|
157
120
|
|
|
158
121
|
## 许可证
|
package/example/demo.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { MqttServer } = require('..');
|
|
4
|
+
|
|
5
|
+
/** 演示监听端口 */
|
|
6
|
+
const DEMO_PORT = 1883;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 注册演示事件:发布主题即事件 target
|
|
10
|
+
* @param {Object} events 事件管理器
|
|
11
|
+
* @returns {void}
|
|
12
|
+
*/
|
|
13
|
+
function bindEvents(events) {
|
|
14
|
+
events.add({ name: 'echo', target: 'demo/echo', end: true }).main = async (params) => ({
|
|
15
|
+
topic: 'demo/echo/out',
|
|
16
|
+
payload: params.payload
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
events.add({ name: 'time', target: 'demo/time', end: true }).main = async () => ({
|
|
20
|
+
topic: 'demo/time/out',
|
|
21
|
+
payload: new Date().toISOString()
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
events.add({ name: 'upper', target: 'demo/upper', end: true }).main = async (params) => ({
|
|
25
|
+
topic: 'demo/lower',
|
|
26
|
+
payload: String(params.payload).toLowerCase()
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 演示入口:启动 MQTT 代理并打印监听地址
|
|
32
|
+
* @returns {Promise<void>} 无返回值
|
|
33
|
+
*/
|
|
34
|
+
async function main() {
|
|
35
|
+
const mqtt = new MqttServer({
|
|
36
|
+
name: 'demo',
|
|
37
|
+
host: '127.0.0.1',
|
|
38
|
+
port: DEMO_PORT
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
bindEvents(mqtt.getEventManager());
|
|
42
|
+
mqtt.on('error', (err) => console.error(`[demo] 错误: ${err.message}`));
|
|
43
|
+
|
|
44
|
+
await mqtt.init();
|
|
45
|
+
await mqtt.start();
|
|
46
|
+
|
|
47
|
+
const address = mqtt.getAddress();
|
|
48
|
+
console.log(`mqtt://127.0.0.1:${address.port}`);
|
|
49
|
+
console.log('订阅 demo/echo/out 后发布到 demo/echo,事件返回的 { topic, payload } 会被代理重发');
|
|
50
|
+
console.log('demo/time 返回当前时间,demo/upper 把内容转小写后发到 demo/lower');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
main().catch((err) => {
|
|
54
|
+
console.error(err);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
});
|
package/index.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const MqttServer = require('./lib/mqtt_server');
|
|
4
|
-
const MessagePlugin = require('./lib/message');
|
|
5
4
|
const Admin = require('mm_corejs').Admin;
|
|
6
5
|
|
|
7
6
|
const admin = new Admin();
|
|
@@ -9,11 +8,10 @@ admin.setClass(MqttServer);
|
|
|
9
8
|
|
|
10
9
|
module.exports = {
|
|
11
10
|
MqttServer,
|
|
12
|
-
MessagePlugin,
|
|
13
11
|
admin,
|
|
14
12
|
/**
|
|
15
13
|
* 获取指定作用域的 MQTT 服务器
|
|
16
|
-
* @param {string} [scope='
|
|
14
|
+
* @param {string} [scope='os'] 作用域名称
|
|
17
15
|
* @param {object} [config] 配置参数
|
|
18
16
|
* @returns {MqttServer} MQTT 服务器实例
|
|
19
17
|
*/
|
|
@@ -21,6 +19,6 @@ module.exports = {
|
|
|
21
19
|
if (!scope) {
|
|
22
20
|
scope = 'os';
|
|
23
21
|
}
|
|
24
|
-
return admin.get(scope,
|
|
22
|
+
return admin.get(scope, config);
|
|
25
23
|
}
|
|
26
24
|
};
|
package/lib/mqtt_server.js
CHANGED
|
@@ -2,49 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
const net = require('net');
|
|
4
4
|
const { Aedes } = require('aedes');
|
|
5
|
-
const { Mod
|
|
6
|
-
const {
|
|
5
|
+
const { Mod } = require('mm_mod');
|
|
6
|
+
const { eventAdmin } = require('mm_event');
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
10
|
-
*/
|
|
11
|
-
const SERVER_SCHEMA = {
|
|
12
|
-
type: 'object',
|
|
13
|
-
properties: {
|
|
14
|
-
host: { type: 'string', min_length: 1 },
|
|
15
|
-
port: { type: 'integer', minimum: 1, maximum: 65535 },
|
|
16
|
-
allow_anonymous: { type: 'boolean' },
|
|
17
|
-
username: { type: 'string' },
|
|
18
|
-
password: { type: 'string' }
|
|
19
|
-
}
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* MqttServer —— MQTT 代理服务器(入站适配器模块)。
|
|
9
|
+
* MqttServer —— 最薄的 MQTT 代理传输层(入站适配器模块)。
|
|
24
10
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* 1. 监听:onStart 创建 aedes broker 与 net.Server,按 host / port 监听 MQTT 连接;
|
|
32
|
-
* 2. 连接管理:#clients 使用 Store 登记在线客户端(主键 id,支持条件查询与分页);
|
|
33
|
-
* 3. 消息管道:入站发布消息经 Handle 的 before/check/main/render 阶段处理,
|
|
34
|
-
* 内置 main 钩子发 message 事件保持兼容;管道产出 { topic, payload } 时
|
|
35
|
-
* 由代理重新发布该消息;getHandle() 可注册自定义钩子(如主题黑名单);
|
|
36
|
-
* 4. 事件分发:connection / message / subscribe / unsubscribe / close / error;
|
|
37
|
-
* 5. 主动操作:run(ctx, params) 按 params.action 派发 publish / close_client / list_clients。
|
|
11
|
+
* 只做三件事:
|
|
12
|
+
* 1. 生命周期:由 Mod 的 init / start / stop 驱动 aedes broker 与 net.Server;
|
|
13
|
+
* 2. 消息转交:以发布主题为 target,直接调用
|
|
14
|
+
* EventManager.run(topic, params, ctx, options),不做路由、不做插件;
|
|
15
|
+
* 3. 结果重发:事件返回 { topic, payload } 时由代理重新发布,事件也可用
|
|
16
|
+
* ctx.broker / ctx.client 自行处理。
|
|
38
17
|
*
|
|
39
18
|
* 用法:
|
|
40
|
-
* const mqtt = new MqttServer({ name: '
|
|
41
|
-
* mqtt.
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
19
|
+
* const mqtt = new MqttServer({ name: 'mqtt', port: 1883 });
|
|
20
|
+
* mqtt.getEventManager().add({ name: 'echo', target: 'mm/*' }).main = async (params) => ({
|
|
21
|
+
* topic: params.topic,
|
|
22
|
+
* payload: params.payload
|
|
23
|
+
* });
|
|
45
24
|
* await mqtt.init();
|
|
46
25
|
* await mqtt.start();
|
|
47
|
-
* await mqtt.run({}, { action: 'publish', topic: 'mm/test', payload: 'hello' });
|
|
48
26
|
*/
|
|
49
27
|
class MqttServer extends Mod {
|
|
50
28
|
/**
|
|
@@ -53,8 +31,8 @@ class MqttServer extends Mod {
|
|
|
53
31
|
static config = {
|
|
54
32
|
name: 'os',
|
|
55
33
|
title: 'MQTT代理服务器模块',
|
|
56
|
-
description: '
|
|
57
|
-
version: '
|
|
34
|
+
description: 'MQTT 传输层:消息直达事件管理器,不做路由与插件',
|
|
35
|
+
version: '2.0.0',
|
|
58
36
|
host: '0.0.0.0',
|
|
59
37
|
port: 1883,
|
|
60
38
|
allow_anonymous: true,
|
|
@@ -65,13 +43,12 @@ class MqttServer extends Mod {
|
|
|
65
43
|
type: 'object',
|
|
66
44
|
required: ['action'],
|
|
67
45
|
properties: {
|
|
68
|
-
action: { type: 'string', enum: ['publish', '
|
|
46
|
+
action: { type: 'string', enum: ['publish', 'close', 'list'] },
|
|
69
47
|
client_id: { type: 'string', min_length: 1 },
|
|
70
48
|
topic: { type: 'string', min_length: 1 },
|
|
71
49
|
payload: { type: 'string' },
|
|
72
50
|
qos: { type: 'integer', minimum: 0, maximum: 2 },
|
|
73
|
-
retain: { type: 'boolean' }
|
|
74
|
-
query: { type: 'object' }
|
|
51
|
+
retain: { type: 'boolean' }
|
|
75
52
|
}
|
|
76
53
|
}
|
|
77
54
|
};
|
|
@@ -87,27 +64,29 @@ class MqttServer extends Mod {
|
|
|
87
64
|
#server = null;
|
|
88
65
|
|
|
89
66
|
/**
|
|
90
|
-
*
|
|
67
|
+
* 事件管理器(取自 mm_event 的 mqtt 作用域,与其他实例共享)
|
|
91
68
|
*/
|
|
92
|
-
#
|
|
69
|
+
#events = null;
|
|
93
70
|
|
|
94
71
|
/**
|
|
95
|
-
*
|
|
96
|
-
* 每个实例独立 Handle,避免多实例共享管道导致钩子泄漏
|
|
72
|
+
* 在线连接表:client_id -> 连接信息
|
|
97
73
|
*/
|
|
98
|
-
#
|
|
74
|
+
#clients = new Map();
|
|
99
75
|
|
|
100
|
-
// ---------- 生命周期钩子 ----------
|
|
101
76
|
/**
|
|
102
|
-
*
|
|
77
|
+
* 构造函数
|
|
78
|
+
* @param {Object} config 模块配置
|
|
79
|
+
* @param {Object} server 框架实例
|
|
103
80
|
*/
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
this
|
|
81
|
+
constructor(config, server) {
|
|
82
|
+
super(config, server);
|
|
83
|
+
this.#events = eventAdmin('mqtt');
|
|
107
84
|
}
|
|
108
85
|
|
|
86
|
+
// ---------- 生命周期钩子 ----------
|
|
109
87
|
/**
|
|
110
88
|
* 启动钩子:创建 broker 与 TCP 服务器并开始监听
|
|
89
|
+
* @returns {Promise<void>} 无返回值
|
|
111
90
|
*/
|
|
112
91
|
async onStart() {
|
|
113
92
|
const broker = await this._createBroker();
|
|
@@ -120,7 +99,8 @@ class MqttServer extends Mod {
|
|
|
120
99
|
}
|
|
121
100
|
|
|
122
101
|
/**
|
|
123
|
-
*
|
|
102
|
+
* 停止钩子:断开全部连接、关闭服务器与 broker(事件管理器与其他实例共享,不清理)
|
|
103
|
+
* @returns {Promise<void>} 无返回值
|
|
124
104
|
*/
|
|
125
105
|
async onStop() {
|
|
126
106
|
this._closeAllClients();
|
|
@@ -128,13 +108,6 @@ class MqttServer extends Mod {
|
|
|
128
108
|
await this._closeBroker();
|
|
129
109
|
}
|
|
130
110
|
|
|
131
|
-
/**
|
|
132
|
-
* 释放钩子:清空连接注册表
|
|
133
|
-
*/
|
|
134
|
-
async onDispose() {
|
|
135
|
-
this.#clients.clear();
|
|
136
|
-
}
|
|
137
|
-
|
|
138
111
|
// ---------- 执行入口 ----------
|
|
139
112
|
/**
|
|
140
113
|
* 模块主逻辑:按 action 派发操作
|
|
@@ -146,8 +119,8 @@ class MqttServer extends Mod {
|
|
|
146
119
|
async main(params, ctx, options) {
|
|
147
120
|
const action = params.action;
|
|
148
121
|
if (action === 'publish') return this._actionPublish(params);
|
|
149
|
-
if (action === '
|
|
150
|
-
return this.
|
|
122
|
+
if (action === 'close') return this._actionClose(params);
|
|
123
|
+
return this._actionList();
|
|
151
124
|
}
|
|
152
125
|
|
|
153
126
|
/**
|
|
@@ -166,19 +139,50 @@ class MqttServer extends Mod {
|
|
|
166
139
|
* @param {Object} params 调用参数,含 client_id
|
|
167
140
|
* @returns {Object} 操作结果
|
|
168
141
|
*/
|
|
169
|
-
|
|
142
|
+
_actionClose(params) {
|
|
170
143
|
this._requireClientId(params);
|
|
171
144
|
const success = this.closeClient(params.client_id);
|
|
172
|
-
return { action: '
|
|
145
|
+
return { action: 'close', client_id: params.client_id, success };
|
|
173
146
|
}
|
|
174
147
|
|
|
175
148
|
/**
|
|
176
|
-
*
|
|
177
|
-
* @param {Object} params 调用参数,可含 query 查询条件(Store 条件/分页)
|
|
149
|
+
* 执行列出连接操作
|
|
178
150
|
* @returns {Object} 操作结果,clients 为在线客户端列表
|
|
179
151
|
*/
|
|
180
|
-
|
|
181
|
-
return { action: '
|
|
152
|
+
_actionList() {
|
|
153
|
+
return { action: 'list', clients: this.listClients() };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ---------- 事件管理器 ----------
|
|
157
|
+
/**
|
|
158
|
+
* 获取事件管理器,用于注册事件
|
|
159
|
+
* @returns {Object} 事件管理器实例
|
|
160
|
+
*/
|
|
161
|
+
getEventManager() {
|
|
162
|
+
return this.#events;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* 注入外部事件管理器
|
|
167
|
+
* @param {Object} manager 事件管理器实例
|
|
168
|
+
* @returns {MqttServer} 当前实例
|
|
169
|
+
* @throws {TypeError} manager 不具备 run 方法时
|
|
170
|
+
*/
|
|
171
|
+
setEventManager(manager) {
|
|
172
|
+
if (!manager || typeof manager.run !== 'function') {
|
|
173
|
+
throw new TypeError(`${this._label()} manager must provide run()`);
|
|
174
|
+
}
|
|
175
|
+
this.#events = manager;
|
|
176
|
+
return this;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* 获取实际监听地址
|
|
181
|
+
* @returns {Object|null} 监听地址信息,未启动时为 null
|
|
182
|
+
*/
|
|
183
|
+
getAddress() {
|
|
184
|
+
const server = this.#server;
|
|
185
|
+
return server ? server.address() : null;
|
|
182
186
|
}
|
|
183
187
|
|
|
184
188
|
// ---------- 公开操作 ----------
|
|
@@ -186,7 +190,7 @@ class MqttServer extends Mod {
|
|
|
186
190
|
* 代理端发布消息到指定主题
|
|
187
191
|
* @param {string} topic 主题
|
|
188
192
|
* @param {string|Buffer} payload 消息内容
|
|
189
|
-
* @param {Object} opts 发布选项,含 qos 与 retain
|
|
193
|
+
* @param {Object} [opts] 发布选项,含 qos 与 retain
|
|
190
194
|
* @returns {Promise<boolean>} 是否发布成功
|
|
191
195
|
*/
|
|
192
196
|
publish(topic, payload, opts = {}) {
|
|
@@ -205,48 +209,27 @@ class MqttServer extends Mod {
|
|
|
205
209
|
}
|
|
206
210
|
|
|
207
211
|
/**
|
|
208
|
-
*
|
|
212
|
+
* 关闭指定客户端连接(连接表由 broker 的断开回调清理)
|
|
209
213
|
* @param {string} client_id 客户端 ID
|
|
210
214
|
* @returns {boolean} 是否发起关闭
|
|
211
215
|
*/
|
|
212
216
|
closeClient(client_id) {
|
|
213
|
-
const
|
|
214
|
-
if (!
|
|
215
|
-
|
|
217
|
+
const client = this.#clients.get(client_id);
|
|
218
|
+
if (!client) return false;
|
|
219
|
+
client.client.close();
|
|
216
220
|
return true;
|
|
217
221
|
}
|
|
218
222
|
|
|
219
223
|
/**
|
|
220
|
-
*
|
|
221
|
-
* @param {Object} [query] 查询条件,缺省时列出全部
|
|
224
|
+
* 列出在线客户端
|
|
222
225
|
* @returns {Array<Object>} 客户端信息列表,含 id 与 address
|
|
223
226
|
*/
|
|
224
|
-
listClients(
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
* @param {string} client_id 客户端 ID
|
|
231
|
-
* @param {Object|null} options 选项对象,null 时清空
|
|
232
|
-
* @returns {boolean} 是否设置成功(客户端不在线时失败)
|
|
233
|
-
*/
|
|
234
|
-
setClientOptions(client_id, options) {
|
|
235
|
-
const entry = this.#clients.getById(client_id);
|
|
236
|
-
if (!entry) return false;
|
|
237
|
-
entry.options = options || null;
|
|
238
|
-
return true;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* 读取连接级选项,未绑定时返回空对象
|
|
243
|
-
* @param {string} client_id 客户端 ID
|
|
244
|
-
* @returns {Object} 连接级选项
|
|
245
|
-
*/
|
|
246
|
-
_clientOptions(client_id) {
|
|
247
|
-
const entry = this.#clients.getById(client_id);
|
|
248
|
-
if (!entry || !entry.options) return {};
|
|
249
|
-
return entry.options;
|
|
227
|
+
listClients() {
|
|
228
|
+
const list = [];
|
|
229
|
+
for (const client of this.#clients.values()) {
|
|
230
|
+
list.push({ id: client.id, address: client.address });
|
|
231
|
+
}
|
|
232
|
+
return list;
|
|
250
233
|
}
|
|
251
234
|
|
|
252
235
|
/**
|
|
@@ -255,40 +238,31 @@ class MqttServer extends Mod {
|
|
|
255
238
|
* @returns {boolean} 是否在线
|
|
256
239
|
*/
|
|
257
240
|
hasClient(client_id) {
|
|
258
|
-
return
|
|
241
|
+
return this.#clients.has(client_id);
|
|
259
242
|
}
|
|
260
243
|
|
|
261
244
|
/**
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
* @param {Object}
|
|
265
|
-
* @returns {
|
|
266
|
-
* @throws {TypeError} plugin 不具备 bind 方法时
|
|
267
|
-
*/
|
|
268
|
-
use(plugin) {
|
|
269
|
-
if (!plugin || typeof plugin.bind !== 'function') {
|
|
270
|
-
throw new TypeError(`${this._label()} plugin must provide bind()`);
|
|
271
|
-
}
|
|
272
|
-
plugin.bind(this);
|
|
273
|
-
return this;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
/**
|
|
277
|
-
* 获取消息处理管道,供外部注册 before/check/main 等钩子;
|
|
278
|
-
* 钩子产出 { topic, payload } 时由代理重新发布该消息
|
|
279
|
-
* @returns {Handle} 消息管道实例
|
|
245
|
+
* 绑定连接级选项,入站消息将作为 options 透传给事件
|
|
246
|
+
* @param {string} client_id 客户端 ID
|
|
247
|
+
* @param {Object|null} options 选项对象,null 时清空
|
|
248
|
+
* @returns {boolean} 是否设置成功(客户端不在线时失败)
|
|
280
249
|
*/
|
|
281
|
-
|
|
282
|
-
|
|
250
|
+
setClientOptions(client_id, options) {
|
|
251
|
+
const client = this.#clients.get(client_id);
|
|
252
|
+
if (!client) return false;
|
|
253
|
+
client.options = options || null;
|
|
254
|
+
return true;
|
|
283
255
|
}
|
|
284
256
|
|
|
285
257
|
/**
|
|
286
|
-
*
|
|
287
|
-
* @
|
|
258
|
+
* 获取连接级选项(复制一份,避免事件管理器回写)
|
|
259
|
+
* @param {string} client_id 客户端 ID
|
|
260
|
+
* @returns {Object} 选项对象
|
|
288
261
|
*/
|
|
289
|
-
|
|
290
|
-
const
|
|
291
|
-
|
|
262
|
+
_clientOptions(client_id) {
|
|
263
|
+
const client = this.#clients.get(client_id);
|
|
264
|
+
if (!client || !client.options) return {};
|
|
265
|
+
return { ...client.options };
|
|
292
266
|
}
|
|
293
267
|
|
|
294
268
|
// ---------- broker 装配 ----------
|
|
@@ -297,50 +271,44 @@ class MqttServer extends Mod {
|
|
|
297
271
|
* @returns {Promise<Object>} broker 实例
|
|
298
272
|
*/
|
|
299
273
|
async _createBroker() {
|
|
300
|
-
|
|
274
|
+
return Aedes.createBroker({
|
|
275
|
+
...this.config.options,
|
|
301
276
|
authenticate: (client, username, password, done) => {
|
|
302
277
|
this._authenticate(client, username, password, done);
|
|
303
278
|
}
|
|
304
279
|
});
|
|
305
|
-
return broker;
|
|
306
280
|
}
|
|
307
281
|
|
|
308
282
|
/**
|
|
309
|
-
*
|
|
283
|
+
* 绑定 broker 事件:维护连接表并把入站发布转交事件管理器
|
|
310
284
|
* @param {Object} broker aedes broker 实例
|
|
311
285
|
*/
|
|
312
286
|
_bindBrokerEvents(broker) {
|
|
313
|
-
broker.on('client', (client) =>
|
|
314
|
-
|
|
315
|
-
this.#clients.add({
|
|
316
|
-
id: client.id,
|
|
317
|
-
name: client.id,
|
|
318
|
-
address: this._remoteAddress(client),
|
|
319
|
-
client
|
|
320
|
-
});
|
|
321
|
-
this._safeEmit('connection', this._clientInfo(client));
|
|
322
|
-
});
|
|
323
|
-
broker.on('clientDisconnect', (client) => {
|
|
324
|
-
const existed = this.#clients.delById(client.id);
|
|
325
|
-
if (existed) this._safeEmit('close', client.id);
|
|
326
|
-
});
|
|
287
|
+
broker.on('client', (client) => this._registerClient(client));
|
|
288
|
+
broker.on('clientDisconnect', (client) => this.#clients.delete(client.id));
|
|
327
289
|
broker.on('publish', (packet, client) => {
|
|
328
290
|
if (!client) return;
|
|
329
|
-
this._handlePublish(client, packet)
|
|
330
|
-
});
|
|
331
|
-
broker.on('subscribe', (subscriptions, client) => {
|
|
332
|
-
const topics = subscriptions.map((item) => item.topic);
|
|
333
|
-
this._safeEmit('subscribe', client.id, topics);
|
|
334
|
-
});
|
|
335
|
-
broker.on('unsubscribe', (topics, client) => {
|
|
336
|
-
this._safeEmit('unsubscribe', client.id, topics);
|
|
291
|
+
this._handlePublish(client, packet);
|
|
337
292
|
});
|
|
338
293
|
broker.on('clientError', (client, err) => this._emitError(err));
|
|
339
294
|
broker.on('connectionError', (client, err) => this._emitError(err));
|
|
340
295
|
}
|
|
341
296
|
|
|
342
297
|
/**
|
|
343
|
-
*
|
|
298
|
+
* 登记在线客户端
|
|
299
|
+
* @param {Object} client aedes client 实例
|
|
300
|
+
*/
|
|
301
|
+
_registerClient(client) {
|
|
302
|
+
this.#clients.set(client.id, {
|
|
303
|
+
id: client.id,
|
|
304
|
+
address: this._remoteAddress(client),
|
|
305
|
+
client,
|
|
306
|
+
options: null
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* 处理入站发布消息:交给事件管理器,返回发布描述时由代理重发
|
|
344
312
|
* @param {Object} client aedes client 实例
|
|
345
313
|
* @param {Object} packet aedes 发布报文
|
|
346
314
|
* @returns {Promise<void>} 无返回值
|
|
@@ -353,17 +321,31 @@ class MqttServer extends Mod {
|
|
|
353
321
|
qos: packet.qos,
|
|
354
322
|
retain: packet.retain
|
|
355
323
|
};
|
|
356
|
-
const ctx = {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
324
|
+
const ctx = {
|
|
325
|
+
client_id: client.id,
|
|
326
|
+
topic: packet.topic,
|
|
327
|
+
qos: packet.qos,
|
|
328
|
+
retain: packet.retain,
|
|
329
|
+
packet,
|
|
330
|
+
client,
|
|
331
|
+
broker: this.#broker
|
|
332
|
+
};
|
|
333
|
+
const options = this._clientOptions(client.id);
|
|
334
|
+
try {
|
|
335
|
+
const result = await this.#events.run(packet.topic, params, ctx, options);
|
|
336
|
+
const error = ctx.error || options.error;
|
|
337
|
+
if (error) this._emitError(error);
|
|
338
|
+
if (this._isRepublish(result)) {
|
|
339
|
+
await this.publish(result.topic, result.payload, result);
|
|
340
|
+
}
|
|
341
|
+
} catch (err) {
|
|
342
|
+
this._emitError(err);
|
|
361
343
|
}
|
|
362
344
|
}
|
|
363
345
|
|
|
364
346
|
/**
|
|
365
|
-
*
|
|
366
|
-
* @param {*} result
|
|
347
|
+
* 判断事件结果是否为待重发的发布描述(须含 topic 与 payload)
|
|
348
|
+
* @param {*} result 事件结果
|
|
367
349
|
* @returns {boolean} 是否为发布描述
|
|
368
350
|
*/
|
|
369
351
|
_isRepublish(result) {
|
|
@@ -400,8 +382,7 @@ class MqttServer extends Mod {
|
|
|
400
382
|
* @returns {boolean} 是否放行
|
|
401
383
|
*/
|
|
402
384
|
_checkAnonymous() {
|
|
403
|
-
|
|
404
|
-
return anonymous;
|
|
385
|
+
return !!(this.config.allow_anonymous && !this.config.username);
|
|
405
386
|
}
|
|
406
387
|
|
|
407
388
|
/**
|
|
@@ -439,24 +420,12 @@ class MqttServer extends Mod {
|
|
|
439
420
|
return {
|
|
440
421
|
cmd: 'publish',
|
|
441
422
|
topic,
|
|
442
|
-
payload: Buffer.from(String(payload)),
|
|
423
|
+
payload: Buffer.isBuffer(payload) ? payload : Buffer.from(String(payload)),
|
|
443
424
|
qos: opts.qos || 0,
|
|
444
425
|
retain: opts.retain || false
|
|
445
426
|
};
|
|
446
427
|
}
|
|
447
428
|
|
|
448
|
-
/**
|
|
449
|
-
* 提取客户端信息
|
|
450
|
-
* @param {Object} client aedes client 实例
|
|
451
|
-
* @returns {Object} 含 id 与 address 的信息
|
|
452
|
-
*/
|
|
453
|
-
_clientInfo(client) {
|
|
454
|
-
return {
|
|
455
|
-
id: client.id,
|
|
456
|
-
address: this._remoteAddress(client)
|
|
457
|
-
};
|
|
458
|
-
}
|
|
459
|
-
|
|
460
429
|
/**
|
|
461
430
|
* 获取远端地址字符串
|
|
462
431
|
* @param {Object} client aedes client 实例
|
|
@@ -487,11 +456,11 @@ class MqttServer extends Mod {
|
|
|
487
456
|
}
|
|
488
457
|
|
|
489
458
|
/**
|
|
490
|
-
*
|
|
459
|
+
* 断开全部客户端连接并清空连接表
|
|
491
460
|
*/
|
|
492
461
|
_closeAllClients() {
|
|
493
|
-
for (const
|
|
494
|
-
|
|
462
|
+
for (const client of this.#clients.values()) {
|
|
463
|
+
client.client.close();
|
|
495
464
|
}
|
|
496
465
|
this.#clients.clear();
|
|
497
466
|
}
|
|
@@ -505,7 +474,7 @@ class MqttServer extends Mod {
|
|
|
505
474
|
this.#server = null;
|
|
506
475
|
if (!server) return Promise.resolve();
|
|
507
476
|
return new Promise((resolve) => {
|
|
508
|
-
server.close(resolve);
|
|
477
|
+
server.close(() => resolve());
|
|
509
478
|
});
|
|
510
479
|
}
|
|
511
480
|
|
|
@@ -518,36 +487,11 @@ class MqttServer extends Mod {
|
|
|
518
487
|
this.#broker = null;
|
|
519
488
|
if (!broker) return Promise.resolve();
|
|
520
489
|
return new Promise((resolve) => {
|
|
521
|
-
broker.close(resolve);
|
|
490
|
+
broker.close(() => resolve());
|
|
522
491
|
});
|
|
523
492
|
}
|
|
524
493
|
|
|
525
|
-
// ---------- 消息管道 ----------
|
|
526
|
-
/**
|
|
527
|
-
* 装配内置管道钩子:main 发 message 事件保持向后兼容
|
|
528
|
-
*/
|
|
529
|
-
_initHandle() {
|
|
530
|
-
this.#handle.on('main', (params, ctx) => {
|
|
531
|
-
this._safeEmit('message', params.client_id, {
|
|
532
|
-
topic: params.topic,
|
|
533
|
-
payload: params.payload,
|
|
534
|
-
qos: params.qos,
|
|
535
|
-
retain: params.retain
|
|
536
|
-
});
|
|
537
|
-
}, 1000, 'emit_message');
|
|
538
|
-
}
|
|
539
|
-
|
|
540
494
|
// ---------- 校验辅助 ----------
|
|
541
|
-
/**
|
|
542
|
-
* 校验顶层 config 中的服务器设置
|
|
543
|
-
* @returns {Promise<void>} 无返回值
|
|
544
|
-
* @throws {Error} 校验不通过时
|
|
545
|
-
*/
|
|
546
|
-
async _validateConfig() {
|
|
547
|
-
const tip = await this._validate(this.config, SERVER_SCHEMA);
|
|
548
|
-
if (tip) throw new Error(`${this._label()} invalid config: ${tip}`);
|
|
549
|
-
}
|
|
550
|
-
|
|
551
495
|
/**
|
|
552
496
|
* 断言调用参数含 client_id
|
|
553
497
|
* @param {Object} params 调用参数
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mm_mqtt_server",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "MQTT
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "最薄的 MQTT 代理服务器入站适配器:消息直达 mm_event 事件管理器,不做路由与插件。",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"超级美眉",
|
|
7
7
|
"入站适配器",
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
"main": "index.js",
|
|
23
23
|
"files": [
|
|
24
24
|
"index.js",
|
|
25
|
-
"lib"
|
|
25
|
+
"lib",
|
|
26
|
+
"example"
|
|
26
27
|
],
|
|
27
28
|
"engines": {
|
|
28
29
|
"node": ">=22.5.0"
|
|
@@ -38,13 +39,15 @@
|
|
|
38
39
|
"scripts": {
|
|
39
40
|
"start": "node example/demo.js",
|
|
40
41
|
"demo": "node example/demo.js",
|
|
41
|
-
"test": "
|
|
42
|
+
"test": "node --test \"test/*.test.js\""
|
|
42
43
|
},
|
|
43
44
|
"dependencies": {
|
|
44
45
|
"aedes": "^1.2.0",
|
|
45
|
-
"mm_corejs": "^1.1.4"
|
|
46
|
+
"mm_corejs": "^1.1.4",
|
|
47
|
+
"mm_event": "^2.2.2",
|
|
48
|
+
"mm_mod": "^1.0.1"
|
|
46
49
|
},
|
|
47
50
|
"devDependencies": {
|
|
48
|
-
"
|
|
51
|
+
"mqtt-packet": "^9.0.2"
|
|
49
52
|
}
|
|
50
53
|
}
|
package/lib/message.js
DELETED
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const { JsonRpc } = require('mm_corejs');
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* 默认应答主题后缀
|
|
7
|
-
*/
|
|
8
|
-
const DEFAULT_REPLY_SUFFIX = '/response';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* 默认主动推送主题前缀(完整主题为 前缀/客户端ID)
|
|
12
|
-
*/
|
|
13
|
-
const DEFAULT_PUSH_PREFIX = 'rpc/push';
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* 默认反向调用主题前缀(完整主题为 前缀/客户端ID,客户端订阅后接收调用请求)
|
|
17
|
-
*/
|
|
18
|
-
const DEFAULT_CALL_PREFIX = 'rpc/call';
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* MessagePlugin —— JSON-RPC 2.0 消息插件(mm_mqtt_server 的可选业务组件)。
|
|
22
|
-
*
|
|
23
|
-
* 职责:
|
|
24
|
-
* 1. 方法表:内部持有 JsonRpc 核心,addMethod / delMethod / listMethods 登记可调用方法;
|
|
25
|
-
* 2. 管道挂载:bind(mqtt) 后向消息管道注册 main 钩子 "json_rpc"(sort 100,
|
|
26
|
-
* 先于内置 emit_message 执行),命中 JSON-RPC 请求时产出重发描述
|
|
27
|
-
* { topic: 请求主题 + reply_suffix, payload: 响应 JSON },由代理发布应答;
|
|
28
|
-
* 3. 通知回推:通知(无 id)处理函数返回非 undefined 结果时,产出服务器发起的
|
|
29
|
-
* 通知对象 { jsonrpc, method, params: result } 并发布到应答主题;无结果或出错时
|
|
30
|
-
* 不产出;非 JSON-RPC 消息(解析失败、缺 jsonrpc 字段)透传,控制权交还管道;
|
|
31
|
-
* 4. 主动推送:push(client_id, data, options) 发布到 options.topic
|
|
32
|
-
* (缺省为 push_prefix/client_id),处理函数或客户端可在任意时刻
|
|
33
|
-
* (如异步任务完成后)向订阅该主题的客户端回推结果;
|
|
34
|
-
* 5. 反向调用:call(client_id, method, params, options) 发布 JSON-RPC 请求到
|
|
35
|
-
* options.topic(缺省为 call_prefix/client_id)并等待反馈;客户端把响应
|
|
36
|
-
* (含 result/error、无 method)发布回 RPC 服务主题,入站时被优先识别并结算;
|
|
37
|
-
* 6. 无回环:代理重发的应答 client 为空,不进入消息管道。
|
|
38
|
-
*
|
|
39
|
-
* 用法:
|
|
40
|
-
* const mqtt = new MqttServer({ name: 'mqtt_server', port: 1883 }, server);
|
|
41
|
-
* const rpc = new MessagePlugin({ reply_suffix: '/response', push_prefix: 'rpc/push' });
|
|
42
|
-
* mqtt.use(rpc);
|
|
43
|
-
* rpc.addMethod('echo', (params) => params);
|
|
44
|
-
* await mqtt.init();
|
|
45
|
-
* await mqtt.start();
|
|
46
|
-
*/
|
|
47
|
-
class MessagePlugin {
|
|
48
|
-
/**
|
|
49
|
-
* JSON-RPC 协议核心
|
|
50
|
-
*/
|
|
51
|
-
#rpc = new JsonRpc();
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* 已挂载的 MqttServer 实例,未挂载时为 null
|
|
55
|
-
*/
|
|
56
|
-
#server = null;
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* 应答策略配置
|
|
60
|
-
*/
|
|
61
|
-
#config = {
|
|
62
|
-
reply_suffix: DEFAULT_REPLY_SUFFIX,
|
|
63
|
-
push_prefix: DEFAULT_PUSH_PREFIX,
|
|
64
|
-
call_prefix: DEFAULT_CALL_PREFIX,
|
|
65
|
-
qos: 0,
|
|
66
|
-
retain: false
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* 构造函数
|
|
71
|
-
* @param {Object} [config] 应答策略配置,含 reply_suffix / push_prefix / call_prefix / qos / retain
|
|
72
|
-
*/
|
|
73
|
-
constructor(config = {}) {
|
|
74
|
-
this.#config = { ...this.#config, ...config };
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* 挂载到 MqttServer 的消息管道,注册内置 main 钩子
|
|
79
|
-
* @param {Object} server 具备 getHandle() 的 MqttServer 实例
|
|
80
|
-
* @returns {MessagePlugin} 当前实例
|
|
81
|
-
* @throws {TypeError} server 不具备 getHandle 方法时
|
|
82
|
-
*/
|
|
83
|
-
bind(server) {
|
|
84
|
-
if (!server || typeof server.getHandle !== 'function') {
|
|
85
|
-
throw new TypeError('[MessagePlugin] server must provide getHandle()');
|
|
86
|
-
}
|
|
87
|
-
this.#server = server;
|
|
88
|
-
server.getHandle().on('main', (params, ctx, options) => {
|
|
89
|
-
return this._execute(params, ctx, options);
|
|
90
|
-
}, 100, 'json_rpc');
|
|
91
|
-
return this;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* 判断是否已挂载
|
|
96
|
-
* @returns {boolean} 是否已挂载到 MqttServer
|
|
97
|
-
*/
|
|
98
|
-
isBound() {
|
|
99
|
-
return this.#server !== null;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* 登记 RPC 方法
|
|
104
|
-
* @param {string} name 方法名
|
|
105
|
-
* @param {Function} handler 处理函数,签名 handler(params, extra, options),extra 含 client_id 与 topic,options 为管道请求选项
|
|
106
|
-
* @returns {MessagePlugin} 当前实例
|
|
107
|
-
*/
|
|
108
|
-
addMethod(name, handler) {
|
|
109
|
-
this.#rpc.addMethod(name, handler);
|
|
110
|
-
return this;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* 删除 RPC 方法
|
|
115
|
-
* @param {string} name 方法名
|
|
116
|
-
* @returns {boolean} 是否删除成功
|
|
117
|
-
*/
|
|
118
|
-
delMethod(name) {
|
|
119
|
-
return this.#rpc.delMethod(name);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* 列出 RPC 方法(支持 Store 条件查询与分页)
|
|
124
|
-
* @param {Object} [query] 查询条件,缺省时列出全部
|
|
125
|
-
* @returns {Array<Object>} 方法列表,含 name
|
|
126
|
-
*/
|
|
127
|
-
listMethods(query) {
|
|
128
|
-
return this.#rpc.listMethods(query);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* 主动推送:发布数据到指定主题(缺省为 push_prefix/client_id,客户端订阅后接收)
|
|
133
|
-
* @param {string} client_id 目标客户端 ID,用于组装缺省主题
|
|
134
|
-
* @param {string|Object} data 待推送数据,对象时 JSON 序列化
|
|
135
|
-
* @param {Object} [options] 推送选项,含 topic(覆盖缺省主题)
|
|
136
|
-
* @returns {Promise<boolean>} 是否发布成功
|
|
137
|
-
* @throws {Error} 插件尚未挂载到服务器时
|
|
138
|
-
*/
|
|
139
|
-
push(client_id, data, options = {}) {
|
|
140
|
-
if (!this.#server) throw new Error('[MessagePlugin] not bound to a server');
|
|
141
|
-
const config = this.#config;
|
|
142
|
-
const topic = options.topic || `${config.push_prefix}/${client_id}`;
|
|
143
|
-
const payload = typeof data === 'string' ? data : JSON.stringify(data);
|
|
144
|
-
return this.#server.publish(topic, payload, { qos: config.qos, retain: config.retain });
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* 实时调用客户端方法:发布 JSON-RPC 请求并等待客户端反馈 result
|
|
149
|
-
* @param {string} client_id 目标客户端 ID,用于组装缺省调用主题
|
|
150
|
-
* @param {string} method 客户端方法名
|
|
151
|
-
* @param {*} [params] 调用参数
|
|
152
|
-
* @param {Object} [options] 选项,含 topic(覆盖缺省调用主题)与 timeout_ms(缺省 30000)
|
|
153
|
-
* @returns {Promise<*>} 客户端反馈的 result;错误响应、送达失败或超时时 reject
|
|
154
|
-
* @throws {Error} 插件尚未挂载到服务器时
|
|
155
|
-
*/
|
|
156
|
-
call(client_id, method, params, options = {}) {
|
|
157
|
-
if (!this.#server) throw new Error('[MessagePlugin] not bound to a server');
|
|
158
|
-
const config = this.#config;
|
|
159
|
-
const topic = options.topic || `${config.call_prefix}/${client_id}`;
|
|
160
|
-
/** 发送函数:把调用请求发布到调用主题 */
|
|
161
|
-
const send = (request) => this.#server.publish(topic, JSON.stringify(request), { qos: config.qos, retain: false });
|
|
162
|
-
return this.#rpc.call(send, method, params, options);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* 内置 main 钩子逻辑:优先识别客户端反馈,其次执行 RPC 请求并产出应答重发描述
|
|
167
|
-
* @param {Object} params 消息参数,含 client_id / topic / payload
|
|
168
|
-
* @param {Object} ctx 执行上下文
|
|
169
|
-
* @param {Object} [options] 管道请求选项(连接级,如 user_id / device_id / agent_id),透传给处理函数
|
|
170
|
-
* @returns {Promise<Object|undefined>} 重发描述 { topic, payload, qos, retain };反馈、透传与无结果通知时为 undefined
|
|
171
|
-
*/
|
|
172
|
-
async _execute(params, ctx, options) {
|
|
173
|
-
if (this.#rpc.acceptResponse(params.payload)) return undefined;
|
|
174
|
-
const extra = { client_id: params.client_id, topic: params.topic };
|
|
175
|
-
const response = await this.#rpc.handle(params.payload, extra, options);
|
|
176
|
-
if (response === undefined) return undefined;
|
|
177
|
-
return this._buildReply(params.topic, response);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* 组装应答重发描述
|
|
182
|
-
* @param {string} topic 请求主题
|
|
183
|
-
* @param {Object} response 响应对象
|
|
184
|
-
* @returns {Object} 重发描述,payload 为响应 JSON 字符串
|
|
185
|
-
*/
|
|
186
|
-
_buildReply(topic, response) {
|
|
187
|
-
const config = this.#config;
|
|
188
|
-
return {
|
|
189
|
-
topic: `${topic}${config.reply_suffix}`,
|
|
190
|
-
payload: JSON.stringify(response),
|
|
191
|
-
qos: config.qos,
|
|
192
|
-
retain: config.retain
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
module.exports = MessagePlugin;
|