Skip to main content

sz_rust_core/runtime/
mqtt.rs

1//! sz-orm-mqtt 长连接接入
2//!
3//! ## PHP 对齐
4//!
5//! 对齐 PHP `workerman/mqtt` 的长连接模型:
6//!
7//! ```php
8//! $mqtt = new Workerman\Mqtt\Client('mqtt://tcp://127.0.0.1:1883');
9//! $mqtt->onConnect = function($mqtt) { $mqtt->subscribe('topic'); };
10//! $mqtt->onMessage = function($topic, $content) { /* ... */ };
11//! $mqtt->loop();
12//! ```
13//!
14//! Rust 端复用 `sz_orm_mqtt::MqttPlugin`(InMemory 模拟)或
15//! `sz_orm_mqtt::RealMqttClient`(feature = "real-broker",真实 rumqttc)。
16//!
17//! ## 设计
18//!
19//! - `MqttRuntime`:封装 MqttPlugin,提供 connect/disconnect/publish/subscribe
20//! - `start_keepalive`:spawn 后台任务定期 ping,监听 `CancellationToken` 优雅退出
21
22use std::sync::Arc;
23use std::time::Duration;
24
25use tokio::sync::Mutex;
26use tokio_util::sync::CancellationToken;
27
28use crate::orm::{MqttConfig, MqttError, MqttPlugin, QoS};
29
30/// MQTT 运行时配置
31#[derive(Debug, Clone)]
32pub struct MqttRuntimeConfig {
33    /// 客户端 ID
34    pub client_id: String,
35    /// 心跳间隔(秒,对齐 `MqttConfig::keep_alive`)
36    pub keep_alive_secs: u16,
37    /// 默认订阅主题列表
38    pub topics: Vec<String>,
39    /// Broker URL(仅元数据,InMemory 模式不实际连接)
40    pub broker_url: String,
41}
42
43impl Default for MqttRuntimeConfig {
44    fn default() -> Self {
45        Self {
46            client_id: "sz-rust-mqtt".to_string(),
47            keep_alive_secs: 60,
48            topics: Vec::new(),
49            broker_url: "tcp://localhost:1883".to_string(),
50        }
51    }
52}
53
54impl MqttRuntimeConfig {
55    /// 创建新配置
56    pub fn new(client_id: impl Into<String>) -> Self {
57        Self {
58            client_id: client_id.into(),
59            ..Default::default()
60        }
61    }
62
63    /// 自定义心跳间隔
64    pub fn with_keep_alive(mut self, secs: u16) -> Self {
65        self.keep_alive_secs = secs;
66        self
67    }
68
69    /// 添加订阅主题
70    pub fn with_topic(mut self, topic: impl Into<String>) -> Self {
71        self.topics.push(topic.into());
72        self
73    }
74
75    /// 自定义 broker URL
76    pub fn with_broker_url(mut self, url: impl Into<String>) -> Self {
77        self.broker_url = url.into();
78        self
79    }
80}
81
82/// MQTT 运行时
83///
84/// 封装 `sz_orm_mqtt::MqttPlugin`,提供长连接 lifecycle 管理。
85///
86/// ## 设计
87///
88/// - 使用 `tokio::sync::Mutex` 保护 MqttPlugin(connect/disconnect 需要 `&mut self`)
89/// - `start_keepalive` spawn 后台任务定期检查连接状态,监听 cancel 退出
90/// - 默认使用 InMemory MqttPlugin;启用 `real-broker` feature 后可切换到 RealMqttClient
91pub struct MqttRuntime {
92    config: MqttRuntimeConfig,
93    plugin: Arc<Mutex<MqttPlugin>>,
94}
95
96impl MqttRuntime {
97    /// 创建 MQTT 运行时
98    pub fn new(config: MqttRuntimeConfig) -> Self {
99        let mqtt_config = MqttConfig::new(config.broker_url.clone())
100            .with_client_id(config.client_id.clone())
101            .with_keep_alive(config.keep_alive_secs);
102        let plugin = MqttPlugin::new(mqtt_config);
103        Self {
104            config,
105            plugin: Arc::new(Mutex::new(plugin)),
106        }
107    }
108
109    /// 连接 MQTT broker(对齐 `workerman/mqtt` connect)
110    pub async fn connect(&self) -> Result<(), MqttError> {
111        let mut plugin = self.plugin.lock().await;
112        plugin.connect().await
113    }
114
115    /// 断开连接
116    pub async fn disconnect(&self) -> Result<(), MqttError> {
117        let mut plugin = self.plugin.lock().await;
118        plugin.disconnect().await
119    }
120
121    /// 是否已连接
122    pub async fn is_connected(&self) -> bool {
123        let plugin = self.plugin.lock().await;
124        plugin.is_connected()
125    }
126
127    /// 订阅主题
128    pub async fn subscribe(&self, topic: &str, qos: QoS) -> Result<(), MqttError> {
129        let plugin = self.plugin.lock().await;
130        plugin.subscribe(topic, qos).await
131    }
132
133    /// 取消订阅
134    pub async fn unsubscribe(&self, topic: &str) -> Result<(), MqttError> {
135        let plugin = self.plugin.lock().await;
136        plugin.unsubscribe(topic).await
137    }
138
139    /// 发布消息
140    pub async fn publish(&self, topic: &str, payload: Vec<u8>, qos: QoS) -> Result<(), MqttError> {
141        let plugin = self.plugin.lock().await;
142        plugin.publish(topic, payload, qos).await
143    }
144
145    /// 启动心跳保活任务(对齐 `workerman/mqtt` 的 `loop()`)
146    ///
147    /// 每 `keep_alive_secs` 秒检查一次连接状态,若断开则尝试重连。
148    /// 监听 `token.cancelled()` 优雅退出。
149    pub fn start_keepalive(&self, token: CancellationToken) -> tokio::task::JoinHandle<()> {
150        let plugin = self.plugin.clone();
151        let interval_secs = self.config.keep_alive_secs.max(1) as u64;
152
153        tokio::spawn(async move {
154            let mut ticker = tokio::time::interval(Duration::from_secs(interval_secs));
155            loop {
156                tokio::select! {
157                    _ = token.cancelled() => break,
158                    _ = ticker.tick() => {
159                        let mut p = plugin.lock().await;
160                        if !p.is_connected() {
161                            // 尝试重连
162                            if let Err(e) = p.connect().await {
163                                tracing::warn!("mqtt reconnect failed: {}", e);
164                            }
165                        }
166                    }
167                }
168            }
169        })
170    }
171
172    /// 订阅配置中的默认主题(在 connect 之后调用)
173    pub async fn subscribe_default_topics(&self) -> Result<(), MqttError> {
174        for topic in &self.config.topics {
175            self.subscribe(topic, QoS::AtLeastOnce).await?;
176        }
177        Ok(())
178    }
179
180    /// 获取配置
181    pub fn config(&self) -> &MqttRuntimeConfig {
182        &self.config
183    }
184
185    /// 获取订阅数量
186    pub async fn subscription_count(&self) -> usize {
187        let plugin = self.plugin.lock().await;
188        plugin.subscription_count().await
189    }
190
191    /// 获取消息总数(InMemory 模式:所有已 publish 的消息)
192    pub async fn message_count(&self) -> usize {
193        let plugin = self.plugin.lock().await;
194        plugin.message_count().await
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201
202    #[test]
203    fn test_mqtt_runtime_config_default() {
204        let config = MqttRuntimeConfig::default();
205        assert_eq!(config.client_id, "sz-rust-mqtt");
206        assert_eq!(config.keep_alive_secs, 60);
207        assert!(config.topics.is_empty());
208        assert_eq!(config.broker_url, "tcp://localhost:1883");
209    }
210
211    #[test]
212    fn test_mqtt_runtime_config_builder() {
213        let config = MqttRuntimeConfig::new("client-1")
214            .with_keep_alive(30)
215            .with_topic("orders/#")
216            .with_topic("payments/#")
217            .with_broker_url("ssl://broker.example.com:8883");
218        assert_eq!(config.client_id, "client-1");
219        assert_eq!(config.keep_alive_secs, 30);
220        assert_eq!(config.topics.len(), 2);
221        assert_eq!(config.broker_url, "ssl://broker.example.com:8883");
222    }
223
224    #[tokio::test]
225    async fn test_mqtt_connect_disconnect() {
226        let runtime = MqttRuntime::new(MqttRuntimeConfig::new("test-client"));
227        assert!(!runtime.is_connected().await);
228        runtime.connect().await.unwrap();
229        assert!(runtime.is_connected().await);
230        runtime.disconnect().await.unwrap();
231        assert!(!runtime.is_connected().await);
232    }
233
234    #[tokio::test]
235    async fn test_mqtt_subscribe_unsubscribe() {
236        let runtime = MqttRuntime::new(MqttRuntimeConfig::new("test-client"));
237        runtime.connect().await.unwrap();
238
239        runtime
240            .subscribe("test/topic", QoS::AtLeastOnce)
241            .await
242            .unwrap();
243        assert_eq!(runtime.subscription_count().await, 1);
244
245        runtime.unsubscribe("test/topic").await.unwrap();
246        assert_eq!(runtime.subscription_count().await, 0);
247    }
248
249    #[tokio::test]
250    async fn test_mqtt_publish() {
251        let runtime = MqttRuntime::new(MqttRuntimeConfig::new("test-client"));
252        runtime.connect().await.unwrap();
253        runtime
254            .subscribe("test/topic", QoS::AtLeastOnce)
255            .await
256            .unwrap();
257
258        runtime
259            .publish("test/topic", b"hello mqtt".to_vec(), QoS::AtLeastOnce)
260            .await
261            .unwrap();
262
263        assert_eq!(runtime.message_count().await, 1);
264    }
265
266    #[tokio::test]
267    async fn test_mqtt_publish_multiple() {
268        let runtime = MqttRuntime::new(MqttRuntimeConfig::new("test-client"));
269        runtime.connect().await.unwrap();
270        runtime
271            .subscribe("test/topic", QoS::AtLeastOnce)
272            .await
273            .unwrap();
274
275        for i in 0..5 {
276            runtime
277                .publish(
278                    "test/topic",
279                    format!("msg-{}", i).into_bytes(),
280                    QoS::AtLeastOnce,
281                )
282                .await
283                .unwrap();
284        }
285        assert_eq!(runtime.message_count().await, 5);
286    }
287
288    #[tokio::test]
289    async fn test_subscribe_default_topics() {
290        let config = MqttRuntimeConfig::new("test-client")
291            .with_topic("orders/#")
292            .with_topic("payments/#");
293        let runtime = MqttRuntime::new(config);
294        runtime.connect().await.unwrap();
295        runtime.subscribe_default_topics().await.unwrap();
296        assert_eq!(runtime.subscription_count().await, 2);
297    }
298
299    #[tokio::test]
300    async fn test_keepalive_task_stops_on_cancel() {
301        let runtime = MqttRuntime::new(MqttRuntimeConfig::new("test-client").with_keep_alive(1));
302        let token = CancellationToken::new();
303        let handle = runtime.start_keepalive(token.clone());
304
305        // 等待一会儿
306        tokio::time::sleep(Duration::from_millis(100)).await;
307        token.cancel();
308
309        // 任务应该退出
310        let result = tokio::time::timeout(Duration::from_secs(2), handle).await;
311        assert!(result.is_ok(), "keepalive task should stop on cancel");
312    }
313
314    #[tokio::test]
315    async fn test_keepalive_reconnects_after_disconnect() {
316        let runtime = MqttRuntime::new(MqttRuntimeConfig::new("test-client").with_keep_alive(1));
317        runtime.connect().await.unwrap();
318        assert!(runtime.is_connected().await);
319
320        let token = CancellationToken::new();
321        let plugin_clone = runtime.plugin.clone();
322        let handle = runtime.start_keepalive(token.clone());
323
324        // 主动断开
325        {
326            let mut p = plugin_clone.lock().await;
327            p.disconnect().await.unwrap();
328        }
329        assert!(!runtime.is_connected().await);
330
331        // 等待 keepalive tick 触发重连
332        tokio::time::sleep(Duration::from_millis(1500)).await;
333        assert!(runtime.is_connected().await);
334
335        token.cancel();
336        let _ = handle.await;
337    }
338
339    #[test]
340    fn test_config_accessor() {
341        let runtime = MqttRuntime::new(MqttRuntimeConfig::new("test").with_keep_alive(45));
342        assert_eq!(runtime.config().client_id, "test");
343        assert_eq!(runtime.config().keep_alive_secs, 45);
344    }
345
346    #[tokio::test]
347    async fn test_publish_without_connect_returns_error() {
348        let runtime = MqttRuntime::new(MqttRuntimeConfig::new("test-client"));
349        // 未连接直接 publish(InMemory 模式可能允许,取决于 MqttPlugin 实现)
350        let result = runtime
351            .publish("test", b"data".to_vec(), QoS::AtMostOnce)
352            .await;
353        // InMemory MqttPlugin 可能允许也可能拒绝,这里只验证不 panic
354        let _ = result;
355    }
356}