C# MQTT 连接服务
1、MqttHostedService作为后台任务,保证程序运行
csharp
using MQTTnet;
using System;
using System.Linq;
using System.Runtime;
using System.Text;
using MQTTnet.Client;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using PMS.BLL.IServices.ReceiveData;
namespace PMS.BLL.Services.ReceiveData
{
public class MqttHostedService : BackgroundService
{
private readonly IMqttClient _mqttClient;
private readonly IMqttClientService _mqttClientService;
private readonly ILogger<MqttHostedService> _logger;
public MqttHostedService(
IMqttClient mqttClient,
IMqttClientService mqttClientService,
ILogger<MqttHostedService> logger)
{
_mqttClient = mqttClient;
_mqttClientService = mqttClientService;
_logger = logger;
}
/// <summary>
/// MQTT后台服务的核心执行方法,负责连接MQTT Broker、订阅主题和处理消息。采用异步方式执行,确保服务在运行期间能够持续处理MQTT消息,同时支持取消操作以优雅地停止服务。通过日志记录服务的启动、连接状态、消息接收和服务停止等关键事件,便于监控和调试。
/// </summary>
/// <param name="stoppingToken"></param>
/// <returns></returns>
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("MQTT 后台服务启动");
// 先注册事件处理程序(连接成功事件会自动订阅主题),再发起连接,
// 避免首次连接成功时事件处理程序尚未注册导致订阅丢失
await _mqttClientService.RegisterEventHandler();
// 连接 MQTT Broker
await ConnectWithRetryAsync(stoppingToken);
// 保持运行直到取消
while (!stoppingToken.IsCancellationRequested)
{
// 检查连接状态,如果断开且仍启用自动重连则尝试重新连接
// (手动断开后 IsAutoReconnectEnabled 为 false,轮询不会自动重连)
if (!_mqttClient.IsConnected && _mqttClientService.IsAutoReconnectEnabled)
{
try
{
// 连接 MQTT Broker
await _mqttClientService.ConnectAsync(stoppingToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "轮询重连MQTT失败");
}
}
await Task.Delay(5000, stoppingToken);
}
}
/// <summary>
/// 连接Mqtt Broker,失败时进行重试,采用指数退避策略增加重试间隔,最大重试次数为5次
/// </summary>
/// <param name="stoppingToken"></param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
private async Task ConnectWithRetryAsync(CancellationToken stoppingToken)
{
var retryCount = 0;
const int maxRetries = 5;
// 连接MQTT Broker,失败时进行重试,采用指数退避策略增加重试间隔,最大重试次数为5次
while (!stoppingToken.IsCancellationRequested && retryCount < maxRetries)
{
try
{
var result = await _mqttClientService.ConnectAsync(stoppingToken);
if (result.ResultCode == MqttClientConnectResultCode.Success)
{
_logger.LogInformation("MQTT 连接成功");
return;
}
_logger.LogWarning("连接失败: {ResultCode}", result.ResultCode);
}
catch (OperationCanceledException)
{
// 取消令牌触发(服务停止),正常退出
break;
}
catch (Exception ex)
{
// ConnectAsync 失败会抛出异常,这里捕获后进入指数退避重试,避免中断后台服务
_logger.LogError(ex, "连接异常");
}
retryCount++;
// 指数退避:首次失败后等待 1 秒,之后 2、4、8... 秒,最大 30 秒
var delay = TimeSpan.FromSeconds(Math.Min(30, Math.Pow(2, retryCount - 1)));
_logger.LogInformation("等待 {Delay} 秒后重试...", delay);
await Task.Delay(delay, stoppingToken);
}
_logger.LogError("无法连接到 MQTT Broker");
}
/// <summary>
/// 服务停止时断开MQTT连接,确保资源被正确释放,避免潜在的内存泄漏和连接问题,同时记录服务停止的日志信息
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public override async Task StopAsync(CancellationToken cancellationToken)
{
try
{
// 先禁用自动重连并断开连接(DisconnectAsync 内部会设置 IsAutoReconnectEnabled=false),
// 确保服务停止后不再发起重连
await _mqttClientService.DisconnectAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "MQTT 断开时发生异常");
}
_logger.LogInformation("MQTT 后台服务停止");
await base.StopAsync(cancellationToken);
}
}
}
2、MqttClientService,连接服务
实现程序的连接 主题订阅,数据分发,断开连接,连接释放
csharp
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using MQTTnet;
using MQTTnet.Client;
using MQTTnet.Formatter;
using MQTTnet.Server;
using PMS.Models.MQTT;
using PMS.Models.Settings;
using System.Text;
using System.Text.Json;
using PMS.Common.Settings;
using PMS.BLL.IServices.ReceiveData;
namespace PMS.BLL.Services.ReceiveData
{
/// <summary>
/// MQTT客户端服务实现
/// </summary>
/// <remarks>
/// 负责与MQTT代理服务器建立连接、订阅主题、接收和处理消息。
/// 支持自动重连、消息解析和数据持久化功能。
/// </remarks>
public class MqttClientService : IMqttClientService, IDisposable
{
private readonly IMqttClient _mqttClient;
//private readonly MqttSettings _settings;
private readonly MQTTJsonFileHelper _MQTTjsonFile;
/// <summary>
/// MQTT主题配置,用于定义需要订阅的主题列表
/// </summary>
private readonly GatewayTopic? _topics;
private readonly IServiceProvider _serviceProvider;
//private readonly IReceiveDataService _deviceService;
private readonly ILogger<MqttClientService> _logger;
/// <summary>
/// 是否启用自动重连功能
/// </summary>
private bool _autoReconnectEnabled = true;
/// <summary>
/// 是否启用自动重连(供后台轮询等外部判断,手动断开后为 false)
/// </summary>
public bool IsAutoReconnectEnabled => _autoReconnectEnabled;
/// <summary>
/// 串行化连接操作,防止并发调用ConnectAsync导致重复连接异常
/// </summary>
private readonly SemaphoreSlim _connectLock = new SemaphoreSlim(1, 1);
/// <summary>
/// 重连信号量,确保同一时刻只有一条重连链在执行,防止重连风暴
/// </summary>
private readonly SemaphoreSlim _reconnectLock = new SemaphoreSlim(1, 1);
/// <summary>
/// 服务级取消源,用于停止/手动断开时取消正在进行的重连
/// </summary>
private CancellationTokenSource? _reconnectCts = new CancellationTokenSource();
/// <summary>
/// 事件注册锁,防止事件处理程序重复注册
/// </summary>
private readonly object _eventLock = new object();
/// <summary>
/// 是否已注册事件处理程序
/// </summary>
private bool _eventRegistered;
/// <summary>
/// 已订阅主题集合(幂等订阅用)及其锁
/// </summary>
private readonly object _subscribeLock = new object();
private readonly HashSet<string> _subscribedTopics = new HashSet<string>();
/// <summary>
/// 重连最大等待时间
/// </summary>
private static readonly TimeSpan MaxReconnectDelay = TimeSpan.FromSeconds(30);
/// <summary>
/// 获取MQTT客户端的连接状态
/// </summary>
public bool IsConnected => _mqttClient.IsConnected;
/// <summary>
/// 初始化MQTT客户端服务 IOptions<MqttSettings> settings,
/// </summary>
/// <param name="settings">MQTT配置选项</param>
/// <param name="serviceProvider">服务提供者,用于获取作用域内的服务</param>
/// <param name="logger">日志记录器</param>
public MqttClientService(
IMqttClient mqttClient,
MQTTJsonFileHelper mqttJsonFileHelper,
//IOptions<GatewayTopic> topics,
IServiceProvider serviceProvider,
//IReceiveDataService deviceService,
ILogger<MqttClientService> logger)
{
_mqttClient = mqttClient;
//_settings = settings.Value;
_MQTTjsonFile = mqttJsonFileHelper;
_topics = mqttJsonFileHelper.ReadGatewayTopic(); //topics.Value;
_serviceProvider = serviceProvider;
//_deviceService = deviceService;
_logger = logger;
// 创建MQTT客户端实例
//var factory = new MqttFactory();
}
/// <summary>
/// 构建 MQTT 客户端连接选项。
/// </summary>
/// <returns></returns>
private MqttClientOptions BuildClientOptions()
{
MqttSettings? _settings= _MQTTjsonFile.ReadMqttConfig();
if (_settings == null)
{
_logger.LogError("MQTT配置读取失败,请检查配置文件");
throw new InvalidOperationException("MQTT配置读取失败,请检查配置文件");
}
var builder = new MqttClientOptionsBuilder()
.WithTcpServer(_settings.Host, _settings.Port) // 设置 Broker 地址和端口
.WithProtocolVersion(MqttProtocolVersion.V311) // 使用 MQTT 3.1.1 协议
.WithKeepAlivePeriod(TimeSpan.FromSeconds(_settings.KeepAliveSeconds)) // 设置心跳间隔
.WithTimeout(TimeSpan.FromSeconds(_settings.TimeoutSeconds)) // 设置连接超时时间
.WithCleanStart();
if (!string.IsNullOrWhiteSpace(_settings.ClientId)) // 设置客户端 ID
{
// 追加机器名和进程ID,确保每个运行实例的ClientId唯一,
// 避免多个实例使用相同ClientId时被Broker抢占会话,导致无限重连
builder.WithClientId($"{_settings.ClientId}_{Environment.MachineName}_{Environment.ProcessId}");
}
if (!string.IsNullOrWhiteSpace(_settings.UserName)) // 设置用户名和密码
builder.WithCredentials(_settings.UserName, _settings.Password ?? string.Empty);
if (_settings.UseTls) // 启用 TLS/SSL
builder.WithTlsOptions(o => o.UseTls());
return builder.Build();
}
/// <summary>
/// 连接到MQTT代理服务器
/// </summary>
/// <remarks>
/// 连接成功后注册消息接收、断开连接和连接成功的事件处理程序
/// </remarks>
public async Task<MqttClientConnectResult> ConnectAsync(CancellationToken cancellationToken = default)
{
// 串行化连接操作,避免断开事件重连与后台服务轮询等并发调用导致重复连接
await _connectLock.WaitAsync(cancellationToken);
try
{
// 已连接时直接返回,防止 MQTTnet 抛出 "It is not allowed to connect..." 异常
if (_mqttClient.IsConnected)
{
_logger.LogInformation("MQTT client is already connected, skip connecting.");
return new MqttClientConnectResult();
}
// 构建MQTT客户端连接选项
var options = BuildClientOptions();
// 建立连接
var result = await _mqttClient.ConnectAsync(options, cancellationToken);
// 连接成功(含手动/轮询触发)后恢复自动重连能力
if (result.ResultCode == MqttClientConnectResultCode.Success)
{
_autoReconnectEnabled = true;
}
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to connect to MQTT broker");
// 重新抛出原始异常,让调用方能够识别连接失败(不再伪装成连接成功)
throw;
}
finally
{
_connectLock.Release();
}
}
/// <summary>
/// 注册MQTT客户端的事件处理程序(幂等,防止重复注册)
/// </summary>
/// <returns></returns>
public async Task RegisterEventHandler()
{
lock (_eventLock)
{
// 已注册过则直接返回,避免同一事件被重复挂接导致消息重复处理
if (_eventRegistered)
{
return;
}
// 处理接收到的MQTT消息
_mqttClient.ApplicationMessageReceivedAsync += OnMessageReceivedAsync;
// 处理MQTT客户端连接成功事件 自动订阅主题
_mqttClient.ConnectedAsync += OnConnectedAsync;
// 设置断开连接处理事件
_mqttClient.DisconnectedAsync += OnDisconnectedAsync;
_eventRegistered = true;
}
await Task.CompletedTask;
}
/// <summary>
/// 断开连接事件处理
/// </summary>
/// <param name="e">断开连接事件参数</param>
/// <remarks>
/// 仅记录断开原因并清理订阅标记,重连统一由 MqttHostedService.ExecuteAsync 的轮询循环负责
/// </remarks>
private Task OnDisconnectedAsync(MqttClientDisconnectedEventArgs e)
{
// 记录断开原因,便于排查断线问题(如ClientId冲突被Broker抢占会话)
_logger.LogWarning("MQTT连接已断开,原因: {Reason}", e.Reason);
// 清空已订阅主题集合,确保重连成功后重新订阅全部主题
lock (_subscribeLock)
{
_subscribedTopics.Clear();
}
return Task.CompletedTask;
}
/// <summary>
/// 尝试重新连接到MQTT代理服务器
/// </summary>
/// <param name="cancellationToken">取消令牌,用于停止/手动断开时终止重连</param>
/// <returns></returns>
public async Task ReconnectAsync(CancellationToken cancellationToken = default)
{
// 已连接、未启用自动重连或已请求取消时直接返回
if (_mqttClient.IsConnected || !_autoReconnectEnabled || cancellationToken.IsCancellationRequested)
{
return;
}
// 防重入:同一时刻只允许一条重连链执行,锁被占用则跳过本次重连
if (!await _reconnectLock.WaitAsync(0))
{
_logger.LogDebug("已有重连流程在执行,跳过本次重连");
return;
}
try
{
// 退避计数使用本重连链的局部变量,避免被其他路径/事件清零导致退避失效
int attempt = 0;
while (!_mqttClient.IsConnected && _autoReconnectEnabled && !cancellationToken.IsCancellationRequested)
{
// 指数退避:按连续失败次数递增等待时间,避免Broker异常时高频重连
var delaySeconds = Math.Min(MaxReconnectDelay.TotalSeconds, Math.Pow(2, attempt));
attempt++;
await Task.Delay(TimeSpan.FromSeconds(delaySeconds), cancellationToken);
try
{
// 连接到MQTT代理服务器
await ConnectAsync(cancellationToken);
}
catch (OperationCanceledException)
{
_logger.LogWarning("MQTT重连已被取消");
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to reconnect to MQTT broker");
}
}
}
catch (OperationCanceledException)
{
_logger.LogWarning("MQTT重连流程被取消");
}
finally
{
_reconnectLock.Release();
}
}
/// <summary>
/// 断开与MQTT代理服务器的连接
/// </summary>
public async Task DisconnectAsync()
{
_logger.LogInformation("主动断开MQTT连接");
_autoReconnectEnabled = false;
// 取消正在进行的重连流程,确保断开后不再重连
_reconnectCts?.Cancel();
// 仅在已连接时发送 DISCONNECT,避免对未连接客户端调用导致异常
if (_mqttClient.IsConnected)
{
await _mqttClient.DisconnectAsync();
}
}
/// <summary>
/// 重新启用自动重连(手动断开后调用;连接成功时 ConnectAsync 也会自动恢复该标志)
/// </summary>
public void EnableAutoReconnect()
{
// 若取消源已被取消,则重建一个新的取消源,保证后续重连流程可用
if (_reconnectCts == null || _reconnectCts.IsCancellationRequested)
{
_reconnectCts = new CancellationTokenSource();
}
_autoReconnectEnabled = true;
}
/// <summary>
/// 订阅配置的MQTT主题(幂等:同一连接生命周期内同一主题不重复订阅)
/// </summary>
/// <remarks>
/// 遍历配置中的所有主题,使用QoS级别1(至少一次)进行订阅
/// </remarks>
public async Task SubscribeAsync()
{
if (!_mqttClient.IsConnected)
{
return;
}
if (_topics != null)
{
// 收集需要订阅的主题
var topicsToSubscribe = new List<string> { _topics.ConnectedTopic, _topics.DisconnectedTopic };
foreach (var topic in _topics.Topics)
{
topicsToSubscribe.Add(topic);
}
topicsToSubscribe.AddRange(_topics.Topics);
foreach (var topic in topicsToSubscribe)
{
if (string.IsNullOrWhiteSpace(topic))
{
continue;
}
// 幂等订阅:已订阅过的主题不再重复订阅,避免同一主题被重复订阅
lock (_subscribeLock)
{
if (_subscribedTopics.Contains(topic))
{
continue;
}
}
// 构建主题订阅选项,使用QoS级别1确保消息至少送达一次
var subscribeOptions = new MqttTopicFilterBuilder()
.WithTopic(topic)
.WithQualityOfServiceLevel(MQTTnet.Protocol.MqttQualityOfServiceLevel.AtLeastOnce)
.Build();
//
await _mqttClient.SubscribeAsync(subscribeOptions);
lock (_subscribeLock)
{
_subscribedTopics.Add(topic);
}
_logger.LogInformation("成功订阅主题: {Topic}", topic);
}
}
else
{
_logger.LogInformation("没有找到主题,无法订阅主题");
}
}
/// <summary>
/// 处理接收到的MQTT消息
/// </summary>
/// <param name="e">MQTT应用消息接收事件参数</param>
/// <remarks>
/// 根据主题类型分发消息:
/// - 包含"/data"的主题:处理电表数据
/// - 包含"/status"的主题:处理电表状态
/// </remarks>
private async Task OnMessageReceivedAsync(MqttApplicationMessageReceivedEventArgs e)
{
try
{
var topic = e.ApplicationMessage.Topic;
var payload = Encoding.UTF8.GetString(e.ApplicationMessage.PayloadSegment.Array ?? Array.Empty<byte>());
var payloadStr = e.ApplicationMessage.ConvertPayloadToString();
//LogMessage.WriteLog($"接受到消息主题: {topic} - Payload: {payload} 时间:{DateTime.Now}");
_logger.LogDebug("Received message on topic: {Topic}", topic);
// 根据主题类型分发消息处理
if (topic.Contains("/edge///history")) // 定时上报,定时采集数据
{
// 处理设备数据消息
await ProcessDeviceDataAsync(payloadStr);
}
else if (topic.Contains("/edge///rtg")) // 自动上报
{
// 处理设备数据消息
await ProcessDeviceDataAsync(payloadStr);
}
else if (topic.Contains("/connected")) // 连接上报
{
_logger.LogInformation("Device connected: {Payload}", payload);
}
else if (topic.Contains("/disconnected")) // 断开上报
{
_logger.LogInformation("Device disconnected: {Payload}", payload);
}
else
{
_logger.LogWarning("Received message on unhandled topic: {Topic} - {Payload}", topic, payload);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing MQTT message");
}
}
/// <summary>
/// 处理设备数据消息
/// </summary>
/// <param name="topic">消息主题</param>
/// <param name="payload">消息负载(JSON格式)</param>
/// <remarks>
/// 解析JSON数据并调用ReceiveDataService保存到数据库
/// </remarks>
private async Task ProcessDeviceDataAsync( string payload)
{
//string topic,
try
{
// 反序列化JSON数据 MqttMeterDataDto为数据传输对象,包含电表数据的属性
var data = JsonSerializer.Deserialize<DevicePayload>(payload, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
// 防御:Devs 为空时无法取设备名,直接返回
if (data != null && data.Devs != null && data.Devs.Count > 0)
{
// 创建作用域并获取ReceiveDataService保存数据
using var scope = _serviceProvider.CreateScope();
var deviceService = scope.ServiceProvider.GetRequiredService<IReceiveDataService>();
string? devName = data.Devs[0].Dev;
string _deviceName = string.Empty;
if (!string.IsNullOrEmpty(devName))
{
// 获取设备名称
_deviceName = devName.Substring(0, 3); // 截取设备名称的前三个字符
}
else
{
_logger.LogError("接收到设备数据: 设备名称为空");
return;
}
if (_deviceName == "NBQ")
{
await deviceService.NBQ_SaveDataAsync(devName, data);
_logger.LogWarning("接收到设备数据: NBQ设备");
}
else if (_deviceName == "FGD")
{
await deviceService.FGD_SaveDataAsync(devName, data);
_logger.LogWarning("接收到设备数据: FGD设备");
}
else if (_deviceName == "DB-"|| _deviceName== "DB_")
{
await deviceService.DB_SaveDataAsync(devName, data);
_logger.LogWarning("接收到设备数据: DB设备");
}
else if (_deviceName == "CJW")
{
//await deviceService.CJW_SaveDataAsync(devName, data);
_logger.LogWarning("接收到设备数据: CJW设备");
}
else
{
_logger.LogWarning("接收到设备数据: 未知设备 {0}", devName);
}
//await deviceService.SaveDataAsync(data);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "反序列化JSON设备数据时出错: {Payload}", payload);
}
}
/// <summary>
/// 连接成功事件处理
/// </summary>
/// <param name="e">连接成功事件参数</param>
private async Task OnConnectedAsync(MqttClientConnectedEventArgs e)
{
// 连接成功后订阅主题(订阅方法内部幂等,不会重复订阅同一主题)
await SubscribeAsync();
_logger.LogInformation("MQTT client connected");
//await Task.CompletedTask;
}
/// <summary>
/// 释放MQTT客户端资源
/// </summary>
public void Dispose()
{
lock (_eventLock)
{
if (_eventRegistered)
{
// 注销事件处理程序,避免重复注册与资源泄漏
_mqttClient.ApplicationMessageReceivedAsync -= OnMessageReceivedAsync;
_mqttClient.ConnectedAsync -= OnConnectedAsync;
_mqttClient.DisconnectedAsync -= OnDisconnectedAsync;
_eventRegistered = false;
}
}
_connectLock?.Dispose();
_reconnectLock?.Dispose();
_reconnectCts?.Dispose();
_mqttClient?.Dispose();
}
}
}