mm_mqtt_server 1.0.1 → 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 +17 -2
- package/lib/mqtt_server.js +151 -205
- package/package.json +14 -8
- 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,9 +1,24 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const MqttServer = require('./lib/mqtt_server');
|
|
4
|
-
const
|
|
4
|
+
const Admin = require('mm_corejs').Admin;
|
|
5
|
+
|
|
6
|
+
const admin = new Admin();
|
|
7
|
+
admin.setClass(MqttServer);
|
|
5
8
|
|
|
6
9
|
module.exports = {
|
|
7
10
|
MqttServer,
|
|
8
|
-
|
|
11
|
+
admin,
|
|
12
|
+
/**
|
|
13
|
+
* 获取指定作用域的 MQTT 服务器
|
|
14
|
+
* @param {string} [scope='os'] 作用域名称
|
|
15
|
+
* @param {object} [config] 配置参数
|
|
16
|
+
* @returns {MqttServer} MQTT 服务器实例
|
|
17
|
+
*/
|
|
18
|
+
mqttServerAdmin(scope, config) {
|
|
19
|
+
if (!scope) {
|
|
20
|
+
scope = 'os';
|
|
21
|
+
}
|
|
22
|
+
return admin.get(scope, config);
|
|
23
|
+
}
|
|
9
24
|
};
|
package/lib/mqtt_server.js
CHANGED
|
@@ -2,58 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
const net = require('net');
|
|
4
4
|
const { Aedes } = require('aedes');
|
|
5
|
-
const { Mod
|
|
5
|
+
const { Mod } = require('mm_mod');
|
|
6
|
+
const { eventAdmin } = require('mm_event');
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
|
-
*
|
|
9
|
-
*/
|
|
10
|
-
const SERVER_SCHEMA = {
|
|
11
|
-
type: 'object',
|
|
12
|
-
properties: {
|
|
13
|
-
host: { type: 'string', min_length: 1 },
|
|
14
|
-
port: { type: 'integer', minimum: 1, maximum: 65535 },
|
|
15
|
-
allow_anonymous: { type: 'boolean' },
|
|
16
|
-
username: { type: 'string' },
|
|
17
|
-
password: { type: 'string' }
|
|
18
|
-
}
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* MqttServer —— MQTT 代理服务器(入站端口模块)。
|
|
9
|
+
* MqttServer —— 最薄的 MQTT 代理传输层(入站适配器模块)。
|
|
23
10
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* 1. 监听:onStart 创建 aedes broker 与 net.Server,按 host / port 监听 MQTT 连接;
|
|
31
|
-
* 2. 连接管理:#clients 使用 Store 登记在线客户端(主键 id,支持条件查询与分页);
|
|
32
|
-
* 3. 消息管道:入站发布消息经 Handle 的 before/check/main/render 阶段处理,
|
|
33
|
-
* 内置 main 钩子发 message 事件保持兼容;管道产出 { topic, payload } 时
|
|
34
|
-
* 由代理重新发布该消息;getHandle() 可注册自定义钩子(如主题黑名单);
|
|
35
|
-
* 4. 事件分发:connection / message / subscribe / unsubscribe / close / error;
|
|
36
|
-
* 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 自行处理。
|
|
37
17
|
*
|
|
38
18
|
* 用法:
|
|
39
|
-
* const mqtt = new MqttServer({ name: '
|
|
40
|
-
* mqtt.
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
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
|
+
* });
|
|
44
24
|
* await mqtt.init();
|
|
45
25
|
* await mqtt.start();
|
|
46
|
-
* await mqtt.run({}, { action: 'publish', topic: 'mm/test', payload: 'hello' });
|
|
47
26
|
*/
|
|
48
27
|
class MqttServer extends Mod {
|
|
49
28
|
/**
|
|
50
29
|
* 模块默认配置
|
|
51
30
|
*/
|
|
52
31
|
static config = {
|
|
53
|
-
name: '
|
|
32
|
+
name: 'os',
|
|
54
33
|
title: 'MQTT代理服务器模块',
|
|
55
|
-
description: '
|
|
56
|
-
version: '
|
|
34
|
+
description: 'MQTT 传输层:消息直达事件管理器,不做路由与插件',
|
|
35
|
+
version: '2.0.0',
|
|
57
36
|
host: '0.0.0.0',
|
|
58
37
|
port: 1883,
|
|
59
38
|
allow_anonymous: true,
|
|
@@ -64,13 +43,12 @@ class MqttServer extends Mod {
|
|
|
64
43
|
type: 'object',
|
|
65
44
|
required: ['action'],
|
|
66
45
|
properties: {
|
|
67
|
-
action: { type: 'string', enum: ['publish', '
|
|
46
|
+
action: { type: 'string', enum: ['publish', 'close', 'list'] },
|
|
68
47
|
client_id: { type: 'string', min_length: 1 },
|
|
69
48
|
topic: { type: 'string', min_length: 1 },
|
|
70
49
|
payload: { type: 'string' },
|
|
71
50
|
qos: { type: 'integer', minimum: 0, maximum: 2 },
|
|
72
|
-
retain: { type: 'boolean' }
|
|
73
|
-
query: { type: 'object' }
|
|
51
|
+
retain: { type: 'boolean' }
|
|
74
52
|
}
|
|
75
53
|
}
|
|
76
54
|
};
|
|
@@ -86,26 +64,29 @@ class MqttServer extends Mod {
|
|
|
86
64
|
#server = null;
|
|
87
65
|
|
|
88
66
|
/**
|
|
89
|
-
*
|
|
67
|
+
* 事件管理器(取自 mm_event 的 mqtt 作用域,与其他实例共享)
|
|
90
68
|
*/
|
|
91
|
-
#
|
|
69
|
+
#events = null;
|
|
92
70
|
|
|
93
71
|
/**
|
|
94
|
-
*
|
|
72
|
+
* 在线连接表:client_id -> 连接信息
|
|
95
73
|
*/
|
|
96
|
-
#
|
|
74
|
+
#clients = new Map();
|
|
97
75
|
|
|
98
|
-
// ---------- 生命周期钩子 ----------
|
|
99
76
|
/**
|
|
100
|
-
*
|
|
77
|
+
* 构造函数
|
|
78
|
+
* @param {Object} config 模块配置
|
|
79
|
+
* @param {Object} server 框架实例
|
|
101
80
|
*/
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
this
|
|
81
|
+
constructor(config, server) {
|
|
82
|
+
super(config, server);
|
|
83
|
+
this.#events = eventAdmin('mqtt');
|
|
105
84
|
}
|
|
106
85
|
|
|
86
|
+
// ---------- 生命周期钩子 ----------
|
|
107
87
|
/**
|
|
108
88
|
* 启动钩子:创建 broker 与 TCP 服务器并开始监听
|
|
89
|
+
* @returns {Promise<void>} 无返回值
|
|
109
90
|
*/
|
|
110
91
|
async onStart() {
|
|
111
92
|
const broker = await this._createBroker();
|
|
@@ -118,7 +99,8 @@ class MqttServer extends Mod {
|
|
|
118
99
|
}
|
|
119
100
|
|
|
120
101
|
/**
|
|
121
|
-
*
|
|
102
|
+
* 停止钩子:断开全部连接、关闭服务器与 broker(事件管理器与其他实例共享,不清理)
|
|
103
|
+
* @returns {Promise<void>} 无返回值
|
|
122
104
|
*/
|
|
123
105
|
async onStop() {
|
|
124
106
|
this._closeAllClients();
|
|
@@ -126,26 +108,19 @@ class MqttServer extends Mod {
|
|
|
126
108
|
await this._closeBroker();
|
|
127
109
|
}
|
|
128
110
|
|
|
129
|
-
/**
|
|
130
|
-
* 释放钩子:清空连接注册表
|
|
131
|
-
*/
|
|
132
|
-
async onDispose() {
|
|
133
|
-
this.#clients.clear();
|
|
134
|
-
}
|
|
135
|
-
|
|
136
111
|
// ---------- 执行入口 ----------
|
|
137
112
|
/**
|
|
138
113
|
* 模块主逻辑:按 action 派发操作
|
|
139
|
-
* @param {Object} ctx 执行上下文
|
|
140
114
|
* @param {Object} params 调用参数,含 action 及操作所需字段
|
|
115
|
+
* @param {Object} ctx 执行上下文
|
|
141
116
|
* @param {Object} options 合并后的选项
|
|
142
117
|
* @returns {Promise<Object>} 操作结果
|
|
143
118
|
*/
|
|
144
|
-
async main(
|
|
119
|
+
async main(params, ctx, options) {
|
|
145
120
|
const action = params.action;
|
|
146
121
|
if (action === 'publish') return this._actionPublish(params);
|
|
147
|
-
if (action === '
|
|
148
|
-
return this.
|
|
122
|
+
if (action === 'close') return this._actionClose(params);
|
|
123
|
+
return this._actionList();
|
|
149
124
|
}
|
|
150
125
|
|
|
151
126
|
/**
|
|
@@ -164,19 +139,50 @@ class MqttServer extends Mod {
|
|
|
164
139
|
* @param {Object} params 调用参数,含 client_id
|
|
165
140
|
* @returns {Object} 操作结果
|
|
166
141
|
*/
|
|
167
|
-
|
|
142
|
+
_actionClose(params) {
|
|
168
143
|
this._requireClientId(params);
|
|
169
144
|
const success = this.closeClient(params.client_id);
|
|
170
|
-
return { action: '
|
|
145
|
+
return { action: 'close', client_id: params.client_id, success };
|
|
171
146
|
}
|
|
172
147
|
|
|
173
148
|
/**
|
|
174
|
-
*
|
|
175
|
-
* @param {Object} params 调用参数,可含 query 查询条件(Store 条件/分页)
|
|
149
|
+
* 执行列出连接操作
|
|
176
150
|
* @returns {Object} 操作结果,clients 为在线客户端列表
|
|
177
151
|
*/
|
|
178
|
-
|
|
179
|
-
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;
|
|
180
186
|
}
|
|
181
187
|
|
|
182
188
|
// ---------- 公开操作 ----------
|
|
@@ -184,7 +190,7 @@ class MqttServer extends Mod {
|
|
|
184
190
|
* 代理端发布消息到指定主题
|
|
185
191
|
* @param {string} topic 主题
|
|
186
192
|
* @param {string|Buffer} payload 消息内容
|
|
187
|
-
* @param {Object} opts 发布选项,含 qos 与 retain
|
|
193
|
+
* @param {Object} [opts] 发布选项,含 qos 与 retain
|
|
188
194
|
* @returns {Promise<boolean>} 是否发布成功
|
|
189
195
|
*/
|
|
190
196
|
publish(topic, payload, opts = {}) {
|
|
@@ -203,48 +209,27 @@ class MqttServer extends Mod {
|
|
|
203
209
|
}
|
|
204
210
|
|
|
205
211
|
/**
|
|
206
|
-
*
|
|
212
|
+
* 关闭指定客户端连接(连接表由 broker 的断开回调清理)
|
|
207
213
|
* @param {string} client_id 客户端 ID
|
|
208
214
|
* @returns {boolean} 是否发起关闭
|
|
209
215
|
*/
|
|
210
216
|
closeClient(client_id) {
|
|
211
|
-
const
|
|
212
|
-
if (!
|
|
213
|
-
|
|
217
|
+
const client = this.#clients.get(client_id);
|
|
218
|
+
if (!client) return false;
|
|
219
|
+
client.client.close();
|
|
214
220
|
return true;
|
|
215
221
|
}
|
|
216
222
|
|
|
217
223
|
/**
|
|
218
|
-
*
|
|
219
|
-
* @param {Object} [query] 查询条件,缺省时列出全部
|
|
224
|
+
* 列出在线客户端
|
|
220
225
|
* @returns {Array<Object>} 客户端信息列表,含 id 与 address
|
|
221
226
|
*/
|
|
222
|
-
listClients(
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
* @param {string} client_id 客户端 ID
|
|
229
|
-
* @param {Object|null} options 选项对象,null 时清空
|
|
230
|
-
* @returns {boolean} 是否设置成功(客户端不在线时失败)
|
|
231
|
-
*/
|
|
232
|
-
setClientOptions(client_id, options) {
|
|
233
|
-
const entry = this.#clients.getById(client_id);
|
|
234
|
-
if (!entry) return false;
|
|
235
|
-
entry.options = options || null;
|
|
236
|
-
return true;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* 读取连接级选项,未绑定时返回空对象
|
|
241
|
-
* @param {string} client_id 客户端 ID
|
|
242
|
-
* @returns {Object} 连接级选项
|
|
243
|
-
*/
|
|
244
|
-
_clientOptions(client_id) {
|
|
245
|
-
const entry = this.#clients.getById(client_id);
|
|
246
|
-
if (!entry || !entry.options) return {};
|
|
247
|
-
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;
|
|
248
233
|
}
|
|
249
234
|
|
|
250
235
|
/**
|
|
@@ -253,40 +238,31 @@ class MqttServer extends Mod {
|
|
|
253
238
|
* @returns {boolean} 是否在线
|
|
254
239
|
*/
|
|
255
240
|
hasClient(client_id) {
|
|
256
|
-
return
|
|
241
|
+
return this.#clients.has(client_id);
|
|
257
242
|
}
|
|
258
243
|
|
|
259
244
|
/**
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
* @param {Object}
|
|
263
|
-
* @returns {
|
|
264
|
-
* @throws {TypeError} plugin 不具备 bind 方法时
|
|
265
|
-
*/
|
|
266
|
-
use(plugin) {
|
|
267
|
-
if (!plugin || typeof plugin.bind !== 'function') {
|
|
268
|
-
throw new TypeError(`${this._label()} plugin must provide bind()`);
|
|
269
|
-
}
|
|
270
|
-
plugin.bind(this);
|
|
271
|
-
return this;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* 获取消息处理管道,供外部注册 before/check/main 等钩子;
|
|
276
|
-
* 钩子产出 { topic, payload } 时由代理重新发布该消息
|
|
277
|
-
* @returns {Handle} 消息管道实例
|
|
245
|
+
* 绑定连接级选项,入站消息将作为 options 透传给事件
|
|
246
|
+
* @param {string} client_id 客户端 ID
|
|
247
|
+
* @param {Object|null} options 选项对象,null 时清空
|
|
248
|
+
* @returns {boolean} 是否设置成功(客户端不在线时失败)
|
|
278
249
|
*/
|
|
279
|
-
|
|
280
|
-
|
|
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;
|
|
281
255
|
}
|
|
282
256
|
|
|
283
257
|
/**
|
|
284
|
-
*
|
|
285
|
-
* @
|
|
258
|
+
* 获取连接级选项(复制一份,避免事件管理器回写)
|
|
259
|
+
* @param {string} client_id 客户端 ID
|
|
260
|
+
* @returns {Object} 选项对象
|
|
286
261
|
*/
|
|
287
|
-
|
|
288
|
-
const
|
|
289
|
-
|
|
262
|
+
_clientOptions(client_id) {
|
|
263
|
+
const client = this.#clients.get(client_id);
|
|
264
|
+
if (!client || !client.options) return {};
|
|
265
|
+
return { ...client.options };
|
|
290
266
|
}
|
|
291
267
|
|
|
292
268
|
// ---------- broker 装配 ----------
|
|
@@ -295,50 +271,44 @@ class MqttServer extends Mod {
|
|
|
295
271
|
* @returns {Promise<Object>} broker 实例
|
|
296
272
|
*/
|
|
297
273
|
async _createBroker() {
|
|
298
|
-
|
|
274
|
+
return Aedes.createBroker({
|
|
275
|
+
...this.config.options,
|
|
299
276
|
authenticate: (client, username, password, done) => {
|
|
300
277
|
this._authenticate(client, username, password, done);
|
|
301
278
|
}
|
|
302
279
|
});
|
|
303
|
-
return broker;
|
|
304
280
|
}
|
|
305
281
|
|
|
306
282
|
/**
|
|
307
|
-
*
|
|
283
|
+
* 绑定 broker 事件:维护连接表并把入站发布转交事件管理器
|
|
308
284
|
* @param {Object} broker aedes broker 实例
|
|
309
285
|
*/
|
|
310
286
|
_bindBrokerEvents(broker) {
|
|
311
|
-
broker.on('client', (client) =>
|
|
312
|
-
|
|
313
|
-
this.#clients.add({
|
|
314
|
-
id: client.id,
|
|
315
|
-
name: client.id,
|
|
316
|
-
address: this._remoteAddress(client),
|
|
317
|
-
client
|
|
318
|
-
});
|
|
319
|
-
this._safeEmit('connection', this._clientInfo(client));
|
|
320
|
-
});
|
|
321
|
-
broker.on('clientDisconnect', (client) => {
|
|
322
|
-
const existed = this.#clients.delById(client.id);
|
|
323
|
-
if (existed) this._safeEmit('close', client.id);
|
|
324
|
-
});
|
|
287
|
+
broker.on('client', (client) => this._registerClient(client));
|
|
288
|
+
broker.on('clientDisconnect', (client) => this.#clients.delete(client.id));
|
|
325
289
|
broker.on('publish', (packet, client) => {
|
|
326
290
|
if (!client) return;
|
|
327
|
-
this._handlePublish(client, packet)
|
|
328
|
-
});
|
|
329
|
-
broker.on('subscribe', (subscriptions, client) => {
|
|
330
|
-
const topics = subscriptions.map((item) => item.topic);
|
|
331
|
-
this._safeEmit('subscribe', client.id, topics);
|
|
332
|
-
});
|
|
333
|
-
broker.on('unsubscribe', (topics, client) => {
|
|
334
|
-
this._safeEmit('unsubscribe', client.id, topics);
|
|
291
|
+
this._handlePublish(client, packet);
|
|
335
292
|
});
|
|
336
293
|
broker.on('clientError', (client, err) => this._emitError(err));
|
|
337
294
|
broker.on('connectionError', (client, err) => this._emitError(err));
|
|
338
295
|
}
|
|
339
296
|
|
|
340
297
|
/**
|
|
341
|
-
*
|
|
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
|
+
* 处理入站发布消息:交给事件管理器,返回发布描述时由代理重发
|
|
342
312
|
* @param {Object} client aedes client 实例
|
|
343
313
|
* @param {Object} packet aedes 发布报文
|
|
344
314
|
* @returns {Promise<void>} 无返回值
|
|
@@ -351,17 +321,31 @@ class MqttServer extends Mod {
|
|
|
351
321
|
qos: packet.qos,
|
|
352
322
|
retain: packet.retain
|
|
353
323
|
};
|
|
354
|
-
const ctx = {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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);
|
|
359
343
|
}
|
|
360
344
|
}
|
|
361
345
|
|
|
362
346
|
/**
|
|
363
|
-
*
|
|
364
|
-
* @param {*} result
|
|
347
|
+
* 判断事件结果是否为待重发的发布描述(须含 topic 与 payload)
|
|
348
|
+
* @param {*} result 事件结果
|
|
365
349
|
* @returns {boolean} 是否为发布描述
|
|
366
350
|
*/
|
|
367
351
|
_isRepublish(result) {
|
|
@@ -398,8 +382,7 @@ class MqttServer extends Mod {
|
|
|
398
382
|
* @returns {boolean} 是否放行
|
|
399
383
|
*/
|
|
400
384
|
_checkAnonymous() {
|
|
401
|
-
|
|
402
|
-
return anonymous;
|
|
385
|
+
return !!(this.config.allow_anonymous && !this.config.username);
|
|
403
386
|
}
|
|
404
387
|
|
|
405
388
|
/**
|
|
@@ -437,24 +420,12 @@ class MqttServer extends Mod {
|
|
|
437
420
|
return {
|
|
438
421
|
cmd: 'publish',
|
|
439
422
|
topic,
|
|
440
|
-
payload: Buffer.from(String(payload)),
|
|
423
|
+
payload: Buffer.isBuffer(payload) ? payload : Buffer.from(String(payload)),
|
|
441
424
|
qos: opts.qos || 0,
|
|
442
425
|
retain: opts.retain || false
|
|
443
426
|
};
|
|
444
427
|
}
|
|
445
428
|
|
|
446
|
-
/**
|
|
447
|
-
* 提取客户端信息
|
|
448
|
-
* @param {Object} client aedes client 实例
|
|
449
|
-
* @returns {Object} 含 id 与 address 的信息
|
|
450
|
-
*/
|
|
451
|
-
_clientInfo(client) {
|
|
452
|
-
return {
|
|
453
|
-
id: client.id,
|
|
454
|
-
address: this._remoteAddress(client)
|
|
455
|
-
};
|
|
456
|
-
}
|
|
457
|
-
|
|
458
429
|
/**
|
|
459
430
|
* 获取远端地址字符串
|
|
460
431
|
* @param {Object} client aedes client 实例
|
|
@@ -485,11 +456,11 @@ class MqttServer extends Mod {
|
|
|
485
456
|
}
|
|
486
457
|
|
|
487
458
|
/**
|
|
488
|
-
*
|
|
459
|
+
* 断开全部客户端连接并清空连接表
|
|
489
460
|
*/
|
|
490
461
|
_closeAllClients() {
|
|
491
|
-
for (const
|
|
492
|
-
|
|
462
|
+
for (const client of this.#clients.values()) {
|
|
463
|
+
client.client.close();
|
|
493
464
|
}
|
|
494
465
|
this.#clients.clear();
|
|
495
466
|
}
|
|
@@ -503,7 +474,7 @@ class MqttServer extends Mod {
|
|
|
503
474
|
this.#server = null;
|
|
504
475
|
if (!server) return Promise.resolve();
|
|
505
476
|
return new Promise((resolve) => {
|
|
506
|
-
server.close(resolve);
|
|
477
|
+
server.close(() => resolve());
|
|
507
478
|
});
|
|
508
479
|
}
|
|
509
480
|
|
|
@@ -516,36 +487,11 @@ class MqttServer extends Mod {
|
|
|
516
487
|
this.#broker = null;
|
|
517
488
|
if (!broker) return Promise.resolve();
|
|
518
489
|
return new Promise((resolve) => {
|
|
519
|
-
broker.close(resolve);
|
|
490
|
+
broker.close(() => resolve());
|
|
520
491
|
});
|
|
521
492
|
}
|
|
522
493
|
|
|
523
|
-
// ---------- 消息管道 ----------
|
|
524
|
-
/**
|
|
525
|
-
* 装配内置管道钩子:main 发 message 事件保持向后兼容
|
|
526
|
-
*/
|
|
527
|
-
_initHandle() {
|
|
528
|
-
this.#handle.on('main', (ctx, params) => {
|
|
529
|
-
this._safeEmit('message', params.client_id, {
|
|
530
|
-
topic: params.topic,
|
|
531
|
-
payload: params.payload,
|
|
532
|
-
qos: params.qos,
|
|
533
|
-
retain: params.retain
|
|
534
|
-
});
|
|
535
|
-
}, 1000, 'emit_message');
|
|
536
|
-
}
|
|
537
|
-
|
|
538
494
|
// ---------- 校验辅助 ----------
|
|
539
|
-
/**
|
|
540
|
-
* 校验顶层 config 中的服务器设置
|
|
541
|
-
* @returns {Promise<void>} 无返回值
|
|
542
|
-
* @throws {Error} 校验不通过时
|
|
543
|
-
*/
|
|
544
|
-
async _validateConfig() {
|
|
545
|
-
const tip = await this._validate(this.config, SERVER_SCHEMA);
|
|
546
|
-
if (tip) throw new Error(`${this._label()} invalid config: ${tip}`);
|
|
547
|
-
}
|
|
548
|
-
|
|
549
495
|
/**
|
|
550
496
|
* 断言调用参数含 client_id
|
|
551
497
|
* @param {Object} params 调用参数
|
package/package.json
CHANGED
|
@@ -1,25 +1,29 @@
|
|
|
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
|
+
"入站适配器",
|
|
8
8
|
"mm_mod",
|
|
9
|
-
"
|
|
9
|
+
"mm_adapter",
|
|
10
|
+
"mm_in_adapter",
|
|
10
11
|
"mm_receiver",
|
|
11
12
|
"mqtt",
|
|
12
13
|
"broker",
|
|
13
14
|
"aedes",
|
|
14
15
|
"server"
|
|
15
16
|
],
|
|
17
|
+
"layer": "adapter",
|
|
18
|
+
"group": "in_adapter",
|
|
16
19
|
"license": "ISC",
|
|
17
20
|
"author": "qww",
|
|
18
21
|
"type": "commonjs",
|
|
19
22
|
"main": "index.js",
|
|
20
23
|
"files": [
|
|
21
24
|
"index.js",
|
|
22
|
-
"lib"
|
|
25
|
+
"lib",
|
|
26
|
+
"example"
|
|
23
27
|
],
|
|
24
28
|
"engines": {
|
|
25
29
|
"node": ">=22.5.0"
|
|
@@ -35,13 +39,15 @@
|
|
|
35
39
|
"scripts": {
|
|
36
40
|
"start": "node example/demo.js",
|
|
37
41
|
"demo": "node example/demo.js",
|
|
38
|
-
"test": "
|
|
42
|
+
"test": "node --test \"test/*.test.js\""
|
|
39
43
|
},
|
|
40
44
|
"dependencies": {
|
|
41
45
|
"aedes": "^1.2.0",
|
|
42
|
-
"mm_corejs": "^1.1.
|
|
46
|
+
"mm_corejs": "^1.1.4",
|
|
47
|
+
"mm_event": "^2.2.2",
|
|
48
|
+
"mm_mod": "^1.0.1"
|
|
43
49
|
},
|
|
44
50
|
"devDependencies": {
|
|
45
|
-
"
|
|
51
|
+
"mqtt-packet": "^9.0.2"
|
|
46
52
|
}
|
|
47
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', (ctx, params, options) => {
|
|
89
|
-
return this._execute(ctx, params, 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} ctx 执行上下文
|
|
168
|
-
* @param {Object} params 消息参数,含 client_id / topic / payload
|
|
169
|
-
* @param {Object} [options] 管道请求选项(连接级,如 user_id / device_id / agent_id),透传给处理函数
|
|
170
|
-
* @returns {Promise<Object|undefined>} 重发描述 { topic, payload, qos, retain };反馈、透传与无结果通知时为 undefined
|
|
171
|
-
*/
|
|
172
|
-
async _execute(ctx, params, 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;
|