Moon NATS Link 是用 MoonBit 编写的 Core NATS 客户端。协议内核把 TCP 分块还原为二进制安全的 MSG/HMSG,native 客户端再用一条连接承载发布订阅、队列任务和请求应答。
flowchart LR
App[应用代码] --> API[Client API]
API -->|PUB / HPUB / SUB / UNSUB / PING| Writer[Writer 邮箱]
Writer --> Server[nats-server]
Server -->|INFO / MSG / HMSG / PING / PONG| Reader[Reader]
Reader --> Decoder[wire.Decoder]
Decoder --> Router[SID 路由]
Router --> Sub[订阅邮箱]
Router --> Inbox[请求 inbox]
Router -->|PONG| Flush[FIFO flush 等待者]
Sub --> App
Inbox --> App
Flush --> API
先在 Linux 或 macOS 启动 nats-server v2.14.6:
nats-server -p 4222另开终端运行请求应答示例:
moon run src/examples/request_reply --target nativerequest/reply ok: headers + 503 no responders + timeout
这个示例先订阅 rpc.echo,携带 Trace-Id 发出请求,再从随机 reply inbox 收到带 Handled-By 的响应。随后它请求一个无人订阅的 subject,确认服务端 503 被映射为 NoResponders;最后让一个已订阅的处理者保持沉默,确认 120 ms 后得到 Timeout,并完成 inbox 取消与 flush 屏障。
每次请求使用 12 个系统随机字节形成 inbox 前缀,再附加连接内递增序号。reply SID 在发布前发送 UNSUB sid 1,因此服务器只会投递第一份响应。
event_fanout 在同一连接上建立精确订阅 events.binary 和通配订阅 events.*,随后发布 00 0d 0a ff。payload 中间的 CRLF 不会被误认成协议边界,两份订阅各收到一份内容相同的消息。
moon run src/examples/event_fanout --target nativeevent fanout ok: 00 0d 0a ff reached both subscriptions
queue_workers 为 jobs.build 建立两个同名为 workers 的队列订阅。服务端从组内选择一个成员交付 task-a,示例通过并发等待两个邮箱来接收被选中的那一份,不假定成员之间存在固定轮转顺序。
moon run src/examples/queue_workers --target nativequeue group ok: one worker claimed task-a
连接先读取 INFO,发出启用 headers 与 No Responders 的 CONNECT,再用一次 PING/PONG 确认握手写入已经到达服务器。token 或用户名/密码由 Options::new 单独接收,只进入 CONNECT JSON,不拼进服务器地址;两种认证不能混用,缺少一半用户名密码也会在连接前被拒绝。服务端在握手阶段返回的授权错误会收敛为 AuthenticationFailed。with_client 随后启动唯一的 reader 和 writer;发布、订阅、取消与 PONG 响应都经过 writer 邮箱。
Options::new(servers=["10.0.0.8:4222", "10.0.0.9:4222"]) 会复制这份地址表,并按给定顺序寻找第一个能完成 INFO、CONNECT 和 PING/PONG 的服务器。服务器通过 connect_urls 通告的成员排在这些种子之后;后续 INFO 会替换旧的发现列表,但不会删除调用者配置的地址。回退只发生在 Client 交给回调之前;回调已经开始后,无论是业务错误还是连接错误都会直接返回,不会在另一台服务器上重放这段代码。单地址参数 server 仍可使用,但不能和 servers 同时传入。
socket EOF 或 writer 失败会先把连接切到 Disconnected。已经等待 writer 或 PONG 的调用会同时被唤醒;这个间隔内新发起的 publish、subscribe、request、flush 和 drain 不进入旧 writer 邮箱。普通订阅保留用于恢复,尚未完成的 request inbox 与正在 drain 的 SID 则直接结束,避免换连接后重放一次语义不明的请求。本版仍会在这一步结束 with_client 作用域,尚未启动新的 TCP 连接。
TLS 不提供跳过验证的开关。TlsOptions::new("nats.internal", ca_file="certs/root.pem") 会以给定名称校验证书,并只信任这份 PEM 中的根;省略 ca_file 时使用系统根证书。默认保持 NATS 的 INFO-first 升级顺序,连接 handshake_first: true 的服务器时显式传入同名选项。TCP 地址、证书名称和信任根分别保存,认证信息不会借 TLS 配置回到地址字符串。
wire.Decoder 在控制行状态和定长 body 状态之间切换。HMSG 的 header length 切出完整 NATS/1.0 header block,total length 决定 payload 终点;消息体不会经过 UTF-8 转换。客户端解析 header 时保留字段大小写、重复值与到达顺序,并拒绝 CRLF 注入或非 ASCII 字段。
每个订阅最多积压 1024 条消息或 8 MiB 的消息体;HMSG 的原始 header block 也计入字节上限。超限的 SID 关闭为 SlowConsumer,writer 发送 UNSUB 并等待后续 PONG 再清理路由,其余 SID 不会被这个慢订阅卡住。
订阅在写入前保存 subject、queue group 和可选消息上限。普通订阅由 writer 安装一条 SUB;request inbox 的 SUB 与 UNSUB sid 1 在同一个 writer 命令中连续写出,不允许并发发布插进两者之间。Client::flush 将等待者排入 FIFO 队列后发送 PING,reader 收到相应的 PONG 才唤醒等待者。普通 unsubscribe 在调用时立即关闭本地投递,再由 writer 原子排入 UNSUB + PING;SID 暂留在路由表中吸收在途消息,后续 PONG 才把它移除。Subscription::drain 在 PONG 前仍接纳消息;消费者处理完最后一条消息,再由下一次 next 观察 SubscriptionClosed 并完成 drain。两种协议动作都由 writer/reader 持有,即使调用任务被取消也能完成 SID 收尾。
Client::drain 先禁止新订阅和 request,批量收束活跃 SID;这个阶段仍允许消费者发布最后的响应。所有消费者观察到邮箱关闭后进入发布收尾,再跨一次 PING/PONG 屏障并关闭所有客户端操作。由于客户端提供的是拉取式邮箱,仍有积压时需要另一个任务持续调用 Subscription::next,直到得到 SubscriptionClosed。
只观察握手时序可以运行:
moon run src/examples/handshake --target nativehandshake ok: INFO -> CONNECT -> PING/PONG
连接与邮箱之间更细的所有权取舍记录在 一条连接里的消息时序。
moon fmt --check
moon check src/wire --target all --deny-warn --warn-list +73
moon test src/wire --target all --deny-warn --warn-list +73
moon check src/client --target native --deny-warn --warn-list +73
moon test src/client --target native --deny-warn --warn-list +732026-09-11 使用 MoonBit 0.1.20260904 在 wasm、wasm-gc、js、native 四个后端分别运行 22 个 wire 测试;Ubuntu 24.04 WSL 使用同版工具链运行 28 个 native 客户端测试。两组测试全部通过。邮箱测试覆盖消息条数、HMSG 字节计数、读取后的配额归还、慢 SID 隔离和 PONG 后的路由清理;CONNECT 编码测试也覆盖凭据中的引号、反斜线与 CRLF。客户端连接校验过 SHA-256 的官方 nats-server v2.14.6 后,实际完成二进制扇出、队列领取、带 headers 的 request/reply、503 No Responders、超时清理、flush 与 unsubscribe;连续发布 1025 条未消费消息时也实际得到 SlowConsumer,随后另一 SID 仍能收到消息。两个受保护实例分别验证过 token 和用户名/密码,正确凭据完成二进制发布订阅,错误 token 与错误密码均返回 AuthenticationFailed。drain 探针进一步确认单订阅收尾后连接仍可复用,整连接收尾会交付 PONG 前的最后一条消息并拒绝后续发布。
2026-09-12 新增的 3 个 TLS 选项测试使 native 客户端测试增至 31 个。使用同一份校验过 SHA-256 的 nats-server v2.14.6 发布包生成一张带 localhost SAN 的临时自签证书,并把它作为自定义信任根后,INFO-first 与 TLS-first 两种服务端配置都完成了 00 0d 0a ff 发布订阅;把校验名称改为 wrong.example 时,握手被拒绝并返回 TlsHandshakeFailed。
同日加入服务器池输入与复制语义测试后,native 客户端测试为 34 个。真实连接探针把未监听的 127.0.0.1:4333 放在首位、nats-server v2.14.6 放在第二位,二进制消息仍完成往返;把可用服务器放在首位并让回调主动失败时,回调计数保持为 1,原错误没有被第二次连接覆盖。
继续处理 INFO 拓扑更新后,native 客户端测试增至 38 个。两个 nats-server v2.14.6 实例在 Ubuntu 24.04 WSL 中组成路由集群,客户端只配置 127.0.0.1:14222,握手后地址表实际得到服务器通告的 127.0.0.1:14333;测试结束后两台服务均停止且端口释放。
订阅安装改由单条 writer 命令持有后,native 客户端测试增至 42 个;记录型 writer 测试观察到 request inbox 的 SUB 与 UNSUB sid 1 相邻写出,订阅状态测试确认 drain 中或已超限的 SID 不会生成恢复命令。随后在 Ubuntu 24.04 WSL 启动校验过 SHA-256 的 nats-server v2.14.6,请求应答示例再次完成 headers、503 No Responders 与 timeout 三条路径。
断线边界加入 6 个 native 测试后,客户端测试为 48 个。测试覆盖未完成 publish 的唤醒、writer 写失败、普通订阅与 request inbox 的分流、重连间隔拒绝新命令、订阅 drain 被断线中止,以及尚未写完 SUB 的 SID 清理;六条路径均返回各自的确定结果,没有把命令或无句柄 SID 留给失去消费者的 writer 邮箱。
Apache-2.0