@lyrify/znl 0.5.2 → 0.6.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 +405 -86
- package/package.json +1 -1
- package/src/ZNL.js +607 -76
- package/src/constants.js +3 -0
- package/src/protocol.js +47 -12
package/README.md
CHANGED
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
# ZNL
|
|
2
2
|
|
|
3
|
-
基于 ZeroMQ `ROUTER/DEALER` 模式的 Node.js
|
|
3
|
+
基于 ZeroMQ `ROUTER / DEALER` 模式的 Node.js 通信库,提供双向 RPC、广播、在线状态感知,以及可选的签名认证与透明加密能力。
|
|
4
4
|
|
|
5
5
|
## 特性
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
7
|
+
- 基于单连接实现双向 RPC 与广播
|
|
8
|
+
- 支持 `Master -> Slave` 与 `Slave -> Master` 双向主动请求
|
|
9
|
+
- `Slave` 自动注册 / 注销,`Master` 实时维护在线节点列表
|
|
10
|
+
- 支持请求超时控制与最大并发限制
|
|
11
|
+
- 心跳采用 `heartbeat -> heartbeat_ack` 应答机制
|
|
12
|
+
- `Slave` 提供主节点在线状态查询 API:`masterOnline` / `isMasterOnline()`
|
|
13
|
+
- 支持安全模式:
|
|
14
|
+
- HMAC 签名
|
|
15
|
+
- 时间戳校验
|
|
16
|
+
- nonce 防重放
|
|
17
|
+
- AES-256-GCM 透明加密
|
|
18
|
+
- 可选 payload 摘要校验
|
|
19
|
+
- 支持 `authKeyMap`,允许 `Master` 按 `slaveId` 使用不同密钥
|
|
19
20
|
- Payload 支持 `string`、`Buffer`、`Uint8Array` 及其数组(多帧)
|
|
20
21
|
|
|
21
22
|
## 安装
|
|
@@ -24,7 +25,7 @@
|
|
|
24
25
|
pnpm add @lyrify/znl
|
|
25
26
|
```
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
本地开发:
|
|
28
29
|
|
|
29
30
|
```bash
|
|
30
31
|
pnpm install
|
|
@@ -32,7 +33,7 @@ pnpm install
|
|
|
32
33
|
|
|
33
34
|
## 快速开始
|
|
34
35
|
|
|
35
|
-
### Master
|
|
36
|
+
### Master
|
|
36
37
|
|
|
37
38
|
```js
|
|
38
39
|
import { ZNL } from "@lyrify/znl";
|
|
@@ -40,29 +41,36 @@ import { ZNL } from "@lyrify/znl";
|
|
|
40
41
|
const master = new ZNL({
|
|
41
42
|
role: "master",
|
|
42
43
|
id: "master-1",
|
|
43
|
-
endpoints: {
|
|
44
|
+
endpoints: {
|
|
45
|
+
router: "tcp://127.0.0.1:6003",
|
|
46
|
+
},
|
|
44
47
|
authKey: "your-shared-key",
|
|
45
|
-
encrypted: true,
|
|
48
|
+
encrypted: true,
|
|
46
49
|
});
|
|
47
50
|
|
|
48
|
-
//
|
|
51
|
+
// 注册自动回复处理器
|
|
49
52
|
master.ROUTER(async ({ identityText, payload }) => {
|
|
50
53
|
const text = Buffer.isBuffer(payload) ? payload.toString() : String(payload);
|
|
51
54
|
return `已收到来自 ${identityText} 的消息:${text}`;
|
|
52
55
|
});
|
|
53
56
|
|
|
54
|
-
//
|
|
55
|
-
master.on("slave_connected",
|
|
56
|
-
|
|
57
|
+
// 监听节点上下线
|
|
58
|
+
master.on("slave_connected", (id) => {
|
|
59
|
+
console.log(`${id} 上线,当前在线:${master.slaves.join(", ")}`);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
master.on("slave_disconnected", (id) => {
|
|
63
|
+
console.log(`${id} 下线,当前在线:${master.slaves.join(", ")}`);
|
|
64
|
+
});
|
|
57
65
|
|
|
58
66
|
await master.start();
|
|
59
67
|
|
|
60
|
-
//
|
|
68
|
+
// 广播消息
|
|
61
69
|
master.publish("news", "今日头条:ZNL 正式发布");
|
|
62
70
|
master.publish("system", JSON.stringify({ status: "ok", time: Date.now() }));
|
|
63
71
|
```
|
|
64
72
|
|
|
65
|
-
### Slave
|
|
73
|
+
### Slave
|
|
66
74
|
|
|
67
75
|
```js
|
|
68
76
|
import { ZNL } from "@lyrify/znl";
|
|
@@ -70,26 +78,29 @@ import { ZNL } from "@lyrify/znl";
|
|
|
70
78
|
const slave = new ZNL({
|
|
71
79
|
role: "slave",
|
|
72
80
|
id: "slave-001",
|
|
73
|
-
endpoints: {
|
|
81
|
+
endpoints: {
|
|
82
|
+
router: "tcp://127.0.0.1:6003",
|
|
83
|
+
},
|
|
74
84
|
authKey: "your-shared-key",
|
|
75
|
-
encrypted: true,
|
|
85
|
+
encrypted: true,
|
|
76
86
|
});
|
|
77
87
|
|
|
78
|
-
//
|
|
88
|
+
// 订阅指定 topic
|
|
79
89
|
slave.subscribe("news", ({ payload }) => {
|
|
80
90
|
console.log("收到新闻:", payload.toString());
|
|
81
91
|
});
|
|
82
92
|
|
|
83
|
-
//
|
|
93
|
+
// 兜底监听所有广播
|
|
84
94
|
slave.on("publish", ({ topic, payload }) => {
|
|
85
95
|
console.log(`[${topic}]`, payload.toString());
|
|
86
96
|
});
|
|
87
97
|
|
|
88
98
|
await slave.start();
|
|
89
99
|
|
|
90
|
-
|
|
91
|
-
const reply = await slave.DEALER("hello master", { timeoutMs: 4000 });
|
|
92
|
-
console.log(reply.toString());
|
|
100
|
+
if (slave.isMasterOnline()) {
|
|
101
|
+
const reply = await slave.DEALER("hello master", { timeoutMs: 4000 });
|
|
102
|
+
console.log(reply.toString());
|
|
103
|
+
}
|
|
93
104
|
```
|
|
94
105
|
|
|
95
106
|
## 构造函数
|
|
@@ -103,6 +114,7 @@ new ZNL({
|
|
|
103
114
|
},
|
|
104
115
|
maxPending: 1000,
|
|
105
116
|
authKey: "",
|
|
117
|
+
authKeyMap: { "slave-001": "k1", "slave-002": "k2" },
|
|
106
118
|
heartbeatInterval: 3000,
|
|
107
119
|
heartbeatTimeoutMs: 0,
|
|
108
120
|
encrypted: false,
|
|
@@ -112,99 +124,384 @@ new ZNL({
|
|
|
112
124
|
});
|
|
113
125
|
```
|
|
114
126
|
|
|
127
|
+
## 参数说明
|
|
128
|
+
|
|
115
129
|
| 参数 | 必填 | 说明 |
|
|
116
130
|
|------|------|------|
|
|
117
131
|
| `role` | ✓ | 节点角色,`"master"` 或 `"slave"` |
|
|
118
|
-
| `id` | ✓ |
|
|
119
|
-
| `endpoints.router` |
|
|
120
|
-
| `maxPending` |
|
|
121
|
-
| `authKey` |
|
|
122
|
-
| `
|
|
123
|
-
| `
|
|
124
|
-
| `
|
|
125
|
-
| `
|
|
126
|
-
| `
|
|
127
|
-
| `
|
|
132
|
+
| `id` | ✓ | 节点唯一标识;`slave` 侧同时作为 ZMQ `routingId` |
|
|
133
|
+
| `endpoints.router` | | ROUTER 端点,默认 `tcp://127.0.0.1:6003` |
|
|
134
|
+
| `maxPending` | | 最大并发 RPC 请求数,默认 `1000`;`0` 表示不限制 |
|
|
135
|
+
| `authKey` | | 共享认证 Key;与 `authKeyMap` 二选一(`encrypted=true` 时至少提供一个) |
|
|
136
|
+
| `authKeyMap` | | `master` 侧 `slaveId -> authKey` 映射;未命中时回退到 `authKey` |
|
|
137
|
+
| `heartbeatInterval` | | 心跳间隔(毫秒),默认 `3000`;`0` 表示禁用心跳 |
|
|
138
|
+
| `heartbeatTimeoutMs` | | 心跳超时时间(毫秒),默认 `0` 表示使用 `heartbeatInterval × 3` |
|
|
139
|
+
| `encrypted` | | 是否启用安全模式:`false`(默认,明文) / `true`(签名、防重放、透明加密) |
|
|
140
|
+
| `enablePayloadDigest` | | 是否启用 payload 摘要校验,默认 `true`;关闭可提升性能 |
|
|
141
|
+
| `maxTimeSkewMs` | | 时间戳最大允许偏移(毫秒),默认 `30000` |
|
|
142
|
+
| `replayWindowMs` | | nonce 重放缓存窗口(毫秒),默认 `120000` |
|
|
143
|
+
|
|
144
|
+
## 使用建议
|
|
145
|
+
|
|
146
|
+
- `master` 与 `slave` 两端应保持一致的 `encrypted` 配置
|
|
147
|
+
- 若使用 `enablePayloadDigest=false`,两端也应保持一致
|
|
148
|
+
- `encrypted=true` 时必须提供非空 `authKey`,或在 `master` 侧提供 `authKeyMap`
|
|
128
149
|
|
|
129
150
|
## API
|
|
130
151
|
|
|
131
|
-
###
|
|
152
|
+
### 生命周期
|
|
153
|
+
|
|
154
|
+
#### `start()`
|
|
155
|
+
|
|
156
|
+
启动节点。
|
|
157
|
+
|
|
158
|
+
`master` 侧:
|
|
159
|
+
|
|
160
|
+
- 绑定 ROUTER socket
|
|
161
|
+
- 启动在线节点心跳检测
|
|
162
|
+
|
|
163
|
+
`slave` 侧:
|
|
164
|
+
|
|
165
|
+
- 连接 DEALER socket
|
|
166
|
+
- 自动尝试发送注册帧
|
|
167
|
+
- 启动心跳流程
|
|
168
|
+
- 发送 `heartbeat` 后等待 `heartbeat_ack`
|
|
169
|
+
- 收到 `heartbeat_ack` 后调度下一次心跳
|
|
170
|
+
|
|
171
|
+
#### `stop()`
|
|
132
172
|
|
|
133
|
-
|
|
173
|
+
停止节点。
|
|
134
174
|
|
|
135
|
-
|
|
136
|
-
- `slave`:连接(connect)DEALER socket,并自动向 master 发送注册帧
|
|
175
|
+
`slave` 侧:
|
|
137
176
|
|
|
138
|
-
|
|
177
|
+
- 停止心跳
|
|
178
|
+
- 发送注销帧
|
|
179
|
+
- 关闭 socket
|
|
139
180
|
|
|
140
|
-
|
|
181
|
+
`master` 侧:
|
|
141
182
|
|
|
142
|
-
|
|
183
|
+
- 清空在线节点表
|
|
184
|
+
- 关闭 socket
|
|
185
|
+
- reject 所有 pending RPC 请求
|
|
143
186
|
|
|
144
|
-
|
|
145
|
-
- `master`:清空在线节点表,关闭 socket,立即 reject 所有 pending RPC 请求
|
|
187
|
+
### 双向 RPC
|
|
146
188
|
|
|
147
|
-
|
|
189
|
+
#### `DEALER(payloadOrHandler, options?)`
|
|
148
190
|
|
|
149
|
-
|
|
191
|
+
仅 `slave` 侧使用。
|
|
150
192
|
|
|
151
|
-
- `payloadOrHandler` 为 payload
|
|
152
|
-
- `payloadOrHandler`
|
|
193
|
+
- 当 `payloadOrHandler` 为 payload 时,向 `Master` 发起 RPC 请求,返回 `Promise<Buffer | Array>`
|
|
194
|
+
- 当 `payloadOrHandler` 为函数时,注册 `slave` 侧自动回复处理器,用于处理 `Master` 主动发来的请求
|
|
153
195
|
|
|
154
|
-
|
|
196
|
+
#### `ROUTER(identityOrHandler, payload?, options?)`
|
|
155
197
|
|
|
156
|
-
|
|
198
|
+
仅 `master` 侧使用。
|
|
157
199
|
|
|
158
|
-
- `identityOrHandler`
|
|
159
|
-
- `identityOrHandler`
|
|
200
|
+
- 当 `identityOrHandler` 为函数时,注册 `master` 侧自动回复处理器,用于处理 `Slave` 发来的请求
|
|
201
|
+
- 当 `identityOrHandler` 为某个 `slaveId` 时,`Master` 主动向指定 `Slave` 发起 RPC 请求,返回 `Promise<Buffer | Array>`
|
|
160
202
|
|
|
161
|
-
|
|
203
|
+
#### `options.timeoutMs`
|
|
162
204
|
|
|
163
|
-
单次 RPC 请求超时时间,默认 `5000`
|
|
205
|
+
单次 RPC 请求超时时间,默认 `5000` 毫秒。
|
|
164
206
|
|
|
165
|
-
###
|
|
207
|
+
### 广播与订阅
|
|
166
208
|
|
|
167
|
-
|
|
209
|
+
#### `publish(topic, payload)`
|
|
168
210
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
211
|
+
仅 `master` 侧使用。
|
|
212
|
+
|
|
213
|
+
向所有当前在线的 `slave` 广播消息(fire-and-forget,无需 `await`)。
|
|
214
|
+
|
|
215
|
+
- `topic`:消息主题
|
|
216
|
+
- `payload`:支持 `string`、`Buffer`、`Uint8Array` 或其数组
|
|
217
|
+
- 若某个 `slave` 发送失败,会自动将其移出在线列表并触发 `slave_disconnected`
|
|
172
218
|
|
|
173
219
|
```js
|
|
174
220
|
master.publish("news", "breaking news!");
|
|
175
221
|
master.publish("metrics", JSON.stringify({ cpu: 0.42 }));
|
|
176
222
|
```
|
|
177
223
|
|
|
178
|
-
|
|
224
|
+
#### `subscribe(topic, handler)`
|
|
179
225
|
|
|
180
|
-
|
|
226
|
+
仅 `slave` 侧使用。
|
|
181
227
|
|
|
182
|
-
|
|
228
|
+
订阅指定 topic。
|
|
229
|
+
|
|
230
|
+
- 可在 `start()` 前后调用
|
|
231
|
+
- 订阅关系跨 `stop()/start()` 周期保留
|
|
183
232
|
- 同一 topic 重复订阅会覆盖旧 handler
|
|
184
|
-
-
|
|
233
|
+
- 返回 `this`,支持链式调用
|
|
185
234
|
|
|
186
235
|
```js
|
|
187
236
|
slave
|
|
188
|
-
.subscribe("news",
|
|
189
|
-
|
|
237
|
+
.subscribe("news", ({ topic, payload }) => {
|
|
238
|
+
// ...
|
|
239
|
+
})
|
|
240
|
+
.subscribe("metrics", ({ topic, payload }) => {
|
|
241
|
+
// ...
|
|
242
|
+
});
|
|
190
243
|
```
|
|
191
244
|
|
|
192
|
-
|
|
245
|
+
#### `unsubscribe(topic)`
|
|
246
|
+
|
|
247
|
+
仅 `slave` 侧使用。
|
|
193
248
|
|
|
194
|
-
|
|
249
|
+
取消订阅指定 topic。
|
|
195
250
|
|
|
196
251
|
```js
|
|
197
252
|
slave.unsubscribe("news");
|
|
198
253
|
```
|
|
199
254
|
|
|
200
|
-
###
|
|
255
|
+
### 在线状态与节点管理
|
|
256
|
+
|
|
257
|
+
#### `slaves`
|
|
258
|
+
|
|
259
|
+
仅 `master` 侧只读属性。
|
|
260
|
+
|
|
261
|
+
返回当前所有在线 `slaveId` 的快照数组。
|
|
262
|
+
|
|
263
|
+
```js
|
|
264
|
+
console.log(master.slaves);
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
#### `masterOnline`
|
|
268
|
+
|
|
269
|
+
仅 `slave` 侧只读属性。
|
|
201
270
|
|
|
202
|
-
|
|
271
|
+
表示最近一次链路确认结果。
|
|
272
|
+
|
|
273
|
+
- `true`:最近收到合法的 `heartbeat_ack`,或收到来自 `master` 的合法业务帧
|
|
274
|
+
- `false`:尚未建立有效链路、心跳应答超时、或节点已停止
|
|
203
275
|
|
|
204
276
|
```js
|
|
205
|
-
console.log(
|
|
277
|
+
console.log(slave.masterOnline);
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
#### `isMasterOnline()`
|
|
281
|
+
|
|
282
|
+
仅 `slave` 侧方法。
|
|
283
|
+
|
|
284
|
+
返回当前主节点在线状态。该值基于最近一次链路确认结果,不会主动发起实时网络探测。
|
|
285
|
+
|
|
286
|
+
```js
|
|
287
|
+
if (slave.isMasterOnline()) {
|
|
288
|
+
console.log("master 在线");
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
#### `addAuthKey(slaveId, authKey)`
|
|
293
|
+
|
|
294
|
+
仅 `master` 侧使用。
|
|
295
|
+
|
|
296
|
+
动态添加或更新某个 `slave` 的 `authKey`,立即生效。
|
|
297
|
+
|
|
298
|
+
#### `removeAuthKey(slaveId)`
|
|
299
|
+
|
|
300
|
+
仅 `master` 侧使用。
|
|
301
|
+
|
|
302
|
+
移除某个 `slave` 的 `authKey`,立即生效,并触发 `slave_disconnected`。
|
|
303
|
+
|
|
304
|
+
## 心跳机制
|
|
305
|
+
|
|
306
|
+
ZNL 使用 `heartbeat -> heartbeat_ack` 进行链路确认。
|
|
307
|
+
|
|
308
|
+
流程如下:
|
|
309
|
+
|
|
310
|
+
1. `slave` 发送一条 `heartbeat`
|
|
311
|
+
2. `master` 收到并验证通过后,立即返回 `heartbeat_ack`
|
|
312
|
+
3. `slave` 收到 `heartbeat_ack` 后,调度下一次 heartbeat
|
|
313
|
+
4. 若超过应答超时时间仍未收到 `heartbeat_ack``
|
|
314
|
+
- `slave` 将 `masterOnline` 置为 `false`
|
|
315
|
+
- 主动重建 `Dealer`
|
|
316
|
+
- 丢弃旧连接残留消息
|
|
317
|
+
- 尝试恢复连接与注册
|
|
318
|
+
|
|
319
|
+
## 安全机制
|
|
320
|
+
|
|
321
|
+
当 `encrypted=true` 时,ZNL 启用安全模式:
|
|
322
|
+
|
|
323
|
+
- HMAC 签名
|
|
324
|
+
- 时间戳校验
|
|
325
|
+
- nonce 防重放
|
|
326
|
+
- AES-256-GCM 透明加密
|
|
327
|
+
- 可选 payload 摘要校验
|
|
328
|
+
|
|
329
|
+
### 安全建议
|
|
330
|
+
|
|
331
|
+
- `authKey` 不要硬编码到公开仓库
|
|
332
|
+
- `master` 与 `slave` 必须使用匹配的密钥配置
|
|
333
|
+
- 若使用 `authKeyMap`,建议按 `slaveId` 做最小权限配置
|
|
334
|
+
- 生产环境建议开启:
|
|
335
|
+
- `encrypted: true`
|
|
336
|
+
- `enablePayloadDigest: true`
|
|
337
|
+
|
|
338
|
+
## 底层帧协议
|
|
339
|
+
|
|
340
|
+
本节说明 ZNL 在 ZeroMQ 上层定义的控制帧结构,用于协议对接、调试与抓包分析。
|
|
341
|
+
|
|
342
|
+
### 总体说明
|
|
343
|
+
|
|
344
|
+
`master` 侧 ROUTER 收包结构:
|
|
345
|
+
|
|
346
|
+
```text
|
|
347
|
+
[identity, ...控制帧]
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
`slave` 侧 DEALER 收包结构:
|
|
351
|
+
|
|
352
|
+
```text
|
|
353
|
+
[...控制帧]
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
控制帧统一前缀:
|
|
357
|
+
|
|
358
|
+
```text
|
|
359
|
+
__znl_v1__
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
认证字段标记:
|
|
363
|
+
|
|
364
|
+
```text
|
|
365
|
+
__znl_v1_auth__
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
### 控制帧类型
|
|
369
|
+
|
|
370
|
+
| 类型 | 方向 | 作用 |
|
|
371
|
+
|------|------|------|
|
|
372
|
+
| `register` | `slave -> master` | 注册上线 |
|
|
373
|
+
| `unregister` | `slave -> master` | 主动下线 |
|
|
374
|
+
| `heartbeat` | `slave -> master` | 心跳请求 |
|
|
375
|
+
| `heartbeat_ack` | `master -> slave` | 心跳应答 |
|
|
376
|
+
| `req` | 双向 | RPC 请求 |
|
|
377
|
+
| `res` | 双向 | RPC 响应 |
|
|
378
|
+
| `pub` | `master -> slave` | 广播消息 |
|
|
379
|
+
|
|
380
|
+
### 各类帧结构
|
|
381
|
+
|
|
382
|
+
#### `register`
|
|
383
|
+
|
|
384
|
+
```text
|
|
385
|
+
[PREFIX, "register", (AUTH_MARKER, authProof)?]
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
示例:
|
|
389
|
+
|
|
390
|
+
```text
|
|
391
|
+
["__znl_v1__", "register"]
|
|
392
|
+
["__znl_v1__", "register", "__znl_v1_auth__", "<proof-token>"]
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
#### `unregister`
|
|
396
|
+
|
|
397
|
+
```text
|
|
398
|
+
[PREFIX, "unregister"]
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
示例:
|
|
402
|
+
|
|
403
|
+
```text
|
|
404
|
+
["__znl_v1__", "unregister"]
|
|
206
405
|
```
|
|
207
406
|
|
|
407
|
+
#### `heartbeat`
|
|
408
|
+
|
|
409
|
+
```text
|
|
410
|
+
[PREFIX, "heartbeat", (AUTH_MARKER, authProof)?]
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
示例:
|
|
414
|
+
|
|
415
|
+
```text
|
|
416
|
+
["__znl_v1__", "heartbeat"]
|
|
417
|
+
["__znl_v1__", "heartbeat", "__znl_v1_auth__", "<proof-token>"]
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
#### `heartbeat_ack`
|
|
421
|
+
|
|
422
|
+
```text
|
|
423
|
+
[PREFIX, "heartbeat_ack", (AUTH_MARKER, authProof)?]
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
示例:
|
|
427
|
+
|
|
428
|
+
```text
|
|
429
|
+
["__znl_v1__", "heartbeat_ack"]
|
|
430
|
+
["__znl_v1__", "heartbeat_ack", "__znl_v1_auth__", "<proof-token>"]
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
#### `req`
|
|
434
|
+
|
|
435
|
+
```text
|
|
436
|
+
[PREFIX, "req", requestId, (AUTH_MARKER, authProof)?, ...payloadFrames]
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
示例:
|
|
440
|
+
|
|
441
|
+
```text
|
|
442
|
+
["__znl_v1__", "req", "<requestId>", ...payloadFrames]
|
|
443
|
+
["__znl_v1__", "req", "<requestId>", "__znl_v1_auth__", "<proof-token>", ...payloadFrames]
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
#### `res`
|
|
447
|
+
|
|
448
|
+
```text
|
|
449
|
+
[PREFIX, "res", requestId, (AUTH_MARKER, authProof)?, ...payloadFrames]
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
示例:
|
|
453
|
+
|
|
454
|
+
```text
|
|
455
|
+
["__znl_v1__", "res", "<requestId>", ...payloadFrames]
|
|
456
|
+
["__znl_v1__", "res", "<requestId>", "__znl_v1_auth__", "<proof-token>", ...payloadFrames]
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
#### `pub`
|
|
460
|
+
|
|
461
|
+
```text
|
|
462
|
+
[PREFIX, "pub", topic, (AUTH_MARKER, authProof)?, ...payloadFrames]
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
示例:
|
|
466
|
+
|
|
467
|
+
```text
|
|
468
|
+
["__znl_v1__", "pub", "news", ...payloadFrames]
|
|
469
|
+
["__znl_v1__", "pub", "news", "__znl_v1_auth__", "<proof-token>", ...payloadFrames]
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
### 认证令牌字段
|
|
473
|
+
|
|
474
|
+
安全模式下,认证令牌内部包含以下字段:
|
|
475
|
+
|
|
476
|
+
- `kind`
|
|
477
|
+
- `nodeId`
|
|
478
|
+
- `requestId`
|
|
479
|
+
- `timestamp`
|
|
480
|
+
- `nonce`
|
|
481
|
+
- `payloadDigest`
|
|
482
|
+
|
|
483
|
+
校验时会验证:
|
|
484
|
+
|
|
485
|
+
- 帧类型是否匹配
|
|
486
|
+
- 节点 ID 是否匹配
|
|
487
|
+
- 请求 ID 是否匹配
|
|
488
|
+
- 时间戳是否在允许偏差内
|
|
489
|
+
- nonce 是否重复
|
|
490
|
+
- payload 摘要是否一致
|
|
491
|
+
|
|
492
|
+
### 明文模式与安全模式
|
|
493
|
+
|
|
494
|
+
#### 明文模式 `encrypted=false`
|
|
495
|
+
|
|
496
|
+
- 不签名
|
|
497
|
+
- 不加密
|
|
498
|
+
- 帧结构更轻量
|
|
499
|
+
|
|
500
|
+
#### 安全模式 `encrypted=true`
|
|
501
|
+
|
|
502
|
+
- 控制帧会附带认证证明
|
|
503
|
+
- 业务 payload 会被透明加密
|
|
504
|
+
|
|
208
505
|
## 事件
|
|
209
506
|
|
|
210
507
|
通过 `node.on(eventName, handler)` 监听:
|
|
@@ -216,9 +513,9 @@ console.log(master.slaves); // ["slave-001", "slave-002"]
|
|
|
216
513
|
| `request` | 两者 | 解析出 RPC 请求帧(认证通过后) |
|
|
217
514
|
| `response` | 两者 | 解析出 RPC 响应帧 |
|
|
218
515
|
| `message` | 两者 | 所有解析消息的统一事件 |
|
|
219
|
-
| `publish` | Slave | 收到 master 广播,携带 `{ topic, payload }` |
|
|
220
|
-
| `slave_connected` | Master | slave 注册成功上线,携带 `slaveId` |
|
|
221
|
-
| `slave_disconnected` | Master | slave 注销或发送失败下线,携带 `slaveId` |
|
|
516
|
+
| `publish` | Slave | 收到 `master` 广播,携带 `{ topic, payload }` |
|
|
517
|
+
| `slave_connected` | Master | `slave` 注册成功上线,携带 `slaveId` |
|
|
518
|
+
| `slave_disconnected` | Master | `slave` 注销或发送失败下线,携带 `slaveId` |
|
|
222
519
|
| `auth_failed` | Master / Slave | 认证失败(签名校验失败、重放检测失败、解密失败等),请求已被丢弃 |
|
|
223
520
|
| `error` | 两者 | 内部错误 |
|
|
224
521
|
|
|
@@ -235,7 +532,15 @@ node test/slave/index.js slave-001
|
|
|
235
532
|
|
|
236
533
|
## 集成测试
|
|
237
534
|
|
|
238
|
-
在同一进程内启动 Master / Slave
|
|
535
|
+
在同一进程内启动 `Master / Slave`,自动验证:
|
|
536
|
+
|
|
537
|
+
- RPC
|
|
538
|
+
- 并发
|
|
539
|
+
- 认证
|
|
540
|
+
- 超时
|
|
541
|
+
- PUB/SUB
|
|
542
|
+
- 心跳恢复
|
|
543
|
+
- 在线状态 API
|
|
239
544
|
|
|
240
545
|
```bash
|
|
241
546
|
pnpm test
|
|
@@ -243,6 +548,8 @@ pnpm test
|
|
|
243
548
|
|
|
244
549
|
## 并发压测
|
|
245
550
|
|
|
551
|
+
### 明文模式
|
|
552
|
+
|
|
246
553
|
```bash
|
|
247
554
|
# 终端 1:启动 Echo 服务端(plain)
|
|
248
555
|
pnpm test:echo
|
|
@@ -251,7 +558,7 @@ pnpm test:echo
|
|
|
251
558
|
pnpm test:100 -- 100 10000 slave-001
|
|
252
559
|
```
|
|
253
560
|
|
|
254
|
-
|
|
561
|
+
### 安全模式
|
|
255
562
|
|
|
256
563
|
```bash
|
|
257
564
|
# 终端 1:加密模式启动 Echo 服务端
|
|
@@ -261,14 +568,26 @@ ZNL_AUTH_KEY=my-secret ZNL_ENCRYPTED=true pnpm test:echo
|
|
|
261
568
|
pnpm test:100 -- 100 10000 slave-001 my-secret true
|
|
262
569
|
```
|
|
263
570
|
|
|
264
|
-
|
|
571
|
+
### 参数说明
|
|
265
572
|
|
|
266
573
|
- 总请求数
|
|
267
574
|
- 超时时间(毫秒)
|
|
268
|
-
- Slave 节点 ID
|
|
575
|
+
- `Slave` 节点 ID
|
|
576
|
+
|
|
577
|
+
## 常见问题
|
|
578
|
+
|
|
579
|
+
### 为什么 `slave.start()` 后立刻发送第一条请求可能失败?
|
|
580
|
+
|
|
581
|
+
当前版本对 `Dealer` 的发送策略更严格。建议先等待 `slave.isMasterOnline() === true`,再发送首个业务请求。
|
|
582
|
+
|
|
583
|
+
### 为什么会出现“令牌已过期或时间戳异常”?
|
|
584
|
+
|
|
585
|
+
常见原因:
|
|
586
|
+
|
|
587
|
+
- 主从机器时间差过大
|
|
588
|
+
- 节点时间被手动修改
|
|
589
|
+
- 历史旧消息在较晚时间才被投递
|
|
269
590
|
|
|
270
|
-
|
|
591
|
+
### `masterOnline=true` 是否表示此刻网络一定可用?
|
|
271
592
|
|
|
272
|
-
|
|
273
|
-
2. 确认 README 中的包名与 import 路径
|
|
274
|
-
3. 按需更新 `LICENSE` 中的版权信息
|
|
593
|
+
不是。该值表示最近一次链路确认成功,适合作为业务层在线状态参考,但不是一次即时网络探针。
|