diff --git a/Build/MQTTnet.AspNetCore.nuspec b/Build/MQTTnet.AspNetCore.nuspec index 5be2f945..20489c78 100644 --- a/Build/MQTTnet.AspNetCore.nuspec +++ b/Build/MQTTnet.AspNetCore.nuspec @@ -10,15 +10,14 @@ https://raw.githubusercontent.com/chkr1011/MQTTnet/master/Images/Logo_128x128.png false This is a support library to integrate MQTTnet into AspNetCore. - * For more release notes please check the MQTTnet release notes. - + For release notes please go to MQTTnet release notes (https://www.nuget.org/packages/MQTTnet/). Copyright Christian Kratky 2016-2018 MQTT Message Queue Telemetry Transport MQTTClient MQTTServer Server MQTTBroker Broker NETStandard IoT InternetOfThings Messaging Hardware Arduino Sensor Actuator M2M ESP Smart Home Cities Automation Xamarin - - - - + + + + diff --git a/Build/MQTTnet.Extensions.ManagedClient.nuspec b/Build/MQTTnet.Extensions.ManagedClient.nuspec index 99a58c4a..572f73d3 100644 --- a/Build/MQTTnet.Extensions.ManagedClient.nuspec +++ b/Build/MQTTnet.Extensions.ManagedClient.nuspec @@ -10,12 +10,11 @@ https://raw.githubusercontent.com/chkr1011/MQTTnet/master/Images/Logo_128x128.png false This is an extension library which provides a managed MQTT client with additional features using MQTTnet. - * For more release notes please check the MQTTnet release notes. - + For release notes please go to MQTTnet release notes (https://www.nuget.org/packages/MQTTnet/). Copyright Christian Kratky 2016-2018 MQTT Message Queue Telemetry Transport MQTTClient MQTTServer Server MQTTBroker Broker NETStandard IoT InternetOfThings Messaging Hardware Arduino Sensor Actuator M2M ESP Smart Home Cities Automation Xamarin - + diff --git a/Build/MQTTnet.Extensions.Rpc.nuspec b/Build/MQTTnet.Extensions.Rpc.nuspec index 736f92da..29cec2b4 100644 --- a/Build/MQTTnet.Extensions.Rpc.nuspec +++ b/Build/MQTTnet.Extensions.Rpc.nuspec @@ -10,12 +10,11 @@ https://raw.githubusercontent.com/chkr1011/MQTTnet/master/Images/Logo_128x128.png false This is an extension library which allows executing synchronous device calls including a response using MQTTnet. - * For more release notes please check the MQTTnet release notes. - + For release notes please go to MQTTnet release notes (https://www.nuget.org/packages/MQTTnet/). Copyright Christian Kratky 2016-2018 MQTT Message Queue Telemetry Transport MQTTClient MQTTServer Server MQTTBroker Broker NETStandard IoT InternetOfThings Messaging Hardware Arduino Sensor Actuator M2M ESP Smart Home Cities Automation Xamarin - + diff --git a/Build/MQTTnet.nuspec b/Build/MQTTnet.nuspec index b92aece6..e3a8d290 100644 --- a/Build/MQTTnet.nuspec +++ b/Build/MQTTnet.nuspec @@ -10,15 +10,14 @@ https://raw.githubusercontent.com/chkr1011/MQTTnet/master/Images/Logo_128x128.png false MQTTnet is a high performance .NET library for MQTT based communication. It provides a MQTT client and a MQTT server (broker). - * [Core] Performance optimizations. -* [Client] Added support for proxies when using web socket connections (thanks to PitySOFT). -* [Client] Refactored TLS parameter usage and added more parameters (thanks to PitySOFT). -* [Client] Fixed a deadlock when clients are disconnecting (thanks to @JanEggers). -* [ManagedClient] Unsubscriptions are now pushed before subscriptions to support re-subscribing (thanks to @cstichlberger). -* [Server] Fixed an issue in client keep alive checks (thanks to @jenscski). -* [Server] Changed the order of _ClientConnected_ and _ClientDisconnected_ events so that _ClientConnected_ is fired at first (thanks to @jenscski). -* [Server] Fixed an iussue which lets the server stop processing messages when using the application message interceptor (thanks to @alamsor). -* [Server] Fixed a deadlock when clients are disconnecting (thanks to @JanEggers). + * [Core] Updated nuget packages due to security issues. +* [Client] Fixed wrong behavior of publish method when client is disconnecting (thanks to @PaulFake). +* [ManagedClient] Added max pending messages count option. +* [ManagedClient] Add pending messages overflow strategy option. +* [Server] Added new method which exposes all retained messages. +* [Server] Removed (wrong) setter from the server options interface. +* [Server] fixed cpu spike in case a client disconnectes (issue 421). +* [Server] fixed concurrent sends with Aspnetcore.Connections.Abstractions based transport. Copyright Christian Kratky 2016-2018 MQTT Message Queue Telemetry Transport MQTTClient MQTTServer Server MQTTBroker Broker NETStandard IoT InternetOfThings Messaging Hardware Arduino Sensor Actuator M2M ESP Smart Home Cities Automation Xamarin @@ -27,7 +26,7 @@ - + @@ -36,7 +35,7 @@ - + diff --git a/README.md b/README.md index 1a34eeed..cb405dd4 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,11 @@

+
+

