From a004de25ac6320fb60e9d12f15300a13d1ef69aa Mon Sep 17 00:00:00 2001 From: Christian <6939810+chkr1011@users.noreply.github.com> Date: Wed, 22 Feb 2023 21:46:33 +0100 Subject: [PATCH 1/9] Fix unit test --- Source/MQTTnet.Tests/Server/Publishing_Tests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Source/MQTTnet.Tests/Server/Publishing_Tests.cs b/Source/MQTTnet.Tests/Server/Publishing_Tests.cs index dbffcf61..63923507 100644 --- a/Source/MQTTnet.Tests/Server/Publishing_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Publishing_Tests.cs @@ -87,6 +87,8 @@ namespace MQTTnet.Tests.Server await sender.PublishStringAsync("A", "Payload", MqttQualityOfServiceLevel.AtLeastOnce); + await LongTestDelay(); + receivedMessages.AssertReceivedCountEquals(1); server.InterceptingClientEnqueueAsync += e => From b4f740f33809b884db373991bce49671d78372ad Mon Sep 17 00:00:00 2001 From: Christian <6939810+chkr1011@users.noreply.github.com> Date: Wed, 5 Apr 2023 22:12:58 +0200 Subject: [PATCH 2/9] Update readme --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index 0aadf450..90c25b1e 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,22 @@ MQTTnet is a high performance .NET library for MQTT based communication. It prov * A custom message interceptor can be added which allows transforming or extending every received application message * Validate subscriptions and deny subscribing of certain topics depending on requesting clients +## Getting Started + +MQTTnet is delivered via NuGet package manager. You can find the packages here: https://www.nuget.org/packages/MQTTnet/ + +Use these command in the Package Manager console to install MQTTnet manually: +``` +Install-Package MQTTnet +``` + +Samples for using MQTTnet are part of this repository. For starters these samples are recommended: + +- [Connect with a broker](https://github.com/dotnet/MQTTnet/blob/master/Samples/Client/Client_Connection_Samples.cs) +- [Subscribing to data](https://github.com/dotnet/MQTTnet/blob/master/Samples/Client/Client_Subscribe_Samples.cs) +- [Publishing data](https://github.com/dotnet/MQTTnet/blob/master/Samples/Client/Client_Publish_Samples.cs) +- [Host own broker](https://github.com/dotnet/MQTTnet/blob/master/Samples/Server/Server_Simple_Samples.cs) + ## Code of Conduct This project has adopted the code of conduct defined by the Contributor Covenant to clarify expected behavior in our community. From d6cab767288c35aa82e0a5dc1b66fceb7a269eac Mon Sep 17 00:00:00 2001 From: Christian <6939810+chkr1011@users.noreply.github.com> Date: Thu, 6 Apr 2023 17:31:42 +0200 Subject: [PATCH 3/9] Add user properties to event args to interceptors for subscribing and unsubscribing (#1707) Add user properties to event args --- .github/workflows/ReleaseNotes.md | 1 + .../MqttClient/MqttClient_Connection_Tests.cs | 2 + .../MQTTnet.Tests/Server/Subscribe_Tests.cs | 27 +++++++++++- .../MQTTnet.Tests/Server/Unsubscribe_Tests.cs | 27 ++++++++++++ .../MqttClientUnsubscribeResult.cs | 2 +- .../InterceptingSubscriptionEventArgs.cs | 5 ++- .../InterceptingUnsubscriptionEventArgs.cs | 11 ++++- .../MqttClientSubscriptionsManager.cs | 42 +++++++++++-------- .../Server/Internal/UnsubscribeResult.cs | 5 ++- 9 files changed, 99 insertions(+), 23 deletions(-) diff --git a/.github/workflows/ReleaseNotes.md b/.github/workflows/ReleaseNotes.md index d534f1b7..22322c50 100644 --- a/.github/workflows/ReleaseNotes.md +++ b/.github/workflows/ReleaseNotes.md @@ -12,3 +12,4 @@ * [Server] Added a new API method which allows reading a single retained message without the need to processing the entire set of retained messages (#1659). * [Server] Added a new event (InterceptingClientEnqueueAsync) which allows intercepting when an application message is enqueued for a client (#1648). * [Server] Fixed race condition when handling connections which leads to stopped message transfers (#1677, thanks to @RazvanEmilR). +* [Server] Added missing user properties to subscribe and unsubscribe interceptor (#1702). \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Clients/MqttClient/MqttClient_Connection_Tests.cs b/Source/MQTTnet.Tests/Clients/MqttClient/MqttClient_Connection_Tests.cs index 87ecf0b4..1cdcd760 100644 --- a/Source/MQTTnet.Tests/Clients/MqttClient/MqttClient_Connection_Tests.cs +++ b/Source/MQTTnet.Tests/Clients/MqttClient/MqttClient_Connection_Tests.cs @@ -99,6 +99,8 @@ namespace MQTTnet.Tests.Clients.MqttClient // Perform a clean disconnect. await client.DisconnectAsync(disconnectOptions); + await LongTestDelay(); + Assert.IsNotNull(eventArgs); Assert.AreEqual(MqttClientDisconnectType.Clean, eventArgs.DisconnectType); } diff --git a/Source/MQTTnet.Tests/Server/Subscribe_Tests.cs b/Source/MQTTnet.Tests/Server/Subscribe_Tests.cs index a4ce93b7..897c3c07 100644 --- a/Source/MQTTnet.Tests/Server/Subscribe_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Subscribe_Tests.cs @@ -62,7 +62,7 @@ namespace MQTTnet.Tests.Server } } } - + [TestMethod] public async Task Deny_Invalid_Topic() { @@ -90,6 +90,29 @@ namespace MQTTnet.Tests.Server } } + [TestMethod] + public async Task Intercept_Subscribe_With_User_Properties() + { + using (var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500)) + { + var server = await testEnvironment.StartServer(); + + InterceptingSubscriptionEventArgs eventArgs = null; + server.InterceptingSubscriptionAsync += e => + { + eventArgs = e; + return CompletedTask.Instance; + }; + + var client = await testEnvironment.ConnectClient(); + + var subscribeOptions = testEnvironment.Factory.CreateSubscribeOptionsBuilder().WithTopicFilter("X").WithUserProperty("A", "1").Build(); + await client.SubscribeAsync(subscribeOptions); + + CollectionAssert.AreEqual(subscribeOptions.UserProperties.ToList(), eventArgs.UserProperties); + } + } + [TestMethod] [ExpectedException(typeof(MqttClientDisconnectedException))] public async Task Disconnect_While_Subscribing() @@ -105,7 +128,7 @@ namespace MQTTnet.Tests.Server await client.SubscribeAsync("#"); } } - + [TestMethod] public async Task Enqueue_Message_After_Subscription() { diff --git a/Source/MQTTnet.Tests/Server/Unsubscribe_Tests.cs b/Source/MQTTnet.Tests/Server/Unsubscribe_Tests.cs index 5692fa8d..4ed527c8 100644 --- a/Source/MQTTnet.Tests/Server/Unsubscribe_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Unsubscribe_Tests.cs @@ -2,10 +2,14 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Linq; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Client; +using MQTTnet.Formatter; +using MQTTnet.Internal; using MQTTnet.Protocol; +using MQTTnet.Server; namespace MQTTnet.Tests.Server { @@ -28,5 +32,28 @@ namespace MQTTnet.Tests.Server await client.UnsubscribeAsync("#"); } } + + [TestMethod] + public async Task Intercept_Unsubscribe_With_User_Properties() + { + using (var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500)) + { + var server = await testEnvironment.StartServer(); + + InterceptingUnsubscriptionEventArgs eventArgs = null; + server.InterceptingUnsubscriptionAsync += e => + { + eventArgs = e; + return CompletedTask.Instance; + }; + + var client = await testEnvironment.ConnectClient(); + + var unsubscribeOptions = testEnvironment.Factory.CreateUnsubscribeOptionsBuilder().WithTopicFilter("X").WithUserProperty("A", "1").Build(); + await client.UnsubscribeAsync(unsubscribeOptions); + + CollectionAssert.AreEqual(unsubscribeOptions.UserProperties.ToList(), eventArgs.UserProperties); + } + } } } \ No newline at end of file diff --git a/Source/MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeResult.cs b/Source/MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeResult.cs index 24b8008f..60aedcdf 100644 --- a/Source/MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeResult.cs +++ b/Source/MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeResult.cs @@ -42,6 +42,6 @@ namespace MQTTnet.Client /// Gets the user properties which were part of the UNSUBACK packet. /// MQTT 5.0.0+ feature. /// - public IReadOnlyCollection UserProperties { get; } + public IReadOnlyCollection UserProperties { get; set; } } } \ No newline at end of file diff --git a/Source/MQTTnet/Server/Events/InterceptingSubscriptionEventArgs.cs b/Source/MQTTnet/Server/Events/InterceptingSubscriptionEventArgs.cs index d7f787e1..fa26d13e 100644 --- a/Source/MQTTnet/Server/Events/InterceptingSubscriptionEventArgs.cs +++ b/Source/MQTTnet/Server/Events/InterceptingSubscriptionEventArgs.cs @@ -16,12 +16,14 @@ namespace MQTTnet.Server CancellationToken cancellationToken, string clientId, MqttSessionStatus session, - MqttTopicFilter topicFilter) + MqttTopicFilter topicFilter, + List userProperties) { CancellationToken = cancellationToken; ClientId = clientId; Session = session; TopicFilter = topicFilter; + UserProperties = userProperties; } /// @@ -75,6 +77,7 @@ namespace MQTTnet.Server /// /// Gets or sets the user properties. + /// MQTT 5.0.0+ feature. /// public List UserProperties { get; set; } } diff --git a/Source/MQTTnet/Server/Events/InterceptingUnsubscriptionEventArgs.cs b/Source/MQTTnet/Server/Events/InterceptingUnsubscriptionEventArgs.cs index 63e8cad5..36d08df3 100644 --- a/Source/MQTTnet/Server/Events/InterceptingUnsubscriptionEventArgs.cs +++ b/Source/MQTTnet/Server/Events/InterceptingUnsubscriptionEventArgs.cs @@ -4,18 +4,21 @@ using System; using System.Collections; +using System.Collections.Generic; using System.Threading; +using MQTTnet.Packets; namespace MQTTnet.Server { public sealed class InterceptingUnsubscriptionEventArgs : EventArgs { - public InterceptingUnsubscriptionEventArgs(CancellationToken cancellationToken, string clientId, IDictionary sessionItems, string topic) + public InterceptingUnsubscriptionEventArgs(CancellationToken cancellationToken, string clientId, IDictionary sessionItems, string topic, List userProperties) { CancellationToken = cancellationToken; ClientId = clientId; SessionItems = sessionItems; Topic = topic; + UserProperties = userProperties; } /// @@ -59,5 +62,11 @@ namespace MQTTnet.Server /// separator). /// public string Topic { get; } + + /// + /// Gets or sets the user properties. + /// MQTT 5.0.0+ feature. + /// + public List UserProperties { get; set; } } } \ No newline at end of file diff --git a/Source/MQTTnet/Server/Internal/MqttClientSubscriptionsManager.cs b/Source/MQTTnet/Server/Internal/MqttClientSubscriptionsManager.cs index af2d8dd3..24595fd4 100644 --- a/Source/MQTTnet/Server/Internal/MqttClientSubscriptionsManager.cs +++ b/Source/MQTTnet/Server/Internal/MqttClientSubscriptionsManager.cs @@ -177,15 +177,15 @@ namespace MQTTnet.Server // lower one. foreach (var topicFilterItem in subscribePacket.TopicFilters.OrderByDescending(f => f.QualityOfServiceLevel)) { - var subscriptionEventArgs = await InterceptSubscribe(topicFilterItem, cancellationToken).ConfigureAwait(false); - var topicFilter = subscriptionEventArgs.TopicFilter; - var processSubscription = subscriptionEventArgs.ProcessSubscription && subscriptionEventArgs.Response.ReasonCode <= MqttSubscribeReasonCode.GrantedQoS2; + var interceptorEventArgs = await InterceptSubscribe(topicFilterItem, subscribePacket.UserProperties, cancellationToken).ConfigureAwait(false); + var topicFilter = interceptorEventArgs.TopicFilter; + var processSubscription = interceptorEventArgs.ProcessSubscription && interceptorEventArgs.Response.ReasonCode <= MqttSubscribeReasonCode.GrantedQoS2; - result.UserProperties = subscriptionEventArgs.UserProperties; - result.ReasonString = subscriptionEventArgs.ReasonString; - result.ReasonCodes.Add(subscriptionEventArgs.Response.ReasonCode); + result.UserProperties = interceptorEventArgs.UserProperties; + result.ReasonString = interceptorEventArgs.ReasonString; + result.ReasonCodes.Add(interceptorEventArgs.Response.ReasonCode); - if (subscriptionEventArgs.CloseConnection) + if (interceptorEventArgs.CloseConnection) { // When any of the interceptor calls leads to a connection close the connection // must be closed. So do not revert to false! @@ -197,7 +197,7 @@ namespace MQTTnet.Server continue; } - var createSubscriptionResult = CreateSubscription(topicFilter, subscribePacket.SubscriptionIdentifier, subscriptionEventArgs.Response.ReasonCode); + var createSubscriptionResult = CreateSubscription(topicFilter, subscribePacket.SubscriptionIdentifier, interceptorEventArgs.Response.ReasonCode); addedSubscriptions.Add(topicFilter.Topic); finalTopicFilters.Add(topicFilter); @@ -238,12 +238,13 @@ namespace MQTTnet.Server { _subscriptions.TryGetValue(topicFilter, out var existingSubscription); - var interceptorContext = await InterceptUnsubscribe(topicFilter, existingSubscription, cancellationToken).ConfigureAwait(false); - var acceptUnsubscription = interceptorContext.Response.ReasonCode == MqttUnsubscribeReasonCode.Success; + var interceptorEventArgs = await InterceptUnsubscribe(topicFilter, existingSubscription, unsubscribePacket.UserProperties, cancellationToken).ConfigureAwait(false); + var acceptUnsubscription = interceptorEventArgs.Response.ReasonCode == MqttUnsubscribeReasonCode.Success; - result.ReasonCodes.Add(interceptorContext.Response.ReasonCode); + result.UserProperties = interceptorEventArgs.UserProperties; + result.ReasonCodes.Add(interceptorEventArgs.Response.ReasonCode); - if (interceptorContext.CloseConnection) + if (interceptorEventArgs.CloseConnection) { // When any of the interceptor calls leads to a connection close the connection // must be closed. So do not revert to false! @@ -255,7 +256,7 @@ namespace MQTTnet.Server continue; } - if (interceptorContext.ProcessUnsubscription) + if (interceptorEventArgs.ProcessUnsubscription) { _subscriptions.Remove(topicFilter); @@ -452,9 +453,12 @@ namespace MQTTnet.Server } } - async Task InterceptSubscribe(MqttTopicFilter topicFilter, CancellationToken cancellationToken) + async Task InterceptSubscribe( + MqttTopicFilter topicFilter, + List userProperties, + CancellationToken cancellationToken) { - var eventArgs = new InterceptingSubscriptionEventArgs(cancellationToken, _session.Id, new MqttSessionStatus(_session), topicFilter); + var eventArgs = new InterceptingSubscriptionEventArgs(cancellationToken, _session.Id, new MqttSessionStatus(_session), topicFilter, userProperties); if (topicFilter.QualityOfServiceLevel == MqttQualityOfServiceLevel.AtMostOnce) { @@ -481,9 +485,13 @@ namespace MQTTnet.Server return eventArgs; } - async Task InterceptUnsubscribe(string topicFilter, MqttSubscription mqttSubscription, CancellationToken cancellationToken) + async Task InterceptUnsubscribe( + string topicFilter, + MqttSubscription mqttSubscription, + List userProperties, + CancellationToken cancellationToken) { - var clientUnsubscribingTopicEventArgs = new InterceptingUnsubscriptionEventArgs(cancellationToken, _session.Id, _session.Items, topicFilter) + var clientUnsubscribingTopicEventArgs = new InterceptingUnsubscriptionEventArgs(cancellationToken, _session.Id, _session.Items, topicFilter, userProperties) { Response = { diff --git a/Source/MQTTnet/Server/Internal/UnsubscribeResult.cs b/Source/MQTTnet/Server/Internal/UnsubscribeResult.cs index 5a93d997..38033b19 100644 --- a/Source/MQTTnet/Server/Internal/UnsubscribeResult.cs +++ b/Source/MQTTnet/Server/Internal/UnsubscribeResult.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Collections.Generic; +using MQTTnet.Packets; using MQTTnet.Protocol; namespace MQTTnet.Server @@ -10,7 +11,9 @@ namespace MQTTnet.Server public sealed class UnsubscribeResult { public List ReasonCodes { get; } = new List(128); - + public bool CloseConnection { get; set; } + + public List UserProperties { get; set; } } } \ No newline at end of file From 36c6d8974eac9ed4f04e342229e7d35b6a836471 Mon Sep 17 00:00:00 2001 From: Christian <6939810+chkr1011@users.noreply.github.com> Date: Thu, 6 Apr 2023 17:54:36 +0200 Subject: [PATCH 4/9] Allow disabling of packet fragmentation (#1709) * Add compatibility option for AWS * Fix UWP build * Fix comment in MqttClientOptions (#1706) * Refactor naming * Fix wrong option value --------- Co-authored-by: Michi --- .github/workflows/ReleaseNotes.md | 1 + Samples/Client/Client_Connection_Samples.cs | 26 +++++++ .../MqttWebSocketServerAdapter.cs | 2 +- .../ChannelAdapterBenchmark.cs | 2 +- .../MQTTnet.Benchmarks/SerializerBenchmark.cs | 2 +- .../ManagedMqttClientOptions.cs | 2 +- .../WebSocket4NetMqttClientAdapterFactory.cs | 12 ++-- ...MqttPacketSerialization_V3_Binary_Tests.cs | 12 +--- .../MqttPacketSerializationHelper.cs | 2 +- Source/MQTTnet/Adapter/MqttChannelAdapter.cs | 42 ++++++----- .../Client/Options/MqttClientOptions.cs | 10 ++- .../Options/MqttClientOptionsBuilder.cs | 70 +++++++++++-------- .../MqttClientAdapterFactory.cs | 7 +- .../MqttTcpServerAdapter.Uwp.cs | 2 +- .../Implementations/MqttTcpServerListener.cs | 2 +- 15 files changed, 123 insertions(+), 71 deletions(-) diff --git a/.github/workflows/ReleaseNotes.md b/.github/workflows/ReleaseNotes.md index 22322c50..e84ee186 100644 --- a/.github/workflows/ReleaseNotes.md +++ b/.github/workflows/ReleaseNotes.md @@ -3,6 +3,7 @@ * [Client] Added support for passing MQTT v5 options (User properties etc.) for disconnects. * [Client] An internal exception is no longer caught silently when calling _DisconnectAsync_ to indicate that the disconnect is not clean (BREAKING CHANGE). * [Client] MQTTv5 features are now checked and an exception is thrown if they are used when using protocol version 3.1.1 and lower. These checks can be disabled in client options. (BREAKING CHANGE!). +* [Client] Added support for disabling packet fragmentation (required for i.e. AWS, #1690, thanks to @logicaloud). * [Server] Exposed MQTT v5 sent properties from the affected client in _ClientDisconnectedAsync_ event. * [Server] Fixed wrong client ID passed to _InterceptingUnsubscriptionEventArgs_ (#1631, thanks to @ghord). * [Server] Exposed socket settings for TCP keep alive in TCP options (#1544). diff --git a/Samples/Client/Client_Connection_Samples.cs b/Samples/Client/Client_Connection_Samples.cs index 81808a4e..d74ccb85 100644 --- a/Samples/Client/Client_Connection_Samples.cs +++ b/Samples/Client/Client_Connection_Samples.cs @@ -70,6 +70,32 @@ public static class Client_Connection_Samples await mqttClient.DisconnectAsync(mqttClientDisconnectOptions, CancellationToken.None); } } + + public static async Task Connect_With_Amazon_AWS() + { + /* + * This sample creates a simple MQTT client and connects to an Amazon Web Services broker. + * + * The broker requires special settings which are set here. + */ + + var mqttFactory = new MqttFactory(); + + using (var mqttClient = mqttFactory.CreateMqttClient()) + { + var mqttClientOptions = new MqttClientOptionsBuilder() + .WithTcpServer("amazon.web.services.broker") + // Disabling packet fragmentation is very important! + .WithoutPacketFragmentation() + .Build(); + + await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); + + Console.WriteLine("The MQTT client is connected."); + + await mqttClient.DisconnectAsync(); + } + } public static async Task Connect_Client_Timeout() { diff --git a/Source/MQTTnet.AspnetCore/MqttWebSocketServerAdapter.cs b/Source/MQTTnet.AspnetCore/MqttWebSocketServerAdapter.cs index a83eb026..0594af78 100644 --- a/Source/MQTTnet.AspnetCore/MqttWebSocketServerAdapter.cs +++ b/Source/MQTTnet.AspnetCore/MqttWebSocketServerAdapter.cs @@ -48,7 +48,7 @@ namespace MQTTnet.AspNetCore var formatter = new MqttPacketFormatterAdapter(new MqttBufferWriter(4096, 65535)); var channel = new MqttWebSocketChannel(webSocket, endpoint, isSecureConnection, clientCertificate); - using (var channelAdapter = new MqttChannelAdapter(channel, formatter, null, _logger)) + using (var channelAdapter = new MqttChannelAdapter(channel, formatter, _logger)) { await clientHandler(channelAdapter).ConfigureAwait(false); } diff --git a/Source/MQTTnet.Benchmarks/ChannelAdapterBenchmark.cs b/Source/MQTTnet.Benchmarks/ChannelAdapterBenchmark.cs index be2e0eb4..3b6a7d85 100644 --- a/Source/MQTTnet.Benchmarks/ChannelAdapterBenchmark.cs +++ b/Source/MQTTnet.Benchmarks/ChannelAdapterBenchmark.cs @@ -73,7 +73,7 @@ namespace MQTTnet.Benchmarks var channel = new MemoryMqttChannel(_stream); - _channelAdapter = new MqttChannelAdapter(channel, serializer, null, new MqttNetEventLogger()); + _channelAdapter = new MqttChannelAdapter(channel, serializer, new MqttNetEventLogger()); } static byte[] Join(params ArraySegment[] chunks) diff --git a/Source/MQTTnet.Benchmarks/SerializerBenchmark.cs b/Source/MQTTnet.Benchmarks/SerializerBenchmark.cs index eca6771a..de6ed3dc 100644 --- a/Source/MQTTnet.Benchmarks/SerializerBenchmark.cs +++ b/Source/MQTTnet.Benchmarks/SerializerBenchmark.cs @@ -54,7 +54,7 @@ namespace MQTTnet.Benchmarks public void Deserialize_10000_Messages() { var channel = new BenchmarkMqttChannel(_serializedPacket); - var reader = new MqttChannelAdapter(channel, new MqttPacketFormatterAdapter(new MqttBufferWriter(4096, 65535)), null, new MqttNetEventLogger()); + var reader = new MqttChannelAdapter(channel, new MqttPacketFormatterAdapter(new MqttBufferWriter(4096, 65535)), new MqttNetEventLogger()); for (var i = 0; i < 10000; i++) { diff --git a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientOptions.cs b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientOptions.cs index dcd0c119..81bf3b93 100644 --- a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientOptions.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientOptions.cs @@ -24,7 +24,7 @@ namespace MQTTnet.Extensions.ManagedClient /// /// Defines the maximum amount of topic filters which will be sent in a SUBSCRIBE/UNSUBSCRIBE packet. - /// Amazon AWS limits this number to 8. The default is int.MaxValue. + /// Amazon Web Services (AWS) limits this number to 8. The default is int.MaxValue. /// public int MaxTopicFiltersInSubscribeUnsubscribePackets { get; set; } = int.MaxValue; } diff --git a/Source/MQTTnet.Extensions.WebSocket4Net/WebSocket4NetMqttClientAdapterFactory.cs b/Source/MQTTnet.Extensions.WebSocket4Net/WebSocket4NetMqttClientAdapterFactory.cs index 27359a1a..30138fe0 100644 --- a/Source/MQTTnet.Extensions.WebSocket4Net/WebSocket4NetMqttClientAdapterFactory.cs +++ b/Source/MQTTnet.Extensions.WebSocket4Net/WebSocket4NetMqttClientAdapterFactory.cs @@ -27,8 +27,10 @@ namespace MQTTnet.Extensions.WebSocket4Net return new MqttChannelAdapter( new MqttTcpChannel(options), new MqttPacketFormatterAdapter(options.ProtocolVersion, new MqttBufferWriter(options.WriterBufferSize, options.WriterBufferSizeMax)), - packetInspector, - logger); + logger) + { + PacketInspector = packetInspector + }; } case MqttClientWebSocketOptions webSocketOptions: @@ -36,8 +38,10 @@ namespace MQTTnet.Extensions.WebSocket4Net return new MqttChannelAdapter( new WebSocket4NetMqttChannel(options, webSocketOptions), new MqttPacketFormatterAdapter(options.ProtocolVersion, new MqttBufferWriter(options.WriterBufferSize, options.WriterBufferSizeMax)), - packetInspector, - logger); + logger) + { + PacketInspector = packetInspector + }; } default: diff --git a/Source/MQTTnet.Tests/Formatter/MqttPacketSerialization_V3_Binary_Tests.cs b/Source/MQTTnet.Tests/Formatter/MqttPacketSerialization_V3_Binary_Tests.cs index 4cda6c0a..b9260848 100644 --- a/Source/MQTTnet.Tests/Formatter/MqttPacketSerialization_V3_Binary_Tests.cs +++ b/Source/MQTTnet.Tests/Formatter/MqttPacketSerialization_V3_Binary_Tests.cs @@ -566,7 +566,6 @@ namespace MQTTnet.Tests.Formatter using (var adapter = new MqttChannelAdapter( channel, new MqttPacketFormatterAdapter(protocolVersion, new MqttBufferWriter(4096, 65535)), - null, new MqttNetEventLogger())) { var receivedPacket = adapter.ReceivePacketAsync(CancellationToken.None).GetAwaiter().GetResult(); @@ -582,19 +581,12 @@ namespace MQTTnet.Tests.Formatter MqttProtocolVersion DeserializeAndDetectVersion(MqttPacketFormatterAdapter packetFormatterAdapter, byte[] buffer) { var channel = new MemoryMqttChannel(buffer); - var adapter = new MqttChannelAdapter(channel, packetFormatterAdapter, null, new MqttNetEventLogger()); + var adapter = new MqttChannelAdapter(channel, packetFormatterAdapter, new MqttNetEventLogger()); adapter.ReceivePacketAsync(CancellationToken.None).GetAwaiter().GetResult(); return packetFormatterAdapter.ProtocolVersion; } - MqttBufferReader ReaderFactory(byte[] data) - { - var reader = new MqttBufferReader(); - reader.SetBuffer(data, 0, data.Length); - return reader; - } - TPacket Roundtrip(TPacket packet, MqttProtocolVersion protocolVersion = MqttProtocolVersion.V311, MqttBufferWriter bufferWriter = null) where TPacket : MqttPacket { var writer = bufferWriter ?? WriterFactory(); @@ -603,7 +595,7 @@ namespace MQTTnet.Tests.Formatter using (var channel = new MemoryMqttChannel(buffer.Join().ToArray())) { - var adapter = new MqttChannelAdapter(channel, new MqttPacketFormatterAdapter(protocolVersion, new MqttBufferWriter(4096, 65535)), null, new MqttNetEventLogger()); + var adapter = new MqttChannelAdapter(channel, new MqttPacketFormatterAdapter(protocolVersion, new MqttBufferWriter(4096, 65535)), new MqttNetEventLogger()); return (TPacket)adapter.ReceivePacketAsync(CancellationToken.None).GetAwaiter().GetResult(); } } diff --git a/Source/MQTTnet.Tests/MqttPacketSerializationHelper.cs b/Source/MQTTnet.Tests/MqttPacketSerializationHelper.cs index 5d93c72a..43d1323d 100644 --- a/Source/MQTTnet.Tests/MqttPacketSerializationHelper.cs +++ b/Source/MQTTnet.Tests/MqttPacketSerializationHelper.cs @@ -31,7 +31,7 @@ namespace MQTTnet.Tests { var formatterAdapter = new MqttPacketFormatterAdapter(_protocolVersion, new MqttBufferWriter(4096, 65535)); - var adapter = new MqttChannelAdapter(channel, formatterAdapter, null, MqttNetNullLogger.Instance); + var adapter = new MqttChannelAdapter(channel, formatterAdapter, MqttNetNullLogger.Instance); return adapter.ReceivePacketAsync(CancellationToken.None).GetAwaiter().GetResult(); } } diff --git a/Source/MQTTnet/Adapter/MqttChannelAdapter.cs b/Source/MQTTnet/Adapter/MqttChannelAdapter.cs index 47e28b31..92a2e012 100644 --- a/Source/MQTTnet/Adapter/MqttChannelAdapter.cs +++ b/Source/MQTTnet/Adapter/MqttChannelAdapter.cs @@ -27,18 +27,15 @@ namespace MQTTnet.Adapter readonly byte[] _fixedHeaderBuffer = new byte[2]; readonly MqttNetSourceLogger _logger; - readonly MqttPacketInspector _packetInspector; - readonly byte[] _singleByteBuffer = new byte[1]; readonly AsyncLock _syncRoot = new AsyncLock(); - Statistics _statistics; // mutable struct, don't make readonly! + Statistics _statistics; // mutable struct, don't make readonly! - public MqttChannelAdapter(IMqttChannel channel, MqttPacketFormatterAdapter packetFormatterAdapter, MqttPacketInspector packetInspector, IMqttNetLogger logger) + public MqttChannelAdapter(IMqttChannel channel, MqttPacketFormatterAdapter packetFormatterAdapter, IMqttNetLogger logger) { _channel = channel ?? throw new ArgumentNullException(nameof(channel)); - _packetInspector = packetInspector; PacketFormatterAdapter = packetFormatterAdapter ?? throw new ArgumentNullException(nameof(packetFormatterAdapter)); @@ -64,6 +61,10 @@ namespace MQTTnet.Adapter public MqttPacketFormatterAdapter PacketFormatterAdapter { get; } + public MqttPacketInspector PacketInspector { get; set; } + + public bool AllowPacketFragmentation { get; set; } = true; + public async Task ConnectAsync(CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); @@ -126,7 +127,7 @@ namespace MQTTnet.Adapter try { - _packetInspector?.BeginReceivePacket(); + PacketInspector?.BeginReceivePacket(); ReceivedMqttPacket receivedPacket; var receivedPacketTask = ReceiveAsync(cancellationToken); @@ -144,7 +145,7 @@ namespace MQTTnet.Adapter return null; } - _packetInspector?.EndReceivePacket(); + PacketInspector?.EndReceivePacket(); Interlocked.Add(ref _statistics._bytesSent, receivedPacket.TotalLength); @@ -180,7 +181,10 @@ namespace MQTTnet.Adapter return null; } - public void ResetStatistics() => _statistics.Reset(); + public void ResetStatistics() + { + _statistics.Reset(); + } public async Task SendPacketAsync(MqttPacket packet, CancellationToken cancellationToken) { @@ -197,18 +201,18 @@ namespace MQTTnet.Adapter try { var packetBuffer = PacketFormatterAdapter.Encode(packet); - _packetInspector?.BeginSendPacket(packetBuffer); + PacketInspector?.BeginSendPacket(packetBuffer); _logger.Verbose("TX ({0} bytes) >>> {1}", packetBuffer.Length, packet); - - if (packetBuffer.Payload.Count > 0) + + if (packetBuffer.Payload.Count == 0 || !AllowPacketFragmentation) { - await _channel.WriteAsync(packetBuffer.Packet, false, cancellationToken).ConfigureAwait(false); - await _channel.WriteAsync(packetBuffer.Payload, true, cancellationToken).ConfigureAwait(false); + await _channel.WriteAsync(packetBuffer.Join(), true, cancellationToken).ConfigureAwait(false); } else { - await _channel.WriteAsync(packetBuffer.Packet, true, cancellationToken).ConfigureAwait(false); + await _channel.WriteAsync(packetBuffer.Packet, false, cancellationToken).ConfigureAwait(false); + await _channel.WriteAsync(packetBuffer.Payload, true, cancellationToken).ConfigureAwait(false); } Interlocked.Add(ref _statistics._bytesReceived, packetBuffer.Length); @@ -270,7 +274,7 @@ namespace MQTTnet.Adapter return 0; } - _packetInspector?.FillReceiveBuffer(_singleByteBuffer); + PacketInspector?.FillReceiveBuffer(_singleByteBuffer); encodedByte = _singleByteBuffer[0]; @@ -309,7 +313,7 @@ namespace MQTTnet.Adapter { return ReadFixedHeaderResult.ConnectionClosed; } - + if (cancellationToken.IsCancellationRequested) { return ReadFixedHeaderResult.Canceled; @@ -323,7 +327,7 @@ namespace MQTTnet.Adapter totalBytesRead += bytesRead; } - _packetInspector?.FillReceiveBuffer(buffer); + PacketInspector?.FillReceiveBuffer(buffer); var hasRemainingLength = buffer[1] != 0; if (!hasRemainingLength) @@ -407,7 +411,7 @@ namespace MQTTnet.Adapter bodyOffset += readBytes; } while (bodyOffset < bodyLength); - _packetInspector?.FillReceiveBuffer(body); + PacketInspector?.FillReceiveBuffer(body); var bodySegment = new ArraySegment(body, 0, bodyLength); return new ReceivedMqttPacket(fixedHeader.Flags, bodySegment, fixedHeader.TotalLength); @@ -455,7 +459,7 @@ namespace MQTTnet.Adapter throw new MqttCommunicationException(exception); } - private struct Statistics + struct Statistics { public long _bytesReceived; public long _bytesSent; diff --git a/Source/MQTTnet/Client/Options/MqttClientOptions.cs b/Source/MQTTnet/Client/Options/MqttClientOptions.cs index 1101b3e5..f296777f 100644 --- a/Source/MQTTnet/Client/Options/MqttClientOptions.cs +++ b/Source/MQTTnet/Client/Options/MqttClientOptions.cs @@ -24,6 +24,14 @@ namespace MQTTnet.Client /// public string AuthenticationMethod { get; set; } + /// + /// Usually the MQTT packets can be send partially. This is done by using multiple TCP packets + /// or WebSocket frames etc. Unfortunately not all brokers (like Amazon Web Services (AWS)) do support this feature and + /// will close the connection when receiving such packets. If such a service is used this flag must + /// be set to _false_. + /// + public bool AllowPacketFragmentation { get; set; } = true; + public IMqttClientChannelOptions ChannelOptions { get; set; } /// @@ -214,4 +222,4 @@ namespace MQTTnet.Client /// public int WriterBufferSizeMax { get; set; } = 65535; } -} \ No newline at end of file +} diff --git a/Source/MQTTnet/Client/Options/MqttClientOptionsBuilder.cs b/Source/MQTTnet/Client/Options/MqttClientOptionsBuilder.cs index 86ac9eec..fb17733e 100644 --- a/Source/MQTTnet/Client/Options/MqttClientOptionsBuilder.cs +++ b/Source/MQTTnet/Client/Options/MqttClientOptionsBuilder.cs @@ -75,10 +75,22 @@ namespace MQTTnet.Client _options.ChannelOptions = (IMqttClientChannelOptions)_tcpOptions ?? _webSocketOptions; MqttClientOptionsValidator.ThrowIfNotSupported(_options); - + return _options; } + /// + /// Usually the MQTT packets can be send partially. This is done by using multiple TCP packets + /// or WebSocket frames etc. Unfortunately not all brokers (like Amazon Web Services (AWS)) do support this feature and + /// will close the connection when receiving such packets. If such a service is used this flag must + /// be set to _true_. + /// + public MqttClientOptionsBuilder WithoutPacketFragmentation() + { + _options.AllowPacketFragmentation = false; + return this; + } + public MqttClientOptionsBuilder WithAuthentication(string method, byte[] data) { _options.AuthenticationMethod = method; @@ -266,7 +278,7 @@ namespace MQTTnet.Client return this; } - + public MqttClientOptionsBuilder WithTcpServer(Action optionsBuilder) { if (optionsBuilder == null) @@ -373,6 +385,18 @@ namespace MQTTnet.Client return this; } + public MqttClientOptionsBuilder WithWillContentType(string willContentType) + { + _options.WillContentType = willContentType; + return this; + } + + public MqttClientOptionsBuilder WithWillCorrelationData(byte[] willCorrelationData) + { + _options.WillCorrelationData = willCorrelationData; + return this; + } + public MqttClientOptionsBuilder WithWillDelayInterval(uint willDelayInterval) { _options.WillDelayInterval = willDelayInterval; @@ -384,27 +408,33 @@ namespace MQTTnet.Client _options.WillPayload = willPayload; return this; } - + public MqttClientOptionsBuilder WithWillPayload(string willPayload) { if (string.IsNullOrEmpty(willPayload)) { return WithWillPayload((byte[])null); } - + _options.WillPayload = Encoding.UTF8.GetBytes(willPayload); return this; } + public MqttClientOptionsBuilder WithWillPayloadFormatIndicator(MqttPayloadFormatIndicator willPayloadFormatIndicator) + { + _options.WillPayloadFormatIndicator = willPayloadFormatIndicator; + return this; + } + public MqttClientOptionsBuilder WithWillQualityOfServiceLevel(MqttQualityOfServiceLevel willQualityOfServiceLevel) { _options.WillQualityOfServiceLevel = willQualityOfServiceLevel; return this; } - public MqttClientOptionsBuilder WithWillTopic(string willTopic) + public MqttClientOptionsBuilder WithWillResponseTopic(string willResponseTopic) { - _options.WillTopic = willTopic; + _options.WillResponseTopic = willResponseTopic; return this; } @@ -413,38 +443,20 @@ namespace MQTTnet.Client _options.WillRetain = willRetain; return this; } - - public MqttClientOptionsBuilder WithWillContentType(string willContentType) + + public MqttClientOptionsBuilder WithWillTopic(string willTopic) { - _options.WillContentType = willContentType; + _options.WillTopic = willTopic; return this; } - - public MqttClientOptionsBuilder WithWillCorrelationData(byte[] willCorrelationData) - { - _options.WillCorrelationData = willCorrelationData; - return this; - } - - public MqttClientOptionsBuilder WithWillResponseTopic(string willResponseTopic) - { - _options.WillResponseTopic = willResponseTopic; - return this; - } - - public MqttClientOptionsBuilder WithWillPayloadFormatIndicator(MqttPayloadFormatIndicator willPayloadFormatIndicator) - { - _options.WillPayloadFormatIndicator = willPayloadFormatIndicator; - return this; - } - + public MqttClientOptionsBuilder WithWillUserProperty(string name, string value) { if (_options.WillUserProperties == null) { _options.WillUserProperties = new List(); } - + _options.WillUserProperties.Add(new MqttUserProperty(name, value)); return this; } diff --git a/Source/MQTTnet/Implementations/MqttClientAdapterFactory.cs b/Source/MQTTnet/Implementations/MqttClientAdapterFactory.cs index 9d2cbd8b..1749817c 100644 --- a/Source/MQTTnet/Implementations/MqttClientAdapterFactory.cs +++ b/Source/MQTTnet/Implementations/MqttClientAdapterFactory.cs @@ -40,7 +40,12 @@ namespace MQTTnet.Implementations var bufferWriter = new MqttBufferWriter(options.WriterBufferSize, options.WriterBufferSizeMax); var packetFormatterAdapter = new MqttPacketFormatterAdapter(options.ProtocolVersion, bufferWriter); - return new MqttChannelAdapter(channel, packetFormatterAdapter, packetInspector, logger); + + return new MqttChannelAdapter(channel, packetFormatterAdapter, logger) + { + AllowPacketFragmentation = options.AllowPacketFragmentation, + PacketInspector = packetInspector + }; } } } diff --git a/Source/MQTTnet/Implementations/MqttTcpServerAdapter.Uwp.cs b/Source/MQTTnet/Implementations/MqttTcpServerAdapter.Uwp.cs index 3efe8409..1a8ee398 100644 --- a/Source/MQTTnet/Implementations/MqttTcpServerAdapter.Uwp.cs +++ b/Source/MQTTnet/Implementations/MqttTcpServerAdapter.Uwp.cs @@ -95,7 +95,7 @@ namespace MQTTnet.Implementations var packetFormatterAdapter = new MqttPacketFormatterAdapter(bufferWriter); var tcpChannel = new MqttTcpChannel(args.Socket, clientCertificate, _options); - using (var clientAdapter = new MqttChannelAdapter(tcpChannel, packetFormatterAdapter, null, _rootLogger)) + using (var clientAdapter = new MqttChannelAdapter(tcpChannel, packetFormatterAdapter, _rootLogger)) { await clientHandler(clientAdapter).ConfigureAwait(false); } diff --git a/Source/MQTTnet/Implementations/MqttTcpServerListener.cs b/Source/MQTTnet/Implementations/MqttTcpServerListener.cs index 60e406d1..d22f4157 100644 --- a/Source/MQTTnet/Implementations/MqttTcpServerListener.cs +++ b/Source/MQTTnet/Implementations/MqttTcpServerListener.cs @@ -229,7 +229,7 @@ namespace MQTTnet.Implementations var bufferWriter = new MqttBufferWriter(_serverOptions.WriterBufferSize, _serverOptions.WriterBufferSizeMax); var packetFormatterAdapter = new MqttPacketFormatterAdapter(bufferWriter); - using (var clientAdapter = new MqttChannelAdapter(tcpChannel, packetFormatterAdapter, null, _rootLogger)) + using (var clientAdapter = new MqttChannelAdapter(tcpChannel, packetFormatterAdapter, _rootLogger)) { await clientHandler(clientAdapter).ConfigureAwait(false); } From 2933b76237a79053f00f8b56c071590a8a7d1882 Mon Sep 17 00:00:00 2001 From: RossHNPC Date: Thu, 6 Apr 2023 16:55:33 +0100 Subject: [PATCH 5/9] ClientMessageQueueInterceptor continues with subscription processing. (#1685) * ClientMessageQueueInterceptor continues with subscription processing. * Update readme --------- Co-authored-by: Christian <6939810+chkr1011@users.noreply.github.com> --- .github/workflows/ReleaseNotes.md | 5 ++- .../MQTTnet.Tests/Server/Publishing_Tests.cs | 37 +++++++++++++++++++ .../Internal/MqttClientSessionsManager.cs | 5 +-- 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ReleaseNotes.md b/.github/workflows/ReleaseNotes.md index e84ee186..85846c97 100644 --- a/.github/workflows/ReleaseNotes.md +++ b/.github/workflows/ReleaseNotes.md @@ -5,7 +5,7 @@ * [Client] MQTTv5 features are now checked and an exception is thrown if they are used when using protocol version 3.1.1 and lower. These checks can be disabled in client options. (BREAKING CHANGE!). * [Client] Added support for disabling packet fragmentation (required for i.e. AWS, #1690, thanks to @logicaloud). * [Server] Exposed MQTT v5 sent properties from the affected client in _ClientDisconnectedAsync_ event. -* [Server] Fixed wrong client ID passed to _InterceptingUnsubscriptionEventArgs_ (#1631, thanks to @ghord). +* [Server] Fixed wrong client ID passed to _InterceptingUnsubscriptionEventArgs_ (#1631, thanks to @ghord). * [Server] Exposed socket settings for TCP keep alive in TCP options (#1544). * [Server] Exposed server session items at server level and allow custom session items for injected application messages (#1638). * [Server] Improved performance for retained message handling when subscribing using "DoNotSendOnSubscribe" or "SendAtSubscribeIfNewSubscriptionOnly" (#1661, thanks to @Int32Overflow). @@ -13,4 +13,5 @@ * [Server] Added a new API method which allows reading a single retained message without the need to processing the entire set of retained messages (#1659). * [Server] Added a new event (InterceptingClientEnqueueAsync) which allows intercepting when an application message is enqueued for a client (#1648). * [Server] Fixed race condition when handling connections which leads to stopped message transfers (#1677, thanks to @RazvanEmilR). -* [Server] Added missing user properties to subscribe and unsubscribe interceptor (#1702). \ No newline at end of file +* [Server] Fixed not delivered packets to other sessions if the _InterceptingClientEnqueueEvent_ is used and accepting the packet is denied (#1685, thanks to @RossHNPC). +* [Server] Added missing user properties to subscribe and unsubscribe interceptor (#1702). diff --git a/Source/MQTTnet.Tests/Server/Publishing_Tests.cs b/Source/MQTTnet.Tests/Server/Publishing_Tests.cs index 63923507..cccdbdf1 100644 --- a/Source/MQTTnet.Tests/Server/Publishing_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Publishing_Tests.cs @@ -105,5 +105,42 @@ namespace MQTTnet.Tests.Server receivedMessages.AssertReceivedCountEquals(1); } } + + [TestMethod] + public async Task Intercept_Client_Enqueue_Multiple_Clients_Subscribed_Messages_Are_Filtered() + { + using (var testEnvironment = CreateTestEnvironment()) + { + var server = await testEnvironment.StartServer(); + + var sender = await testEnvironment.ConnectClient(); + + var receiverOne = await testEnvironment.ConnectClient(o => o.WithClientId("One")); + await receiverOne.SubscribeAsync("A"); + var receivedMessagesOne = testEnvironment.CreateApplicationMessageHandler(receiverOne); + + var receiverTwo = await testEnvironment.ConnectClient(o => o.WithClientId("Two")); + await receiverTwo.SubscribeAsync("A"); + var receivedMessagesTwo = testEnvironment.CreateApplicationMessageHandler(receiverTwo); + + var receiverThree = await testEnvironment.ConnectClient(o => o.WithClientId("Three")); + await receiverThree.SubscribeAsync("A"); + var receivedMessagesThree = testEnvironment.CreateApplicationMessageHandler(receiverThree); + + server.InterceptingClientEnqueueAsync += e => + { + if (e.ReceiverClientId.Contains("Two")) e.AcceptEnqueue = false; + return CompletedTask.Instance; + }; + + await sender.PublishStringAsync("A", "Payload", MqttQualityOfServiceLevel.AtLeastOnce); + + await LongTestDelay(); + + receivedMessagesOne.AssertReceivedCountEquals(1); + receivedMessagesTwo.AssertReceivedCountEquals(0); + receivedMessagesThree.AssertReceivedCountEquals(1); + } + } } } \ No newline at end of file diff --git a/Source/MQTTnet/Server/Internal/MqttClientSessionsManager.cs b/Source/MQTTnet/Server/Internal/MqttClientSessionsManager.cs index d7a3cdb5..3c49871a 100644 --- a/Source/MQTTnet/Server/Internal/MqttClientSessionsManager.cs +++ b/Source/MQTTnet/Server/Internal/MqttClientSessionsManager.cs @@ -207,9 +207,8 @@ namespace MQTTnet.Server if (!eventArgs.AcceptEnqueue) { - // There will be no reason string and use properties because in this case the clients will - // not receive a packet at all. - return new DispatchApplicationMessageResult(reasonCode, eventArgs.CloseSenderConnection, null, null); + // Continue checking the other subscriptions + continue; } } From 59d99509c896e27d0b417dc986ce8f4fa849fc06 Mon Sep 17 00:00:00 2001 From: scottbrogden-iheartmedia <74422469+scottbrogden-iheartmedia@users.noreply.github.com> Date: Sat, 8 Apr 2023 06:03:58 -0600 Subject: [PATCH 6/9] =?UTF-8?q?Notify=20what=20subscription=20topics=20fai?= =?UTF-8?q?led=20as=20part=20of=20the=20SynchronizingSu=E2=80=A6=20(#1678)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Notify what subscription topics failed as part of the SynchronizingSubscriptionsFailedAsync callback * Update ReleaseNotes.md --------- Co-authored-by: Christian <6939810+chkr1011@users.noreply.github.com> --- .github/workflows/ReleaseNotes.md | 1 + .../ManagedMqttClient.cs | 15 ++++++++++----- .../ManagedProcessFailedEventArgs.cs | 18 +++++++++++++++++- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ReleaseNotes.md b/.github/workflows/ReleaseNotes.md index 85846c97..ea58c283 100644 --- a/.github/workflows/ReleaseNotes.md +++ b/.github/workflows/ReleaseNotes.md @@ -3,6 +3,7 @@ * [Client] Added support for passing MQTT v5 options (User properties etc.) for disconnects. * [Client] An internal exception is no longer caught silently when calling _DisconnectAsync_ to indicate that the disconnect is not clean (BREAKING CHANGE). * [Client] MQTTv5 features are now checked and an exception is thrown if they are used when using protocol version 3.1.1 and lower. These checks can be disabled in client options. (BREAKING CHANGE!). +* [ManagedClient] Exposed added and removed topics in _ManagedProcessFailedEventArgs_ (#1678, thanks to @scottbrogden-iheartmedia). * [Client] Added support for disabling packet fragmentation (required for i.e. AWS, #1690, thanks to @logicaloud). * [Server] Exposed MQTT v5 sent properties from the affected client in _ClientDisconnectedAsync_ event. * [Server] Fixed wrong client ID passed to _InterceptingUnsubscriptionEventArgs_ (#1631, thanks to @ghord). diff --git a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClient.cs b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClient.cs index 0c04c663..3690be8c 100644 --- a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClient.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClient.cs @@ -353,13 +353,13 @@ namespace MQTTnet.Extensions.ManagedClient return remainingTime < TimeSpan.Zero ? TimeSpan.Zero : remainingTime; } - async Task HandleSubscriptionExceptionAsync(Exception exception) + async Task HandleSubscriptionExceptionAsync(Exception exception, List addedSubscriptions, List removedSubscriptions) { _logger.Warning(exception, "Synchronizing subscriptions failed."); if (_synchronizingSubscriptionsFailedEvent.HasHandlers) { - await _synchronizingSubscriptionsFailedEvent.InvokeAsync(new ManagedProcessFailedEventArgs(exception)).ConfigureAwait(false); + await _synchronizingSubscriptionsFailedEvent.InvokeAsync(new ManagedProcessFailedEventArgs(exception, addedSubscriptions, removedSubscriptions)).ConfigureAwait(false); } } @@ -457,11 +457,13 @@ namespace MQTTnet.Extensions.ManagedClient { _logger.Info("Publishing subscriptions at reconnect"); + List topicFilters = null; + try { if (_reconnectSubscriptions.Any()) { - var topicFilters = new List(); + topicFilters = new List(); foreach (var sub in _reconnectSubscriptions) { @@ -479,7 +481,7 @@ namespace MQTTnet.Extensions.ManagedClient } catch (Exception exception) { - await HandleSubscriptionExceptionAsync(exception).ConfigureAwait(false); + await HandleSubscriptionExceptionAsync(exception, topicFilters, null).ConfigureAwait(false); } } @@ -591,6 +593,9 @@ namespace MQTTnet.Extensions.ManagedClient } await InternalClient.UnsubscribeAsync(unsubscribeOptionsBuilder.Build()).ConfigureAwait(false); + + //clear because these worked, maybe the subscribe below will fail, only report those + removedSubscriptions.Clear(); } if (addedSubscriptions != null && addedSubscriptions.Any()) @@ -607,7 +612,7 @@ namespace MQTTnet.Extensions.ManagedClient } catch (Exception exception) { - await HandleSubscriptionExceptionAsync(exception).ConfigureAwait(false); + await HandleSubscriptionExceptionAsync(exception, addedSubscriptions, removedSubscriptions).ConfigureAwait(false); } } diff --git a/Source/MQTTnet.Extensions.ManagedClient/ManagedProcessFailedEventArgs.cs b/Source/MQTTnet.Extensions.ManagedClient/ManagedProcessFailedEventArgs.cs index 73eee45e..937c7d83 100644 --- a/Source/MQTTnet.Extensions.ManagedClient/ManagedProcessFailedEventArgs.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/ManagedProcessFailedEventArgs.cs @@ -2,17 +2,33 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using MQTTnet.Packets; using System; +using System.Collections.Generic; +using System.Linq; namespace MQTTnet.Extensions.ManagedClient { public class ManagedProcessFailedEventArgs : EventArgs { - public ManagedProcessFailedEventArgs(Exception exception) + public ManagedProcessFailedEventArgs(Exception exception, List addedSubscriptions, List removedSubscriptions) { Exception = exception ?? throw new ArgumentNullException(nameof(exception)); + + if (addedSubscriptions != null) + { + AddedSubscriptions = new List(addedSubscriptions.Select(item => item.Topic)); + } + + if (removedSubscriptions != null) + { + RemovedSubscriptions = new List(removedSubscriptions); + } } public Exception Exception { get; } + + public List AddedSubscriptions { get; } + public List RemovedSubscriptions { get; } } } \ No newline at end of file From 8b3b9d33de1fcf1348d736b8958bd18e05da9223 Mon Sep 17 00:00:00 2001 From: Christian <6939810+chkr1011@users.noreply.github.com> Date: Tue, 11 Apr 2023 22:11:16 +0200 Subject: [PATCH 7/9] Add overloads for options builder (#1711) * Add new overload for options builder * Refactor code * Update ReleaseNotes.md --- .github/workflows/ReleaseNotes.md | 3 +- .../MqttWebSocketServerAdapter.cs | 37 +-- .../Server/MqttSubscriptionsManager_Tests.cs | 2 +- .../Server/Subscription_TopicHash_Tests.cs | 22 +- .../TopicFilterComparer_Tests.cs | 14 +- Source/MQTTnet/Client/MqttClient.cs | 4 +- .../Options/MqttClientOptionsBuilder.cs | 36 ++- .../Implementations/MqttWebSocketChannel.cs | 4 +- .../Internal/MqttClientSessionsManager.cs | 2 +- .../MqttClientSubscriptionsManager.cs | 2 +- .../Server/Internal/MqttSubscription.cs | 261 +---------------- .../MQTTnet/Server/Internal/MqttTopicHash.cs | 268 ++++++++++++++++++ 12 files changed, 335 insertions(+), 320 deletions(-) create mode 100644 Source/MQTTnet/Server/Internal/MqttTopicHash.cs diff --git a/.github/workflows/ReleaseNotes.md b/.github/workflows/ReleaseNotes.md index ea58c283..2d31e272 100644 --- a/.github/workflows/ReleaseNotes.md +++ b/.github/workflows/ReleaseNotes.md @@ -3,8 +3,9 @@ * [Client] Added support for passing MQTT v5 options (User properties etc.) for disconnects. * [Client] An internal exception is no longer caught silently when calling _DisconnectAsync_ to indicate that the disconnect is not clean (BREAKING CHANGE). * [Client] MQTTv5 features are now checked and an exception is thrown if they are used when using protocol version 3.1.1 and lower. These checks can be disabled in client options. (BREAKING CHANGE!). -* [ManagedClient] Exposed added and removed topics in _ManagedProcessFailedEventArgs_ (#1678, thanks to @scottbrogden-iheartmedia). * [Client] Added support for disabling packet fragmentation (required for i.e. AWS, #1690, thanks to @logicaloud). +* [Client] Added new overloads for options builder. +* [ManagedClient] Exposed added and removed topics in _ManagedProcessFailedEventArgs_ (#1678, thanks to @scottbrogden-iheartmedia). * [Server] Exposed MQTT v5 sent properties from the affected client in _ClientDisconnectedAsync_ event. * [Server] Fixed wrong client ID passed to _InterceptingUnsubscriptionEventArgs_ (#1631, thanks to @ghord). * [Server] Exposed socket settings for TCP keep alive in TCP options (#1544). diff --git a/Source/MQTTnet.AspnetCore/MqttWebSocketServerAdapter.cs b/Source/MQTTnet.AspnetCore/MqttWebSocketServerAdapter.cs index 0594af78..183fbfb6 100644 --- a/Source/MQTTnet.AspnetCore/MqttWebSocketServerAdapter.cs +++ b/Source/MQTTnet.AspnetCore/MqttWebSocketServerAdapter.cs @@ -2,38 +2,34 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using Microsoft.AspNetCore.Http; -using MQTTnet.Adapter; -using MQTTnet.Formatter; -using MQTTnet.Implementations; -using MQTTnet.Server; using System; using System.Net.WebSockets; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using MQTTnet.Adapter; using MQTTnet.Diagnostics; +using MQTTnet.Formatter; +using MQTTnet.Implementations; +using MQTTnet.Server; namespace MQTTnet.AspNetCore { public sealed class MqttWebSocketServerAdapter : IMqttServerAdapter { - IMqttNetLogger _logger = new MqttNetNullLogger(); - + IMqttNetLogger _logger = MqttNetNullLogger.Instance; + public Func ClientHandler { get; set; } - public Task StartAsync(MqttServerOptions options, IMqttNetLogger logger) + public void Dispose() { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - return Task.CompletedTask; - } - - public Task StopAsync() - { - return Task.CompletedTask; } public async Task RunWebSocketConnectionAsync(WebSocket webSocket, HttpContext httpContext) { - if (webSocket == null) throw new ArgumentNullException(nameof(webSocket)); + if (webSocket == null) + { + throw new ArgumentNullException(nameof(webSocket)); + } var endpoint = $"{httpContext.Connection.RemoteIpAddress}:{httpContext.Connection.RemotePort}"; @@ -60,8 +56,15 @@ namespace MQTTnet.AspNetCore } } - public void Dispose() + public Task StartAsync(MqttServerOptions options, IMqttNetLogger logger) { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + return Task.CompletedTask; + } + + public Task StopAsync() + { + return Task.CompletedTask; } } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Server/MqttSubscriptionsManager_Tests.cs b/Source/MQTTnet.Tests/Server/MqttSubscriptionsManager_Tests.cs index ff81c1c6..ec4aef90 100644 --- a/Source/MQTTnet.Tests/Server/MqttSubscriptionsManager_Tests.cs +++ b/Source/MQTTnet.Tests/Server/MqttSubscriptionsManager_Tests.cs @@ -228,7 +228,7 @@ namespace MQTTnet.Tests.Server CheckSubscriptionsResult CheckSubscriptions(string topic, MqttQualityOfServiceLevel applicationMessageQoSLevel, string senderClientId) { - MqttSubscription.CalculateTopicHash(topic, out var topicHash, out _, out _); + MqttTopicHash.Calculate(topic, out var topicHash, out _, out _); return _subscriptionsManager.CheckSubscriptions(topic, topicHash, applicationMessageQoSLevel, senderClientId); } } diff --git a/Source/MQTTnet.Tests/Server/Subscription_TopicHash_Tests.cs b/Source/MQTTnet.Tests/Server/Subscription_TopicHash_Tests.cs index 31e1ab05..2dd9fe5f 100644 --- a/Source/MQTTnet.Tests/Server/Subscription_TopicHash_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Subscription_TopicHash_Tests.cs @@ -47,7 +47,7 @@ namespace MQTTnet.Tests.Server var topics = t.Value; foreach (var topic in topics) { - MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var hashMask, out var hasWildcard); + MqttTopicHash.Calculate(topic, out var topicHash, out var hashMask, out var hasWildcard); bucketDepths.TryGetValue(topicHash, out var currentValue); ++currentValue; @@ -96,7 +96,7 @@ namespace MQTTnet.Tests.Server { var topic = "asdfasdf/asdfasdf/asdfasdf/asdfasdf/asdfas/dfaf/assfdgsdfgdf/#"; - MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard); + MqttTopicHash.Calculate(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard); Assert.IsTrue(topicHasWildcard, "Wildcard not detected"); } @@ -110,7 +110,7 @@ namespace MQTTnet.Tests.Server { var topic = "asdfasdf/asdfasdf/asdfasdf/asdfasdf/asdfas/dfaf/assfdgsdfgdf/+"; - MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard); + MqttTopicHash.Calculate(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard); Assert.IsTrue(topicHasWildcard, "Wildcard not detected"); } @@ -150,7 +150,7 @@ namespace MQTTnet.Tests.Server // UInt64 is limited to 8 levels - MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard); + MqttTopicHash.Calculate(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard); Assert.IsTrue(topicHasWildcard, "Wildcard not detected"); @@ -212,7 +212,7 @@ namespace MQTTnet.Tests.Server var topic = sb.ToString(); - MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard); + MqttTopicHash.Calculate(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard); Assert.IsFalse(topicHasWildcard, "Wildcard detected when not present"); @@ -269,7 +269,7 @@ namespace MQTTnet.Tests.Server var topic = sb.ToString(); - MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard); + MqttTopicHash.Calculate(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard); Assert.IsTrue(topicHasWildcard, "Wildcard not detected"); @@ -319,7 +319,7 @@ namespace MQTTnet.Tests.Server var l3 = "#"; var topic = $"{l0}/{l1}/{l2}/{l3}"; - MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard); + MqttTopicHash.Calculate(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard); Assert.IsTrue(topicHasWildcard, "Wildcard not detected"); @@ -354,7 +354,7 @@ namespace MQTTnet.Tests.Server var l3 = "prop1"; var topic = $"{l0}/{l1}/{l2}/{l3}"; - MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard); + MqttTopicHash.Calculate(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard); Assert.IsFalse(topicHasWildcard, "Wildcard detected when not wildcard present"); @@ -447,7 +447,7 @@ namespace MQTTnet.Tests.Server var l3 = "prop1"; var topic = $"{l0}/{l1}/{l2}/{l3}"; - MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard); + MqttTopicHash.Calculate(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard); Assert.IsTrue(topicHasWildcard, "Wildcard not detected"); @@ -475,7 +475,7 @@ namespace MQTTnet.Tests.Server void CheckTopicHash(string topic, ulong expectedHash, ulong expectedHashMask) { - MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var hashMask, out var hasWildcard); + MqttTopicHash.Calculate(topic, out var topicHash, out var hashMask, out var hasWildcard); Console.WriteLine(); Console.WriteLine("Topic: " + topic); @@ -501,7 +501,7 @@ namespace MQTTnet.Tests.Server { var topicToFind = topicsToFind[countDown]; - MqttSubscription.CalculateTopicHash(topicToFind, out var topicHash, out _, out _); + MqttTopicHash.Calculate(topicToFind, out var topicHash, out _, out _); clientSession.TryCheckSubscriptions(topicToFind, topicHash, MqttQualityOfServiceLevel.AtMostOnce, "OtherClient", out var result); if (result.IsSubscribed) diff --git a/Source/MQTTnet.Tests/TopicFilterComparer_Tests.cs b/Source/MQTTnet.Tests/TopicFilterComparer_Tests.cs index 60be4363..6451a077 100644 --- a/Source/MQTTnet.Tests/TopicFilterComparer_Tests.cs +++ b/Source/MQTTnet.Tests/TopicFilterComparer_Tests.cs @@ -150,18 +150,8 @@ namespace MQTTnet.Tests { Assert.AreEqual(expectedResult, MqttTopicFilterComparer.Compare(topic, filter)); - ulong topicHash; - ulong topicHashMask; - bool topicHasWildcard; - - MqttSubscription.CalculateTopicHash(topic, out topicHash, out topicHashMask, out topicHasWildcard); - - - ulong filterTopicHash; - ulong filterTopicHashMask; - bool filterTopicHasWildcard; - - MqttSubscription.CalculateTopicHash(filter, out filterTopicHash, out filterTopicHashMask, out filterTopicHasWildcard); + MqttTopicHash.Calculate(topic, out var topicHash, out _, out _); + MqttTopicHash.Calculate(filter, out var filterTopicHash, out var filterTopicHashMask, out _); if (expectedResult == MqttTopicFilterCompareResult.IsMatch) { diff --git a/Source/MQTTnet/Client/MqttClient.cs b/Source/MQTTnet/Client/MqttClient.cs index 3ed4dd80..e6cd7fbe 100644 --- a/Source/MQTTnet/Client/MqttClient.cs +++ b/Source/MQTTnet/Client/MqttClient.cs @@ -488,9 +488,9 @@ namespace MQTTnet.Client using (var effectiveCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(backgroundCancellationToken, cancellationToken)) { - _logger.Verbose("Trying to connect with server '{0}'.", Options.ChannelOptions); + _logger.Verbose("Trying to connect with server '{0}'", Options.ChannelOptions); await _adapter.ConnectAsync(effectiveCancellationToken.Token).ConfigureAwait(false); - _logger.Verbose("Connection with server established."); + _logger.Verbose("Connection with server established"); _publishPacketReceiverQueue?.Dispose(); _publishPacketReceiverQueue = new AsyncQueue(); diff --git a/Source/MQTTnet/Client/Options/MqttClientOptionsBuilder.cs b/Source/MQTTnet/Client/Options/MqttClientOptionsBuilder.cs index fb17733e..a7a0099f 100644 --- a/Source/MQTTnet/Client/Options/MqttClientOptionsBuilder.cs +++ b/Source/MQTTnet/Client/Options/MqttClientOptionsBuilder.cs @@ -79,18 +79,6 @@ namespace MQTTnet.Client return _options; } - /// - /// Usually the MQTT packets can be send partially. This is done by using multiple TCP packets - /// or WebSocket frames etc. Unfortunately not all brokers (like Amazon Web Services (AWS)) do support this feature and - /// will close the connection when receiving such packets. If such a service is used this flag must - /// be set to _true_. - /// - public MqttClientOptionsBuilder WithoutPacketFragmentation() - { - _options.AllowPacketFragmentation = false; - return this; - } - public MqttClientOptionsBuilder WithAuthentication(string method, byte[] data) { _options.AuthenticationMethod = method; @@ -98,12 +86,24 @@ namespace MQTTnet.Client return this; } + /// + /// Clean session is used in MQTT versions below 5.0.0. It is the same as setting "CleanStart". + /// public MqttClientOptionsBuilder WithCleanSession(bool value = true) { _options.CleanSession = value; return this; } + /// + /// Clean start is used in MQTT versions 5.0.0 and higher. It is the same as setting "CleanSession". + /// + public MqttClientOptionsBuilder WithCleanStart(bool value = true) + { + _options.CleanSession = value; + return this; + } + public MqttClientOptionsBuilder WithClientId(string value) { _options.ClientId = value; @@ -200,6 +200,18 @@ namespace MQTTnet.Client return WithKeepAlivePeriod(TimeSpan.Zero); } + /// + /// Usually the MQTT packets can be send partially. This is done by using multiple TCP packets + /// or WebSocket frames etc. Unfortunately not all brokers (like Amazon Web Services (AWS)) do support this feature and + /// will close the connection when receiving such packets. If such a service is used this flag must + /// be set to _true_. + /// + public MqttClientOptionsBuilder WithoutPacketFragmentation() + { + _options.AllowPacketFragmentation = false; + return this; + } + public MqttClientOptionsBuilder WithProtocolVersion(MqttProtocolVersion value) { if (value == MqttProtocolVersion.Unknown) diff --git a/Source/MQTTnet/Implementations/MqttWebSocketChannel.cs b/Source/MQTTnet/Implementations/MqttWebSocketChannel.cs index 90e3666e..4760d328 100644 --- a/Source/MQTTnet/Implementations/MqttWebSocketChannel.cs +++ b/Source/MQTTnet/Implementations/MqttWebSocketChannel.cs @@ -63,7 +63,7 @@ namespace MQTTnet.Implementations await clientWebSocket.ConnectAsync(new Uri(uri), cancellationToken).ConfigureAwait(false); } - catch (Exception) + catch { // Prevent a memory leak when always creating new instance which will fail while connecting. clientWebSocket.Dispose(); @@ -103,7 +103,7 @@ namespace MQTTnet.Implementations public async Task WriteAsync(ArraySegment buffer, bool isEndOfPacket, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - + #if NET5_0_OR_GREATER // MQTT Control Packets MUST be sent in WebSocket binary data frames. If any other type of data frame is received the recipient MUST close the Network Connection [MQTT-6.0.0-1]. // A single WebSocket data frame can contain multiple or partial MQTT Control Packets. The receiver MUST NOT assume that MQTT Control Packets are aligned on WebSocket frame boundaries [MQTT-6.0.0-2]. diff --git a/Source/MQTTnet/Server/Internal/MqttClientSessionsManager.cs b/Source/MQTTnet/Server/Internal/MqttClientSessionsManager.cs index 3c49871a..627ffe6d 100644 --- a/Source/MQTTnet/Server/Internal/MqttClientSessionsManager.cs +++ b/Source/MQTTnet/Server/Internal/MqttClientSessionsManager.cs @@ -179,7 +179,7 @@ namespace MQTTnet.Server } // Calculate application message topic hash once for subscription checks - MqttSubscription.CalculateTopicHash(applicationMessage.Topic, out var topicHash, out _, out _); + MqttTopicHash.Calculate(applicationMessage.Topic, out var topicHash, out _, out _); foreach (var session in subscriberSessions) { diff --git a/Source/MQTTnet/Server/Internal/MqttClientSubscriptionsManager.cs b/Source/MQTTnet/Server/Internal/MqttClientSubscriptionsManager.cs index 24595fd4..a54a0045 100644 --- a/Source/MQTTnet/Server/Internal/MqttClientSubscriptionsManager.cs +++ b/Source/MQTTnet/Server/Internal/MqttClientSubscriptionsManager.cs @@ -343,7 +343,7 @@ namespace MQTTnet.Server using (_subscriptionsLock.EnterAsync(CancellationToken.None).GetAwaiter().GetResult()) { - MqttSubscription.CalculateTopicHash(topicFilter.Topic, out var topicHash, out _, out var hasWildcard); + MqttTopicHash.Calculate(topicFilter.Topic, out var topicHash, out _, out var hasWildcard); if (_subscriptions.TryGetValue(topicFilter.Topic, out var existingSubscription)) { diff --git a/Source/MQTTnet/Server/Internal/MqttSubscription.cs b/Source/MQTTnet/Server/Internal/MqttSubscription.cs index 930d3c72..c1117ccb 100644 --- a/Source/MQTTnet/Server/Internal/MqttSubscription.cs +++ b/Source/MQTTnet/Server/Internal/MqttSubscription.cs @@ -6,163 +6,6 @@ using MQTTnet.Protocol; namespace MQTTnet.Server { -/* -* The MqttSubscription object stores subscription parameters and calculates -* topic hashes. -* -* Use of Topic Hashes to improve message processing performance -* ============================================================= -* -* Motivation -* ----------- -* In a typical use case for MQTT there may be many publishers (sensors or -* other devices in the field) and few subscribers (monitoring all or many topics). -* Each publisher may have one or more topic(s) to publish and therefore both, the -* number of publishers and the number of topics may be large. -* -* Maintaining subscribers in a separate container -* ----------------------------------------------- -* Instead of placing all sessions into a single _sessions container, subscribers -* are added into another _subscriberSessions container (if a client is a -* subscriber and a publisher then the client is present in both containers). The -* cost is some additional bookkeeping work upon subscription where each client -* session needs to maintain a list of subscribed topics. -* -* When the client subscribes to the first topic, then the session manager adds -* the client to the _subscriberSessions container, and when the client -* unsubscribes from the last topic then the session manager removes the client -* from the container. Now, when an application message arrives, only the list of -* subscribers need processing instead of looping through potentially thousands of -* publishers. -* -* Improving subscriber topic lookup -* --------------------------------- -* For each subscriber, it needs to be determined whether an application message -* matches any topic the subscriber has subscribed to. There may only be few -* subscribers but there may be many subscribed topics, including wildcard topics. -* -* The implemented approach uses a topic hash and a hash mask calculated on the -* subscribed topic and the published topic (the application message topic) to -* find candidates for a match, with the existing match logic evaluating a reduced -* number of candidates. -* -* For each subscription, the topic hash and a hash mask is stored with the -* subscription, and for each application message received, the hash is calculated -* for the published topic before attempting to find matching subscriptions. The -* hash calculation itself is simple and does not have a large performance impact. -* -* We'll first explain how topic hashes and hash masks are constructed and then how -* they are used. -* -* Topic hash -* ---------- -* Topic hashes are stored as 64-bit numbers. Each byte within the 64-bit number -* relates to one MQTT topic level. A checksum is calculated for each topic level -* by iterating over the characters within the topic level (cast to byte) and the -* result is stored into the corresponding byte of the 64-bit number. If a topic -* level contains a wildcard character, then 0x00 is stored instead of the -* checksum. -* -* If there are less than 8 levels then the rest of the 64-bit number is filled -* with 0xff. If there are more than 8 levels then topics where the first 8 MQTT -* topic levels are identical will have the same hash value. -* -* This is the topic hash for the MQTT topic below: 0x655D4AF1FFFFFF -* -* client1/building1/level1/sensor1 (empty) (empty) (empty) (empty) -* \_____/ \_______/ \____/ \_____/ \_____/ \_____/ \_____/ \_____/ -* | | | | | | | | -* 0x65 0x5D 0x4A 0xF1 0xFF 0xFF 0xFF 0xFF -* -* This is the topic hash for an MQTT topic containing a wildcard: 0x655D00F1FFFFFF -* -* client1/building1/ + /sensor1 (empty) (empty) (empty) (empty) -* \_____/ \_______/ \_/ \_____/ \_____/ \_____/ \_____/ \_____/ -* | | | | | | | | -* 0x65 0x5D 0 0xF1 0xFF 0xFF 0xFF 0xFF -* -* For topics that contain the multi level wildcard # at the end, the topic hash -* is filled with 0x00: 0x65004A00000000 -* -* client1/ + /level1/ # (empty) (empty) (empty) (empty) -* \_____/ \_/ \____/ \_/ \_____/ \_____/ \_____/ \_____/ -* | | | | | | | | -* 0x65 0 0x4A 0 0 0 0 0 -* -* -* Topic hash mask -* --------------- -* The hash mask simply contains 0xFF for non-wildcard topic levels and 0x00 for -* wildcard topic levels. Here are the topic hash masks for the examples above. -* -* client1/building1/level1/sensor1 (empty) (empty) (empty) (empty) -* \_____/ \_______/ \____/ \_____/ \_____/ \_____/ \_____/ \_____/ -* | | | | | | | | -* 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF -* -* client1/building1/ + /sensor1 (empty) (empty) (empty) (empty) -* \_____/ \_______/ \_/ \_____/ \_____/ \_____/ \_____/ \_____/ -* | | | | | | | | -* 0xFF 0xFF 0 0xFF 0xFF 0xFF 0xFF 0xFF -* -* client1/ + /level1/ # (empty) (empty) (empty) (empty) -* \_____/ \_/ \____/ \_/ \_____/ \_____/ \_____/ \_____/ -* | | | | | | | | -* 0xFF 0 0xFF 0 0 0 0 0 -* -* -* Topic hash and hash mask properties -* ----------------------------------- -* The following properties of topic hashes and hash masks can be exploited to -* find potentially matching subscribed topics for a given published topic. -* -* (1) If a subscribed topic does not contain wildcards then the topic hash of the -* subscribed topic must be equal to the topic hash of the published topic, -* otherwise the subscribed topic cannot be a candidate for a match. -* -* (2) If a subscribed topic contains wildcards then the hash of the published -* topic masked with the subscribed topic's hash mask must be equal to the hash of -* the subscribed topic. I.e. a subscribed topic is a candidate for a match if: -* (publishedTopicHash & subscribedTopicHashMask) == subscribedTopicHash -* -* (3) If a subscribed topic contains wildcards then any potentially matching -* published topic must have a hash value that is greater than or equal to the -* hash value of the subscribed topic (because the subscribed topic contains -* zeroes in wildcard positions). -* -* Match finding -* ------------- -* The subscription manager maintains two separate dictionaries to assist finding -* matches using topic hashes: a _noWildcardSubscriptionsByTopicHash dictionary -* containing all subscriptions that do not have wildcards, and a -* _wildcardSubscriptionsByTopicHash dictionary containing subscriptions with -* wildcards. -* -* For subscriptions without wildcards, all potential candidates for a match are -* obtained by a single look-up (exploiting point 1 above). -* -* For subscriptions with wildcards, the subscription manager loops through the -* wildcard subscriptions and selects candidates that satisfy condition -* (publishedTopicHash & subscribedTopicMask) == subscribedTopicHash (point 2). -* The loop could exit early if wildcard subscriptions were stored into a sorted -* dictionary (utilizing point 3), but, after testing, there does not seem to be -* any real benefit doing so. -* -* Other considerations -* -------------------- -* Characters in the topic string are cast to byte and any additional bytes in a -* multi-byte character are disregarded. Best guess is that this does not impact -* performance in practice. -* -* Instead of one-byte checksums per topic level, one-word checksums per topic -* level could be used. If most topics contained four levels or less then hash -* buckets would be shallower. -* -* For very large numbers of topics, performing a parallel search may help further. -* -* To also handle a larger number of subscribers, it may be beneficial to maintain -* a subscribers-by-subscription-topic dictionary. -*/ public sealed class MqttSubscription { public MqttSubscription( @@ -180,7 +23,7 @@ namespace MQTTnet.Server GrantedQualityOfServiceLevel = qualityOfServiceLevel; Identifier = identifier; - CalculateTopicHash(Topic, out var hash, out var hashMask, out var hasWildcard); + MqttTopicHash.Calculate(Topic, out var hash, out var hashMask, out var hasWildcard); TopicHash = hash; TopicHashMask = hashMask; TopicHasWildcard = hasWildcard; @@ -203,107 +46,5 @@ namespace MQTTnet.Server public ulong TopicHashMask { get; } public bool TopicHasWildcard { get; } - - public static void CalculateTopicHash(string topic, out ulong resultHash, out ulong resultHashMask, out bool resultHasWildcard) - { - // calculate topic hash - ulong hash = 0; - ulong hashMaskInverted = 0; - ulong levelBitMask = 0; - ulong fillLevelBitMask = 0; - var hasWildcard = false; - byte checkSum = 0; - var level = 0; - - var i = 0; - while (i < topic.Length) - { - var c = topic[i]; - if (c == MqttTopicFilterComparer.LevelSeparator) - { - // done with this level - hash <<= 8; - hash |= checkSum; - hashMaskInverted <<= 8; - hashMaskInverted |= levelBitMask; - checkSum = 0; - levelBitMask = 0; - ++level; - if (level >= 8) - { - break; - } - } - else if (c == MqttTopicFilterComparer.SingleLevelWildcard) - { - levelBitMask = 0xff; - hasWildcard = true; - } - else if (c == MqttTopicFilterComparer.MultiLevelWildcard) - { - // checksum is zero for a valid topic - levelBitMask = 0xff; - // fill rest with this fillLevelBitMask - fillLevelBitMask = 0xff; - hasWildcard = true; - break; - } - else - { - // The checksum should be designed to reduce the hash bucket depth for the expected - // fairly regularly named MQTT topics that don't differ much, - // i.e. "room1/sensor1" - // "room1/sensor2" - // "room1/sensor3" - // etc. - if ((c & 1) == 0) - { - checkSum += (byte)c; - } - else - { - checkSum ^= (byte)(c >> 1); - } - } - - ++i; - } - - // Shift hash left and leave zeroes to fill ulong - if (level < 8) - { - hash <<= 8; - hash |= checkSum; - hashMaskInverted <<= 8; - hashMaskInverted |= levelBitMask; - ++level; - while (level < 8) - { - hash <<= 8; - hashMaskInverted <<= 8; - hashMaskInverted |= fillLevelBitMask; - ++level; - } - } - - if (!hasWildcard) - { - while (i < topic.Length) - { - var c = topic[i]; - if (c == MqttTopicFilterComparer.SingleLevelWildcard || c == MqttTopicFilterComparer.MultiLevelWildcard) - { - hasWildcard = true; - break; - } - - ++i; - } - } - - resultHash = hash; - resultHashMask = ~hashMaskInverted; - resultHasWildcard = hasWildcard; - } } } \ No newline at end of file diff --git a/Source/MQTTnet/Server/Internal/MqttTopicHash.cs b/Source/MQTTnet/Server/Internal/MqttTopicHash.cs new file mode 100644 index 00000000..a1169f3c --- /dev/null +++ b/Source/MQTTnet/Server/Internal/MqttTopicHash.cs @@ -0,0 +1,268 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace MQTTnet.Server +{ + /* + * The MqttSubscription object stores subscription parameters and calculates + * topic hashes. + * + * Use of Topic Hashes to improve message processing performance + * ============================================================= + * + * Motivation + * ----------- + * In a typical use case for MQTT there may be many publishers (sensors or + * other devices in the field) and few subscribers (monitoring all or many topics). + * Each publisher may have one or more topic(s) to publish and therefore both, the + * number of publishers and the number of topics may be large. + * + * Maintaining subscribers in a separate container + * ----------------------------------------------- + * Instead of placing all sessions into a single _sessions container, subscribers + * are added into another _subscriberSessions container (if a client is a + * subscriber and a publisher then the client is present in both containers). The + * cost is some additional bookkeeping work upon subscription where each client + * session needs to maintain a list of subscribed topics. + * + * When the client subscribes to the first topic, then the session manager adds + * the client to the _subscriberSessions container, and when the client + * unsubscribes from the last topic then the session manager removes the client + * from the container. Now, when an application message arrives, only the list of + * subscribers need processing instead of looping through potentially thousands of + * publishers. + * + * Improving subscriber topic lookup + * --------------------------------- + * For each subscriber, it needs to be determined whether an application message + * matches any topic the subscriber has subscribed to. There may only be few + * subscribers but there may be many subscribed topics, including wildcard topics. + * + * The implemented approach uses a topic hash and a hash mask calculated on the + * subscribed topic and the published topic (the application message topic) to + * find candidates for a match, with the existing match logic evaluating a reduced + * number of candidates. + * + * For each subscription, the topic hash and a hash mask is stored with the + * subscription, and for each application message received, the hash is calculated + * for the published topic before attempting to find matching subscriptions. The + * hash calculation itself is simple and does not have a large performance impact. + * + * We'll first explain how topic hashes and hash masks are constructed and then how + * they are used. + * + * Topic hash + * ---------- + * Topic hashes are stored as 64-bit numbers. Each byte within the 64-bit number + * relates to one MQTT topic level. A checksum is calculated for each topic level + * by iterating over the characters within the topic level (cast to byte) and the + * result is stored into the corresponding byte of the 64-bit number. If a topic + * level contains a wildcard character, then 0x00 is stored instead of the + * checksum. + * + * If there are less than 8 levels then the rest of the 64-bit number is filled + * with 0xff. If there are more than 8 levels then topics where the first 8 MQTT + * topic levels are identical will have the same hash value. + * + * This is the topic hash for the MQTT topic below: 0x655D4AF1FFFFFF + * + * client1/building1/level1/sensor1 (empty) (empty) (empty) (empty) + * \_____/ \_______/ \____/ \_____/ \_____/ \_____/ \_____/ \_____/ + * | | | | | | | | + * 0x65 0x5D 0x4A 0xF1 0xFF 0xFF 0xFF 0xFF + * + * This is the topic hash for an MQTT topic containing a wildcard: 0x655D00F1FFFFFF + * + * client1/building1/ + /sensor1 (empty) (empty) (empty) (empty) + * \_____/ \_______/ \_/ \_____/ \_____/ \_____/ \_____/ \_____/ + * | | | | | | | | + * 0x65 0x5D 0 0xF1 0xFF 0xFF 0xFF 0xFF + * + * For topics that contain the multi level wildcard # at the end, the topic hash + * is filled with 0x00: 0x65004A00000000 + * + * client1/ + /level1/ # (empty) (empty) (empty) (empty) + * \_____/ \_/ \____/ \_/ \_____/ \_____/ \_____/ \_____/ + * | | | | | | | | + * 0x65 0 0x4A 0 0 0 0 0 + * + * + * Topic hash mask + * --------------- + * The hash mask simply contains 0xFF for non-wildcard topic levels and 0x00 for + * wildcard topic levels. Here are the topic hash masks for the examples above. + * + * client1/building1/level1/sensor1 (empty) (empty) (empty) (empty) + * \_____/ \_______/ \____/ \_____/ \_____/ \_____/ \_____/ \_____/ + * | | | | | | | | + * 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF + * + * client1/building1/ + /sensor1 (empty) (empty) (empty) (empty) + * \_____/ \_______/ \_/ \_____/ \_____/ \_____/ \_____/ \_____/ + * | | | | | | | | + * 0xFF 0xFF 0 0xFF 0xFF 0xFF 0xFF 0xFF + * + * client1/ + /level1/ # (empty) (empty) (empty) (empty) + * \_____/ \_/ \____/ \_/ \_____/ \_____/ \_____/ \_____/ + * | | | | | | | | + * 0xFF 0 0xFF 0 0 0 0 0 + * + * + * Topic hash and hash mask properties + * ----------------------------------- + * The following properties of topic hashes and hash masks can be exploited to + * find potentially matching subscribed topics for a given published topic. + * + * (1) If a subscribed topic does not contain wildcards then the topic hash of the + * subscribed topic must be equal to the topic hash of the published topic, + * otherwise the subscribed topic cannot be a candidate for a match. + * + * (2) If a subscribed topic contains wildcards then the hash of the published + * topic masked with the subscribed topic's hash mask must be equal to the hash of + * the subscribed topic. I.e. a subscribed topic is a candidate for a match if: + * (publishedTopicHash & subscribedTopicHashMask) == subscribedTopicHash + * + * (3) If a subscribed topic contains wildcards then any potentially matching + * published topic must have a hash value that is greater than or equal to the + * hash value of the subscribed topic (because the subscribed topic contains + * zeroes in wildcard positions). + * + * Match finding + * ------------- + * The subscription manager maintains two separate dictionaries to assist finding + * matches using topic hashes: a _noWildcardSubscriptionsByTopicHash dictionary + * containing all subscriptions that do not have wildcards, and a + * _wildcardSubscriptionsByTopicHash dictionary containing subscriptions with + * wildcards. + * + * For subscriptions without wildcards, all potential candidates for a match are + * obtained by a single look-up (exploiting point 1 above). + * + * For subscriptions with wildcards, the subscription manager loops through the + * wildcard subscriptions and selects candidates that satisfy condition + * (publishedTopicHash & subscribedTopicMask) == subscribedTopicHash (point 2). + * The loop could exit early if wildcard subscriptions were stored into a sorted + * dictionary (utilizing point 3), but, after testing, there does not seem to be + * any real benefit doing so. + * + * Other considerations + * -------------------- + * Characters in the topic string are cast to byte and any additional bytes in a + * multi-byte character are disregarded. Best guess is that this does not impact + * performance in practice. + * + * Instead of one-byte checksums per topic level, one-word checksums per topic + * level could be used. If most topics contained four levels or less then hash + * buckets would be shallower. + * + * For very large numbers of topics, performing a parallel search may help further. + * + * To also handle a larger number of subscribers, it may be beneficial to maintain + * a subscribers-by-subscription-topic dictionary. + */ + public static class MqttTopicHash + { + public static void Calculate(string topic, out ulong resultHash, out ulong resultHashMask, out bool resultHasWildcard) + { + // calculate topic hash + ulong hash = 0; + ulong hashMaskInverted = 0; + ulong levelBitMask = 0; + ulong fillLevelBitMask = 0; + var hasWildcard = false; + byte checkSum = 0; + var level = 0; + + var i = 0; + while (i < topic.Length) + { + var c = topic[i]; + if (c == MqttTopicFilterComparer.LevelSeparator) + { + // done with this level + hash <<= 8; + hash |= checkSum; + hashMaskInverted <<= 8; + hashMaskInverted |= levelBitMask; + checkSum = 0; + levelBitMask = 0; + ++level; + if (level >= 8) + { + break; + } + } + else if (c == MqttTopicFilterComparer.SingleLevelWildcard) + { + levelBitMask = 0xff; + hasWildcard = true; + } + else if (c == MqttTopicFilterComparer.MultiLevelWildcard) + { + // checksum is zero for a valid topic + levelBitMask = 0xff; + // fill rest with this fillLevelBitMask + fillLevelBitMask = 0xff; + hasWildcard = true; + break; + } + else + { + // The checksum should be designed to reduce the hash bucket depth for the expected + // fairly regularly named MQTT topics that don't differ much, + // i.e. "room1/sensor1" + // "room1/sensor2" + // "room1/sensor3" + // etc. + if ((c & 1) == 0) + { + checkSum += (byte)c; + } + else + { + checkSum ^= (byte)(c >> 1); + } + } + + ++i; + } + + // Shift hash left and leave zeroes to fill ulong + if (level < 8) + { + hash <<= 8; + hash |= checkSum; + hashMaskInverted <<= 8; + hashMaskInverted |= levelBitMask; + ++level; + while (level < 8) + { + hash <<= 8; + hashMaskInverted <<= 8; + hashMaskInverted |= fillLevelBitMask; + ++level; + } + } + + if (!hasWildcard) + { + while (i < topic.Length) + { + var c = topic[i]; + if (c == MqttTopicFilterComparer.SingleLevelWildcard || c == MqttTopicFilterComparer.MultiLevelWildcard) + { + hasWildcard = true; + break; + } + + ++i; + } + } + + resultHash = hash; + resultHashMask = ~hashMaskInverted; + resultHasWildcard = hasWildcard; + } + } +} \ No newline at end of file From cb3c44f48d04cd545082ce8c324ff813da31a005 Mon Sep 17 00:00:00 2001 From: Christian <6939810+chkr1011@users.noreply.github.com> Date: Sun, 23 Apr 2023 13:57:59 +0200 Subject: [PATCH 8/9] Rename InspectPackage method (#1713) --- .github/workflows/ReleaseNotes.md | 1 + Samples/Diagnostics/PackageInspection_Samples.cs | 4 ++-- .../Diagnostics/PacketInspection_Tests.cs | 2 +- Source/MQTTnet.Tests/Server/General.cs | 2 +- Source/MQTTnet/Client/IMqttClient.cs | 2 +- Source/MQTTnet/Client/MqttClient.cs | 2 +- .../MQTTnet/LowLevelClient/ILowLevelMqttClient.cs | 2 +- .../MQTTnet/LowLevelClient/LowLevelMqttClient.cs | 14 ++++++++++---- 8 files changed, 18 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ReleaseNotes.md b/.github/workflows/ReleaseNotes.md index 2d31e272..b590561b 100644 --- a/.github/workflows/ReleaseNotes.md +++ b/.github/workflows/ReleaseNotes.md @@ -5,6 +5,7 @@ * [Client] MQTTv5 features are now checked and an exception is thrown if they are used when using protocol version 3.1.1 and lower. These checks can be disabled in client options. (BREAKING CHANGE!). * [Client] Added support for disabling packet fragmentation (required for i.e. AWS, #1690, thanks to @logicaloud). * [Client] Added new overloads for options builder. +* [Client] Renamed _InspectPackage_ to _InspectPacketAsync_ (BREAKING CHANGE). * [ManagedClient] Exposed added and removed topics in _ManagedProcessFailedEventArgs_ (#1678, thanks to @scottbrogden-iheartmedia). * [Server] Exposed MQTT v5 sent properties from the affected client in _ClientDisconnectedAsync_ event. * [Server] Fixed wrong client ID passed to _InterceptingUnsubscriptionEventArgs_ (#1631, thanks to @ghord). diff --git a/Samples/Diagnostics/PackageInspection_Samples.cs b/Samples/Diagnostics/PackageInspection_Samples.cs index c4d4b7ac..1480246e 100644 --- a/Samples/Diagnostics/PackageInspection_Samples.cs +++ b/Samples/Diagnostics/PackageInspection_Samples.cs @@ -26,7 +26,7 @@ public static class PackageInspection_Samples .WithTcpServer("broker.hivemq.com") .Build(); - mqttClient.InspectPackage += OnInspectPackage; + mqttClient.InspectPacketAsync += OnInspectPacket; await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); @@ -39,7 +39,7 @@ public static class PackageInspection_Samples } } - static Task OnInspectPackage(InspectMqttPacketEventArgs eventArgs) + static Task OnInspectPacket(InspectMqttPacketEventArgs eventArgs) { if (eventArgs.Direction == MqttPacketFlowDirection.Inbound) { diff --git a/Source/MQTTnet.Tests/Diagnostics/PacketInspection_Tests.cs b/Source/MQTTnet.Tests/Diagnostics/PacketInspection_Tests.cs index 5589a37d..31ea7be5 100644 --- a/Source/MQTTnet.Tests/Diagnostics/PacketInspection_Tests.cs +++ b/Source/MQTTnet.Tests/Diagnostics/PacketInspection_Tests.cs @@ -30,7 +30,7 @@ namespace MQTTnet.Tests.Diagnostics var packets = new List(); - mqttClient.InspectPackage += eventArgs => + mqttClient.InspectPacketAsync += eventArgs => { packets.Add(eventArgs.Direction + ":" + Convert.ToBase64String(eventArgs.Buffer)); return CompletedTask.Instance; diff --git a/Source/MQTTnet.Tests/Server/General.cs b/Source/MQTTnet.Tests/Server/General.cs index 9d1b0c25..2e0061e1 100644 --- a/Source/MQTTnet.Tests/Server/General.cs +++ b/Source/MQTTnet.Tests/Server/General.cs @@ -954,7 +954,7 @@ namespace MQTTnet.Tests.Server var client = testEnvironment.CreateClient(); - client.InspectPackage += e => + client.InspectPacketAsync += e => { if (e.Buffer.Length > 0) { diff --git a/Source/MQTTnet/Client/IMqttClient.cs b/Source/MQTTnet/Client/IMqttClient.cs index 849bff12..033672d5 100644 --- a/Source/MQTTnet/Client/IMqttClient.cs +++ b/Source/MQTTnet/Client/IMqttClient.cs @@ -15,7 +15,7 @@ namespace MQTTnet.Client event Func DisconnectedAsync; - event Func InspectPackage; + event Func InspectPacketAsync; bool IsConnected { get; } diff --git a/Source/MQTTnet/Client/MqttClient.cs b/Source/MQTTnet/Client/MqttClient.cs index e6cd7fbe..876c1390 100644 --- a/Source/MQTTnet/Client/MqttClient.cs +++ b/Source/MQTTnet/Client/MqttClient.cs @@ -87,7 +87,7 @@ namespace MQTTnet.Client remove => _disconnectedEvent.RemoveHandler(value); } - public event Func InspectPackage + public event Func InspectPacketAsync { add => _inspectPacketEvent.AddHandler(value); remove => _inspectPacketEvent.RemoveHandler(value); diff --git a/Source/MQTTnet/LowLevelClient/ILowLevelMqttClient.cs b/Source/MQTTnet/LowLevelClient/ILowLevelMqttClient.cs index 40132f28..00933381 100644 --- a/Source/MQTTnet/LowLevelClient/ILowLevelMqttClient.cs +++ b/Source/MQTTnet/LowLevelClient/ILowLevelMqttClient.cs @@ -9,7 +9,7 @@ namespace MQTTnet.LowLevelClient { public interface ILowLevelMqttClient : IDisposable { - event Func InspectPackage; + event Func InspectPacketAsync; bool IsConnected { get; } diff --git a/Source/MQTTnet/LowLevelClient/LowLevelMqttClient.cs b/Source/MQTTnet/LowLevelClient/LowLevelMqttClient.cs index a70b58da..75074320 100644 --- a/Source/MQTTnet/LowLevelClient/LowLevelMqttClient.cs +++ b/Source/MQTTnet/LowLevelClient/LowLevelMqttClient.cs @@ -32,7 +32,7 @@ namespace MQTTnet.LowLevelClient _logger = logger.WithSource(nameof(LowLevelMqttClient)); } - public event Func InspectPackage + public event Func InspectPacketAsync { add => _inspectPacketEvent.AddHandler(value); remove => _inspectPacketEvent.RemoveHandler(value); @@ -52,13 +52,19 @@ namespace MQTTnet.LowLevelClient throw new InvalidOperationException("Low level MQTT client is already connected. Disconnect first before connecting again."); } - var newAdapter = _clientAdapterFactory.CreateClientAdapter(options, new MqttPacketInspector(_inspectPacketEvent, _rootLogger), _rootLogger); + MqttPacketInspector packetInspector = null; + if (_inspectPacketEvent.HasHandlers) + { + packetInspector = new MqttPacketInspector(_inspectPacketEvent, _rootLogger); + } + + var newAdapter = _clientAdapterFactory.CreateClientAdapter(options, packetInspector, _rootLogger); try { - _logger.Verbose("Trying to connect with server '{0}'.", options.ChannelOptions); + _logger.Verbose("Trying to connect with server '{0}'", options.ChannelOptions); await newAdapter.ConnectAsync(cancellationToken).ConfigureAwait(false); - _logger.Verbose("Connection with server established."); + _logger.Verbose("Connection with server established"); } catch (Exception) { From 4b7d035e9efecde51992dd5f51810d5c63ab835f Mon Sep 17 00:00:00 2001 From: Christian <6939810+chkr1011@users.noreply.github.com> Date: Sat, 29 Apr 2023 12:37:36 +0200 Subject: [PATCH 9/9] Reset release notes --- .github/workflows/ReleaseNotes.md | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/.github/workflows/ReleaseNotes.md b/.github/workflows/ReleaseNotes.md index b590561b..e69de29b 100644 --- a/.github/workflows/ReleaseNotes.md +++ b/.github/workflows/ReleaseNotes.md @@ -1,20 +0,0 @@ -* [Core] Added net7.0 builds (#1635, thanks to @YAJeff). -* [Core] Added support for payload segments (#1585, thanks to @xljiulang) -* [Client] Added support for passing MQTT v5 options (User properties etc.) for disconnects. -* [Client] An internal exception is no longer caught silently when calling _DisconnectAsync_ to indicate that the disconnect is not clean (BREAKING CHANGE). -* [Client] MQTTv5 features are now checked and an exception is thrown if they are used when using protocol version 3.1.1 and lower. These checks can be disabled in client options. (BREAKING CHANGE!). -* [Client] Added support for disabling packet fragmentation (required for i.e. AWS, #1690, thanks to @logicaloud). -* [Client] Added new overloads for options builder. -* [Client] Renamed _InspectPackage_ to _InspectPacketAsync_ (BREAKING CHANGE). -* [ManagedClient] Exposed added and removed topics in _ManagedProcessFailedEventArgs_ (#1678, thanks to @scottbrogden-iheartmedia). -* [Server] Exposed MQTT v5 sent properties from the affected client in _ClientDisconnectedAsync_ event. -* [Server] Fixed wrong client ID passed to _InterceptingUnsubscriptionEventArgs_ (#1631, thanks to @ghord). -* [Server] Exposed socket settings for TCP keep alive in TCP options (#1544). -* [Server] Exposed server session items at server level and allow custom session items for injected application messages (#1638). -* [Server] Improved performance for retained message handling when subscribing using "DoNotSendOnSubscribe" or "SendAtSubscribeIfNewSubscriptionOnly" (#1661, thanks to @Int32Overflow). -* [Server] Added support for changing the used TLS certificate while the server is running (#1652, thanks to @YAJeff). The certificate provider will now be invoked for every new connection! -* [Server] Added a new API method which allows reading a single retained message without the need to processing the entire set of retained messages (#1659). -* [Server] Added a new event (InterceptingClientEnqueueAsync) which allows intercepting when an application message is enqueued for a client (#1648). -* [Server] Fixed race condition when handling connections which leads to stopped message transfers (#1677, thanks to @RazvanEmilR). -* [Server] Fixed not delivered packets to other sessions if the _InterceptingClientEnqueueEvent_ is used and accepting the packet is denied (#1685, thanks to @RossHNPC). -* [Server] Added missing user properties to subscribe and unsubscribe interceptor (#1702).