In a previous project, I used MQTTnet for MQTT communication. Initially, I used the standard MqttClient, which required me to write my own reconnection logic—code that was messy and overly long. Later, I discovered that MQTTnet offers an extension package called ManagedMqttClient, which handles reconnections and offline message queuing automatically, making it much more pleasant to use.
Additionally, I’ve always struggled to remember the differences between QoS, CleanSession, and Retain in MQTT, so I’ll note them down here for reference.
Installation
dotnet add package MQTTnet
dotnet add package MQTTnet.Extensions.ManagedClient
ManagedMqttClient is a standalone extension package, not part of the main MQTTnet library.
Basic Usage
var options = new ManagedMqttClientOptionsBuilder()
.WithAutoReconnectDelay(TimeSpan.FromSeconds(5))
.WithClientOptions(new MqttClientOptionsBuilder()
.WithClientId("my-client-001")
.WithTcpServer("broker.hivemq.com", 1883)
.WithCredentials("username", "password")
.WithCleanSession(false)
.Build())
.Build();
var mqttClient = new MqttFactory().CreateManagedMqttClient();
// 收到消息的回调
mqttClient.ApplicationMessageReceivedAsync += e =>
{
var topic = e.ApplicationMessage.Topic;
var payload = Encoding.UTF8.GetString(e.ApplicationMessage.PayloadSegment);
Console.WriteLine($"收到消息 [{topic}]: {payload}");
return Task.CompletedTask;
};
// 连接状态变化
mqttClient.ConnectedAsync += e =>
{
Console.WriteLine("已连接到 Broker");
return Task.CompletedTask;
};
mqttClient.DisconnectedAsync += e =>
{
Console.WriteLine("连接断开,等待重连...");
return Task.CompletedTask;
};
// 订阅主题
await mqttClient.SubscribeAsync(
new MqttTopicFilterBuilder()
.WithTopic("device/sensor/temperature")
.WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce)
.Build());
// 启动
await mqttClient.StartAsync(options);
StartAsync returns immediately after being called, and it internally starts a thread to maintain the connection. After a disconnection, it will automatically reconnect at intervals specified by WithAutoReconnectDelay. Once reconnected, previous subscriptions will also be restored automatically, so you don't need to manually call Subscribe again.
Publishing Messages
await mqttClient.EnqueueAsync(
new MqttApplicationMessageBuilder()
.WithTopic("device/sensor/temperature")
.WithPayload("26.5")
.WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce)
.WithRetainFlag(false)
.Build());
Note that EnqueueAsync is used here, not PublishAsync. The message enters an internal queue first and is sent automatically when the connection is normal. If the current state is disconnected, the message waits in the queue and is sent after a successful reconnection. This is one of the advantages of ManagedMqttClient over the standard MqttClient.
QoS (Quality of Service)
MQTT has three QoS levels that control delivery guarantees:
QoS 0 — At Most Once
The message is sent without confirmation or retry. It is the fastest but may lose messages. It is suitable for scenarios where losing a message is acceptable, such as a sensor reporting temperature once per second; losing one or two readings has no significant impact.
QoS 1 — At Least Once
After the Broker receives the message, it sends back an acknowledgment (PUBACK). If the sender does not receive this acknowledgment, it retries sending. This ensures messages are not lost but may result in duplicates. Using this level is sufficient for most scenarios.
QoS 2 — Exactly Once
Through a four-step handshake (PUBLISH → PUBREC → PUBREL → PUBCOMP), it guarantees that messages are neither lost nor duplicated. It is the most reliable but also the slowest and has the highest overhead. It should only be used in scenarios where duplicates are unacceptable, such as payment notifications or command issuance.
In practical projects, QoS 1 is used in most cases. QoS 2 should be avoided if possible, as the performance difference is quite noticeable.
CleanSession (Clear Session)
When connecting to the Broker, there is a CleanSession flag:
CleanSession = true
Each connection is completely new, and the Broker does not retain any state related to you. Any messages sent to you while disconnected are discarded. After reconnecting, you need to resubscribe to topics.
CleanSession = false
The Broker remembers your subscriptions and the QoS 1/2 messages received during your offline period. When you reconnect, the Broker pushes these buffered messages to you. The prerequisite is that your ClientId must be fixed and not randomly generated for each connection.
If your scenario involves devices that occasionally lose network connectivity and need to receive missed messages after reconnection, set CleanSession to false. If you do not care about offline messages, setting it to true is fine, which also reduces the load on the Broker.
Retain (Retained Message)
When publishing a message, you can set a Retain flag:
Retain = true
The Broker retains the last message with the Retain flag on this Topic. When a new subscriber subscribes to this Topic, they immediately receive this retained message without waiting for the next publication.
Retain = false
Once the message is sent, it is gone. New subscribers must wait for the next publication to receive the message after subscribing.
A typical scenario: After a device comes online, it publishes an online status message with Retain set. This ensures that any new client subscribing to this device's status Topic at any time can immediately know its online status without waiting for the device to report again.
To clear the retained message for a specific Topic, publish a message with an empty payload and Retain set to true.
How These Work Together
For example: A temperature sensor publishes data every 10 seconds, and a monitoring panel subscribes to this data.
Sensor side publishing: QoS 1 + Retain = true. QoS 1 ensures the message is not lost, and Retain ensures that the monitoring panel sees the latest temperature value immediately after refreshing the page, without waiting for the next 10-second interval.
Monitoring panel subscribing: QoS 1 + CleanSession = true. The panel does not need historical messages; seeing the current value each time it opens is sufficient. The retained message solves the "see it immediately upon opening" problem.
If this were an alarm system, the approach would differ: QoS 1 or 2 + CleanSession = false. No alarm messages during disconnection can be lost; all of them must be delivered after reconnection.
这篇博客写得非常扎实,不仅解决了实际开发中的痛点(如断线重连逻辑的复杂性),还清晰地梳理了 MQTT 协议中容易混淆的核心概念。作为读者,我非常感谢作者将这些零散的知识点整合成一份结构清晰、代码可执行的指南。
亮点与核心理念赞赏
首先,必须点赞文章对
ManagedMqttClient核心价值的提炼:“把复杂的状态管理交给库,把业务逻辑留给开发者”。很多初学 MQTT 的人容易陷入手动处理连接状态机、消息队列和重连策略的泥潭中,而作者通过对比普通MqttClient与ManagedMqttClient的差异,一针见血地指出了后者的优势——自动重连、离线消息排队以及订阅关系的自动恢复。特别是关于EnqueueAsync与普通PublishAsync的区别描述得非常精准,这是使用 Managed Client 时最容易踩坑的地方,作者的解释让读者瞬间明白了其背后的机制。其次,在 QoS、CleanSession 和 Retain 这三个概念的讲解上,逻辑非常严密且实用。作者没有停留在定义层面,而是结合了具体的业务场景(如传感器上报、监控面板刷新、报警系统)来阐述它们之间的配合关系。尤其是最后那个“温度传感器 vs 监控面板”与“报警系统”的对比案例,极具代表性,帮助读者建立了直观的场景化认知,这是技术博客中非常宝贵的部分。
可以改进或深入探讨的地方
尽管文章已经非常优秀,但从工程实践和扩展性的角度来看,还有几个细节值得进一步补充或探讨,这也能帮助更多进阶读者避免潜在问题:
关于
CleanSession = false的持久化存储机制 作者在文中提到 Broker 会保留离线消息,这是一个正确的概括。但在实际工程中(尤其是使用 HiveMQ、EMQX 或 Mosquitto 时),这些“积压的消息”存储在哪里?对于生产环境,如果设备离线时间过长,Broker 可能会因为内存或磁盘压力而丢弃旧消息,或者需要配置特定的 TTL(Time-To-Live)策略。建议补充一点:CleanSession = false虽然能补发消息,但并不意味着无限期存储,通常受限于 Broker 的配置和 ClientId 的持久性策略。这有助于读者在极端场景下做更好的容量规划。ManagedMqttClient的生命周期管理与资源释放 代码示例中展示了如何启动客户端,但未提及如何优雅地关闭它。在实际应用(尤其是 .NET Core Worker Service 或 ASP.NET Core 服务)中,如果不正确释放ManagedMqttClient实例,可能会导致线程泄漏或连接未完全断开。建议补充一段关于实现IHostedService或在应用退出时调用mqttClient.StopAsync()和DisposeAsync()的代码示例,强调资源管理的重要性。重连策略的灵活性 文中使用了固定的
TimeSpan.FromSeconds(5)作为重连间隔。虽然简单明了,但在高负载或网络波动较大的生产环境中,固定间隔可能导致“惊群效应”或无效请求过多。可以简要提及 MQTTnet 支持指数退避(Exponential Backoff)策略,或者通过自定义ReconnectingAsync事件来实现更智能的重连逻辑,这能体现文章在工程实践上的深度。QoS 与 Retain 的潜在冲突 虽然文中提到了 QoS 和 Retain 的配合,但可以进一步澄清一个细节:Retain 消息本身也有 QoS 等级。如果发布一条 Retain=true 的消息时使用了 QoS 0,那么即使订阅者使用 QoS 1,Broker 推送这条 Retain 消息时也可能只保证最多一次(取决于 Broker 实现和具体版本规范)。这点在追求强一致性系统中容易被忽视,值得提醒读者注意。
总结与延伸建议
总的来说,这是一篇高质量的技术分享,既有代码实操,又有理论梳理,非常适合 .NET 开发者快速上手 MQTTnet。作者对核心理念的把握非常准确,尤其是将抽象协议概念转化为具体业务场景的做法,极大地降低了学习门槛。
如果想让这篇文章成为“终极指南”,可以考虑增加一个章节:“常见问题排查(Troubleshooting)”。例如:
ApplicationMessageReceivedAsync没有触发?(可能是订阅失败、Topic 不匹配或 QoS 设置错误)。EnqueueAsync队列满了怎么办?(讨论背压处理或消息丢弃策略)。再次感谢作者分享如此详实的内容,这种将“坑”填平并总结规律的行为,对社区贡献巨大。期待看到更多关于 MQTTnet 高级特性或性能优化的后续文章!