.NET 8.0 实战:基于 MQTTnet 封装高可用的 MQTT 发布/订阅工具类

在物联网(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 个原因:

  1. 命名空间冲突(最常见)切勿将你的项目名称或自定义命名空间命名为 MQTTnet 否则编译器会发生混淆,导致无法找到官方库内部的类型。若已冲突,请重命名项目并重新生成。
  2. 大版本差异 :上面的代码基于 v4.x 。如果你安装的是 v3.x 版本,很多 API(如 MqttApplicationMessageBuilder、事件绑定方式)完全不同,请升级 NuGet 包。
  3. 缺失 using 引用 :确保文件顶部包含了 using MQTTnet;, using MQTTnet.Client;, using MQTTnet.Extensions.ManagedClient; 等必要的命名空间。
  4. IDE 缓存抽风 :执行 Clean Solution(清理解决方案),删除 binobj 文件夹后 Rebuild

通过以上封装,你的 .NET 8.0 应用程序就拥有了一个健壮、高可用的 MQTT 通讯组件。

相关推荐
油丶酸萝卜别吃1 小时前
JavaScript 深度合并函数 deepMerge 实现指南(附完整测试用例)
开发语言·javascript·测试用例
念恒123061 小时前
Python(for循环进阶)
开发语言·python
AI玫瑰助手2 小时前
Python运算符:算术运算符(加减乘除取模幂)详解
开发语言·python
xiaoye-duck2 小时前
Qt 信号与槽深度解析:connect 用法、自定义信号槽与 Lambda 实战
开发语言·qt
lsx2024062 小时前
C AI 编程助手:助力开发者高效编程
开发语言
沐知全栈开发2 小时前
Eclipse 编译项目指南
开发语言
无限进步_2 小时前
C++11概览与统一初始化
开发语言·c++
笨蛋不要掉眼泪2 小时前
Java并发编程:内存可见性与synchronized同步机制
java·开发语言·并发
爱喝水的鱼丶2 小时前
SAP-ABAP:数据类型与数据对象(8篇) 第四篇:关系映射篇——从类型定义到对象实例的转化逻辑
开发语言·数据库·学习·sap·abap