From 3749b9bdf1c827d666c9e7fe0396b72f60df6b06 Mon Sep 17 00:00:00 2001 From: Nixevol Date: Wed, 30 Sep 2026 14:56:25 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E5=86=99=E5=85=A5=20K-00=20SDK=20?= =?UTF-8?q?=E8=A1=8C=E4=B8=BA=E7=BA=A6=E5=AE=9A=E9=99=84=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/DEVELOPMENT.md | 35 +++++++++++++++++++++++++++++++++-- docs/DEVIATIONS.md | 9 +++++++++ sdk/go/README.md | 12 ++++++++++++ sdk/java/README.md | 12 ++++++++++++ sdk/js/README.md | 12 ++++++++++++ sdk/python/README.md | 12 ++++++++++++ 6 files changed, 90 insertions(+), 2 deletions(-) diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 71654de..bc78844 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -958,7 +958,7 @@ close() 1. WebSocket 到 `路径/mqtt`,子协议 `mqtt`。裸 TCP 只在选项里显式打开时使用。浏览器页面是 HTTPS 时,服务器也必须用 TLS:浏览器不允许 HTTPS 页面连 `ws://` 或请求 `http://`。 2. 每次连接都带 Clean Start,包括重连。Go 的 autopaho 只在第一次连接使用 `CleanStartOnInitialConnection`,必须用 `ConnectPacketBuilder` 在每一次连接把 Clean Start 设为 true,会话过期间隔设为 0。 3. 订阅 down,发 `hello`,等到成功响应。连接超时默认 30 秒(autopaho 默认 10 秒,要改):服务器重启后大量端同时重连,密码校验要排队。 -4. 重连退避:1 秒起,加倍,上限 30 秒,加减 30% 抖动。稳定在线 60 秒后把退避恢复到 1 秒。 +4. 重连退避:见本节附录「SDK 行为约定」。 5. 会话令牌:用密码连上后,把握手响应里的 `session_token` 通过 `onSession` 交给应用,之后自动重连都用这个令牌;应用下次启动可以直接用保存的令牌 `connect`。SDK 自己不落盘保存令牌和密码。 6. 停止重连的情况:收到 `fatal`;被 `0x8E` 接管;CONNACK 为 `0x86` 用户名或密码错误、`0x87` 未授权、`0x8A` 已封禁(3.1.1 为返回码 4、5)。这时事件 `auth_failed` 带原因:用令牌连接被拒为 `session_invalid`(在别处登录过、令牌过期、密码被改或重置、已退出登录、编号被停用或删除),用密码被拒为 `bad_credentials`。其他失败,包括网络错误、`0x88` 服务器不可用、`0x89` 服务器忙、连接被直接关闭,都继续重连。 @@ -966,7 +966,7 @@ close() - 发送队列在内存,默认最多 1000 条,包括已发出但没收到 `resp` 的。连不上时 `send` 入队;重连后按原消息号、原请求内容再交。 - `sendAt` 在调用 `send` 时就换算成 `send_at_ms`,重交时不重算,否则请求指纹变了会被当成冲突。 -- 同时在途(已发出、没收到 `resp`)的请求不超过 100 个。发送收到 `rate_limited` 时按退避自动重交,不算失败;其他请求收到 `rate_limited` 直接返回给应用。 +- 同时在途(已发出、没收到 `resp`)的请求不超过 100 个。发送收到 `rate_limited` 时按附录「SDK 行为约定」自动重交,不算失败;其他请求收到 `rate_limited` 直接返回给应用。 - 进程退出则队列丢失。SDK 停止重连(被踢、认证失败、`logout`、`close`)时,队列里的发送全部以对应错误结束。 - 其他调用在未握手时返回未连接。 @@ -1019,6 +1019,37 @@ close() 14. 仅 JS:浏览器页面和服务器不同域名时,注册和 WebSocket 连接都成功。 15. 会话令牌:密码登录后收到令牌;断开后用令牌重连成功;另一处用密码登录后,原来那处用旧令牌重连被拒(`session_invalid`)且不再重连;`logout` 后令牌失效。 +### 附录:SDK 行为约定 + +四种语言同一套对外语义。实现以本附录为准,不以任何一套现有 SDK 为参照。Issue K-00 (#57) 及第二轮审查「补充」条目已并入下表。 + +| 项 | 约定 | +|---|---| +| 首次连接超时 | 默认 30 秒内没完成握手:停止重连,返回 `not_connected`;Client 可再次调用 connect | +| 认证失败错误码 | 直接用原因:`bad_credentials`、`session_invalid`、`disabled`、`deleted`、`password_reset`、`taken_over`、`rate_limited`;事件 `auth_failed` / `kicked` 带同一原因 | +| 顶号原因名 | MQTT DISCONNECT `0x8E` 一律报 `taken_over`,不报 `"0x8E"` | +| 服务器关闭 | DISCONNECT `0x8B` 按可重试处理,继续重连 | +| 本地队列满 | 统一 `queue_full`,不用服务端的 `quota_exceeded` | +| 请求返回值 | 只返回 resp 的 `data`,不返回整个信封 | +| sendAt 与 delay 同时给 | 本地返回 `bad_request`(PRD F11) | +| 停止重连后再 send | 立即以停止原因失败,不入队 | +| logout | 服务端请求失败(含离线)时把错误返回给应用;本地照常停止重连、清空令牌、断开传输;队列里的发送按「停止重连后再 send」的停止原因结束,并发 `offline` 事件 | +| 时长参数 | 毫秒时长和时间戳一律用 64 位整数(Java 用 `long` / `Long`) | +| max_receive_bytes 小于 1024 | 本地返回 `bad_request` | +| URL 映射 | `http`→`ws`、`https`→`wss`;`ws`/`wss` 原样;`mqtt`/`mqtts` 只在显式开启裸 TCP 选项时允许;路径为空或 `/` 时用 `/mqtt`,否则按原路径 | +| 取消与超时 | 尚未发出的条目取消即出队;已发出的返回「结果未知」错误(错误码 `result_unknown`),应用应以同一消息号重试 | +| rate_limited 重交退避 | 每条发送单独计数:第 n 次等待 `min(1 秒 × 2^(n-1), 30 秒) × 随机(0.7, 1.3)`,成功后清零。每次重交(包括断线后的重交)都重新生成 `rid` 并重新序列化帧;消息 `id`、正文和 `send_at_ms` 保持不变 | +| 重连退避 | 只维护一个连续失败计数 n(n ≥ 1)。第 n 次等待 `min(1 秒 × 2^(n-1), 30 秒) × 随机(0.7, 1.3)`。只有应用调用 connect 后的第一次连接可以不等待;已建立的连接断开后,第一次重连也按 n=1 等约 1 秒。连接尝试失败时 n 加 1;连上后不足 60 秒又断开,也按一次失败计(n 加 1);稳定在线满 60 秒后断开,n 重置为 1。不能再另把 base 翻倍。连接超时覆盖等 CONNACK 的时间 | +| 心跳 | 默认 30 秒 | +| Receive Maximum | CONNECT 不带该属性 | +| 默认请求超时 | 非发送请求默认 60 秒 | + +重连退避标称间隔(去掉抖动):n=1…6 为 1、2、4、8、16、30 秒。rate_limited 重交用同一套标称间隔,但按**该条发送**自己的计数,与连接失败计数无关。 + +四套 SDK 为上表每一项使用同名用例(Go `TestK00*`,JS/Python `test_k00_*`,Java `testK00*`): + +`FirstConnectTimeout`、`AuthErrorCodes`、`TakenOverReason`、`Disconnect8BRetryable`、`QueueFull`、`RequestReturnsData`、`SendAtAndDelayConflict`、`SendAfterStopped`、`LogoutReturnsError`、`DurationInt64`、`MaxReceiveBytesMin`、`URLMapping`、`CancelUnsent`、`RateLimitedBackoff`、`ReconnectBackoff`、`KeepaliveDefault`、`NoReceiveMaximum`。 + ## 10. 无 SDK 的设备 使用 MQTT 5 客户端(例如 ESP-IDF 的 mqtt,走 WebSocket 或 TCP)。ClientID 和用户名是端编号,密码是登录密码。订阅 `nix/c/{编号}/down`,向 `nix/c/{编号}/up` 发第 6 节的 JSON。收到 `msg` 后处理完发 `ack`。用「from + id」去重,已确认过的再到达时再回一次 `ack`。在 `hello` 里按自己的接收缓冲声明 `max_receive_bytes`(不小于 1024)。裸设备要么实现 `receipt_ack`,要么发送时带 `receipt:false`,否则未确认的回执会占满推送窗口。 diff --git a/docs/DEVIATIONS.md b/docs/DEVIATIONS.md index 2e38d01..6cd5092 100644 --- a/docs/DEVIATIONS.md +++ b/docs/DEVIATIONS.md @@ -1153,6 +1153,15 @@ - 备选:ack 发后不等 `resp`;未采用,以免丢「ack 结果当 revoked」语义。 - 影响:行为更接近文档;单测仍绿。 +### 复审修复 K-00 + +- 日期:2026-09-30 +- 原条款:PRD F19「四种 SDK 行为一致」;DEVELOPMENT 第 9 节重连退避原文过简;issue #57 及第二轮审查修订。 +- 实际做法:在 DEVELOPMENT 第 9 节增加附录「SDK 行为约定」,四套 README 各加摘要。重连退避按 `min(1s×2^(n-1), 30s)×随机(0.7,1.3)`、只维护一个计数 n;不以现有 JS 实现为参照。rate_limited 与断线重交每次新 rid;logout 失败返回应用;时长用 64 位整数;CONNECT 不带 Receive Maximum;0x8B 可重试;顶号原因 `taken_over`。 +- 原因:四套对外语义不一致;第二轮审查核实 JS 退避双重翻倍,照搬会把错误带进四套。 +- 备选方案:统一采用旧 S1.3 状态机;否决。 +- 影响:K-01 至 K-04 按本附录实现;本条只改文档。 + ## SDK 二 S2 ### S2-PY/JAVA 1–3 2026-09-30 diff --git a/sdk/go/README.md b/sdk/go/README.md index 6ccde5f..7b40cfb 100644 --- a/sdk/go/README.md +++ b/sdk/go/README.md @@ -39,6 +39,18 @@ res, err := nixmsg.Register(ctx, "ws://127.0.0.1:7443/mqtt", "reg-code", nixmsg.RegisterOptions{ID: "device-1", LoginPassword: "secret", Name: "门口"}) ``` +## 行为约定 + +四种 SDK 同一套对外语义,详见仓库 `docs/DEVELOPMENT.md` 第 9 节附录「SDK 行为约定」。要点: + +- 首次连接默认 30 秒内未握手则停止重连,返回 `not_connected`;Client 可再次 `Connect`。 +- 顶号原因一律 `taken_over`;DISCONNECT `0x8B` 可重试;CONNECT 不带 Receive Maximum;心跳默认 30 秒。 +- 重连退避只维护一个计数 n:第 n 次等待 `min(1s×2^(n-1), 30s)×随机(0.7,1.3)`。仅 `Connect` 后第一次可不等待;断线后首次重连也约 1 秒;在线不足 60 秒断开则 n+1,稳定 60 秒后断开 n=1。 +- `rate_limited` 与断线重交:每次重新生成 `rid` 并重新序列化;消息 `id`、正文、`send_at_ms` 不变。 +- `Logout` 请求失败返回给应用;本地仍停止重连、清空令牌、结束队列并发 `offline`。 +- 时长与时间戳用 64 位整数。`sendAt` 与 `delay` 同时给时本地 `bad_request`。停止重连后再 `Send` 立即失败。 +- 取消尚未发出的条目即出队;已发出的返回 `result_unknown`,应用应以同一消息号重试。 + ## 许可证 见 `LICENSE`(专有)。 diff --git a/sdk/java/README.md b/sdk/java/README.md index 1b994cd..4a197e8 100644 --- a/sdk/java/README.md +++ b/sdk/java/README.md @@ -45,6 +45,18 @@ c.close(); 命令行示例类:`asia.asio.nixmsg.examples.MinimalExample`。 +## 行为约定 + +四种 SDK 同一套对外语义,详见仓库 `docs/DEVELOPMENT.md` 第 9 节附录「SDK 行为约定」。要点: + +- 首次连接默认 30 秒内未握手则停止重连,以 `not_connected` 失败;Client 可再次 `connect`。 +- 顶号原因一律 `taken_over`;DISCONNECT `0x8B` 可重试;CONNECT 不带 Receive Maximum;心跳默认 30 秒。 +- 重连退避只维护一个计数 n:第 n 次等待 `min(1s×2^(n-1), 30s)×随机(0.7,1.3)`。仅 `connect` 后第一次可不等待;断线后首次重连也约 1 秒;在线不足 60 秒断开则 n+1,稳定 60 秒后断开 n=1。 +- `rate_limited` 与断线重交:每次重新生成 `rid` 并重新序列化;消息 `id`、正文、`send_at_ms` 不变。 +- `logout` 请求失败时 future 以异常完成;本地仍停止重连、清空令牌、结束队列并发 `offline`。 +- 时长与时间戳用 `long` / `Long`(含 `updateSelf` 的 `defaultDelayMs`)。`sendAt` 是本机时间(epoch 毫秒或 `java.util.Date`,入队时加偏差);`sendAtMs` 是服务器时间。两者与 `delayMs` 同时给时本地 `bad_request`。停止重连后再 `send` 立即失败。 +- 取消尚未发出的条目即出队;已发出的返回 `result_unknown`,应用应以同一消息号重试。 + ## 打包(不发布) ```bash diff --git a/sdk/js/README.md b/sdk/js/README.md index 6075870..b46a3bc 100644 --- a/sdk/js/README.md +++ b/sdk/js/README.md @@ -39,6 +39,18 @@ await c.close(); 浏览器页面与服务器不同源时,注册接口已回 `Access-Control-Allow-Origin: *`,WebSocket `/mqtt` 不校验 Origin,可直接连接。 +## 行为约定 + +四种 SDK 同一套对外语义,详见仓库 `docs/DEVELOPMENT.md` 第 9 节附录「SDK 行为约定」。要点: + +- 首次连接默认 30 秒内未握手则停止重连,`connect()` 返回 `not_connected`;Client 可再次 `connect`。 +- 顶号原因一律 `taken_over`;DISCONNECT `0x8B` 可重试;CONNECT 不带 Receive Maximum;心跳默认 30 秒。 +- 重连退避只维护一个计数 n:第 n 次等待 `min(1s×2^(n-1), 30s)×随机(0.7,1.3)`。仅 `connect` 后第一次可不等待;断线后首次重连也约 1 秒;在线不足 60 秒断开则 n+1,稳定 60 秒后断开 n=1。不要用「attempt 与 base 同时翻倍」。 +- `rate_limited` 与断线重交:每次重新生成 `rid` 并重新序列化;消息 `id`、正文、`send_at_ms` 不变。 +- `logout` 请求失败返回给应用;本地仍停止重连、清空令牌、结束队列并发 `offline`。 +- 时长与时间戳用 64 位整数。`sendAt` 与 `delay` 同时给时本地 `bad_request`。停止重连后再 `send` 立即失败。 +- 取消尚未发出的条目即出队;已发出的返回 `result_unknown`,应用应以同一消息号重试。 + ## 打包试运行 ```bash diff --git a/sdk/python/README.md b/sdk/python/README.md index 2ee04e3..bdb5f63 100644 --- a/sdk/python/README.md +++ b/sdk/python/README.md @@ -38,6 +38,18 @@ c.close() 更完整的命令行示例见 `examples/minimal.py`。 +## 行为约定 + +四种 SDK 同一套对外语义,详见仓库 `docs/DEVELOPMENT.md` 第 9 节附录「SDK 行为约定」。要点: + +- 首次连接默认 30 秒内未握手则停止重连,抛出 `not_connected`;Client 可再次 `connect`。 +- 顶号原因一律 `taken_over`;DISCONNECT `0x8B` 可重试;CONNECT 不带 Receive Maximum;心跳默认 30 秒。 +- 重连退避只维护一个计数 n:第 n 次等待 `min(1s×2^(n-1), 30s)×随机(0.7,1.3)`。仅 `connect` 后第一次可不等待;断线后首次重连也约 1 秒;在线不足 60 秒断开则 n+1,稳定 60 秒后断开 n=1。 +- `rate_limited` 与断线重交:每次重新生成 `rid` 并重新序列化;消息 `id`、正文、`send_at_ms` 不变。 +- `logout` 请求失败抛给应用;本地仍停止重连、清空令牌、结束队列并发 `offline`。 +- 时长与时间戳用 64 位整数。`send_at` 是本机时间(会加时钟偏差写成 `send_at_ms`);`send_at_ms` 是服务器时间。两者与 `delay_ms` 同时给时本地 `bad_request`。停止重连后再 `send` 立即失败。 +- 取消尚未发出的条目即出队;已发出的返回 `result_unknown`,应用应以同一消息号重试。 + ## 打包(不发布) ```bash