@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 CHANGED
@@ -1,21 +1,22 @@
1
1
  # ZNL
2
2
 
3
- 基于 ZeroMQ `ROUTER/DEALER` 模式的 Node.js 通信库,提供开箱即用的双向 RPC 与 PUB/SUB 广播能力。
3
+ 基于 ZeroMQ `ROUTER / DEALER` 模式的 Node.js 通信库,提供双向 RPC、广播、在线状态感知,以及可选的签名认证与透明加密能力。
4
4
 
5
5
  ## 特性
6
6
 
7
- - 基于 `ROUTER/DEALER` 同时实现 RPC 请求-响应与 PUB/SUB 广播,一套连接两种模式
8
- - 自动处理并发消息匹配、自动处理心跳包、超时控制、最大并发限制
9
- - 支持 Master Slave 主动发起请求(双向 RPC)
10
- - 基于 ROUTER 实现 PUB/SUB 广播,无需额外 socket 或端口
11
- - Slave 自动注册/注销,Master 实时感知在线节点
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` 时必填
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: { router: "tcp://127.0.0.1:6003" },
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
- // RPC:自动回复 slave 的请求
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
- // PUB/SUB:感知节点上下线
55
- master.on("slave_connected", (id) => console.log(`${id} 上线,在线:${master.slaves}`));
56
- master.on("slave_disconnected", (id) => console.log(`${id} 下线,在线:${master.slaves}`));
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
- // PUB/SUB:广播消息(fire-and-forget)
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: { router: "tcp://127.0.0.1:6003" },
81
+ endpoints: {
82
+ router: "tcp://127.0.0.1:6003",
83
+ },
74
84
  authKey: "your-shared-key",
75
- encrypted: true, // 需与 master 一致
85
+ encrypted: true,
76
86
  });
77
87
 
78
- // PUB/SUB:精确订阅(可在 start 前调用)
88
+ // 订阅指定 topic
79
89
  slave.subscribe("news", ({ payload }) => {
80
90
  console.log("收到新闻:", payload.toString());
81
91
  });
82
92
 
83
- // PUB/SUB:兜底监听所有 topic
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
- // RPC:向 master 发请求并等待响应
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` | ✓ | 节点唯一标识;slave 端同时作为 ZMQ `routingId` |
119
- | `endpoints.router` | | ROUTER 端点,默认 `tcp://127.0.0.1:6003` |
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` |
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
- ### `start()`
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
- - `master`:绑定(bind)ROUTER socket
136
- - `slave`:连接(connect)DEALER socket,并自动向 master 发送注册帧
175
+ `slave` 侧:
137
176
 
138
- 重复调用安全,若正在启动中则等待同一个 Promise。
177
+ - 停止心跳
178
+ - 发送注销帧
179
+ - 关闭 socket
139
180
 
140
- ### `stop()`
181
+ `master` 侧:
141
182
 
142
- 停止节点:
183
+ - 清空在线节点表
184
+ - 关闭 socket
185
+ - reject 所有 pending RPC 请求
143
186
 
144
- - `slave`:先向 master 发送注销帧,再关闭 socket
145
- - `master`:清空在线节点表,关闭 socket,立即 reject 所有 pending RPC 请求
187
+ ### 双向 RPC
146
188
 
147
- ### `DEALER(payloadOrHandler, options?)`
189
+ #### `DEALER(payloadOrHandler, options?)`
148
190
 
149
- **Slave 侧调用:**
191
+ `slave` 侧使用。
150
192
 
151
- - `payloadOrHandler` 为 payload 时:向 Master 发送 RPC 请求并等待响应,返回 `Promise<Buffer | Array>`
152
- - `payloadOrHandler` 为函数时:注册 slave 侧自动回复处理器(Master 主动发来 RPC 请求时触发)
193
+ - `payloadOrHandler` 为 payload 时,向 `Master` 发起 RPC 请求,返回 `Promise<Buffer | Array>`
194
+ - `payloadOrHandler` 为函数时,注册 `slave` 侧自动回复处理器,用于处理 `Master` 主动发来的请求
153
195
 
154
- ### `ROUTER(identityOrHandler, payload?, options?)`
196
+ #### `ROUTER(identityOrHandler, payload?, options?)`
155
197
 
156
- **Master 侧调用:**
198
+ `master` 侧使用。
157
199
 
158
- - `identityOrHandler` 为函数时:注册 master 侧自动回复处理器(Slave 发来 RPC 请求时触发)
159
- - `identityOrHandler` identity(slave ID)时:Master 主动向指定 Slave 发送 RPC 请求并等待响应,返回 `Promise<Buffer | Array>`
200
+ - `identityOrHandler` 为函数时,注册 `master` 侧自动回复处理器,用于处理 `Slave` 发来的请求
201
+ - `identityOrHandler` 为某个 `slaveId` 时,`Master` 主动向指定 `Slave` 发起 RPC 请求,返回 `Promise<Buffer | Array>`
160
202
 
161
- ### `options.timeoutMs`
203
+ #### `options.timeoutMs`
162
204
 
163
- 单次 RPC 请求超时时间,默认 `5000` ms。
205
+ 单次 RPC 请求超时时间,默认 `5000` 毫秒。
164
206
 
165
- ### `publish(topic, payload)`
207
+ ### 广播与订阅
166
208
 
167
- **Master 侧调用**,向所有当前在线的 slave 广播消息(fire-and-forget,无需 await)。
209
+ #### `publish(topic, payload)`
168
210
 
169
- - `topic`:消息主题字符串,slave 侧可按 topic 精确过滤
170
- - `payload`:同 RPC,支持 `string`、`Buffer`、`Uint8Array` 或其数组
171
- - 若某个 slave 发送失败,自动将其从在线列表移除并触发 `slave_disconnected`
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
- ### `subscribe(topic, handler)`
224
+ #### `subscribe(topic, handler)`
179
225
 
180
- **Slave 侧调用**,订阅指定 topic,master 广播时触发 handler。
226
+ `slave` 侧使用。
181
227
 
182
- - 可在 `start()` 前后任意时刻调用,订阅信息跨 stop/start 周期保留
228
+ 订阅指定 topic。
229
+
230
+ - 可在 `start()` 前后调用
231
+ - 订阅关系跨 `stop()/start()` 周期保留
183
232
  - 同一 topic 重复订阅会覆盖旧 handler
184
- - 支持链式调用(返回 `this`)
233
+ - 返回 `this`,支持链式调用
185
234
 
186
235
  ```js