[![NuGet Badge](https://buildstats.info/nuget/MQTTnet)](https://www.nuget.org/packages/MQTTnet) -[![Build status](https://ci.appveyor.com/api/projects/status/ycit86voxfevm2aa/branch/master?svg=true)](https://ci.appveyor.com/project/chkr1011/mqttnet/branch/develop) +[![Build status](https://ci.appveyor.com/api/projects/status/ycit86voxfevm2aa/branch/master?svg=true)](https://ci.appveyor.com/project/chkr1011/mqttnet) [![BCH compliance](https://bettercodehub.com/edge/badge/chkr1011/MQTTnet?branch=master)](https://bettercodehub.com/) [![OpenCollective](https://opencollective.com/mqttnet/backers/badge.svg)](https://opencollective.com/mqttnet) [![OpenCollective](https://opencollective.com/mqttnet/sponsors/badge.svg)](https://opencollective.com/mqttnet) @@ -23,7 +25,7 @@ MQTTnet is a high performance .NET library for MQTT based communication. It prov * Performance optimized (processing ~70.000 messages / second)* * Interfaces included for mocking and testing * Access to internal trace messages -* Unit tested (~90 tests) +* Unit tested (~100 tests) \* Tested on local machine (Intel i7 8700K) with MQTTnet client and server running in the same process using the TCP channel. The app for verification is part of this repository and stored in _/Tests/MQTTnet.TestApp.NetCore_. diff --git a/Source/MQTTnet.AspnetCore/AspNetMqttServerOptionsBuilder.cs b/Source/MQTTnet.AspnetCore/AspNetMqttServerOptionsBuilder.cs new file mode 100644 index 00000000..e0ae3330 --- /dev/null +++ b/Source/MQTTnet.AspnetCore/AspNetMqttServerOptionsBuilder.cs @@ -0,0 +1,15 @@ +using MQTTnet.Server; +using System; + +namespace MQTTnet.AspNetCore +{ + public class AspNetMqttServerOptionsBuilder : MqttServerOptionsBuilder + { + public AspNetMqttServerOptionsBuilder(IServiceProvider serviceProvider) + { + ServiceProvider = serviceProvider; + } + + public IServiceProvider ServiceProvider { get; } + } +} diff --git a/Source/MQTTnet.AspnetCore/MQTTnet.AspNetCore.csproj b/Source/MQTTnet.AspnetCore/MQTTnet.AspNetCore.csproj index dd3a4d55..dc477f76 100644 --- a/Source/MQTTnet.AspnetCore/MQTTnet.AspNetCore.csproj +++ b/Source/MQTTnet.AspnetCore/MQTTnet.AspNetCore.csproj @@ -14,9 +14,9 @@ - - - + + + diff --git a/Source/MQTTnet.AspnetCore/MqttConnectionContext.cs b/Source/MQTTnet.AspnetCore/MqttConnectionContext.cs index 2d27894d..56c094c0 100644 --- a/Source/MQTTnet.AspnetCore/MqttConnectionContext.cs +++ b/Source/MQTTnet.AspnetCore/MqttConnectionContext.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Connections; using MQTTnet.Adapter; using MQTTnet.AspNetCore.Client.Tcp; +using MQTTnet.Exceptions; using MQTTnet.Packets; using MQTTnet.Serializer; using System; @@ -24,6 +25,8 @@ namespace MQTTnet.AspNetCore public event EventHandler ReadingPacketStarted; public event EventHandler ReadingPacketCompleted; + private readonly SemaphoreSlim _writerSemaphore = new SemaphoreSlim(1, 1); + public Task ConnectAsync(TimeSpan timeout, CancellationToken cancellationToken) { if (Connection is TcpConnection tcp && !tcp.IsConnected) @@ -57,7 +60,7 @@ namespace MQTTnet.AspNetCore } else { - readResult = await readTask; + readResult = await readTask.ConfigureAwait(false); } var buffer = readResult.Buffer; @@ -81,7 +84,7 @@ namespace MQTTnet.AspNetCore } else if (readResult.IsCompleted) { - break; + throw new MqttCommunicationException("Connection Aborted"); } } finally @@ -102,10 +105,20 @@ namespace MQTTnet.AspNetCore return null; } - public Task SendPacketAsync(MqttBasePacket packet, CancellationToken cancellationToken) + public async Task SendPacketAsync(MqttBasePacket packet, CancellationToken cancellationToken) { - var buffer = PacketSerializerAdapter.Serialize(packet); - return Connection.Transport.Output.WriteAsync(buffer.AsMemory(), cancellationToken).AsTask(); + var buffer = PacketSerializer.Serialize(packet).AsMemory(); + var output = Connection.Transport.Output; + + await _writerSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + await output.WriteAsync(buffer, cancellationToken).ConfigureAwait(false); + } + finally + { + _writerSemaphore.Release(); + } } public void Dispose() diff --git a/Source/MQTTnet.AspnetCore/ServiceCollectionExtensions.cs b/Source/MQTTnet.AspnetCore/ServiceCollectionExtensions.cs index b5d0440a..d9d9fdb8 100644 --- a/Source/MQTTnet.AspnetCore/ServiceCollectionExtensions.cs +++ b/Source/MQTTnet.AspnetCore/ServiceCollectionExtensions.cs @@ -20,14 +20,27 @@ namespace MQTTnet.AspNetCore return services; } - + public static IServiceCollection AddHostedMqttServer(this IServiceCollection services, Action configure) { - var builder = new MqttServerOptionsBuilder(); + services.AddSingleton(s => { + var builder = new MqttServerOptionsBuilder(); + configure(builder); + return builder.Build(); + }); - configure(builder); - - services.AddSingleton(builder.Build()); + services.AddHostedMqttServer(); + + return services; + } + + public static IServiceCollection AddHostedMqttServerWithServices(this IServiceCollection services, Action configure) + { + services.AddSingleton(s => { + var builder = new AspNetMqttServerOptionsBuilder(s); + configure(builder); + return builder.Build(); + }); services.AddHostedMqttServer(); diff --git a/Source/MQTTnet.Extensions.ManagedClient/ApplicationMessageSkippedEventArgs.cs b/Source/MQTTnet.Extensions.ManagedClient/ApplicationMessageSkippedEventArgs.cs new file mode 100644 index 00000000..0f1613d4 --- /dev/null +++ b/Source/MQTTnet.Extensions.ManagedClient/ApplicationMessageSkippedEventArgs.cs @@ -0,0 +1,14 @@ +using System; + +namespace MQTTnet.Extensions.ManagedClient +{ + public class ApplicationMessageSkippedEventArgs : EventArgs + { + public ApplicationMessageSkippedEventArgs(ManagedMqttApplicationMessage applicationMessage) + { + ApplicationMessage = applicationMessage ?? throw new ArgumentNullException(nameof(applicationMessage)); + } + + public ManagedMqttApplicationMessage ApplicationMessage { get; } + } +} diff --git a/Source/MQTTnet.Extensions.ManagedClient/IManagedMqttClient.cs b/Source/MQTTnet.Extensions.ManagedClient/IManagedMqttClient.cs index b5a6c50b..d0035849 100644 --- a/Source/MQTTnet.Extensions.ManagedClient/IManagedMqttClient.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/IManagedMqttClient.cs @@ -15,6 +15,7 @@ namespace MQTTnet.Extensions.ManagedClient event EventHandler Disconnected; event EventHandler ApplicationMessageProcessed; + event EventHandler ApplicationMessageSkipped; event EventHandler ConnectingFailed; event EventHandler SynchronizingSubscriptionsFailed; diff --git a/Source/MQTTnet.Extensions.ManagedClient/IManagedMqttClientOptions.cs b/Source/MQTTnet.Extensions.ManagedClient/IManagedMqttClientOptions.cs index 8d48babd..f59aca7c 100644 --- a/Source/MQTTnet.Extensions.ManagedClient/IManagedMqttClientOptions.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/IManagedMqttClientOptions.cs @@ -1,5 +1,6 @@ using System; using MQTTnet.Client; +using MQTTnet.Server; namespace MQTTnet.Extensions.ManagedClient { @@ -12,5 +13,9 @@ namespace MQTTnet.Extensions.ManagedClient TimeSpan ConnectionCheckInterval { get; } IManagedMqttClientStorage Storage { get; } + + int MaxPendingMessages { get; } + + MqttPendingMessagesOverflowStrategy PendingMessagesOverflowStrategy { get; } } } \ No newline at end of file diff --git a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClient.cs b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClient.cs index a38bcc27..271e6b93 100644 --- a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClient.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClient.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -7,13 +6,17 @@ using System.Threading.Tasks; using MQTTnet.Client; using MQTTnet.Diagnostics; using MQTTnet.Exceptions; +using MQTTnet.Internal; using MQTTnet.Protocol; +using MQTTnet.Server; +using MqttClientConnectedEventArgs = MQTTnet.Client.MqttClientConnectedEventArgs; +using MqttClientDisconnectedEventArgs = MQTTnet.Client.MqttClientDisconnectedEventArgs; namespace MQTTnet.Extensions.ManagedClient { public class ManagedMqttClient : IManagedMqttClient { - private readonly BlockingCollection _messageQueue = new BlockingCollection(); + private readonly BlockingQueue _messageQueue = new BlockingQueue(); private readonly Dictionary _subscriptions = new Dictionary(); private readonly HashSet _unsubscriptions = new HashSet(); @@ -50,6 +53,7 @@ namespace MQTTnet.Extensions.ManagedClient public event EventHandler ApplicationMessageReceived; public event EventHandler ApplicationMessageProcessed; + public event EventHandler ApplicationMessageSkipped; public event EventHandler ConnectingFailed; public event EventHandler SynchronizingSubscriptionsFailed; @@ -75,7 +79,7 @@ namespace MQTTnet.Extensions.ManagedClient foreach (var message in messages) { - _messageQueue.Add(message); + _messageQueue.Enqueue(message); } } @@ -93,10 +97,7 @@ namespace MQTTnet.Extensions.ManagedClient StopPublishing(); StopMaintainingConnection(); - while (_messageQueue.Any()) - { - _messageQueue.Take(); - } + _messageQueue.Clear(); return Task.FromResult(0); } @@ -112,12 +113,38 @@ namespace MQTTnet.Extensions.ManagedClient { if (applicationMessage == null) throw new ArgumentNullException(nameof(applicationMessage)); - if (_storageManager != null) + ManagedMqttApplicationMessage removedMessage = null; + lock (_messageQueue) { - await _storageManager.AddAsync(applicationMessage).ConfigureAwait(false); + if (_messageQueue.Count >= _options.MaxPendingMessages) + { + if (_options.PendingMessagesOverflowStrategy == MqttPendingMessagesOverflowStrategy.DropNewMessage) + { + _logger.Verbose("Skipping publish of new application message because internal queue is full."); + ApplicationMessageSkipped?.Invoke(this, new ApplicationMessageSkippedEventArgs(applicationMessage)); + return; + } + + if (_options.PendingMessagesOverflowStrategy == MqttPendingMessagesOverflowStrategy.DropOldestQueuedMessage) + { + removedMessage = _messageQueue.RemoveFirst(); + _logger.Verbose("Removed oldest application message from internal queue because it is full."); + ApplicationMessageSkipped?.Invoke(this, new ApplicationMessageSkippedEventArgs(removedMessage)); + } + } + + _messageQueue.Enqueue(applicationMessage); } - _messageQueue.Add(applicationMessage); + if (_storageManager != null) + { + if (removedMessage != null) + { + await _storageManager.RemoveAsync(removedMessage).ConfigureAwait(false); + } + + await _storageManager.AddAsync(applicationMessage).ConfigureAwait(false); + } } public Task SubscribeAsync(IEnumerable topicFilters) @@ -157,7 +184,6 @@ namespace MQTTnet.Extensions.ManagedClient public void Dispose() { - _messageQueue?.Dispose(); _connectionCancellationToken?.Dispose(); _publishingCancellationToken?.Dispose(); } @@ -226,9 +252,9 @@ namespace MQTTnet.Extensions.ManagedClient { try { - while (!cancellationToken.IsCancellationRequested) + while (!cancellationToken.IsCancellationRequested && _mqttClient.IsConnected) { - var message = _messageQueue.Take(cancellationToken); + var message = _messageQueue.Dequeue(); if (message == null) { continue; @@ -268,7 +294,7 @@ namespace MQTTnet.Extensions.ManagedClient if (message.ApplicationMessage.QualityOfServiceLevel > MqttQualityOfServiceLevel.AtMostOnce) { - _messageQueue.Add(message); + _messageQueue.Enqueue(message); } } catch (Exception exception) @@ -367,7 +393,6 @@ namespace MQTTnet.Extensions.ManagedClient } var cts = new CancellationTokenSource(); - _publishingCancellationToken = cts; Task.Factory.StartNew(() => PublishQueuedMessages(cts.Token), cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); diff --git a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientOptions.cs b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientOptions.cs index cecf2274..cba801bb 100644 --- a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientOptions.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientOptions.cs @@ -1,5 +1,6 @@ using System; using MQTTnet.Client; +using MQTTnet.Server; namespace MQTTnet.Extensions.ManagedClient { @@ -12,5 +13,9 @@ namespace MQTTnet.Extensions.ManagedClient public TimeSpan ConnectionCheckInterval { get; set; } = TimeSpan.FromSeconds(1); public IManagedMqttClientStorage Storage { get; set; } + + public int MaxPendingMessages { get; set; } = int.MaxValue; + + public MqttPendingMessagesOverflowStrategy PendingMessagesOverflowStrategy { get; set; } = MqttPendingMessagesOverflowStrategy.DropNewMessage; } } diff --git a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientOptionsBuilder.cs b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientOptionsBuilder.cs index ef774c59..3ea5b050 100644 --- a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientOptionsBuilder.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientOptionsBuilder.cs @@ -1,5 +1,6 @@ using System; using MQTTnet.Client; +using MQTTnet.Server; namespace MQTTnet.Extensions.ManagedClient { @@ -8,6 +9,18 @@ namespace MQTTnet.Extensions.ManagedClient private readonly ManagedMqttClientOptions _options = new ManagedMqttClientOptions(); private MqttClientOptionsBuilder _clientOptionsBuilder; + public ManagedMqttClientOptionsBuilder WithMaxPendingMessages(int value) + { + _options.MaxPendingMessages = value; + return this; + } + + public ManagedMqttClientOptionsBuilder WithPendingMessagesOverflowStrategy(MqttPendingMessagesOverflowStrategy value) + { + _options.PendingMessagesOverflowStrategy = value; + return this; + } + public ManagedMqttClientOptionsBuilder WithAutoReconnectDelay(TimeSpan value) { _options.AutoReconnectDelay = value; diff --git a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientStorageManager.cs b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientStorageManager.cs index 0f4e33e3..821a7d92 100644 --- a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientStorageManager.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientStorageManager.cs @@ -28,6 +28,8 @@ namespace MQTTnet.Extensions.ManagedClient public async Task AddAsync(ManagedMqttApplicationMessage applicationMessage) { + if (applicationMessage == null) throw new ArgumentNullException(nameof(applicationMessage)); + using (await _messagesLock.LockAsync(CancellationToken.None).ConfigureAwait(false)) { _messages.Add(applicationMessage); @@ -37,6 +39,8 @@ namespace MQTTnet.Extensions.ManagedClient public async Task RemoveAsync(ManagedMqttApplicationMessage applicationMessage) { + if (applicationMessage == null) throw new ArgumentNullException(nameof(applicationMessage)); + using (await _messagesLock.LockAsync(CancellationToken.None).ConfigureAwait(false)) { var index = _messages.IndexOf(applicationMessage); diff --git a/Source/MQTTnet/Adapter/MqttChannelAdapter.cs b/Source/MQTTnet/Adapter/MqttChannelAdapter.cs index 01d252b0..dc95152f 100644 --- a/Source/MQTTnet/Adapter/MqttChannelAdapter.cs +++ b/Source/MQTTnet/Adapter/MqttChannelAdapter.cs @@ -154,6 +154,9 @@ namespace MQTTnet.Adapter return packet; } + catch (OperationCanceledException) + { + } catch (Exception exception) { if (IsWrappedException(exception)) @@ -241,7 +244,8 @@ namespace MQTTnet.Adapter { if (exception is IOException && exception.InnerException is SocketException socketException) { - if (socketException.SocketErrorCode == SocketError.ConnectionAborted) + if (socketException.SocketErrorCode == SocketError.ConnectionAborted || + socketException.SocketErrorCode == SocketError.OperationAborted) { throw new OperationCanceledException(); } diff --git a/Source/MQTTnet/Client/IMqttClientFactory.cs b/Source/MQTTnet/Client/IMqttClientFactory.cs index a15b499b..cd687afc 100644 --- a/Source/MQTTnet/Client/IMqttClientFactory.cs +++ b/Source/MQTTnet/Client/IMqttClientFactory.cs @@ -7,5 +7,9 @@ namespace MQTTnet.Client IMqttClient CreateMqttClient(); IMqttClient CreateMqttClient(IMqttNetLogger logger); + + IMqttClient CreateMqttClient(IMqttClientAdapterFactory adapterFactory); + + IMqttClient CreateMqttClient(IMqttNetLogger logger, IMqttClientAdapterFactory adapterFactory); } } \ No newline at end of file diff --git a/Source/MQTTnet/Client/MqttClient.cs b/Source/MQTTnet/Client/MqttClient.cs index 643d0ef9..ce91a487 100644 --- a/Source/MQTTnet/Client/MqttClient.cs +++ b/Source/MQTTnet/Client/MqttClient.cs @@ -30,7 +30,7 @@ namespace MQTTnet.Client internal Task _keepAliveMessageSenderTask; private IMqttChannelAdapter _adapter; private bool _cleanDisconnectInitiated; - private TaskCompletionSource _disconnectReason; + private long _disconnectGate; public MqttClient(IMqttClientAdapterFactory channelFactory, IMqttNetLogger logger) { @@ -55,12 +55,12 @@ namespace MQTTnet.Client try { - _cancellationTokenSource = new CancellationTokenSource(); - _disconnectReason = new TaskCompletionSource(); _options = options; _packetIdentifierProvider.Reset(); _packetDispatcher.Reset(); + _cancellationTokenSource = new CancellationTokenSource(); + _disconnectGate = 0; _adapter = _adapterFactory.CreateClientAdapter(options, _logger); _logger.Verbose($"Trying to connect with server ({_options.ChannelOptions})."); @@ -88,10 +88,12 @@ namespace MQTTnet.Client catch (Exception exception) { _logger.Error(exception, "Error while connecting with server."); - if (_disconnectReason.TrySetException(exception)) + + if (!DisconnectIsPending()) { await DisconnectInternalAsync(null, exception).ConfigureAwait(false); } + throw; } } @@ -109,7 +111,7 @@ namespace MQTTnet.Client } finally { - if (_disconnectReason.TrySetCanceled()) + if (!DisconnectIsPending()) { await DisconnectInternalAsync(null, null).ConfigureAwait(false); } @@ -184,10 +186,12 @@ namespace MQTTnet.Client public void Dispose() { + _cancellationTokenSource?.Cancel(false); _cancellationTokenSource?.Dispose(); _cancellationTokenSource = null; _adapter?.Dispose(); + _adapter = null; } private async Task AuthenticateAsync(MqttApplicationMessage willApplicationMessage, CancellationToken cancellationToken) @@ -213,7 +217,7 @@ namespace MQTTnet.Client private void ThrowIfNotConnected() { - if (!IsConnected) throw new MqttCommunicationException("The client is not connected."); + if (!IsConnected || Interlocked.Read(ref _disconnectGate) == 1) throw new MqttCommunicationException("The client is not connected."); } private void ThrowIfConnected(string message) @@ -223,9 +227,10 @@ namespace MQTTnet.Client private async Task DisconnectInternalAsync(Task sender, Exception exception) { - InitiateDisconnect(); - var clientWasConnected = IsConnected; + + InitiateDisconnect(); + IsConnected = false; try @@ -246,10 +251,7 @@ namespace MQTTnet.Client } finally { - _adapter?.Dispose(); - _adapter = null; - _cancellationTokenSource?.Dispose(); - _cancellationTokenSource = null; + Dispose(); _cleanDisconnectInitiated = false; _logger.Info("Disconnected."); @@ -263,16 +265,16 @@ namespace MQTTnet.Client { try { - if (_cancellationTokenSource == null || _cancellationTokenSource.IsCancellationRequested) + if (_cancellationTokenSource?.IsCancellationRequested == true) { return; } - _cancellationTokenSource.Cancel(false); + _cancellationTokenSource?.Cancel(false); } - catch (Exception adapterException) + catch (Exception exception) { - _logger.Warning(adapterException, "Error while initiating disconnect."); + _logger.Warning(exception, "Error while initiating disconnect."); } } } @@ -360,7 +362,7 @@ namespace MQTTnet.Client _logger.Error(exception, "Unhandled exception while sending/receiving keep alive packets."); } - if (_disconnectReason.TrySetException(exception)) + if (!DisconnectIsPending()) { await DisconnectInternalAsync(_keepAliveMessageSenderTask, exception).ConfigureAwait(false); } @@ -379,8 +381,10 @@ namespace MQTTnet.Client { while (!cancellationToken.IsCancellationRequested) { - var packet = await _adapter.ReceivePacketAsync(TimeSpan.Zero, cancellationToken).ConfigureAwait(false); - if (packet != null) + var packet = await _adapter.ReceivePacketAsync(TimeSpan.Zero, cancellationToken) + .ConfigureAwait(false); + + if (packet != null && !cancellationToken.IsCancellationRequested) { await ProcessReceivedPacketAsync(packet, cancellationToken).ConfigureAwait(false); } @@ -407,7 +411,7 @@ namespace MQTTnet.Client _packetDispatcher.Dispatch(exception); - if (_disconnectReason.TrySetException(exception)) + if (!DisconnectIsPending()) { await DisconnectInternalAsync(_packetReceiverTask, exception).ConfigureAwait(false); } @@ -493,16 +497,20 @@ namespace MQTTnet.Client private void StartReceivingPackets(CancellationToken cancellationToken) { - _packetReceiverTask = Task.Run( + _packetReceiverTask = Task.Factory.StartNew( () => ReceivePacketsAsync(cancellationToken), - cancellationToken); + cancellationToken, + TaskCreationOptions.LongRunning, + TaskScheduler.Default); } private void StartSendingKeepAliveMessages(CancellationToken cancellationToken) { - _keepAliveMessageSenderTask = Task.Run( + _keepAliveMessageSenderTask = Task.Factory.StartNew( () => SendKeepAliveMessagesAsync(cancellationToken), - cancellationToken); + cancellationToken, + TaskCreationOptions.LongRunning, + TaskScheduler.Default); } private void FireApplicationMessageReceivedEvent(MqttPublishPacket publishPacket) @@ -538,5 +546,10 @@ namespace MQTTnet.Client { } } + + private bool DisconnectIsPending() + { + return Interlocked.CompareExchange(ref _disconnectGate, 1, 0) != 0; + } } -} \ No newline at end of file +} diff --git a/Source/MQTTnet/Implementations/MqttTcpChannel.cs b/Source/MQTTnet/Implementations/MqttTcpChannel.cs index bc10d65c..39283288 100644 --- a/Source/MQTTnet/Implementations/MqttTcpChannel.cs +++ b/Source/MQTTnet/Implementations/MqttTcpChannel.cs @@ -116,7 +116,7 @@ namespace MQTTnet.Implementations return true; } - if (chain.ChainStatus.Any(c => c.Status == X509ChainStatusFlags.RevocationStatusUnknown || c.Status == X509ChainStatusFlags.Revoked || c.Status == X509ChainStatusFlags.RevocationStatusUnknown)) + if (chain.ChainStatus.Any(c => c.Status == X509ChainStatusFlags.RevocationStatusUnknown || c.Status == X509ChainStatusFlags.Revoked || c.Status == X509ChainStatusFlags.OfflineRevocation)) { if (!_options.TlsOptions.IgnoreCertificateRevocationErrors) { diff --git a/Source/MQTTnet/Implementations/MqttTcpServerListener.cs b/Source/MQTTnet/Implementations/MqttTcpServerListener.cs index d91b66e6..59da016a 100644 --- a/Source/MQTTnet/Implementations/MqttTcpServerListener.cs +++ b/Source/MQTTnet/Implementations/MqttTcpServerListener.cs @@ -20,8 +20,8 @@ namespace MQTTnet.Implementations private readonly CancellationToken _cancellationToken; private readonly AddressFamily _addressFamily; private readonly MqttServerTcpEndpointBaseOptions _options; + private readonly MqttServerTlsTcpEndpointOptions _tlsOptions; private readonly X509Certificate2 _tlsCertificate; - private Socket _socket; public MqttTcpServerListener( @@ -36,6 +36,11 @@ namespace MQTTnet.Implementations _tlsCertificate = tlsCertificate; _cancellationToken = cancellationToken; _logger = logger.CreateChildLogger(nameof(MqttTcpServerListener)); + + if (_options is MqttServerTlsTcpEndpointOptions tlsOptions) + { + _tlsOptions = tlsOptions; + } } public event EventHandler ClientAccepted; @@ -75,7 +80,7 @@ namespace MQTTnet.Implementations if (_tlsCertificate != null) { sslStream = new SslStream(new NetworkStream(clientSocket), false); - await sslStream.AuthenticateAsServerAsync(_tlsCertificate, false, SslProtocols.Tls12, false).ConfigureAwait(false); + await sslStream.AuthenticateAsServerAsync(_tlsCertificate, false, _tlsOptions.SslProtocol, false).ConfigureAwait(false); } _logger.Verbose("Client '{0}' accepted by TCP listener '{1}, {2}'.", diff --git a/Source/MQTTnet/Internal/BlockingQueue.cs b/Source/MQTTnet/Internal/BlockingQueue.cs new file mode 100644 index 00000000..d6c28f46 --- /dev/null +++ b/Source/MQTTnet/Internal/BlockingQueue.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Threading; + +namespace MQTTnet.Internal +{ + public class BlockingQueue + { + private readonly object _syncRoot = new object(); + private readonly LinkedList _items = new LinkedList(); + private readonly ManualResetEvent _gate = new ManualResetEvent(false); + + public int Count + { + get + { + lock (_syncRoot) + { + return _items.Count; + } + } + } + + public void Enqueue(TItem item) + { + if (item == null) throw new ArgumentNullException(nameof(item)); + + lock (_syncRoot) + { + _items.AddLast(item); + _gate.Set(); + } + } + + public TItem Dequeue() + { + while (true) + { + lock (_syncRoot) + { + if (_items.Count > 0) + { + var item = _items.First.Value; + _items.RemoveFirst(); + + return item; + } + + if (_items.Count == 0) + { + _gate.Reset(); + } + } + + _gate.WaitOne(); + } + } + + public TItem RemoveFirst() + { + lock (_syncRoot) + { + var item = _items.First; + _items.RemoveFirst(); + + return item.Value; + } + } + + public void Clear() + { + lock (_syncRoot) + { + _items.Clear(); + } + } + } +} diff --git a/Source/MQTTnet/MQTTnet.csproj b/Source/MQTTnet/MQTTnet.csproj index 1704d7d0..5c9ba727 100644 --- a/Source/MQTTnet/MQTTnet.csproj +++ b/Source/MQTTnet/MQTTnet.csproj @@ -54,7 +54,7 @@ - + diff --git a/Source/MQTTnet/Server/IMqttServer.cs b/Source/MQTTnet/Server/IMqttServer.cs index e724a65f..3a45973b 100644 --- a/Source/MQTTnet/Server/IMqttServer.cs +++ b/Source/MQTTnet/Server/IMqttServer.cs @@ -16,8 +16,14 @@ namespace MQTTnet.Server IMqttServerOptions Options { get; } + [Obsolete("This method is no longer async. Use the not async method.")] Task> GetClientSessionsStatusAsync(); + IList GetClientSessionsStatus(); + + IList GetRetainedMessages(); + Task ClearRetainedMessagesAsync(); + Task SubscribeAsync(string clientId, IList topicFilters); Task UnsubscribeAsync(string clientId, IList topicFilters); diff --git a/Source/MQTTnet/Server/IMqttServerFactory.cs b/Source/MQTTnet/Server/IMqttServerFactory.cs index 6e399cae..7d23ed36 100644 --- a/Source/MQTTnet/Server/IMqttServerFactory.cs +++ b/Source/MQTTnet/Server/IMqttServerFactory.cs @@ -8,6 +8,8 @@ namespace MQTTnet.Server { IMqttServer CreateMqttServer(); + IMqttServer CreateMqttServer(IMqttNetLogger logger); + IMqttServer CreateMqttServer(IEnumerable adapters, IMqttNetLogger logger); } } \ No newline at end of file diff --git a/Source/MQTTnet/Server/IMqttServerOptions.cs b/Source/MQTTnet/Server/IMqttServerOptions.cs index 2ae4c9fa..f9334fd7 100644 --- a/Source/MQTTnet/Server/IMqttServerOptions.cs +++ b/Source/MQTTnet/Server/IMqttServerOptions.cs @@ -14,7 +14,7 @@ namespace MQTTnet.Server Action ConnectionValidator { get; } Action SubscriptionInterceptor { get; } Action ApplicationMessageInterceptor { get; } - Action ClientMessageQueueInterceptor { get; set; } + Action ClientMessageQueueInterceptor { get; } MqttServerTcpEndpointOptions DefaultEndpointOptions { get; } MqttServerTlsTcpEndpointOptions TlsEndpointOptions { get; } diff --git a/Source/MQTTnet/Server/MqttClientSession.cs b/Source/MQTTnet/Server/MqttClientSession.cs index d5448e80..1005110b 100644 --- a/Source/MQTTnet/Server/MqttClientSession.cs +++ b/Source/MQTTnet/Server/MqttClientSession.cs @@ -137,7 +137,9 @@ namespace MQTTnet.Server _cleanupHandle?.Dispose(); _cleanupHandle = null; - + + _adapter = null; + _cancellationTokenSource?.Cancel(false); _cancellationTokenSource?.Dispose(); _cancellationTokenSource = null; } @@ -289,7 +291,9 @@ namespace MQTTnet.Server { _pendingPacketsQueue?.Dispose(); + _cancellationTokenSource?.Cancel (); _cancellationTokenSource?.Dispose(); + _cancellationTokenSource = null; } private void ProcessReceivedPacket(IMqttChannelAdapter adapter, MqttBasePacket packet, CancellationToken cancellationToken) diff --git a/Source/MQTTnet/Server/MqttClientSessionsManager.cs b/Source/MQTTnet/Server/MqttClientSessionsManager.cs index a6a6b9df..f180f685 100644 --- a/Source/MQTTnet/Server/MqttClientSessionsManager.cs +++ b/Source/MQTTnet/Server/MqttClientSessionsManager.cs @@ -66,7 +66,7 @@ namespace MQTTnet.Server return Task.Run(() => RunSession(clientAdapter, _cancellationToken), _cancellationToken); } - public Task> GetClientStatusAsync() + public IList GetClientStatus() { var result = new List(); @@ -78,7 +78,7 @@ namespace MQTTnet.Server result.Add(status); } - return Task.FromResult((IList)result); + return result; } public void EnqueueApplicationMessage(MqttClientSession senderClientSession, MqttPublishPacket publishPacket) diff --git a/Source/MQTTnet/Server/MqttRetainedMessagesManager.cs b/Source/MQTTnet/Server/MqttRetainedMessagesManager.cs index 86c321fc..bf5de984 100644 --- a/Source/MQTTnet/Server/MqttRetainedMessagesManager.cs +++ b/Source/MQTTnet/Server/MqttRetainedMessagesManager.cs @@ -52,7 +52,51 @@ namespace MQTTnet.Server try { - await HandleMessageInternalAsync(clientId, applicationMessage).ConfigureAwait(false); + List messagesForSave = null; + lock (_messages) + { + var saveIsRequired = false; + + if (applicationMessage.Payload?.Length == 0) + { + saveIsRequired = _messages.Remove(applicationMessage.Topic); + _logger.Verbose("Client '{0}' cleared retained message for topic '{1}'.", clientId, applicationMessage.Topic); + } + else + { + if (!_messages.TryGetValue(applicationMessage.Topic, out var existingMessage)) + { + _messages[applicationMessage.Topic] = applicationMessage; + saveIsRequired = true; + } + else + { + if (existingMessage.QualityOfServiceLevel != applicationMessage.QualityOfServiceLevel || !existingMessage.Payload.SequenceEqual(applicationMessage.Payload ?? new byte[0])) + { + _messages[applicationMessage.Topic] = applicationMessage; + saveIsRequired = true; + } + } + + _logger.Verbose("Client '{0}' set retained message for topic '{1}'.", clientId, applicationMessage.Topic); + } + + if (saveIsRequired) + { + messagesForSave = new List(_messages.Values); + } + } + + if (messagesForSave == null) + { + _logger.Verbose("Skipped saving retained messages because no changes were detected."); + return; + } + + if (_options.Storage != null) + { + await _options.Storage.SaveRetainedMessagesAsync(messagesForSave).ConfigureAwait(false); + } } catch (Exception exception) { @@ -84,52 +128,27 @@ namespace MQTTnet.Server return retainedMessages; } - private async Task HandleMessageInternalAsync(string clientId, MqttApplicationMessage applicationMessage) + public IList GetMessages() { - var saveIsRequired = false; - lock (_messages) { - if (applicationMessage.Payload?.Length == 0) - { - saveIsRequired = _messages.Remove(applicationMessage.Topic); - _logger.Info("Client '{0}' cleared retained message for topic '{1}'.", clientId, applicationMessage.Topic); - } - else - { - if (!_messages.TryGetValue(applicationMessage.Topic, out var existingMessage)) - { - _messages[applicationMessage.Topic] = applicationMessage; - saveIsRequired = true; - } - else - { - if (existingMessage.QualityOfServiceLevel != applicationMessage.QualityOfServiceLevel || !existingMessage.Payload.SequenceEqual(applicationMessage.Payload ?? new byte[0])) - { - _messages[applicationMessage.Topic] = applicationMessage; - saveIsRequired = true; - } - } - - _logger.Info("Client '{0}' set retained message for topic '{1}'.", clientId, applicationMessage.Topic); - } + return _messages.Values.ToList(); } + } - if (!saveIsRequired) + public Task ClearMessagesAsync() + { + lock (_messages) { - _logger.Verbose("Skipped saving retained messages because no changes were detected."); + _messages.Clear(); } - if (saveIsRequired && _options.Storage != null) + if (_options.Storage != null) { - List messages; - lock (_messages) - { - messages = _messages.Values.ToList(); - } - - await _options.Storage.SaveRetainedMessagesAsync(messages).ConfigureAwait(false); + return _options.Storage.SaveRetainedMessagesAsync(new List()); } + + return Task.FromResult((object)null); } } } diff --git a/Source/MQTTnet/Server/MqttServer.cs b/Source/MQTTnet/Server/MqttServer.cs index 6d532917..a960ab00 100644 --- a/Source/MQTTnet/Server/MqttServer.cs +++ b/Source/MQTTnet/Server/MqttServer.cs @@ -41,7 +41,17 @@ namespace MQTTnet.Server public Task> GetClientSessionsStatusAsync() { - return _clientSessionsManager.GetClientStatusAsync(); + return Task.FromResult(_clientSessionsManager.GetClientStatus()); + } + + public IList GetClientSessionsStatus() + { + return _clientSessionsManager.GetClientStatus(); + } + + public IList GetRetainedMessages() + { + return _retainedMessagesManager.GetMessages(); } public Task SubscribeAsync(string clientId, IList topicFilters) @@ -129,6 +139,11 @@ namespace MQTTnet.Server } } + public Task ClearRetainedMessagesAsync() + { + return _retainedMessagesManager?.ClearMessagesAsync(); + } + internal void OnClientConnected(string clientId) { _logger.Info("Client '{0}': Connected.", clientId); diff --git a/Source/MQTTnet/Server/MqttServerOptionsBuilder.cs b/Source/MQTTnet/Server/MqttServerOptionsBuilder.cs index f54f379f..45400620 100644 --- a/Source/MQTTnet/Server/MqttServerOptionsBuilder.cs +++ b/Source/MQTTnet/Server/MqttServerOptionsBuilder.cs @@ -1,5 +1,6 @@ using System; using System.Net; +using System.Security.Authentication; namespace MQTTnet.Server { @@ -80,6 +81,12 @@ namespace MQTTnet.Server return this; } + public MqttServerOptionsBuilder WithEncryptionSslProtocol(SslProtocols value) + { + _options.TlsEndpointOptions.SslProtocol = value; + return this; + } + public MqttServerOptionsBuilder WithoutEncryptedEndpoint() { _options.TlsEndpointOptions.IsEnabled = false; diff --git a/Source/MQTTnet/Server/MqttServerTlsTcpEndpointOptions.cs b/Source/MQTTnet/Server/MqttServerTlsTcpEndpointOptions.cs index 76d6c862..88d2f86d 100644 --- a/Source/MQTTnet/Server/MqttServerTlsTcpEndpointOptions.cs +++ b/Source/MQTTnet/Server/MqttServerTlsTcpEndpointOptions.cs @@ -1,4 +1,6 @@ -namespace MQTTnet.Server +using System.Security.Authentication; + +namespace MQTTnet.Server { public class MqttServerTlsTcpEndpointOptions : MqttServerTcpEndpointBaseOptions { @@ -8,5 +10,8 @@ } public byte[] Certificate { get; set; } + + + public SslProtocols SslProtocol { get; set; } = SslProtocols.Tls12; } } diff --git a/Tests/MQTTnet.AspNetCore.Tests/MQTTnet.AspNetCore.Tests.csproj b/Tests/MQTTnet.AspNetCore.Tests/MQTTnet.AspNetCore.Tests.csproj index 77dfff97..998acd2f 100644 --- a/Tests/MQTTnet.AspNetCore.Tests/MQTTnet.AspNetCore.Tests.csproj +++ b/Tests/MQTTnet.AspNetCore.Tests/MQTTnet.AspNetCore.Tests.csproj @@ -1,4 +1,4 @@ - + netcoreapp2.1 @@ -7,9 +7,9 @@ - - - + + + diff --git a/Tests/MQTTnet.AspNetCore.Tests/Mockups/ConnectionContextMockup.cs b/Tests/MQTTnet.AspNetCore.Tests/Mockups/ConnectionContextMockup.cs new file mode 100644 index 00000000..296959ef --- /dev/null +++ b/Tests/MQTTnet.AspNetCore.Tests/Mockups/ConnectionContextMockup.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.IO.Pipelines; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Http.Features; + +namespace MQTTnet.AspNetCore.Tests.Mockups +{ + public class ConnectionContextMockup : ConnectionContext + { + public override string ConnectionId { get; set; } + + public override IFeatureCollection Features { get; } + + public override IDictionary Items { get; set; } + public override IDuplexPipe Transport { get; set; } + + public ConnectionContextMockup() + { + //Transport = new DefaultConnectionContext + } + } +} diff --git a/Tests/MQTTnet.AspNetCore.Tests/Mockups/DuplexPipeMockup.cs b/Tests/MQTTnet.AspNetCore.Tests/Mockups/DuplexPipeMockup.cs new file mode 100644 index 00000000..1774f180 --- /dev/null +++ b/Tests/MQTTnet.AspNetCore.Tests/Mockups/DuplexPipeMockup.cs @@ -0,0 +1,16 @@ +using System.IO.Pipelines; + +namespace MQTTnet.AspNetCore.Tests.Mockups +{ + public class DuplexPipeMockup : IDuplexPipe + { + PipeReader IDuplexPipe.Input => Receive.Reader; + + PipeWriter IDuplexPipe.Output => Send.Writer; + + + public Pipe Receive { get; set; } = new Pipe(); + + public Pipe Send { get; set; } = new Pipe(); + } +} diff --git a/Tests/MQTTnet.AspNetCore.Tests/MqttConnectionContextTest.cs b/Tests/MQTTnet.AspNetCore.Tests/MqttConnectionContextTest.cs new file mode 100644 index 00000000..699aa65b --- /dev/null +++ b/Tests/MQTTnet.AspNetCore.Tests/MqttConnectionContextTest.cs @@ -0,0 +1,51 @@ +using Microsoft.AspNetCore.Connections; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.AspNetCore.Tests.Mockups; +using MQTTnet.Exceptions; +using MQTTnet.Packets; +using MQTTnet.Serializer; +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace MQTTnet.AspNetCore.Tests +{ + [TestClass] + public class MqttConnectionContextTest + { + [TestMethod] + public async Task TestReceivePacketAsyncThrowsWhenReaderCompleted() + { + var serializer = new MqttPacketSerializer {}; + var pipe = new DuplexPipeMockup(); + var connection = new DefaultConnectionContext(); + connection.Transport = pipe; + var ctx = new MqttConnectionContext(serializer, connection); + + pipe.Receive.Writer.Complete(); + + await Assert.ThrowsExceptionAsync(() => ctx.ReceivePacketAsync(TimeSpan.FromSeconds(1), CancellationToken.None)); + } + + [TestMethod] + public async Task TestParallelWrites() + { + var serializer = new MqttPacketSerializer { }; + var pipe = new DuplexPipeMockup(); + var connection = new DefaultConnectionContext(); + connection.Transport = pipe; + var ctx = new MqttConnectionContext(serializer, connection); + + var tasks = Enumerable.Range(1, 10).Select(_ => Task.Run(async () => + { + for (int i = 0; i < 100; i++) + { + await ctx.SendPacketAsync(new MqttPublishPacket(), CancellationToken.None).ConfigureAwait(false); + } + })); + + await Task.WhenAll(tasks).ConfigureAwait(false); + } + } +} diff --git a/Tests/MQTTnet.Benchmarks/LoggerBenchmark.cs b/Tests/MQTTnet.Benchmarks/LoggerBenchmark.cs index 2dce560a..0039434e 100644 --- a/Tests/MQTTnet.Benchmarks/LoggerBenchmark.cs +++ b/Tests/MQTTnet.Benchmarks/LoggerBenchmark.cs @@ -1,6 +1,4 @@ using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Attributes.Exporters; -using BenchmarkDotNet.Attributes.Jobs; using MQTTnet.Diagnostics; namespace MQTTnet.Benchmarks diff --git a/Tests/MQTTnet.Benchmarks/MQTTnet.Benchmarks.csproj b/Tests/MQTTnet.Benchmarks/MQTTnet.Benchmarks.csproj index 2a6b91a5..33f08667 100644 --- a/Tests/MQTTnet.Benchmarks/MQTTnet.Benchmarks.csproj +++ b/Tests/MQTTnet.Benchmarks/MQTTnet.Benchmarks.csproj @@ -8,9 +8,9 @@ - - - + + + diff --git a/Tests/MQTTnet.Benchmarks/MessageProcessingBenchmark.cs b/Tests/MQTTnet.Benchmarks/MessageProcessingBenchmark.cs index f6ff61a3..5e99201c 100644 --- a/Tests/MQTTnet.Benchmarks/MessageProcessingBenchmark.cs +++ b/Tests/MQTTnet.Benchmarks/MessageProcessingBenchmark.cs @@ -1,7 +1,4 @@ using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Attributes.Columns; -using BenchmarkDotNet.Attributes.Exporters; -using BenchmarkDotNet.Attributes.Jobs; using MQTTnet.Client; using MQTTnet.Server; diff --git a/Tests/MQTTnet.Benchmarks/SerializerBenchmark.cs b/Tests/MQTTnet.Benchmarks/SerializerBenchmark.cs index ed7e5631..fa407085 100644 --- a/Tests/MQTTnet.Benchmarks/SerializerBenchmark.cs +++ b/Tests/MQTTnet.Benchmarks/SerializerBenchmark.cs @@ -2,8 +2,6 @@ using MQTTnet.Packets; using MQTTnet.Serializer; using MQTTnet.Internal; -using BenchmarkDotNet.Attributes.Jobs; -using BenchmarkDotNet.Attributes.Exporters; using System; using System.Threading; using System.Threading.Tasks; diff --git a/Tests/MQTTnet.Benchmarks/TopicFilterComparerBenchmark.cs b/Tests/MQTTnet.Benchmarks/TopicFilterComparerBenchmark.cs index 6d9a0321..bbce6305 100644 --- a/Tests/MQTTnet.Benchmarks/TopicFilterComparerBenchmark.cs +++ b/Tests/MQTTnet.Benchmarks/TopicFilterComparerBenchmark.cs @@ -1,6 +1,4 @@ using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Attributes.Exporters; -using BenchmarkDotNet.Attributes.Jobs; using MQTTnet.Server; using System; diff --git a/Tests/MQTTnet.Core.Tests/BlockingQueueTests.cs b/Tests/MQTTnet.Core.Tests/BlockingQueueTests.cs new file mode 100644 index 00000000..80e0d724 --- /dev/null +++ b/Tests/MQTTnet.Core.Tests/BlockingQueueTests.cs @@ -0,0 +1,108 @@ +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Internal; + +namespace MQTTnet.Core.Tests +{ + [TestClass] + public class BlockingQueueTests + { + [TestMethod] + public void Preserve_Order() + { + var queue = new BlockingQueue(); + queue.Enqueue("a"); + queue.Enqueue("b"); + queue.Enqueue("c"); + + Assert.AreEqual(3, queue.Count); + + Assert.AreEqual("a", queue.Dequeue()); + Assert.AreEqual("b", queue.Dequeue()); + Assert.AreEqual("c", queue.Dequeue()); + } + + [TestMethod] + public void Remove_First_Items() + { + var queue = new BlockingQueue(); + queue.Enqueue("a"); + queue.Enqueue("b"); + queue.Enqueue("c"); + + Assert.AreEqual("a", queue.RemoveFirst()); + Assert.AreEqual("b", queue.RemoveFirst()); + + Assert.AreEqual(1, queue.Count); + + Assert.AreEqual("c", queue.Dequeue()); + } + + [TestMethod] + public void Clear_Items() + { + var queue = new BlockingQueue(); + queue.Enqueue("a"); + queue.Enqueue("b"); + queue.Enqueue("c"); + + Assert.AreEqual(3, queue.Count); + + queue.Clear(); + + Assert.AreEqual(0, queue.Count); + } + + [TestMethod] + public async Task Wait_For_Item() + { + var queue = new BlockingQueue(); + + string item = null; +#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + Task.Run(() => + { + item = queue.Dequeue(); + }); +#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + + await Task.Delay(100); + + Assert.AreEqual(0, queue.Count); + Assert.AreEqual(null, item); + + queue.Enqueue("x"); + + await Task.Delay(100); + + Assert.AreEqual("x", item); + Assert.AreEqual(0, queue.Count); + } + + [TestMethod] + public void Wait_For_Times() + { + var number = 0; + + var queue = new BlockingQueue(); + +#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + Task.Run(async () => + { + while (true) + { + queue.Enqueue(1); + await Task.Delay(100); + } + }); +#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + + while (number < 50) + { + queue.Dequeue(); + Interlocked.Increment(ref number); + } + } + } +} diff --git a/Tests/MQTTnet.Core.Tests/MQTTnet.Core.Tests.csproj b/Tests/MQTTnet.Core.Tests/MQTTnet.Core.Tests.csproj index 40697e94..2baaa286 100644 --- a/Tests/MQTTnet.Core.Tests/MQTTnet.Core.Tests.csproj +++ b/Tests/MQTTnet.Core.Tests/MQTTnet.Core.Tests.csproj @@ -3,16 +3,17 @@ Exe Full - netcoreapp2.0 + netcoreapp2.1 - + + diff --git a/Tests/MQTTnet.Core.Tests/ManagedMqttClientTests.cs b/Tests/MQTTnet.Core.Tests/ManagedMqttClientTests.cs new file mode 100644 index 00000000..670b8a1c --- /dev/null +++ b/Tests/MQTTnet.Core.Tests/ManagedMqttClientTests.cs @@ -0,0 +1,38 @@ +using MQTTnet.Extensions.ManagedClient; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Server; + +namespace MQTTnet.Core.Tests +{ + [TestClass] + public class ManagedMqttClientTests + { + [TestMethod] + public async Task Drop_New_Messages_On_Full_Queue() + { + var factory = new MqttFactory(); + var managedClient = factory.CreateManagedMqttClient(); + + var clientOptions = new ManagedMqttClientOptionsBuilder() + .WithMaxPendingMessages(5) + .WithPendingMessagesOverflowStrategy(MqttPendingMessagesOverflowStrategy.DropNewMessage); + + clientOptions.WithClientOptions(o => o.WithTcpServer("localhost")); + + await managedClient.StartAsync(clientOptions.Build()); + + await managedClient.PublishAsync(new MqttApplicationMessage { Topic = "1" }); + await managedClient.PublishAsync(new MqttApplicationMessage { Topic = "2" }); + await managedClient.PublishAsync(new MqttApplicationMessage { Topic = "3" }); + await managedClient.PublishAsync(new MqttApplicationMessage { Topic = "4" }); + await managedClient.PublishAsync(new MqttApplicationMessage { Topic = "5" }); + + await managedClient.PublishAsync(new MqttApplicationMessage { Topic = "6" }); + await managedClient.PublishAsync(new MqttApplicationMessage { Topic = "7" }); + await managedClient.PublishAsync(new MqttApplicationMessage { Topic = "8" }); + + Assert.AreEqual(5, managedClient.PendingApplicationMessagesCount); + } + } +} diff --git a/Tests/MQTTnet.Core.Tests/MqttServerTests.cs b/Tests/MQTTnet.Core.Tests/MqttServerTests.cs index 941e86b2..c1f0ed89 100644 --- a/Tests/MQTTnet.Core.Tests/MqttServerTests.cs +++ b/Tests/MQTTnet.Core.Tests/MqttServerTests.cs @@ -5,9 +5,11 @@ using MQTTnet.Protocol; using MQTTnet.Server; using System; using System.Collections.Generic; +using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; +using MQTTnet.Adapter; using MQTTnet.Implementations; namespace MQTTnet.Core.Tests @@ -97,11 +99,11 @@ namespace MQTTnet.Core.Tests c1.ApplicationMessageReceived += (_, __) => receivedMessagesCount++; var message = new MqttApplicationMessageBuilder().WithTopic("a").WithAtLeastOnceQoS().Build(); - + await c2.PublishAsync(message); await Task.Delay(1000); Assert.AreEqual(0, receivedMessagesCount); - + var subscribeEventCalled = false; s.ClientSubscribedTopic += (_, e) => { @@ -115,7 +117,7 @@ namespace MQTTnet.Core.Tests await c2.PublishAsync(message); await Task.Delay(500); Assert.AreEqual(1, receivedMessagesCount); - + var unsubscribeEventCalled = false; s.ClientUnsubscribedTopic += (_, e) => { @@ -233,7 +235,7 @@ namespace MQTTnet.Core.Tests await c1.PublishAsync(message); } } - + [TestMethod] public async Task MqttServer_ShutdownDisconnectsClientsGracefully() { @@ -302,6 +304,59 @@ namespace MQTTnet.Core.Tests Assert.AreEqual(clientConnectedCalled, clientDisconnectedCalled); } + [TestMethod] + public async Task MqttServer_LotsOfRetainedMessages() + { + const int ClientCount = 100; + + var server = new MqttFactory().CreateMqttServer(); + try + { + await server.StartAsync(new MqttServerOptionsBuilder().Build()); + + Parallel.For( + 0, + ClientCount, + new ParallelOptions { MaxDegreeOfParallelism = 10 }, + i => + { + using (var client = new MqttFactory().CreateMqttClient()) + { + client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("localhost").Build()) + .GetAwaiter().GetResult(); + + for (var j = 0; j < 10; j++) + { + // Clear retained message. + client.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("r" + i) + .WithPayload(new byte[0]).WithRetainFlag().Build()).GetAwaiter().GetResult(); + + // Set retained message. + client.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("r" + i) + .WithPayload("value" + j).WithRetainFlag().Build()).GetAwaiter().GetResult(); + } + + client.DisconnectAsync().GetAwaiter().GetResult(); + } + }); + + await Task.Delay(100); + + var retainedMessages = server.GetRetainedMessages(); + + Assert.AreEqual(ClientCount, retainedMessages.Count); + + for (var i = 0; i < ClientCount; i++) + { + Assert.IsTrue(retainedMessages.Any(m => m.Topic == "r" + i)); + } + } + finally + { + await server.StopAsync(); + } + } + [TestMethod] public async Task MqttServer_RetainedMessagesFlow() { @@ -552,6 +607,51 @@ namespace MQTTnet.Core.Tests Assert.IsTrue(bodyIsMatching); } + [TestMethod] + public async Task MqttServer_ConnectionDenied() + { + var server = new MqttFactory().CreateMqttServer(); + var client = new MqttFactory().CreateMqttClient(); + + try + { + var options = new MqttServerOptionsBuilder().WithConnectionValidator(context => + { + context.ReturnCode = MqttConnectReturnCode.ConnectionRefusedNotAuthorized; + }).Build(); + + await server.StartAsync(options); + + + var clientOptions = new MqttClientOptionsBuilder() + .WithTcpServer("localhost").Build(); + + try + { + await client.ConnectAsync(clientOptions); + Assert.Fail("An exception should be raised."); + } + catch (Exception exception) + { + if (exception is MqttConnectingFailedException) + { + + } + else + { + Assert.Fail("Wrong exception."); + } + } + } + finally + { + await client.DisconnectAsync(); + await server.StopAsync(); + + client.Dispose(); + } + } + [TestMethod] public async Task MqttServer_SameClientIdConnectDisconnectEventOrder() { @@ -600,6 +700,33 @@ namespace MQTTnet.Core.Tests Assert.IsFalse(connecteCalledBeforeConnectedClients, "ClientConnected was called before ClientDisconnect was called"); } + + [TestMethod] + public async Task MqttServer_StopAndRestart() + { + var server = new MqttFactory().CreateMqttServer(); + await server.StartAsync(new MqttServerOptions()); + + var client = new MqttFactory().CreateMqttClient(); + await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("localhost").Build()); + await server.StopAsync(); + + try + { + var client2 = new MqttFactory().CreateMqttClient(); + await client2.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("localhost").Build()); + + Assert.Fail("Connecting should fail."); + } + catch (Exception) + { + } + + await server.StartAsync(new MqttServerOptions()); + var client3 = new MqttFactory().CreateMqttClient(); + await client3.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("localhost").Build()); + } + private class TestStorage : IMqttServerStorage { public IList Messages = new List(); @@ -623,25 +750,24 @@ namespace MQTTnet.Core.Tests MqttQualityOfServiceLevel filterQualityOfServiceLevel, int expectedReceivedMessagesCount) { - var serverAdapter = new TestMqttServerAdapter(); - var s = new MqttFactory().CreateMqttServer(new[] { serverAdapter }, new MqttNetLogger()); + var s = new MqttFactory().CreateMqttServer(); var receivedMessagesCount = 0; try { await s.StartAsync(new MqttServerOptions()); - var c1 = await serverAdapter.ConnectTestClient("c1"); - var c2 = await serverAdapter.ConnectTestClient("c2"); - + var c1 = new MqttFactory().CreateMqttClient(); c1.ApplicationMessageReceived += (_, __) => receivedMessagesCount++; - + await c1.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("localhost").Build()); await c1.SubscribeAsync(new TopicFilterBuilder().WithTopic(topicFilter).WithQualityOfServiceLevel(filterQualityOfServiceLevel).Build()); + + var c2 = new MqttFactory().CreateMqttClient(); + await c2.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("localhost").Build()); await c2.PublishAsync(builder => builder.WithTopic(topic).WithPayload(new byte[0]).WithQualityOfServiceLevel(qualityOfServiceLevel)); await Task.Delay(500); await c1.UnsubscribeAsync(topicFilter); - await Task.Delay(500); } finally diff --git a/Tests/MQTTnet.Core.Tests/RoundtripTimeTests.cs b/Tests/MQTTnet.Core.Tests/RoundtripTimeTests.cs new file mode 100644 index 00000000..c405f5fe --- /dev/null +++ b/Tests/MQTTnet.Core.Tests/RoundtripTimeTests.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Client; +using MQTTnet.Server; + +namespace MQTTnet.Core.Tests +{ + [TestClass] + public class RoundtripTimeTests + { + [TestMethod] + public async Task Round_Trip_Time() + { + var factory = new MqttFactory(); + var server = factory.CreateMqttServer(); + var receiverClient = factory.CreateMqttClient(); + var senderClient = factory.CreateMqttClient(); + + await server.StartAsync(new MqttServerOptionsBuilder().Build()); + + await receiverClient.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("localhost").Build()); + await receiverClient.SubscribeAsync("#"); + + await senderClient.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("localhost").Build()); + + TaskCompletionSource response = null; + + receiverClient.ApplicationMessageReceived += (sender, args) => + { + response?.SetResult(args.ApplicationMessage.ConvertPayloadToString()); + }; + + var times = new List(); + var stopwatch = Stopwatch.StartNew(); + + for (var i = 0; i < 100; i++) + { + response = new TaskCompletionSource(); + await senderClient.PublishAsync("test", DateTime.UtcNow.Ticks.ToString()); + response.Task.GetAwaiter().GetResult(); + + stopwatch.Stop(); + times.Add(stopwatch.Elapsed); + stopwatch.Restart(); + } + + await server.StopAsync(); + } + } +} diff --git a/Tests/MQTTnet.Core.Tests/TestServerExtensions.cs b/Tests/MQTTnet.Core.Tests/TestServerExtensions.cs index cbb278ca..7636b1a2 100644 --- a/Tests/MQTTnet.Core.Tests/TestServerExtensions.cs +++ b/Tests/MQTTnet.Core.Tests/TestServerExtensions.cs @@ -11,19 +11,20 @@ namespace MQTTnet.Core.Tests /// publishes a message with a client and waits in the server until a message with the same topic is received /// /// - public static async Task PublishAndWaitForAsync(this IMqttClient client, IMqttServer server, MqttApplicationMessage message) + public static async Task PublishAndWaitForAsync(this IMqttClient client, IMqttServer server, MqttApplicationMessage message) { var tcs = new TaskCompletionSource(); - EventHandler handler = (sender, args) => + void Handler(object sender, MqttApplicationMessageReceivedEventArgs args) { if (args.ApplicationMessage.Topic == message.Topic) { tcs.SetResult(true); } - }; - server.ApplicationMessageReceived += handler; - + } + + server.ApplicationMessageReceived += Handler; + try { await client.PublishAsync(message).ConfigureAwait(false); @@ -31,7 +32,7 @@ namespace MQTTnet.Core.Tests } finally { - server.ApplicationMessageReceived -= handler; + server.ApplicationMessageReceived -= Handler; } } } diff --git a/Tests/MQTTnet.TestApp.AspNetCore2/MQTTnet.TestApp.AspNetCore2.csproj b/Tests/MQTTnet.TestApp.AspNetCore2/MQTTnet.TestApp.AspNetCore2.csproj index a216d728..a23f309b 100644 --- a/Tests/MQTTnet.TestApp.AspNetCore2/MQTTnet.TestApp.AspNetCore2.csproj +++ b/Tests/MQTTnet.TestApp.AspNetCore2/MQTTnet.TestApp.AspNetCore2.csproj @@ -10,8 +10,10 @@ - - + + + + diff --git a/Tests/MQTTnet.TestApp.NetCore/MQTTnet.TestApp.NetCore.csproj b/Tests/MQTTnet.TestApp.NetCore/MQTTnet.TestApp.NetCore.csproj index 2a738a08..af5f321c 100644 --- a/Tests/MQTTnet.TestApp.NetCore/MQTTnet.TestApp.NetCore.csproj +++ b/Tests/MQTTnet.TestApp.NetCore/MQTTnet.TestApp.NetCore.csproj @@ -3,11 +3,11 @@ Exe Full - netcoreapp2.0;net452;net461 + netcoreapp2.1;net452;net461 - - RELEASE;NETCOREAPP2_0 + + RELEASE;NETCOREAPP2_1 diff --git a/Tests/MQTTnet.TestApp.UniversalWindows/MQTTnet.TestApp.UniversalWindows.csproj b/Tests/MQTTnet.TestApp.UniversalWindows/MQTTnet.TestApp.UniversalWindows.csproj index a6f4566b..f8583aa1 100644 --- a/Tests/MQTTnet.TestApp.UniversalWindows/MQTTnet.TestApp.UniversalWindows.csproj +++ b/Tests/MQTTnet.TestApp.UniversalWindows/MQTTnet.TestApp.UniversalWindows.csproj @@ -142,10 +142,10 @@ - 6.1.5 + 6.1.9 - 3.0.0 + 4.0.0