You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

305 lines
11 KiB

  1. using System;
  2. using System.Collections.Concurrent;
  3. using System.Collections.Generic;
  4. using System.Linq;
  5. using System.Threading;
  6. using System.Threading.Tasks;
  7. using MQTTnet.Core.Client;
  8. using MQTTnet.Core.Exceptions;
  9. using MQTTnet.Core.Packets;
  10. using MQTTnet.Core.Protocol;
  11. using Microsoft.Extensions.Logging;
  12. namespace MQTTnet.Core.ManagedClient
  13. {
  14. public class ManagedMqttClient : IApplicationMessageReceiver
  15. {
  16. private readonly ManagedMqttClientStorageManager _storageManager = new ManagedMqttClientStorageManager();
  17. private readonly BlockingCollection<MqttApplicationMessage> _messageQueue = new BlockingCollection<MqttApplicationMessage>();
  18. private readonly HashSet<TopicFilter> _subscriptions = new HashSet<TopicFilter>();
  19. private readonly IMqttClient _mqttClient;
  20. private readonly ILogger<ManagedMqttClient> _logger;
  21. private CancellationTokenSource _connectionCancellationToken;
  22. private CancellationTokenSource _publishingCancellationToken;
  23. private IManagedMqttClientOptions _options;
  24. private bool _subscriptionsNotPushed;
  25. public ManagedMqttClient(ILogger<ManagedMqttClient> logger, IMqttClient mqttClient)
  26. {
  27. _logger = logger ?? throw new ArgumentNullException(nameof(logger));
  28. _mqttClient = mqttClient ?? throw new ArgumentNullException(nameof(mqttClient));
  29. _mqttClient.Connected += OnConnected;
  30. _mqttClient.Disconnected += OnDisconnected;
  31. _mqttClient.ApplicationMessageReceived += OnApplicationMessageReceived;
  32. }
  33. public bool IsConnected => _mqttClient.IsConnected;
  34. public event EventHandler<MqttClientConnectedEventArgs> Connected;
  35. public event EventHandler<MqttClientDisconnectedEventArgs> Disconnected;
  36. public event EventHandler<MqttApplicationMessageReceivedEventArgs> ApplicationMessageReceived;
  37. public async Task StartAsync(IManagedMqttClientOptions options)
  38. {
  39. if (options == null) throw new ArgumentNullException(nameof(options));
  40. if (options.ClientOptions == null) throw new ArgumentException("The client options are not set.", nameof(options));
  41. if (!options.ClientOptions.CleanSession)
  42. {
  43. throw new NotSupportedException("The managed client does not support existing sessions.");
  44. }
  45. if (_connectionCancellationToken != null)
  46. {
  47. throw new InvalidOperationException("The managed client is already started.");
  48. }
  49. _options = options;
  50. await _storageManager.SetStorageAsync(_options.Storage).ConfigureAwait(false);
  51. if (_options.Storage != null)
  52. {
  53. var loadedMessages = await _options.Storage.LoadQueuedMessagesAsync().ConfigureAwait(false);
  54. foreach (var loadedMessage in loadedMessages)
  55. {
  56. _messageQueue.Add(loadedMessage);
  57. }
  58. }
  59. _connectionCancellationToken = new CancellationTokenSource();
  60. #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
  61. Task.Factory.StartNew(() => MaintainConnectionAsync(_connectionCancellationToken.Token), _connectionCancellationToken.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default).ConfigureAwait(false);
  62. #pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
  63. _logger.LogInformation("Started");
  64. }
  65. public Task StopAsync()
  66. {
  67. _connectionCancellationToken?.Cancel(false);
  68. _connectionCancellationToken = null;
  69. while (_messageQueue.Any())
  70. {
  71. _messageQueue.Take();
  72. }
  73. return Task.FromResult(0);
  74. }
  75. public Task EnqueueAsync(IEnumerable<MqttApplicationMessage> applicationMessages)
  76. {
  77. if (applicationMessages == null) throw new ArgumentNullException(nameof(applicationMessages));
  78. foreach (var applicationMessage in applicationMessages)
  79. {
  80. _messageQueue.Add(applicationMessage);
  81. }
  82. return Task.FromResult(0);
  83. }
  84. public Task SubscribeAsync(IEnumerable<TopicFilter> topicFilters)
  85. {
  86. if (topicFilters == null) throw new ArgumentNullException(nameof(topicFilters));
  87. lock (_subscriptions)
  88. {
  89. foreach (var topicFilter in topicFilters)
  90. {
  91. if (_subscriptions.Add(topicFilter))
  92. {
  93. _subscriptionsNotPushed = true;
  94. }
  95. }
  96. }
  97. return Task.FromResult(0);
  98. }
  99. public Task UnsubscribeAsync(IEnumerable<TopicFilter> topicFilters)
  100. {
  101. lock (_subscriptions)
  102. {
  103. foreach (var topicFilter in topicFilters)
  104. {
  105. if (_subscriptions.Remove(topicFilter))
  106. {
  107. _subscriptionsNotPushed = true;
  108. }
  109. }
  110. }
  111. return Task.FromResult(0);
  112. }
  113. private async Task MaintainConnectionAsync(CancellationToken cancellationToken)
  114. {
  115. try
  116. {
  117. while (!cancellationToken.IsCancellationRequested)
  118. {
  119. var connectionState = await ReconnectIfRequiredAsync().ConfigureAwait(false);
  120. if (connectionState == ReconnectionResult.NotConnected)
  121. {
  122. _publishingCancellationToken?.Cancel(false);
  123. _publishingCancellationToken = null;
  124. await Task.Delay(_options.AutoReconnectDelay, cancellationToken).ConfigureAwait(false);
  125. continue;
  126. }
  127. if (connectionState == ReconnectionResult.Reconnected || _subscriptionsNotPushed)
  128. {
  129. await PushSubscriptionsAsync();
  130. _publishingCancellationToken = new CancellationTokenSource();
  131. #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
  132. Task.Factory.StartNew(() => PublishQueuedMessagesAsync(_publishingCancellationToken.Token), _publishingCancellationToken.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default).ConfigureAwait(false);
  133. #pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
  134. continue;
  135. }
  136. if (connectionState == ReconnectionResult.StillConnected)
  137. {
  138. await Task.Delay(100, _connectionCancellationToken.Token).ConfigureAwait(false); // Consider using the _Disconnected_ event here. (TaskCompletionSource)
  139. }
  140. }
  141. }
  142. catch (OperationCanceledException)
  143. {
  144. }
  145. catch (MqttCommunicationException exception)
  146. {
  147. _logger.LogWarning(new EventId(), exception, "Communication exception while maintaining connection.");
  148. }
  149. catch (Exception exception)
  150. {
  151. _logger.LogError(new EventId(), exception, "Unhandled exception while maintaining connection.");
  152. }
  153. finally
  154. {
  155. await _mqttClient.DisconnectAsync().ConfigureAwait(false);
  156. _logger.LogInformation("Stopped");
  157. }
  158. }
  159. private async Task PublishQueuedMessagesAsync(CancellationToken cancellationToken)
  160. {
  161. try
  162. {
  163. while (!cancellationToken.IsCancellationRequested)
  164. {
  165. var message = _messageQueue.Take(cancellationToken);
  166. if (message == null)
  167. {
  168. continue;
  169. }
  170. if (cancellationToken.IsCancellationRequested)
  171. {
  172. continue;
  173. }
  174. await TryPublishQueuedMessageAsync(message).ConfigureAwait(false);
  175. }
  176. }
  177. catch (OperationCanceledException)
  178. {
  179. }
  180. finally
  181. {
  182. _logger.LogInformation("Stopped publishing messages");
  183. }
  184. }
  185. private async Task TryPublishQueuedMessageAsync(MqttApplicationMessage message)
  186. {
  187. try
  188. {
  189. await _mqttClient.PublishAsync(message).ConfigureAwait(false);
  190. }
  191. catch (MqttCommunicationException exception)
  192. {
  193. _logger.LogWarning(new EventId(), exception, "Publishing application message failed.");
  194. if (message.QualityOfServiceLevel > MqttQualityOfServiceLevel.AtMostOnce)
  195. {
  196. _messageQueue.Add(message);
  197. }
  198. }
  199. catch (Exception exception)
  200. {
  201. _logger.LogError(new EventId(), exception, "Unhandled exception while publishing queued application message.");
  202. }
  203. }
  204. private async Task PushSubscriptionsAsync()
  205. {
  206. _logger.LogInformation(nameof(ManagedMqttClient), "Synchronizing subscriptions");
  207. List<TopicFilter> subscriptions;
  208. lock (_subscriptions)
  209. {
  210. subscriptions = _subscriptions.ToList();
  211. _subscriptionsNotPushed = false;
  212. }
  213. if (!_subscriptions.Any())
  214. {
  215. return;
  216. }
  217. try
  218. {
  219. await _mqttClient.SubscribeAsync(subscriptions).ConfigureAwait(false);
  220. }
  221. catch (Exception exception)
  222. {
  223. _logger.LogWarning(new EventId(), exception, "Synchronizing subscriptions failed");
  224. _subscriptionsNotPushed = true;
  225. }
  226. }
  227. private async Task<ReconnectionResult> ReconnectIfRequiredAsync()
  228. {
  229. if (_mqttClient.IsConnected)
  230. {
  231. return ReconnectionResult.StillConnected;
  232. }
  233. try
  234. {
  235. await _mqttClient.ConnectAsync(_options.ClientOptions).ConfigureAwait(false);
  236. return ReconnectionResult.Reconnected;
  237. }
  238. catch (Exception)
  239. {
  240. return ReconnectionResult.NotConnected;
  241. }
  242. }
  243. private void OnApplicationMessageReceived(object sender, MqttApplicationMessageReceivedEventArgs eventArgs)
  244. {
  245. ApplicationMessageReceived?.Invoke(this, eventArgs);
  246. }
  247. private void OnDisconnected(object sender, MqttClientDisconnectedEventArgs eventArgs)
  248. {
  249. Disconnected?.Invoke(this, eventArgs);
  250. }
  251. private void OnConnected(object sender, MqttClientConnectedEventArgs eventArgs)
  252. {
  253. Connected?.Invoke(this, eventArgs);
  254. }
  255. }
  256. }