feat: Python/Java SDK 接入清单与打包文档

对真实 nixmsg 跑 DEVELOPMENT 第 9 节接入清单(跳过仅 JS 跨域),补 README/示例,并修 Paho/HiveMQ 真机联调死锁与鉴权分类。
This commit is contained in:
Nixevol
2026-09-30 08:57:06 +08:00
parent 2f750bcb0f
commit 1f715e4e51
15 changed files with 1952 additions and 148 deletions
+71 -29
View File
@@ -11,6 +11,7 @@ import urllib.request
from collections import OrderedDict
from dataclasses import dataclass, field
from typing import Any, Optional
from queue import SimpleQueue
from .errors import ClosedError, NixMsgError, NotConnectedError
from .protocol import dumps, down_topic, loads, normalize_mqtt_ws_url, register_url_from_connect, up_topic
@@ -133,6 +134,9 @@ class Client:
self._worker: Optional[threading.Thread] = None
self._wake = threading.Event()
self._want_connected = False
self._down_q: SimpleQueue = SimpleQueue()
self._down_thread = threading.Thread(target=self._down_loop, name="nixmsg-down", daemon=True)
self._down_thread.start()
self._transport.set_handlers(self._on_transport_connected, self._on_transport_disconnected, self._on_down)
@@ -218,6 +222,10 @@ class Client:
self._transport.disconnect()
except Exception:
pass
try:
self._down_q.put(None)
except Exception:
pass
self._wake.set()
def logout(self) -> None:
@@ -632,40 +640,74 @@ class Client:
self._wake.set()
def _on_down(self, payload: bytes) -> None:
# resp 必须立即完成 pending(含 auto_ack 等待),不能进 down 队列,否则自死锁。
try:
frame = loads(payload)
except Exception:
self._down_q.put(payload)
return
if frame.get("type") == "resp":
self._dispatch_resp(frame)
return
if threading.current_thread() is self._down_thread:
self._dispatch_down_body(frame)
return
self._down_q.put(payload)
def _down_loop(self) -> None:
while True:
payload = self._down_q.get()
if payload is None:
return
try:
frame = loads(payload)
except Exception:
continue
try:
self._dispatch_down_body(frame)
except Exception:
log.exception("处理下行帧失败")
def _dispatch_resp(self, frame: dict[str, Any]) -> None:
rid = str(frame.get("rid", ""))
with self._lock:
pending = self._pending.pop(rid, None)
if not pending:
return
pending.response = frame
if pending.is_send:
with self._lock:
self._inflight_sends = max(0, self._inflight_sends - 1)
err = (frame.get("error") or {}) if not frame.get("ok") else {}
if not frame.get("ok") and str(err.get("code")) == "rate_limited":
with self._lock:
pending.rid = ""
pending.response = None
pending.error = None
pending.event.clear()
self._wake.set()
return
with self._lock:
self._send_queue = [it for it in self._send_queue if it.pending is not pending]
if not frame.get("ok"):
pending.error = NixMsgError(str(err.get("code", "bad_request")), str(err.get("message", "")))
pending.event.set()
self._wake.set()
else:
pending.event.set()
def _dispatch_down(self, payload: bytes) -> None:
try:
frame = loads(payload)
except Exception:
return
ftype = frame.get("type")
if ftype == "resp":
rid = str(frame.get("rid", ""))
with self._lock:
pending = self._pending.pop(rid, None)
if pending:
pending.response = frame
if pending.is_send:
with self._lock:
self._inflight_sends = max(0, self._inflight_sends - 1)
# rate_limited 重交:清状态后不 set event
err = (frame.get("error") or {}) if not frame.get("ok") else {}
if not frame.get("ok") and str(err.get("code")) == "rate_limited":
with self._lock:
pending.rid = ""
pending.response = None
pending.error = None
pending.event.clear()
self._wake.set()
return
# 从发送队列移除
with self._lock:
self._send_queue = [it for it in self._send_queue if it.pending is not pending]
if not frame.get("ok"):
pending.error = NixMsgError(str(err.get("code", "bad_request")), str(err.get("message", "")))
pending.event.set()
self._wake.set()
else:
pending.event.set()
if frame.get("type") == "resp":
self._dispatch_resp(frame)
return
self._dispatch_down_body(frame)
def _dispatch_down_body(self, frame: dict[str, Any]) -> None:
ftype = frame.get("type")
if ftype == "msg":
self._handle_msg(frame)
return