refactor(core): 拆分 model/mqtt/update 为单一职责子模块
- model/ 按领域拆为 connection/subscription/message/publish/variable/log/settings - mqtt/ 拆为门面(引擎与事件)、tls、v5、v4 四块 - update/ 拆为门面(版本工具与命名约定)、check(检查)、install(下载/安装/清理) - sysmon 只保留进程资源采样,窗口置顶迁至新 platform.rs - 对外路径经 mod.rs 再导出保持不变,测试随门面保留(33 通过)
This commit is contained in:
@@ -0,0 +1,317 @@
|
||||
//! MQTT 3.1.1:MqttOptions/last-will 组装与事件循环泵。
|
||||
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use rumqttc::mqttbytes::v5::{
|
||||
ConnectReturnCode as V5ReturnCode, Packet as V5Packet,
|
||||
};
|
||||
use rumqttc::Event as V5Event;
|
||||
use rumqttc_v4 as rv4;
|
||||
use tokio::sync::watch;
|
||||
|
||||
use crate::model::{
|
||||
ConnectionConfig, ConnectionStatus, Direction, LogLevel, MqttRecord, TransportKind,
|
||||
};// ─── 引擎事件 ────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
use super::*;
|
||||
|
||||
pub(super) fn build_v4(
|
||||
cfg: &ConnectionConfig,
|
||||
) -> Result<(rv4::AsyncClient, rv4::EventLoop), String> {
|
||||
use rv4::{Broker as V4Broker, MqttOptions as V4Options, Transport as V4Transport};
|
||||
|
||||
let custom_tls = resolve_tls_config(cfg)?;
|
||||
let mut opts = match cfg.transport {
|
||||
TransportKind::Tcp | TransportKind::Tls => {
|
||||
V4Options::new(cfg.client_id.clone(), V4Broker::tcp(&cfg.host, cfg.port))
|
||||
}
|
||||
TransportKind::Ws => {
|
||||
let url = format!("ws://{}:{}{}", cfg.host, cfg.port, normalize_path(&cfg.path));
|
||||
V4Options::new(cfg.client_id.clone(), V4Broker::websocket(url).map_err(|e| format!("{e:?}"))?)
|
||||
}
|
||||
TransportKind::Wss => {
|
||||
let url = format!("wss://{}:{}{}", cfg.host, cfg.port, normalize_path(&cfg.path));
|
||||
match &custom_tls {
|
||||
Some(tls) => V4Options::websocket_with_tls_config(
|
||||
cfg.client_id.clone(),
|
||||
url,
|
||||
tls.clone(),
|
||||
)
|
||||
.map_err(|e| format!("{e:?}"))?,
|
||||
None => V4Options::try_websocket_with_default_tls(cfg.client_id.clone(), url)
|
||||
.map_err(|e| format!("{e:?}"))?,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if cfg.transport == TransportKind::Tls {
|
||||
match &custom_tls {
|
||||
Some(tls) => {
|
||||
opts.set_transport(V4Transport::tls_with_config(tls.clone()));
|
||||
}
|
||||
None => {
|
||||
opts.set_transport(
|
||||
V4Transport::try_tls_with_default_config()
|
||||
.map_err(|e| format!("TLS 初始化失败: {e:?}"))?,
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
opts.set_keep_alive(cfg.keep_alive);
|
||||
opts.set_clean_session(cfg.clean_start);
|
||||
if !cfg.username.is_empty() {
|
||||
opts.set_credentials(cfg.username.clone(), cfg.password.clone());
|
||||
}
|
||||
if let Some(will) = &cfg.last_will
|
||||
&& !will.topic.is_empty() {
|
||||
// 3.1.1 的遗嘱没有 content_type / response_topic 属性,忽略
|
||||
let lw = rv4::mqttbytes::v4::LastWill::new(
|
||||
will.topic.clone(),
|
||||
will.payload.as_bytes().to_vec(),
|
||||
v4_qos(will.qos),
|
||||
will.retain,
|
||||
);
|
||||
opts.set_last_will(lw);
|
||||
}
|
||||
|
||||
let (client, mut eventloop) = rv4::AsyncClient::builder(opts).build();
|
||||
// v4 的连接超时(含 CONNACK 等待)由 EventLoop 的 NetworkOptions 控制
|
||||
let mut network_options = rv4::NetworkOptions::new();
|
||||
network_options.set_connection_timeout(connection_timeout_secs(cfg));
|
||||
eventloop.set_network_options(network_options);
|
||||
Ok((client, eventloop))
|
||||
}
|
||||
|
||||
pub(super) fn v4_qos(qos: u8) -> rv4::QoS {
|
||||
match qos {
|
||||
1 => rv4::QoS::AtLeastOnce,
|
||||
2 => rv4::QoS::ExactlyOnce,
|
||||
_ => rv4::QoS::AtMostOnce,
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 测试连接握手 ────────────────────────────────────────────────────────────
|
||||
|
||||
pub(super) async fn test_handshake(cfg_in: &ConnectionConfig) -> Result<(), String> {
|
||||
// 握手测试必须使用独立 client_id,否则会把使用相同 client_id 的现有连接
|
||||
// 从 broker 顶下线(MQTT 同 client_id 单会话规则)。
|
||||
let mut cfg = cfg_in.clone();
|
||||
cfg.client_id = format!("mqttx_test_{}", &uuid::Uuid::new_v4().simple().to_string()[..8]);
|
||||
let cfg = &cfg;
|
||||
if cfg.protocol.is_v5() {
|
||||
let (client, mut eventloop) = build_v5(cfg)?;
|
||||
loop {
|
||||
match eventloop.poll().await {
|
||||
Ok(V5Event::Incoming(V5Packet::ConnAck(ack))) => {
|
||||
if ack.code == V5ReturnCode::Success {
|
||||
// 测试完成,主动断开避免在 broker 侧残留会话。
|
||||
// disconnect() 只是把请求入队,需再 poll 才会把
|
||||
// DISCONNECT 真正写上网络。
|
||||
let _ = client.disconnect().await;
|
||||
for _ in 0..3 {
|
||||
if eventloop.poll().await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
return Err(format!("连接被拒绝: {:?}", ack.code));
|
||||
}
|
||||
Ok(V5Event::Incoming(V5Packet::Disconnect(d))) => {
|
||||
return Err(format!("服务端断开: {:?}", d.reason_code));
|
||||
}
|
||||
Err(e) => return Err(format!("连接失败: {e}")),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let (client, mut eventloop) = build_v4(cfg)?;
|
||||
loop {
|
||||
match eventloop.poll().await {
|
||||
Ok(rv4::Event::Incoming(rv4::Packet::ConnAck(ack))) => {
|
||||
if ack.code == rv4::ConnectReturnCode::Success {
|
||||
// 同 v5:disconnect() 只入队,需 poll 把报文冲出
|
||||
let _ = client.disconnect().await;
|
||||
for _ in 0..3 {
|
||||
if eventloop.poll().await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
return Err(format!("连接被拒绝: {:?}", ack.code));
|
||||
}
|
||||
Ok(rv4::Event::Incoming(rv4::Packet::Disconnect)) => {
|
||||
return Err("服务端主动断开".into());
|
||||
}
|
||||
Err(e) => return Err(format!("连接失败: {e}")),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 事件循环 ────────────────────────────────────────────────────────────────
|
||||
|
||||
pub(super) fn spawn_v4_loop(
|
||||
engine: Arc<MqttEngine>,
|
||||
id: String,
|
||||
mut eventloop: rv4::EventLoop,
|
||||
mut cancel_rx: watch::Receiver<bool>,
|
||||
connected: Arc<AtomicBool>,
|
||||
generation: u64,
|
||||
auto_reconnect: bool,
|
||||
max_reconnect_times: u32,
|
||||
) {
|
||||
#![allow(clippy::too_many_arguments)]
|
||||
let runtime = engine.runtime.handle().clone();
|
||||
runtime.spawn(async move {
|
||||
let mut first_connect = true;
|
||||
// 已执行的重连次数,用于 max_reconnect_times(0=无限)限流
|
||||
let mut reconnect_attempts: u32 = 0;
|
||||
loop {
|
||||
tokio::select! {
|
||||
event = eventloop.poll() => {
|
||||
match event {
|
||||
Ok(rv4::Event::Incoming(rv4::Packet::Publish(p))) => {
|
||||
let (payload, raw_bytes, payload_truncated) =
|
||||
crate::model::retained_payload(&p.payload);
|
||||
let record = MqttRecord {
|
||||
seq: engine.next_seq(),
|
||||
connection_id: engine.intern(&id),
|
||||
direction: Direction::Received,
|
||||
topic: engine.intern(&String::from_utf8_lossy(&p.topic)),
|
||||
payload,
|
||||
qos: match p.qos {
|
||||
rv4::QoS::AtMostOnce => 0,
|
||||
rv4::QoS::AtLeastOnce => 1,
|
||||
rv4::QoS::ExactlyOnce => 2,
|
||||
},
|
||||
retain: p.retain,
|
||||
timestamp: now_ms(),
|
||||
user_properties: Vec::new(),
|
||||
content_type: None,
|
||||
response_topic: None,
|
||||
correlation_data: None,
|
||||
message_expiry_interval: None,
|
||||
subscription_identifier: None,
|
||||
payload_truncated,
|
||||
raw_bytes,
|
||||
};
|
||||
engine.log(
|
||||
&id, LogLevel::Info, "publish_received",
|
||||
format!("收到消息: {} ({} 字节)", record.topic, p.payload.len()), None);
|
||||
engine.emit(EngineEvent::Message(record));
|
||||
}
|
||||
Ok(rv4::Event::Incoming(rv4::Packet::ConnAck(ack))) => {
|
||||
connected.store(ack.code == rv4::ConnectReturnCode::Success, Ordering::SeqCst);
|
||||
if ack.code == rv4::ConnectReturnCode::Success {
|
||||
first_connect = false;
|
||||
// 重连成功后清零计数:max_reconnect_times 限流的是
|
||||
// 「连续失败次数」,不能把历史错误累计进来
|
||||
reconnect_attempts = 0;
|
||||
engine.log(&id, LogLevel::Info, "connack",
|
||||
format!("连接成功 (session_present: {})", ack.session_present), None);
|
||||
engine.emit(EngineEvent::Status {
|
||||
connection_id: id.clone(),
|
||||
status: ConnectionStatus::Connected,
|
||||
error: None,
|
||||
reason_code: Some(0),
|
||||
session_present: Some(ack.session_present),
|
||||
});
|
||||
} else {
|
||||
engine.log(&id, LogLevel::Error, "connack",
|
||||
format!("连接被拒绝: {:?}", ack.code), None);
|
||||
engine.emit(EngineEvent::Status {
|
||||
connection_id: id.clone(),
|
||||
status: ConnectionStatus::Error,
|
||||
error: Some(format!("{:?}", ack.code)),
|
||||
// 上报真实的 CONNACK 拒绝码,不再硬编码为 1
|
||||
reason_code: Some(ack.code as u8),
|
||||
session_present: Some(ack.session_present),
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(rv4::Event::Incoming(rv4::Packet::Disconnect)) => {
|
||||
connected.store(false, Ordering::SeqCst);
|
||||
engine.emit(EngineEvent::Status {
|
||||
connection_id: id.clone(),
|
||||
status: ConnectionStatus::Disconnected,
|
||||
error: Some("服务端断开".into()),
|
||||
reason_code: None,
|
||||
session_present: None,
|
||||
});
|
||||
break;
|
||||
}
|
||||
Ok(rv4::Event::Incoming(rv4::Packet::SubAck(s))) => {
|
||||
engine.log(&id, LogLevel::Info, "suback",
|
||||
format!("订阅确认 (packet {})", s.pkid), None);
|
||||
}
|
||||
Ok(rv4::Event::Incoming(rv4::Packet::UnsubAck(a))) => {
|
||||
engine.log(&id, LogLevel::Info, "unsuback",
|
||||
format!("取消订阅确认 (packet {})", a.pkid), None);
|
||||
}
|
||||
Ok(rv4::Event::Incoming(rv4::Packet::PubAck(a))) => {
|
||||
engine.log(&id, LogLevel::Debug, "puback",
|
||||
format!("发布确认 (packet {})", a.pkid), None);
|
||||
}
|
||||
Ok(rv4::Event::Outgoing(rv4::Outgoing::Publish(pkid))) => {
|
||||
engine.log(&id, LogLevel::Debug, "publish_outgoing",
|
||||
format!("消息已发出 (packet {pkid})"), None);
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(rv4::ConnectionError::RequestsDone) => {
|
||||
connected.store(false, Ordering::SeqCst);
|
||||
engine.emit(EngineEvent::Status {
|
||||
connection_id: id.clone(),
|
||||
status: ConnectionStatus::Disconnected,
|
||||
error: None, reason_code: None, session_present: None,
|
||||
});
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
connected.store(false, Ordering::SeqCst);
|
||||
engine.emit(EngineEvent::Status {
|
||||
connection_id: id.clone(),
|
||||
status: ConnectionStatus::Error,
|
||||
error: Some(e.to_string()),
|
||||
reason_code: None, session_present: None,
|
||||
});
|
||||
if !auto_reconnect || first_connect {
|
||||
engine.log(&id, LogLevel::Error, "connection_error",
|
||||
format!("连接错误: {e}"), None);
|
||||
break;
|
||||
}
|
||||
// max_reconnect_times>0 时按次数停止重试;0 表示无限重连
|
||||
if max_reconnect_times > 0 && reconnect_attempts >= max_reconnect_times {
|
||||
engine.log(&id, LogLevel::Error, "reconnect_give_up",
|
||||
format!("已重连 {reconnect_attempts} 次仍失败,达到最大重连次数 {max_reconnect_times},停止重连: {e}"), None);
|
||||
break;
|
||||
}
|
||||
reconnect_attempts += 1;
|
||||
engine.log(&id, LogLevel::Warn, "connection_error",
|
||||
format!("连接错误,2 秒后重连(第 {reconnect_attempts} 次): {e}"), None);
|
||||
// 退避等待也必须可取消:否则 close() 的取消信号最长
|
||||
// 2 秒不可见,期间会对用户已关闭的连接继续重连。
|
||||
tokio::select! {
|
||||
_ = cancel_rx.changed() => break,
|
||||
_ = tokio::time::sleep(Duration::from_secs(2)) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = cancel_rx.changed() => {
|
||||
engine.log(&id, LogLevel::Info, "disconnect_cancelled", "连接已取消".into(), None);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 仅当 map 中仍是本代句柄时才移除,避免误删重连后的新句柄
|
||||
engine.remove_handle_if(&id, generation);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user