在物联网(IoT)和分布式系统中,MQTT 协议因其轻量级和高效的特性被广泛应用。而在实际的生产环境中,网络波动、服务端重启等情况时有发生,因此实现一个高可用(支持断线重连、离线消息缓存)的 MQTT 客户端至关重要。
本文将手把手教你在 .NET 8.0 中,利用 MQTTnet 及其强大的 ManagedClient(托管客户端)功能,封装一个可复用、高可用的 MQTT 工具类。
为什么选择 ManagedClient?
原生 MQTT 客户端在断网时需要开发者手动捕获异常并编写重连逻辑,同时还要自己维护离线消息队列。而 MQTTnet.Extensions.ManagedClient 原生提供了以下高可用特性:
- 自动断线重连:后台自动尝试恢复连接。
- 离线消息缓存队列:断网期间发布的消息会自动进入内部队列,待网络恢复后自动发送。
- 自动恢复订阅:重连成功后,自动恢复之前的 Topic 订阅。
1. 安装必要的 NuGet 包
首先,为你的 .NET 8.0 项目安装以下 NuGet 包(请确保安装 v4.3.x 或以上的较新版本,且MQTTnet 和 MQTTnet.Extensions.ManagedClient保持版本一致性):
bash
dotnet add package MQTTnet
dotnet add package MQTTnet.Extensions.ManagedClient
dotnet add package Microsoft.Extensions.Options
dotnet add package Microsoft.Extensions.Hosting
2. 核心代码实现
为了在项目中做到"即插即用",我们采用配置类 + 接口 + 实现类 + 依赖注入的标准化做法。
第一步:定义配置选项 (Options)
用于映射 appsettings.json 中的配置信息。
csharp
/// <summary>
/// MqttOptions配置类
/// </summary>
public class MqttOptions
{
/// <summary>
/// 主机名
/// </summary>
public const string SectionName = "MqttSettings";
/// <summary>
/// 地址
/// </summary>
public string Host { get; set; } = string.Empty;
/// <summary>
/// 端口
/// </summary>
public int Port { get; set; } = 1883;
/// <summary>
/// 链接Id
/// </summary>
public string ClientId { get; set; } = Guid.NewGuid().ToString();
/// <summary>
/// 登陆账户
/// </summary>
public string Username { get; set; } = string.Empty;
/// <summary>
/// 密码
/// </summary>
public string Password { get; set; } = string.Empty;
/// <summary>
/// 断线重连延迟(秒)
/// </summary>
public int ReconnectDelaySeconds { get; set; } = 5;
}
}
第二步:定义工具类接口
暴露最常用的发布、订阅功能,以及全局消息接收事件。
csharp
using MQTTnet.Client;
using MQTTnet.Protocol;
/// <summary>
/// MQTT工具接口
/// </summary>
public interface IMqttTool
{
/// <summary>
/// 全局消息接收事件
/// </summary>
event Func<MqttApplicationMessageReceivedEventArgs, Task>? OnMessageReceivedAsync;
/// <summary>
/// 发布消息 (高可用:断网时会自动进入本地队列,恢复后发出)
/// </summary>
Task PublishAsync(string topic, string payload, MqttQualityOfServiceLevel qos = MqttQualityOfServiceLevel.AtLeastOnce, bool retain = false);
/// <summary>
/// 订阅主题 (高可用:断网重连后会自动重新订阅)
/// </summary>
Task SubscribeAsync(string topic, MqttQualityOfServiceLevel qos = MqttQualityOfServiceLevel.AtLeastOnce);
/// <summary>
/// 取消订阅
/// </summary>
Task UnsubscribeAsync(string topic);
}
第三步:具体实现类
这里是高可用的核心,我们实例化并配置 IManagedMqttClient。注意发布方法中使用的是 EnqueueAsync 而不是底层的 PublishAsync,这正是离线消息不丢失的关键。
csharp
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MQTTnet;
using MQTTnet.Client;
using MQTTnet.Extensions.ManagedClient;
using MQTTnet.Packets;
using MQTTnet.Protocol;
/// <summary>
/// MQTT工具类实现
/// </summary>
public class MqttTool : IMqttTool, IDisposable
{
private readonly IManagedMqttClient _managedClient;
private readonly ILogger<MqttTool> _logger;
public event Func<MqttApplicationMessageReceivedEventArgs, Task>? OnMessageReceivedAsync;
public MqttTool(IOptions<MqttOptions> options, ILogger<MqttTool> logger)
{
_logger = logger;
var config = options.Value;
// 1. 创建托管客户端实例
var factory = new MqttFactory();
_managedClient = factory.CreateManagedMqttClient();
// 2. 配置基础客户端选项
var clientOptionsBuilder = new MqttClientOptionsBuilder()
.WithClientId(config.ClientId)
.WithTcpServer(config.Host, config.Port)
.WithCleanSession(false); // 保持会话以防消息丢失
if (!string.IsNullOrEmpty(config.Username))
{
clientOptionsBuilder.WithCredentials(config.Username, config.Password);
}
var clientOptions = clientOptionsBuilder.Build();
// 3. 配置托管选项(高可用核心逻辑)
var managedOptions = new ManagedMqttClientOptionsBuilder()
.WithAutoReconnectDelay(TimeSpan.FromSeconds(config.ReconnectDelaySeconds))
.WithClientOptions(clientOptions)
.Build();
// 4. 注册事件钩子
_managedClient.ConnectedAsync += e =>
{
_logger.LogInformation("MQTT Client Connected to {Host}:{Port}", config.Host, config.Port);
return Task.CompletedTask;
};
_managedClient.DisconnectedAsync += e =>
{
_logger.LogWarning("MQTT Client Disconnected. Reason: {Reason}", e.Reason);
return Task.CompletedTask;
};
_managedClient.ApplicationMessageReceivedAsync += async e =>
{
if (OnMessageReceivedAsync != null)
{
await OnMessageReceivedAsync.Invoke(e);
}
};
// 5. 启动客户端 (非阻塞)
_managedClient.StartAsync(managedOptions).GetAwaiter().GetResult();
}
/// <summary>
/// 发布消息 (高可用:断网时会自动进入本地队列,恢复后发出)
/// </summary>
public async Task PublishAsync(string topic, string payload, MqttQualityOfServiceLevel qos = MqttQualityOfServiceLevel.AtLeastOnce, bool retain = false)
{
var message = new MqttApplicationMessageBuilder()
.WithTopic(topic)
.WithPayload(payload)
.WithQualityOfServiceLevel(qos)
.WithRetainFlag(retain)
.Build();
// EnqueueAsync 是高可用的关键:网络正常时立即发送,断网时加入队列等待恢复
await _managedClient.EnqueueAsync(message);
_logger.LogDebug("Message enqueued to topic: {Topic}", topic);
}
/// <summary>
/// 订阅主题 (高可用:断网重连后会自动重新订阅)
/// </summary>
public async Task SubscribeAsync(string topic, MqttQualityOfServiceLevel qos = MqttQualityOfServiceLevel.AtLeastOnce)
{
var filter = new MqttTopicFilterBuilder()
.WithTopic(topic)
.WithQualityOfServiceLevel(qos)
.Build();
await _managedClient.SubscribeAsync(new[] { filter });
_logger.LogInformation("Subscribed to topic: {Topic}", topic);
}
/// <summary>
/// 取消订阅
/// </summary>
public async Task UnsubscribeAsync(string topic)
{
await _managedClient.UnsubscribeAsync(new[] { topic });
_logger.LogInformation("Unsubscribed from topic: {Topic}", topic);
}
public void Dispose()
{
_managedClient?.Dispose();
}
}
第四步:注册依赖注入扩展
为方便调用,编写一个扩展方法:
csharp
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
public static class MqttServiceExtensions
{
public static IServiceCollection AddMqttTool(this IServiceCollection services, IConfiguration configuration)
{
services.Configure<MqttOptions>(configuration.GetSection(MqttOptions.SectionName));
// 必须注册为单例,确保全局复用同一个长连接和消息队列
services.AddSingleton<IMqttTool, MqttTool>();
return services;
}
}
3. 在项目中的实际运用
在 appsettings.json 中添加配置:
json
{
"MqttSettings": {
"Host": "127.0.0.1",
"Port": 1883,
"ClientId": "MyService_01",
"ReconnectDelaySeconds": 5
}
}
在 Program.cs 中一句代码注册:
csharp
builder.Services.AddMqttTool(builder.Configuration);
在 BackgroundService 或 Controller 中调用:
csharp
public class MyWorker : BackgroundService
{
private readonly IMqttTool _mqtt;
public MyWorker(IMqttTool mqtt)
{
_mqtt = mqtt;
_mqtt.OnMessageReceivedAsync += e =>
{
var msg = System.Text.Encoding.UTF8.GetString(e.ApplicationMessage.PayloadSegment);
Console.WriteLine($"收到消息: {msg}");
return Task.CompletedTask;
};
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await _mqtt.SubscribeAsync("sensor/data/#");
while (!stoppingToken.IsCancellationRequested)
{
await _mqtt.PublishAsync("system/heartbeat", "alive");
await Task.Delay(5000);
}
}
}
踩坑指南:常见编译错误排查
在集成 MQTTnet 时,你可能会遇到类似如下的报错:
未能找到类型或命名空间名"MqttFactory"
对类型"MqttClientOptions"的引用声称该类型是在"MQTTnet"中定义的,但未能找到
如果你遇到了这些报错,请依次排查以下 4 个原因:
- 命名空间冲突(最常见) :切勿将你的项目名称或自定义命名空间命名为
MQTTnet! 否则编译器会发生混淆,导致无法找到官方库内部的类型。若已冲突,请重命名项目并重新生成。 - 大版本差异 :上面的代码基于 v4.x 。如果你安装的是 v3.x 版本,很多 API(如
MqttApplicationMessageBuilder、事件绑定方式)完全不同,请升级 NuGet 包。 - 缺失 using 引用 :确保文件顶部包含了
using MQTTnet;,using MQTTnet.Client;,using MQTTnet.Extensions.ManagedClient;等必要的命名空间。 - IDE 缓存抽风 :执行
Clean Solution(清理解决方案),删除bin和obj文件夹后Rebuild。
通过以上封装,你的 .NET 8.0 应用程序就拥有了一个健壮、高可用的 MQTT 通讯组件。