187
236
  slave
188
- .subscribe("news", ({ topic, payload }) => { /* ... */ })
189
- .subscribe("metrics", ({ topic, payload }) => { /* ... */ });
237
+ .subscribe("news", ({ topic, payload }) => {
238
+ // ...
239
+ })
240
+ .subscribe("metrics", ({ topic, payload }) => {
241
+ // ...
242
+ });
190
243
  ```
191
244
 
192
- ### `unsubscribe(topic)`
245
+ #### `unsubscribe(topic)`
246
+
247
+ 仅 `slave` 侧使用。
193
248
 
194
- **Slave 侧调用**,取消订阅指定 topic,支持链式调用。
249
+ 取消订阅指定 topic
195
250
 
196
251
  ```js
197
252
  slave.unsubscribe("news");
198
253
  ```
199
254
 
200
- ### `slaves`
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
- **Master 侧只读属性**,返回当前所有在线 slave ID 的快照数组。
271
+ 表示最近一次链路确认结果。
272
+
273
+ - `true`:最近收到合法的 `heartbeat_ack`,或收到来自 `master` 的合法业务帧
274
+ - `false`:尚未建立有效链路、心跳应答超时、或节点已停止
203
275
 
204
276
  ```js
205
- console.log(master.slaves); // ["slave-001", "slave-002"]
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,自动验证 RPC、并发、认证、超时、PUB/SUB 等全部功能:
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
- 1. 更新 `package.json` 中的 `name`、`version`、`author`、`repository`
273
- 2. 确认 README 中的包名与 import 路径
274
- 3. 按需更新 `LICENSE` 中的版权信息
593
+ 不是。该值表示最近一次链路确认成功,适合作为业务层在线状态参考,但不是一次即时网络探针。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lyrify/znl",
3
- "version": "0.5.2",
3
+ "version": "0.6.0",
4
4
  "description": "ZNL - ZeroMQ Node Link",
5
5
  "type": "module",
6
6
  "main": "./index.js",