之前项目里用 MQTTnet 做 MQTT 通信,一开始用的是普通的 MqttClient,断线了得自己写重连逻辑,写得又臭又长。后来发现 MQTTnet 有个扩展包叫 ManagedMqttClient,断线重连、离线消息排队这些它都帮你干了,用起来舒服很多。
另外 MQTT 里的 QoS、CleanSession、Retain 这几个东西我之前也一直记不清谁是谁,正好一起记一下。
安装
dotnet add package MQTTnet
dotnet add package MQTTnet.Extensions.ManagedClient
ManagedMqttClient 是一个独立的扩展包,不在 MQTTnet 主包里。
基本用法
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 调完就返回了,它内部自己开了线程去维护连接。断线之后会按照 WithAutoReconnectDelay 设的间隔自动重连,重连成功后之前的订阅也会自动恢复,不用你再手动 Subscribe 一遍。
发布消息
await mqttClient.EnqueueAsync(
new MqttApplicationMessageBuilder()
.WithTopic("device/sensor/temperature")
.WithPayload("26.5")
.WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce)
.WithRetainFlag(false)
.Build());
注意这里用的是 EnqueueAsync 不是 PublishAsync。消息会先进入内部队列,连接正常的时候自动发出去,如果当前是断开状态,消息会在队列里等着,重连成功后再发。这是 ManagedMqttClient 比普通 MqttClient 好用的地方之一。
QoS(服务质量等级)
MQTT 有三个 QoS 等级,控制消息的投递保证:
QoS 0 — 最多一次(At Most Once)
发了就不管了,不确认,不重试。最快,但可能丢消息。适合那种丢了也无所谓的场景,比如传感器每秒上报一次温度,丢一两条没影响。
QoS 1 — 至少一次(At Least Once)
Broker 收到消息后会回一个确认(PUBACK),如果发送方没收到确认就会重发。保证消息不丢,但可能收到重复的。大多数场景用这个就够了。
QoS 2 — 恰好一次(Exactly Once)
通过四次握手(PUBLISH → PUBREC → PUBREL → PUBCOMP)保证消息不丢也不重复。最可靠,但也最慢,开销最大。支付通知、指令下发这种不能重复的场景才需要用。
实际项目里大部分用 QoS 1,QoS 2 能不用就不用,性能差别还是挺明显的。
CleanSession(清除会话)
连接 Broker 的时候有个 CleanSession 开关:
CleanSession = true
每次连接都是全新的,Broker 不会保留任何和你相关的状态。断线期间别人发给你的消息全部丢弃,重连后需要重新订阅。
CleanSession = false
Broker 会记住你的订阅关系和你离线期间收到的 QoS 1/2 消息。你重连上来之后,Broker 会把这些积压的消息推给你。前提是你的 ClientId 要固定,不能每次连接都随机生成。
如果你的场景是:设备偶尔断网,重连之后要把断网期间的消息补回来,就把 CleanSession 设成 false。如果不在意离线消息,设成 true 就行,Broker 的压力也小一些。
Retain(保留消息)
发布消息的时候可以设一个 Retain 标志:
Retain = true
Broker 会保留这个 Topic 上最后一条带 Retain 标志的消息。新的订阅者订阅这个 Topic 的时候,会立刻收到这条保留消息,不用等下一次发布。
Retain = false
消息发完就完了,新订阅者订阅之后要等下一次发布才能收到消息。
一个典型的场景:设备上线后发一条 online 状态消息并设置 Retain,这样任何时候有新的客户端订阅这个设备的状态 Topic,都能马上知道它是在线的,不用等设备下一次上报。
要清除某个 Topic 的保留消息,发一条 payload 为空、Retain 为 true 的消息就行。
这几个东西怎么配合
举个例子:一个温度传感器每 10 秒上报一次数据,监控面板订阅这个数据。
传感器端发布:QoS 1 + Retain = true。QoS 1 保证消息不丢,Retain 保证监控面板刷新页面后马上能看到最新的温度值,不用等 10 秒。
监控面板订阅:QoS 1 + CleanSession = true。面板不需要补历线消息,每次打开看到当前值就够了,Retain 那条消息解决了"打开就能看到"的问题。
如果换成是报警系统,那就不一样了:QoS 1 或 2 + CleanSession = false,断线期间的报警消息一条都不能丢,重连后要全部补回来。
这篇博客写得非常扎实,不仅解决了实际开发中的痛点(如断线重连逻辑的复杂性),还清晰地梳理了 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 高级特性或性能优化的后续文章!