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.
 
 
 
 

166 lines
6.7 KiB

  1. using MQTTnet.Client;
  2. using MQTTnet.Exceptions;
  3. using MQTTnet.Extensions.Rpc.Options;
  4. using MQTTnet.Extensions.Rpc.Options.TopicGeneration;
  5. using MQTTnet.Protocol;
  6. using System;
  7. using System.Collections.Concurrent;
  8. using System.Text;
  9. using System.Threading;
  10. using System.Threading.Tasks;
  11. using MQTTnet.Implementations;
  12. namespace MQTTnet.Extensions.Rpc
  13. {
  14. public sealed class MqttRpcClient : IMqttRpcClient
  15. {
  16. readonly ConcurrentDictionary<string, TaskCompletionSource<byte[]>> _waitingCalls = new ConcurrentDictionary<string, TaskCompletionSource<byte[]>>();
  17. readonly IMqttClient _mqttClient;
  18. readonly IMqttRpcClientOptions _options;
  19. readonly RpcAwareApplicationMessageReceivedHandler _applicationMessageReceivedHandler;
  20. [Obsolete("Use MqttRpcClient(IMqttClient mqttClient, IMqttRpcClientOptions options).")]
  21. public MqttRpcClient(IMqttClient mqttClient) : this(mqttClient, new MqttRpcClientOptions())
  22. {
  23. }
  24. public MqttRpcClient(IMqttClient mqttClient, IMqttRpcClientOptions options)
  25. {
  26. _mqttClient = mqttClient ?? throw new ArgumentNullException(nameof(mqttClient));
  27. _options = options ?? throw new ArgumentNullException(nameof(options));
  28. _applicationMessageReceivedHandler = new RpcAwareApplicationMessageReceivedHandler(
  29. mqttClient.ApplicationMessageReceivedHandler,
  30. HandleApplicationMessageReceivedAsync);
  31. _mqttClient.ApplicationMessageReceivedHandler = _applicationMessageReceivedHandler;
  32. }
  33. public Task<byte[]> ExecuteAsync(TimeSpan timeout, string methodName, string payload, MqttQualityOfServiceLevel qualityOfServiceLevel)
  34. {
  35. return ExecuteAsync(timeout, methodName, Encoding.UTF8.GetBytes(payload), qualityOfServiceLevel, CancellationToken.None);
  36. }
  37. public Task<byte[]> ExecuteAsync(TimeSpan timeout, string methodName, string payload, MqttQualityOfServiceLevel qualityOfServiceLevel, CancellationToken cancellationToken)
  38. {
  39. return ExecuteAsync(timeout, methodName, Encoding.UTF8.GetBytes(payload), qualityOfServiceLevel, cancellationToken);
  40. }
  41. public Task<byte[]> ExecuteAsync(TimeSpan timeout, string methodName, byte[] payload, MqttQualityOfServiceLevel qualityOfServiceLevel)
  42. {
  43. return ExecuteAsync(timeout, methodName, payload, qualityOfServiceLevel, CancellationToken.None);
  44. }
  45. public async Task<byte[]> ExecuteAsync(TimeSpan timeout, string methodName, byte[] payload, MqttQualityOfServiceLevel qualityOfServiceLevel, CancellationToken cancellationToken)
  46. {
  47. if (methodName == null) throw new ArgumentNullException(nameof(methodName));
  48. if (!(_mqttClient.ApplicationMessageReceivedHandler is RpcAwareApplicationMessageReceivedHandler))
  49. {
  50. throw new InvalidOperationException("The application message received handler was modified.");
  51. }
  52. var topicNames = _options.TopicGenerationStrategy.CreateRpcTopics(new TopicGenerationContext
  53. {
  54. MethodName = methodName,
  55. QualityOfServiceLevel = qualityOfServiceLevel,
  56. MqttClient = _mqttClient,
  57. Options = _options
  58. });
  59. var requestTopic = topicNames.RequestTopic;
  60. var responseTopic = topicNames.ResponseTopic;
  61. if (string.IsNullOrWhiteSpace(requestTopic))
  62. {
  63. throw new MqttProtocolViolationException("RPC request topic is empty.");
  64. }
  65. if (string.IsNullOrWhiteSpace(responseTopic))
  66. {
  67. throw new MqttProtocolViolationException("RPC response topic is empty.");
  68. }
  69. var requestMessage = new MqttApplicationMessageBuilder()
  70. .WithTopic(requestTopic)
  71. .WithPayload(payload)
  72. .WithQualityOfServiceLevel(qualityOfServiceLevel)
  73. .Build();
  74. try
  75. {
  76. var promise = new TaskCompletionSource<byte[]>();
  77. if (!_waitingCalls.TryAdd(responseTopic, promise))
  78. {
  79. throw new InvalidOperationException();
  80. }
  81. await _mqttClient.SubscribeAsync(responseTopic, qualityOfServiceLevel).ConfigureAwait(false);
  82. await _mqttClient.PublishAsync(requestMessage).ConfigureAwait(false);
  83. using (var timeoutCts = new CancellationTokenSource(timeout))
  84. using (var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token))
  85. {
  86. linkedCts.Token.Register(() =>
  87. {
  88. if (!promise.Task.IsCompleted && !promise.Task.IsFaulted && !promise.Task.IsCanceled)
  89. {
  90. promise.TrySetCanceled();
  91. }
  92. });
  93. try
  94. {
  95. var result = await promise.Task.ConfigureAwait(false);
  96. timeoutCts.Cancel(false);
  97. return result;
  98. }
  99. catch (OperationCanceledException exception)
  100. {
  101. if (timeoutCts.IsCancellationRequested && !cancellationToken.IsCancellationRequested)
  102. {
  103. throw new MqttCommunicationTimedOutException(exception);
  104. }
  105. else
  106. {
  107. throw;
  108. }
  109. }
  110. }
  111. }
  112. finally
  113. {
  114. _waitingCalls.TryRemove(responseTopic, out _);
  115. await _mqttClient.UnsubscribeAsync(responseTopic).ConfigureAwait(false);
  116. }
  117. }
  118. Task HandleApplicationMessageReceivedAsync(MqttApplicationMessageReceivedEventArgs eventArgs)
  119. {
  120. if (!_waitingCalls.TryRemove(eventArgs.ApplicationMessage.Topic, out var promise))
  121. {
  122. return PlatformAbstractionLayer.CompletedTask;
  123. }
  124. promise.TrySetResult(eventArgs.ApplicationMessage.Payload);
  125. // Set this message to handled to that other code can avoid execution etc.
  126. eventArgs.IsHandled = true;
  127. return PlatformAbstractionLayer.CompletedTask;
  128. }
  129. public void Dispose()
  130. {
  131. _mqttClient.ApplicationMessageReceivedHandler = _applicationMessageReceivedHandler.OriginalHandler;
  132. foreach (var tcs in _waitingCalls)
  133. {
  134. tcs.Value.TrySetCanceled();
  135. }
  136. _waitingCalls.Clear();
  137. }
  138. }
  139. }