C# MQTT 连接服务

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();
		}
	}

}
相关推荐
卷无止境13 分钟前
SigV4与HTTPS,两套完全不同维度的安全机制
后端·python·fastapi
青石路14 分钟前
好好的OceanBase官方驱动你不用,非要用第三方驱动,ArrayIndexOutOfBoundsException了吧
java·后端
深念Y30 分钟前
微服务抽取路线图:从胖单体到 ARM 集群
前端·arm开发·数据库·后端·微服务·云原生·架构
苏灿烤鱼30 分钟前
一个用 Nim 写的隐私优先 Twitter 替代前端,源码级技术剖析
后端·github·twitter
LccKyI1 小时前
C#基础编程 面试题汇总(二)
java·面试·c#
灯澜忆梦1 小时前
【基于GO的Web开发14】gin请求重定向
前端·后端·golang·gin
东风破_1 小时前
博客数据库进阶设计:点赞、收藏、评论、标签与文件表怎么建?
后端·mysql
夕除1 小时前
redis--009
redis·后端
东风破_10 小时前
从输入 juejin.cn 到看到页面:DNS、Nginx、服务器集群与 CDN 到底在做什么?
后端·mysql