基于 ZeroMQ ROUTER / DEALER 模式的 Node.js 通信库,提供:
- 双向 RPC
- 广播 / 订阅
Slave -> Master单向推送- 在线状态感知
- 心跳保活
- 可选签名认证与透明加密
- 内建独立
fs文件服务
为了让文档结构更清晰,详细内容已经拆分到独立文件:
- API 参考:docs/README.api.md
- 使用示例与实践:docs/README.examples.md
- 底层帧协议:docs/README.protocol.md
建议阅读顺序:
- 本文档:快速了解项目
- docs/README.examples.md:先看怎么用
- docs/README.api.md:再查完整 API
- docs/README.protocol.md:最后看底层协议细节
- 基于单连接实现双向 RPC 与广播
- 支持
Master -> Slave与Slave -> Master双向主动请求 - 支持
Slave -> Master单向推送(PUSH) Slave自动注册 / 注销,Master实时维护在线节点列表- 支持请求超时控制与最大并发限制
- 心跳采用
heartbeat -> heartbeat_ack应答机制 Slave提供主节点在线状态查询 API:masterOnline/isMasterOnline()- 内建
fs文件服务命名空间:master.fs.list/get/create/mkdir/patch/delete/rename/statmaster.fs.upload/downloadslave.fs.setRoot("./")
fs走独立 service 通道,不占用业务request/response- 支持安全模式:
- HMAC 签名
- 时间戳校验
- nonce 防重放
- AES-256-GCM 透明加密
- 可选 payload 摘要校验
- 支持
authKeyMap,允许Master按slaveId使用不同密钥 - Payload 支持
string、Buffer、Uint8Array及其数组(多帧)
pnpm add @lyrify/znl本地开发:
pnpm installimport { ZNL } from "@lyrify/znl";
const master = new ZNL({
role: "master",
id: "master-1",
endpoints: {
router: "tcp://127.0.0.1:6003",
},
});
master.ROUTER(async ({ identityText, payload }) => {
const text = Buffer.isBuffer(payload) ? payload.toString("utf8") : String(payload);
return `已收到来自 ${identityText} 的消息:${text}`;
});
master.on("slave_connected", (id) => {
console.log(`${id} 上线,当前在线:${master.slaves.join(", ")}`);
});
await master.start();import { ZNL } from "@lyrify/znl";
const slave = new ZNL({
role: "slave",
id: "slave-001",
endpoints: {
router: "tcp://127.0.0.1:6003",
},
});
await slave.start();
if (slave.isMasterOnline()) {
const reply = await slave.DEALER("hello master", { timeoutMs: 4000 });
console.log(reply.toString());
}如果你想直接看更多完整示例,请阅读:
new ZNL({
role: "master" | "slave",
id: "node-id",
endpoints: {
router: "tcp://127.0.0.1:6003",
},
maxPending: 1000,
authKey: "",
kdfSalt: "your-kdf-salt",
authKeyMap: { "slave-001": "k1", "slave-002": "k2" },
heartbeatInterval: 3000,
heartbeatTimeoutMs: 0,
encrypted: false,
enablePayloadDigest: false,
maxTimeSkewMs: 30000,
replayWindowMs: 120000,
});完整参数说明请查看:
性能建议:
- 安全模式默认关闭
enablePayloadDigest - 如需额外的 payload 摘要一致性校验,可显式设置
enablePayloadDigest: true - 在高并发短消息场景下,关闭
enablePayloadDigest通常可以获得更高吞吐
slave -> master:slave.DEALER(payload, options?)master -> slave:master.ROUTER(slaveId, payload, options?)
同时两边都支持注册自动回复处理器:
master.ROUTER(handler)slave.DEALER(handler)
- 广播:
master.PUBLISH(topic, payload) - 订阅:
slave.SUBSCRIBE(topic, handler) - 取消订阅:
slave.UNSUBSCRIBE(topic) - 单向推送:
slave.PUSH(topic, payload)
master.slavesslave.masterOnlineslave.isMasterOnline()
当 encrypted=true 时,ZNL 启用:
- HMAC 签名
- 时间戳校验
- nonce 防重放
- AES-256-GCM 透明加密
- 可选 payload 摘要校验
ZNL 内建了一个独立于业务 RPC 的文件服务通道:
master.fs.*通过内部service通道与指定slave通信- 不会占用或污染现有
DEALER()/ROUTER()业务请求流 encrypted=true时,fs通道同样复用签名、防重放与透明加密机制
slave 侧通过 slave.fs.setRoot(rootPath, policy?) 设置根目录并启用内建 fs 服务:
const slave = new ZNL({
role: "slave",
id: "slave-001",
endpoints: {
router: "tcp://127.0.0.1:6003",
},
});
slave.fs.setRoot("./storage", {
readOnly: false,
allowDelete: true,
allowPatch: true,
allowUpload: true,
allowedPaths: ["public/**", "configs/app.json"],
denyGlobs: ["**/*.secret.txt", "private/**"],
getAllowedExtensions: ["txt", "md", "json", "js", "ts", "toml", "yaml", "yml"],
maxGetFileMb: 4,
});
await slave.start();说明:
rootPath会被解析为绝对路径,并作为所有远端文件访问的根目录policy为可选策略对象;不传时,默认允许根目录内的常规读写操作- 所有远端路径都必须落在
rootPath范围内 - 当前实现会拒绝穿过符号链接(Linux/macOS symlink)或目录联接(Windows junction)的访问,避免通过链接跳出
root - 如果路径命中
allowedPaths/denyGlobs或写操作策略限制,请求会被直接拒绝 master.fs.upload()会阻止“目录被文件覆盖”:当目标路径是目录时,不会执行目录删除后再写入文件的行为
policy 支持以下字段:
readOnly: boolean- 设为
true时,拒绝所有写操作 - 当前会拦截:
create / mkdir / patch / delete / rename / upload
- 设为
allowDelete: boolean- 是否允许
master.fs.delete() - 默认
true
- 是否允许
allowPatch: boolean- 是否允许
master.fs.patch() - 默认
true
- 是否允许
allowUpload: boolean- 是否允许
master.fs.upload() - 也会限制
master.fs.create()与master.fs.mkdir() - 默认
true
- 是否允许
allowedPaths: string[]- 可选白名单路径/模式
- 非空时,只有命中的路径才允许访问
- 支持目录前缀和 glob 风格,例如:
"public","public/**","configs/*.json"
denyGlobs: string[]- 可选黑名单 glob
- 命中后直接拒绝访问
- 例如:
"**/*.secret.txt","private/**"
getAllowedExtensions: string[]- 限制
master.fs.get()允许读取的文件扩展名(仅文本类文件) - 默认包含常见文本扩展名(如
js、txt、toml等) - 不在列表中的文件会被拒绝,并提示改用
master.fs.download()
- 限制
maxGetFileMb: number- 限制
master.fs.get()单文件最大读取大小(单位 MB) - 默认
4 - 超出限制会报错,并提示改用
master.fs.download()
- 限制
建议:
- 在
start()前调用slave.fs.setRoot(...) - 生产环境优先配合
encrypted=true - 对外提供远程文件能力时,优先使用最小权限策略,例如只读模式或显式白名单
slave.fs.setRoot(rootPath, policy?)master.fs.list(slaveId, path, options?)master.fs.get(slaveId, path, options?)master.fs.create(slaveId, path, options?)master.fs.mkdir(slaveId, path, options?)master.fs.patch(slaveId, path, unifiedDiff, options?)master.fs.delete(slaveId, path, options?)master.fs.rename(slaveId, from, to, options?)master.fs.stat(slaveId, path, options?)master.fs.upload(slaveId, localPath, remotePath, options?)master.fs.download(slaveId, remotePath, localPath, options?)
上传/下载进度回调(options.onProgress(event)):
master.fs.upload()与master.fs.download()都支持可选onProgress回调event.phase:"init"、"chunk"、"complete"- 常用字段:
direction、phase、slaveId、sessionId、localPath、remotePath、transferred、total、percent、chunkId、totalChunks、size - 速率与预计时间:
speedBps(字节/秒)、etaSeconds(预计剩余秒数;无法估算时为null) complete阶段额外包含meta(完成阶段返回元信息)
upload(remotePath) 路径语义(localPath 必须是本地文件):
- 当
remotePath以/或\结尾时,按“目录路径”处理,自动落盘为remotePath + basename(localPath)- 例如:
assets/+banner.txt=>assets/banner.txt
- 例如:
- 当
remotePath为././/.\时,按fs root根目录处理,自动落盘为basename(localPath) - 当
remotePath不带结尾斜杠,但远端该路径已存在且是目录时,仍按目录处理,自动落盘为remotePath/basename(localPath)- 例如:
test已存在目录时,上传banner.txt=>test/banner.txt
- 例如:
- 其他情况按“明确文件路径”处理(可覆盖已有文件)
- 出于安全考虑,上传不会把已有目录替换成文件;若最终目标是目录会直接拒绝
详细 API 和完整示例请分别查看:
当前文档只保留:
- 项目简介
- 快速开始
- 核心能力概览
- 文档入口导航
详细内容已拆分为独立文档:
适合查阅:
- 构造参数
- 生命周期 API
- 双向 RPC API
- 广播 / 订阅 / 推送 API
- 在线状态与密钥管理 API
- 完整
fsAPI - 事件列表
- 返回值与使用建议
适合查阅:
- 最小可运行示例
- Master / Slave 启动方式
- 双向 RPC 示例
- 广播、订阅、推送示例
- 安全模式示例
fs的 CRUD / patch / upload / download 示例- 实战组织建议与排障建议
适合查阅:
- 真实 ZeroMQ 外层控制帧
req/res与svc_req/svc_res的区别- 明文模式与安全模式的差异
fsservice 的底层承载方式- 抓包、协议对接、排障参考
通过 node.on(eventName, handler) 监听:
| 事件 | 触发方 | 说明 |
|---|---|---|
router |
Master | ROUTER socket 收到原始帧(所有类型) |
dealer |
Slave | DEALER socket 收到原始帧(所有类型) |
request |
两者 | 解析出 RPC 请求帧(认证通过后) |
response |
两者 | 解析出 RPC 响应帧 |
message |
两者 | 所有解析消息的统一事件 |
publish |
Slave | 收到 master 广播,携带 { topic, payload } |
push |
Master | 收到 slave 推送,携带 { identityText, topic, payload } |
slave_connected |
Master | slave 注册成功上线,携带 slaveId |
slave_disconnected |
Master | slave 注销或发送失败下线,携带 slaveId |
auth_failed |
Master / Slave | 认证失败(签名校验失败、重放检测失败、解密失败等),请求已被丢弃 |
error |
两者 | 内部错误 |
⚠️ 建议:请始终为error事件注册监听器(node.on("error", handler))。Node.js 的
EventEmitter在触发error且没有任何监听器时,会抛出异常并可能导致进程退出。 请在生产环境中自行监听并接入日志/告警系统。
完整事件说明请查看:
docs/README.api.md
# 终端 1:启动 Master
pnpm example:master
# 终端 2:启动 Slave(可指定 ID)
pnpm example:slave
node test/slave/index.js slave-001如需查看更多实践示例,请阅读:
docs/README.examples.md
在同一进程内启动 Master / Slave,自动验证:
- RPC
- 并发
- 认证
- 超时
- PUB/SUB
- 心跳恢复
- 在线状态 API
- 内建
fs文件服务
pnpm test如需输出详细测试日志(包含每个用例与断言),可开启 verbose:
ZNL_TEST_VERBOSE=1 pnpm test# 终端 1:启动 Echo 服务端(plain)
pnpm test:echo
# 终端 2:发起并发压测(plain)
pnpm test:100 -- 100 10000 slave-001# 终端 1:加密模式启动 Echo 服务端
ZNL_AUTH_KEY=my-secret ZNL_ENCRYPTED=true pnpm test:echo
# 终端 2:加密模式压测
pnpm test:100 -- 100 10000 slave-001 my-secret true当前版本对 Dealer 的发送策略更严格。建议先等待 slave.isMasterOnline() === true,再发送首个业务请求。
更完整的启动建议请查看:
docs/README.examples.md
常见原因:
- 主从机器时间差过大
- 节点时间被手动修改
- 历史旧消息在较晚时间才被投递
协议与安全细节请查看:
docs/README.protocol.md
不是。该值表示最近一次链路确认成功,适合作为业务层在线状态参考,但不是一次即时网络探针。
如果你只想快速记住 ZNL 的核心入口,可以先记住这些:
- 双向 RPC:
DEALER()/ROUTER() - 广播:
PUBLISH()/SUBSCRIBE() - 推送:
PUSH() - 在线状态:
masterOnline/isMasterOnline() - 文件服务:
slave.fs.setRoot()+master.fs.* - 安全模式:
encrypted: true+authKey/authKeyMap
详细内容请继续阅读: