@lyrify/znl 0.4.1 → 0.5.2
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 +38 -8
- package/package.json +14 -2
- package/src/SendQueue.js +51 -17
- package/src/ZNL.js +657 -83
- package/src/constants.js +33 -0
- package/src/protocol.js +74 -12
- package/src/security.js +578 -0
package/README.md
CHANGED
|
@@ -5,11 +5,17 @@
|
|
|
5
5
|
## 特性
|
|
6
6
|
|
|
7
7
|
- 基于 `ROUTER/DEALER` 同时实现 RPC 请求-响应与 PUB/SUB 广播,一套连接两种模式
|
|
8
|
-
-
|
|
8
|
+
- 自动处理并发消息匹配、自动处理心跳包、超时控制、最大并发限制
|
|
9
9
|
- 支持 Master → Slave 主动发起请求(双向 RPC)
|
|
10
10
|
- 基于 ROUTER 实现 PUB/SUB 广播,无需额外 socket 或端口
|
|
11
11
|
- Slave 自动注册/注销,Master 实时感知在线节点
|
|
12
|
-
-
|
|
12
|
+
- 支持可选加密认证(签名 + 防重放 + AES-256-GCM 透明加密)
|
|
13
|
+
- 加密开关 `encrypted`:
|
|
14
|
+
- `false`:明文模式(不签名/不加密)
|
|
15
|
+
- `true`:签名 + 防重放 + payload 透明加密(AES-256-GCM)
|
|
16
|
+
- 可选关闭 payload 摘要校验(`enablePayloadDigest=false`)以提升性能
|
|
17
|
+
- 建议 master/slave 两端保持一致配置,避免认证不一致
|
|
18
|
+
- `authKey` 仅在 `encrypted=true` 时必填
|
|
13
19
|
- Payload 支持 `string`、`Buffer`、`Uint8Array` 及其数组(多帧)
|
|
14
20
|
|
|
15
21
|
## 安装
|
|
@@ -36,6 +42,7 @@ const master = new ZNL({
|
|
|
36
42
|
id: "master-1",
|
|
37
43
|
endpoints: { router: "tcp://127.0.0.1:6003" },
|
|
38
44
|
authKey: "your-shared-key",
|
|
45
|
+
encrypted: true, // 推荐:透明加密 + 防重放
|
|
39
46
|
});
|
|
40
47
|
|
|
41
48
|
// RPC:自动回复 slave 的请求
|
|
@@ -65,6 +72,7 @@ const slave = new ZNL({
|
|
|
65
72
|
id: "slave-001",
|
|
66
73
|
endpoints: { router: "tcp://127.0.0.1:6003" },
|
|
67
74
|
authKey: "your-shared-key",
|
|
75
|
+
encrypted: true, // 需与 master 一致
|
|
68
76
|
});
|
|
69
77
|
|
|
70
78
|
// PUB/SUB:精确订阅(可在 start 前调用)
|
|
@@ -93,8 +101,14 @@ new ZNL({
|
|
|
93
101
|
endpoints: {
|
|
94
102
|
router: "tcp://127.0.0.1:6003",
|
|
95
103
|
},
|
|
96
|
-
maxPending:
|
|
104
|
+
maxPending: 1000,
|
|
97
105
|
authKey: "",
|
|
106
|
+
heartbeatInterval: 3000,
|
|
107
|
+
heartbeatTimeoutMs: 0,
|
|
108
|
+
encrypted: false,
|
|
109
|
+
enablePayloadDigest: true,
|
|
110
|
+
maxTimeSkewMs: 30000,
|
|
111
|
+
replayWindowMs: 120000,
|
|
98
112
|
});
|
|
99
113
|
```
|
|
100
114
|
|
|
@@ -103,8 +117,14 @@ new ZNL({
|
|
|
103
117
|
| `role` | ✓ | 节点角色,`"master"` 或 `"slave"` |
|
|
104
118
|
| `id` | ✓ | 节点唯一标识;slave 端同时作为 ZMQ `routingId` |
|
|
105
119
|
| `endpoints.router` | | ROUTER 端点,默认 `tcp://127.0.0.1:6003` |
|
|
106
|
-
| `maxPending` | | 最大并发 RPC
|
|
107
|
-
| `authKey` | |
|
|
120
|
+
| `maxPending` | | 最大并发 RPC 请求数,默认 `1000`;`0` 表示不限制 |
|
|
121
|
+
| `authKey` | | 共享认证 Key;仅在 `encrypted=true` 时必填(用于签名/加密) |
|
|
122
|
+
| `heartbeatInterval` | | 心跳间隔(毫秒),默认 `3000`,`0` 表示禁用心跳 |
|
|
123
|
+
| `heartbeatTimeoutMs` | | 心跳超时时间(毫秒),默认 `0` 表示使用 `heartbeatInterval × 3` |
|
|
124
|
+
| `encrypted` | | 是否启用加密:`false`(默认,明文) / `true`(签名+防重放+透明加密) |
|
|
125
|
+
| `enablePayloadDigest` | | 是否启用 payload 摘要校验,默认 `true`(关闭可提升性能) |
|
|
126
|
+
| `maxTimeSkewMs` | | 时间戳最大允许偏移(毫秒),默认 `30000`,用于防重放校验 |
|
|
127
|
+
| `replayWindowMs` | | nonce 重放缓存窗口(毫秒),默认 `120000` |
|
|
108
128
|
|
|
109
129
|
## API
|
|
110
130
|
|
|
@@ -199,7 +219,7 @@ console.log(master.slaves); // ["slave-001", "slave-002"]
|
|
|
199
219
|
| `publish` | Slave | 收到 master 广播,携带 `{ topic, payload }` |
|
|
200
220
|
| `slave_connected` | Master | slave 注册成功上线,携带 `slaveId` |
|
|
201
221
|
| `slave_disconnected` | Master | slave 注销或发送失败下线,携带 `slaveId` |
|
|
202
|
-
| `auth_failed` | Master |
|
|
222
|
+
| `auth_failed` | Master / Slave | 认证失败(签名校验失败、重放检测失败、解密失败等),请求已被丢弃 |
|
|
203
223
|
| `error` | 两者 | 内部错误 |
|
|
204
224
|
|
|
205
225
|
## 本地示例
|
|
@@ -224,13 +244,23 @@ pnpm test
|
|
|
224
244
|
## 并发压测
|
|
225
245
|
|
|
226
246
|
```bash
|
|
227
|
-
# 终端 1:启动 Echo
|
|
247
|
+
# 终端 1:启动 Echo 服务端(plain)
|
|
228
248
|
pnpm test:echo
|
|
229
249
|
|
|
230
|
-
# 终端 2
|
|
250
|
+
# 终端 2:发起并发压测(plain)
|
|
231
251
|
pnpm test:100 -- 100 10000 slave-001
|
|
232
252
|
```
|
|
233
253
|
|
|
254
|
+
启用安全模式示例:
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
# 终端 1:加密模式启动 Echo 服务端
|
|
258
|
+
ZNL_AUTH_KEY=my-secret ZNL_ENCRYPTED=true pnpm test:echo
|
|
259
|
+
|
|
260
|
+
# 终端 2:加密模式压测
|
|
261
|
+
pnpm test:100 -- 100 10000 slave-001 my-secret true
|
|
262
|
+
```
|
|
263
|
+
|
|
234
264
|
参数说明:
|
|
235
265
|
|
|
236
266
|
- 总请求数
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lyrify/znl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "ZNL - ZeroMQ Node Link",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.js",
|
|
@@ -35,12 +35,24 @@
|
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"zeromq": "^6.5.0"
|
|
37
37
|
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"eslint": "^9.39.4",
|
|
40
|
+
"eslint-plugin-import": "^2.32.0",
|
|
41
|
+
"prettier": "^3.8.1"
|
|
42
|
+
},
|
|
38
43
|
"scripts": {
|
|
39
44
|
"example:master": "node test/master/index.js",
|
|
40
45
|
"example:slave": "node test/slave/index.js",
|
|
41
46
|
"test": "node test/run.js",
|
|
47
|
+
"test:unit": "node --test \"test/unit/**/*.test.js\"",
|
|
48
|
+
"test:all": "pnpm test && pnpm test:unit",
|
|
42
49
|
"test:echo": "node test/master/test-echo-server.js",
|
|
43
50
|
"test:100": "node test/slave/test-100-concurrent.js",
|
|
44
|
-
"
|
|
51
|
+
"test:capture": "node test/capture-frames.js",
|
|
52
|
+
"check": "node --check index.js && node --check src/constants.js && node --check src/protocol.js && node --check src/security.js && node --check src/PendingManager.js && node --check src/SendQueue.js && node --check src/ZNL.js && node --check test/master/index.js && node --check test/slave/index.js && node --check test/master/test-echo-server.js && node --check test/slave/test-100-concurrent.js && node --check test/run.js",
|
|
53
|
+
"lint": "eslint .",
|
|
54
|
+
"lint:fix": "eslint . --fix",
|
|
55
|
+
"format": "prettier . --check",
|
|
56
|
+
"format:write": "prettier . --write"
|
|
45
57
|
}
|
|
46
58
|
}
|
package/src/SendQueue.js
CHANGED
|
@@ -6,17 +6,15 @@
|
|
|
6
6
|
* 虽然 zeromq.js v6 在 JS 层做了一定保护,但显式串行化更安全可靠。
|
|
7
7
|
*
|
|
8
8
|
* 实现原理:
|
|
9
|
-
* 为每个 socket
|
|
10
|
-
*
|
|
11
|
-
* 无论前一个任务成功或失败,队列都会继续执行下一个任务。
|
|
9
|
+
* 为每个 socket 通道维护一个任务队列与独立消费循环。
|
|
10
|
+
* 通过单线程 async loop 串行执行,避免长 Promise 链增长。
|
|
12
11
|
*/
|
|
13
|
-
|
|
14
12
|
export class SendQueue {
|
|
15
13
|
/**
|
|
16
|
-
*
|
|
17
|
-
* @type {Map<string,
|
|
14
|
+
* 通道级队列与运行状态
|
|
15
|
+
* @type {Map<string, { queue: Array<{ task: Function, resolve: Function, reject: Function }>, running: boolean }>}
|
|
18
16
|
*/
|
|
19
|
-
#
|
|
17
|
+
#channels = new Map();
|
|
20
18
|
|
|
21
19
|
/**
|
|
22
20
|
* 将发送任务追加到指定通道的队列尾部
|
|
@@ -26,22 +24,58 @@ export class SendQueue {
|
|
|
26
24
|
* @returns {Promise<void>} 本次任务的 Promise(可用于错误捕获)
|
|
27
25
|
*/
|
|
28
26
|
enqueue(channel, task) {
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
// 无论前一个任务成功或失败,都继续执行当前任务
|
|
32
|
-
const run = tail.then(task, task);
|
|
27
|
+
const name = String(channel);
|
|
28
|
+
const entry = this.#ensureChannel(name);
|
|
33
29
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
entry.queue.push({ task, resolve, reject });
|
|
32
|
+
if (!entry.running) this.#drain(name, entry);
|
|
33
|
+
});
|
|
38
34
|
}
|
|
39
35
|
|
|
40
36
|
/**
|
|
41
37
|
* 清空所有通道的队列引用(节点停止时调用)
|
|
42
|
-
*
|
|
38
|
+
* 注意:已入队但未执行的任务不会被取消,仅释放引用
|
|
43
39
|
*/
|
|
44
40
|
clear() {
|
|
45
|
-
this.#
|
|
41
|
+
this.#channels.clear();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 确保通道数据结构存在
|
|
46
|
+
* @param {string} name
|
|
47
|
+
* @returns {{ queue: Array, running: boolean }}
|
|
48
|
+
*/
|
|
49
|
+
#ensureChannel(name) {
|
|
50
|
+
let entry = this.#channels.get(name);
|
|
51
|
+
if (!entry) {
|
|
52
|
+
entry = { queue: [], running: false };
|
|
53
|
+
this.#channels.set(name, entry);
|
|
54
|
+
}
|
|
55
|
+
return entry;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 串行消费队列
|
|
60
|
+
* @param {string} name
|
|
61
|
+
* @param {{ queue: Array, running: boolean }} entry
|
|
62
|
+
*/
|
|
63
|
+
async #drain(name, entry) {
|
|
64
|
+
entry.running = true;
|
|
65
|
+
|
|
66
|
+
while (entry.queue.length > 0) {
|
|
67
|
+
const { task, resolve, reject } = entry.queue.shift();
|
|
68
|
+
try {
|
|
69
|
+
await task();
|
|
70
|
+
resolve();
|
|
71
|
+
} catch (error) {
|
|
72
|
+
reject(error);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
entry.running = false;
|
|
77
|
+
|
|
78
|
+
// 清理空通道,避免 Map 无限增长
|
|
79
|
+
if (entry.queue.length === 0) this.#channels.delete(name);
|
|
46
80
|
}
|
|
47
81
|
}
|