diff --git a/.gitignore b/.gitignore index d44c4553..569e43ac 100644 --- a/.gitignore +++ b/.gitignore @@ -197,6 +197,7 @@ ClientBin/ *.dbmdl *.dbproj.schemaview *.jfm +# *.pfx *.publishsettings orleans.codegen.cs @@ -290,3 +291,5 @@ __pycache__/ Build/nuget.exe *.js *.map + +/Tests/MQTTnet.TestApp.NetCore/RetainedMessages.json diff --git a/Build/MQTTnet.AspNetCore.nuspec b/Build/MQTTnet.AspNetCore.nuspec index eb5f637f..4245077b 100644 --- a/Build/MQTTnet.AspNetCore.nuspec +++ b/Build/MQTTnet.AspNetCore.nuspec @@ -2,7 +2,7 @@ MQTTnet.AspNetCore - 2.5.0 + 2.7.5 Christian Kratky Christian Kratky https://github.com/chkr1011/MQTTnet/blob/master/LICENSE @@ -10,14 +10,13 @@ https://raw.githubusercontent.com/chkr1011/MQTTnet/master/Images/Logo_128x128.png false This is a support library to integrate MQTTnet into AspNetCore. - initial version + * Updated to MQTTnet 2.7.5. - Copyright Christian Kratky 2016-2017 - MQTT Message Queue Telemetry Transport MQTTClient MQTTServer Server MQTTBroker Broker NETStandard IoT InternetOfThings Messaging Hardware Arduino Sensor Actuator M2M ESP Smart Home Cities Automation + 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 - - + @@ -25,6 +24,5 @@ - \ No newline at end of file diff --git a/Build/MQTTnet.Extensions.Rpc.nuspec b/Build/MQTTnet.Extensions.Rpc.nuspec new file mode 100644 index 00000000..cb31c21b --- /dev/null +++ b/Build/MQTTnet.Extensions.Rpc.nuspec @@ -0,0 +1,47 @@ + + + + MQTTnet.Extensions.Rpc + 2.7.5 + Christian Kratky + Christian Kratky + https://github.com/chkr1011/MQTTnet/blob/master/LICENSE + https://github.com/chkr1011/MQTTnet + https://raw.githubusercontent.com/chkr1011/MQTTnet/master/Images/Logo_128x128.png + false + This is a extension library which allows executing synchronous device calls including a response using MQTTnet. + * Updated to MQTTnet 2.7.5. + + Copyright Christian Kratky 2016-2017 + 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Build/MQTTnet.nuspec b/Build/MQTTnet.nuspec index fe5d708b..4146cd38 100644 --- a/Build/MQTTnet.nuspec +++ b/Build/MQTTnet.nuspec @@ -2,7 +2,7 @@ MQTTnet - 2.5.0 + 2.7.5 Christian Kratky Christian Kratky https://github.com/chkr1011/MQTTnet/blob/master/LICENSE @@ -10,49 +10,59 @@ 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] Merged the .NET Framwork and netstandard projects (Thanks to @JanEggers) -* [Core] Migrated the trace to a non-static approach (Breaking Change!) -* [Core] Added a builder for application messages using a fluent API -* [Core] Introduced CI (Thanks to @JanEggers) -* [Core] Added interfaces for publishing and receiving which applies to every client and server (Thanks to @ChristianRiedl) -* [Client] Added a first version of a managed client which will manage the connection, subscription etc. automatically (Thanks to @JTrotta) -* [Client] The session state response from the server is now returned in the _ConnectAsync_ method and also part of the _Connected_ event args -* [Client] Added a _TopicFilterBuilder_ using a fluent API (Namespace Changes!) -* [Client] Added several new options for the WebSocket channel (Thanks to @ChristianRiedl) -* [Client] Refactored the options and added a builder using a fluent API (Breaking Change!) -* [Client] Added more options for WebSocket connections like RequestHeaders, SubProtocol etc. -* [Server] Added support for WebSockets via ASP.NET Core 2.0 (Thanks to @ChristianRiedl) -* [Server] Added support for a custom application message interceptor -* [Server] Fixed an issue with dropped connections on UWP (Thanks to @haeberle) + * [Client] Fixed a deadlock while the client disconnects. +* [Client] Fixed broken support for protocol version 3.1.0. +* [Server] The _MqttTcpServerAdapter_ is now added to the ASP.NET services. +* [Server] _MqttServerAdapter_ is renamed to _MqttTcpServerAdapter_ (BREAKING CHANGE!). +* [Server] The server no longer sends the will message of a client if the disconnect was clean (via _Disconnect_ packet). +* [Server] The application message interceptor now allows closing the connection. +* [Server] Added a new flag for the _ClientDisconnected_ event which contains a value indicating whether the disconnect was clean (via _Disconnect_ packet). - Copyright Christian Kratky 2016-2017 - MQTT Message Queue Telemetry Transport MQTTClient MQTTServer Server MQTTBroker Broker NETStandard IoT InternetOfThings Messaging Hardware Arduino Sensor Actuator M2M ESP Smart Home Cities Automation + 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 - + + + + + + + + + + - + + + + + + + + - + + + - - - + + \ No newline at end of file diff --git a/Build/build.ps1 b/Build/build.ps1 index 6284473a..021ca1d6 100644 --- a/Build/build.ps1 +++ b/Build/build.ps1 @@ -1,6 +1,7 @@ -param([string]$version) +param([string]$assemblyVersion, [string]$nugetVersion) -if ([string]::IsNullOrEmpty($version)) {$version = "0.0.1"} +if ([string]::IsNullOrEmpty($assemblyVersion)) {$assemblyVersion = "0.0.1"} +if ([string]::IsNullOrEmpty($nugetVersion)) {$nugetVersion = "0.0.1"} $vswhere = ${Env:\ProgramFiles(x86)} + '\Microsoft Visual Studio\Installer\vswhere' @@ -8,14 +9,27 @@ $path = &$vswhere -latest -products * -requires Microsoft.Component.MSBuild -pro if ($path) { $msbuild = join-path $path 'MSBuild\15.0\Bin\MSBuild.exe' - &$msbuild ..\Frameworks\MQTTnet.Netstandard\MQTTnet.Netstandard.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="net451" /p:FileVersion=$version /p:AssemblyVersion=$version /verbosity:m - &$msbuild ..\Frameworks\MQTTnet.Netstandard\MQTTnet.Netstandard.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="netstandard1.3" /p:FileVersion=$version /p:AssemblyVersion=$version /verbosity:m - &$msbuild ..\Frameworks\MQTTnet.Netstandard\MQTTnet.Netstandard.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="uap10.0" /p:FileVersion=$version /p:AssemblyVersion=$version /verbosity:m - &dotnet build ..\Frameworks\MQTTnet.AspNetCore\MQTTnet.AspNetCore.csproj -c="Release" /p:FileVersion=$version /p:AssemblyVersion=$version + # Build the core library + &$msbuild ..\Frameworks\MQTTnet.Netstandard\MQTTnet.Netstandard.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="net452" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" + &$msbuild ..\Frameworks\MQTTnet.Netstandard\MQTTnet.Netstandard.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="net461" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" + &$msbuild ..\Frameworks\MQTTnet.Netstandard\MQTTnet.Netstandard.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="netstandard1.3" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" + &$msbuild ..\Frameworks\MQTTnet.Netstandard\MQTTnet.Netstandard.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="netstandard2.0" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" + &$msbuild ..\Frameworks\MQTTnet.Netstandard\MQTTnet.Netstandard.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="uap10.0" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" + + # Build the ASP.NET Core 2.0 extension + &$msbuild ..\Frameworks\MQTTnet.AspNetCore\MQTTnet.AspNetCore.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="netstandard2.0" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" + + # Build the RPC extension + &$msbuild ..\Extensions\MQTTnet.Extensions.Rpc\MQTTnet.Extensions.Rpc.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="net452" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m + &$msbuild ..\Extensions\MQTTnet.Extensions.Rpc\MQTTnet.Extensions.Rpc.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="net461" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m + &$msbuild ..\Extensions\MQTTnet.Extensions.Rpc\MQTTnet.Extensions.Rpc.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="netstandard1.3" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m + &$msbuild ..\Extensions\MQTTnet.Extensions.Rpc\MQTTnet.Extensions.Rpc.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="netstandard2.0" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m + &$msbuild ..\Extensions\MQTTnet.Extensions.Rpc\MQTTnet.Extensions.Rpc.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="uap10.0" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m + + Remove-Item .\NuGet -Force -Recurse -ErrorAction SilentlyContinue - Remove-Item .\NuGet -Force -Recurse New-Item -ItemType Directory -Force -Path .\NuGet - .\NuGet.exe pack MQTTnet.nuspec -Verbosity detailed -Symbols -OutputDir "NuGet" -Version $version - .\NuGet.exe pack MQTTnet.AspNetCore.nuspec -Verbosity detailed -Symbols -OutputDir "NuGet" -Version $version -} - + .\NuGet.exe pack MQTTnet.nuspec -Verbosity detailed -Symbols -OutputDir "NuGet" -Version $nugetVersion + .\NuGet.exe pack MQTTnet.AspNetCore.nuspec -Verbosity detailed -Symbols -OutputDir "NuGet" -Version $nugetVersion + .\NuGet.exe pack MQTTnet.Extensions.Rpc.nuspec -Verbosity detailed -Symbols -OutputDir "NuGet" -Version $nugetVersion +} \ No newline at end of file diff --git a/Build/codeSigningKey.pfx b/Build/codeSigningKey.pfx new file mode 100644 index 00000000..9374a5db Binary files /dev/null and b/Build/codeSigningKey.pfx differ diff --git a/Documents/Import_CodeSigningKey.md b/Documents/Import_CodeSigningKey.md new file mode 100644 index 00000000..7e966e4a --- /dev/null +++ b/Documents/Import_CodeSigningKey.md @@ -0,0 +1,9 @@ +# Import codeSigningKey.pfx +In order to import the key for code signing on a new developer machine use the following command within the VS developer command line: + +> sn –i codeSigningKey.pfx VS_KEY_EFCA4C5B6DFD4B4F + +The container name may be different. + +# Check if the assembly has a strong name +> sn -vf MQTTnet.dll \ No newline at end of file diff --git a/Extensions/MQTTnet.Extensions.Rpc/MQTTnet.Extensions.Rpc.csproj b/Extensions/MQTTnet.Extensions.Rpc/MQTTnet.Extensions.Rpc.csproj new file mode 100644 index 00000000..3f4128e0 --- /dev/null +++ b/Extensions/MQTTnet.Extensions.Rpc/MQTTnet.Extensions.Rpc.csproj @@ -0,0 +1,31 @@ + + + + netstandard1.3;netstandard2.0;net452;net461;uap10.0 + 0.0.0.0 + 0.0.0.0 + + + + + 0.0.0.0 + + + + false + UAP,Version=v10.0 + UAP + 10.0.16299.0 + 10.0.10240.0 + .NETCore + v5.0 + $(DefineConstants);WINDOWS_UWP + en + $(MSBuildExtensionsPath)\Microsoft\WindowsXaml\v$(VisualStudioVersion)\Microsoft.Windows.UI.Xaml.CSharp.targets + + + + + + + diff --git a/Extensions/MQTTnet.Extensions.Rpc/MqttRpcClient.cs b/Extensions/MQTTnet.Extensions.Rpc/MqttRpcClient.cs new file mode 100644 index 00000000..938fcb4c --- /dev/null +++ b/Extensions/MQTTnet.Extensions.Rpc/MqttRpcClient.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Concurrent; +using System.Threading.Tasks; +using MQTTnet.Client; +using MQTTnet.Internal; +using MQTTnet.Protocol; + +namespace MQTTnet.Extensions.Rpc +{ + public sealed class MqttRpcClient : IDisposable + { + private const string ResponseTopic = "$MQTTnet.RPC/+/+/response"; + private readonly ConcurrentDictionary> _waitingCalls = new ConcurrentDictionary>(); + private readonly IMqttClient _mqttClient; + private bool _isEnabled; + + public MqttRpcClient(IMqttClient mqttClient) + { + _mqttClient = mqttClient ?? throw new ArgumentNullException(nameof(mqttClient)); + + _mqttClient.ApplicationMessageReceived += OnApplicationMessageReceived; + } + + public async Task EnableAsync() + { + await _mqttClient.SubscribeAsync(new TopicFilterBuilder().WithTopic(ResponseTopic).WithAtLeastOnceQoS().Build()); + _isEnabled = true; + } + + public async Task DisableAsync() + { + await _mqttClient.UnsubscribeAsync(ResponseTopic); + _isEnabled = false; + } + + public async Task ExecuteAsync(TimeSpan timeout, string methodName, byte[] payload, MqttQualityOfServiceLevel qualityOfServiceLevel) + { + if (methodName == null) throw new ArgumentNullException(nameof(methodName)); + + if (methodName.Contains("/") || methodName.Contains("+") || methodName.Contains("#")) + { + throw new ArgumentException("The method name cannot contain /, + or #."); + } + + if (!_isEnabled) + { + throw new InvalidOperationException("The RPC client is not enabled."); + } + + var requestTopic = $"$MQTTnet.RPC/{Guid.NewGuid():N}/{methodName}"; + var responseTopic = requestTopic + "/response"; + + var requestMessage = new MqttApplicationMessageBuilder() + .WithTopic(requestTopic) + .WithPayload(payload) + .WithQualityOfServiceLevel(qualityOfServiceLevel) + .Build(); + + try + { + var tcs = new TaskCompletionSource(); + if (!_waitingCalls.TryAdd(responseTopic, tcs)) + { + throw new InvalidOperationException(); + } + + await _mqttClient.PublishAsync(requestMessage); + return await tcs.Task.TimeoutAfter(timeout); + } + finally + { + _waitingCalls.TryRemove(responseTopic, out _); + } + } + + private void OnApplicationMessageReceived(object sender, MqttApplicationMessageReceivedEventArgs eventArgs) + { + if (!_waitingCalls.TryRemove(eventArgs.ApplicationMessage.Topic, out TaskCompletionSource tcs)) + { + return; + } + + if (tcs.Task.IsCompleted || tcs.Task.IsCanceled) + { + return; + } + + tcs.TrySetResult(eventArgs.ApplicationMessage.Payload); + } + + public void Dispose() + { + foreach (var tcs in _waitingCalls) + { + tcs.Value.SetCanceled(); + } + + _waitingCalls.Clear(); + } + } +} diff --git a/Extensions/MQTTnet.Extensions.Rpc/SampleCCode.c b/Extensions/MQTTnet.Extensions.Rpc/SampleCCode.c new file mode 100644 index 00000000..5f282702 --- /dev/null +++ b/Extensions/MQTTnet.Extensions.Rpc/SampleCCode.c @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frameworks/MQTTnet.AspnetCore/ApplicationBuilderExtensions.cs b/Frameworks/MQTTnet.AspnetCore/ApplicationBuilderExtensions.cs index 06ca74d4..5a25712d 100644 --- a/Frameworks/MQTTnet.AspnetCore/ApplicationBuilderExtensions.cs +++ b/Frameworks/MQTTnet.AspnetCore/ApplicationBuilderExtensions.cs @@ -1,7 +1,8 @@ using System; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; -using MQTTnet.Core.Server; +using System.Linq; +using MQTTnet.Server; namespace MQTTnet.AspNetCore { @@ -14,8 +15,18 @@ namespace MQTTnet.AspNetCore { if (context.Request.Path == path && context.WebSockets.IsWebSocketRequest) { + string subprotocol = null; + + if (context.Request.Headers.TryGetValue("Sec-WebSocket-Protocol", out var requestedSubProtocolValues) + && requestedSubProtocolValues.Count > 0 + && requestedSubProtocolValues.Any(v => v.ToLower() == "mqtt") + ) + { + subprotocol = "mqtt"; + } + var adapter = app.ApplicationServices.GetRequiredService(); - using (var webSocket = await context.WebSockets.AcceptWebSocketAsync("mqtt")) + using (var webSocket = await context.WebSockets.AcceptWebSocketAsync(subprotocol)) { await adapter.AcceptWebSocketAsync(webSocket); } diff --git a/Frameworks/MQTTnet.AspnetCore/MQTTnet.AspnetCore.csproj b/Frameworks/MQTTnet.AspnetCore/MQTTnet.AspnetCore.csproj index 5142cc15..a19f2b3b 100644 --- a/Frameworks/MQTTnet.AspnetCore/MQTTnet.AspnetCore.csproj +++ b/Frameworks/MQTTnet.AspnetCore/MQTTnet.AspnetCore.csproj @@ -2,12 +2,23 @@ netstandard2.0 + 0.0.0.0 + 0.0.0.0 + 0.0.0.0 + + + + + + + + RELEASE;NETSTANDARD2_0 - - - + + + diff --git a/Frameworks/MQTTnet.AspnetCore/MqttHostedServer.cs b/Frameworks/MQTTnet.AspnetCore/MqttHostedServer.cs index db725735..8e6761ad 100644 --- a/Frameworks/MQTTnet.AspnetCore/MqttHostedServer.cs +++ b/Frameworks/MQTTnet.AspnetCore/MqttHostedServer.cs @@ -1,24 +1,26 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using MQTTnet.Core.Adapter; -using MQTTnet.Core.Server; +using MQTTnet.Adapter; +using MQTTnet.Diagnostics; +using MQTTnet.Server; namespace MQTTnet.AspNetCore { public class MqttHostedServer : MqttServer, IHostedService { - public MqttHostedServer(IOptions options, IEnumerable adapters, ILogger logger, MqttClientSessionsManager clientSessionsManager) - : base(options, adapters, logger, clientSessionsManager) + private readonly IMqttServerOptions _options; + + public MqttHostedServer(IMqttServerOptions options, IEnumerable adapters, IMqttNetLogger logger) : base(adapters, logger) { + _options = options ?? throw new ArgumentNullException(nameof(options)); } public Task StartAsync(CancellationToken cancellationToken) { - return StartAsync(); + return StartAsync(_options); } public Task StopAsync(CancellationToken cancellationToken) diff --git a/Frameworks/MQTTnet.AspnetCore/MqttWebSocketServerAdapter.cs b/Frameworks/MQTTnet.AspnetCore/MqttWebSocketServerAdapter.cs index 5cd5d555..c84b9be3 100644 --- a/Frameworks/MQTTnet.AspnetCore/MqttWebSocketServerAdapter.cs +++ b/Frameworks/MQTTnet.AspnetCore/MqttWebSocketServerAdapter.cs @@ -1,27 +1,18 @@ using System; -using System.IO; using System.Net.WebSockets; -using System.Threading; using System.Threading.Tasks; -using MQTTnet.Core.Adapter; -using MQTTnet.Core.Channel; -using MQTTnet.Core.Server; -using MQTTnet.Implementations; +using MQTTnet.Adapter; +using MQTTnet.Diagnostics; +using MQTTnet.Serializer; +using MQTTnet.Server; namespace MQTTnet.AspNetCore { - public class MqttWebSocketServerAdapter : IMqttServerAdapter, IDisposable + public sealed class MqttWebSocketServerAdapter : IMqttServerAdapter, IDisposable { - private readonly IMqttCommunicationAdapterFactory _mqttCommunicationAdapterFactory; - - public MqttWebSocketServerAdapter(IMqttCommunicationAdapterFactory mqttCommunicationAdapterFactory) - { - _mqttCommunicationAdapterFactory = mqttCommunicationAdapterFactory ?? throw new ArgumentNullException(nameof(mqttCommunicationAdapterFactory)); - } - public event EventHandler ClientAccepted; - public Task StartAsync(MqttServerOptions options) + public Task StartAsync(IMqttServerOptions options) { return Task.CompletedTask; } @@ -36,7 +27,7 @@ namespace MQTTnet.AspNetCore if (webSocket == null) throw new ArgumentNullException(nameof(webSocket)); var channel = new MqttWebSocketServerChannel(webSocket); - var clientAdapter = _mqttCommunicationAdapterFactory.CreateServerMqttCommunicationAdapter(channel); + var clientAdapter = new MqttChannelAdapter(channel, new MqttPacketSerializer(), new MqttNetLogger()); var eventArgs = new MqttServerAdapterClientAcceptedEventArgs(clientAdapter); ClientAccepted?.Invoke(this, eventArgs); @@ -47,47 +38,5 @@ namespace MQTTnet.AspNetCore { StopAsync(); } - - private class MqttWebSocketServerChannel : IMqttCommunicationChannel, IDisposable - { - private readonly WebSocket _webSocket; - - public MqttWebSocketServerChannel(WebSocket webSocket) - { - _webSocket = webSocket ?? throw new ArgumentNullException(nameof(webSocket)); - - RawReceiveStream = new WebSocketStream(_webSocket); - } - - public Stream SendStream => RawReceiveStream; - public Stream ReceiveStream => RawReceiveStream; - public Stream RawReceiveStream { get; } - - public Task ConnectAsync() - { - return Task.CompletedTask; - } - - public Task DisconnectAsync() - { - RawReceiveStream?.Dispose(); - - if (_webSocket == null) - { - return Task.CompletedTask; - } - - return _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); - } - - public void Dispose() - { - RawReceiveStream?.Dispose(); - SendStream?.Dispose(); - ReceiveStream?.Dispose(); - - _webSocket?.Dispose(); - } - } } } \ No newline at end of file diff --git a/Frameworks/MQTTnet.AspnetCore/MqttWebSocketServerChannel.cs b/Frameworks/MQTTnet.AspnetCore/MqttWebSocketServerChannel.cs new file mode 100644 index 00000000..7325a5f8 --- /dev/null +++ b/Frameworks/MQTTnet.AspnetCore/MqttWebSocketServerChannel.cs @@ -0,0 +1,60 @@ +using System; +using System.IO; +using System.Net.WebSockets; +using System.Threading; +using System.Threading.Tasks; +using MQTTnet.Channel; +using MQTTnet.Implementations; + +namespace MQTTnet.AspNetCore +{ + public class MqttWebSocketServerChannel : IMqttChannel, IDisposable + { + private WebSocket _webSocket; + + public MqttWebSocketServerChannel(WebSocket webSocket) + { + _webSocket = webSocket ?? throw new ArgumentNullException(nameof(webSocket)); + + SendStream = new WebSocketStream(_webSocket); + ReceiveStream = SendStream; + } + + public Stream SendStream { get; private set; } + public Stream ReceiveStream { get; private set; } + + public Task ConnectAsync() + { + return Task.CompletedTask; + } + + public async Task DisconnectAsync() + { + if (_webSocket == null) + { + return; + } + + try + { + await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); + } + finally + { + Dispose(); + } + } + + public void Dispose() + { + SendStream?.Dispose(); + ReceiveStream?.Dispose(); + + _webSocket?.Dispose(); + + SendStream = null; + ReceiveStream = null; + _webSocket = null; + } + } +} \ No newline at end of file diff --git a/Frameworks/MQTTnet.AspnetCore/ServiceCollectionExtensions.cs b/Frameworks/MQTTnet.AspnetCore/ServiceCollectionExtensions.cs index f915ab43..61354cd9 100644 --- a/Frameworks/MQTTnet.AspnetCore/ServiceCollectionExtensions.cs +++ b/Frameworks/MQTTnet.AspnetCore/ServiceCollectionExtensions.cs @@ -1,22 +1,29 @@ -using Microsoft.Extensions.DependencyInjection; +using System; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using MQTTnet.Core.Adapter; -using MQTTnet.Core.Server; +using MQTTnet.Adapter; +using MQTTnet.Diagnostics; +using MQTTnet.Server; +using MQTTnet.Implementations; namespace MQTTnet.AspNetCore { public static class ServiceCollectionExtensions { - public static IServiceCollection AddHostedMqttServer(this IServiceCollection services) + public static IServiceCollection AddHostedMqttServer(this IServiceCollection services, IMqttServerOptions options) { - services.AddMqttServerServices(); + if (options == null) throw new ArgumentNullException(nameof(options)); + services.AddSingleton(options); + services.AddSingleton(new MqttNetLogger()); + services.AddSingleton(); services.AddSingleton(s => s.GetService()); services.AddSingleton(s => s.GetService()); - services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(s => s.GetService()); + services.AddSingleton(); + services.AddSingleton(s => s.GetService()); + services.AddSingleton(s => s.GetService()); return services; } diff --git a/MQTTnet.Core/Adapter/IMqttCommunicationAdapter.cs b/Frameworks/MQTTnet.NetStandard/Adapter/IMqttChannelAdapter.cs similarity index 78% rename from MQTTnet.Core/Adapter/IMqttCommunicationAdapter.cs rename to Frameworks/MQTTnet.NetStandard/Adapter/IMqttChannelAdapter.cs index 78586f66..7a5c02a2 100644 --- a/MQTTnet.Core/Adapter/IMqttCommunicationAdapter.cs +++ b/Frameworks/MQTTnet.NetStandard/Adapter/IMqttChannelAdapter.cs @@ -2,12 +2,12 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using MQTTnet.Core.Packets; -using MQTTnet.Core.Serializer; +using MQTTnet.Packets; +using MQTTnet.Serializer; -namespace MQTTnet.Core.Adapter +namespace MQTTnet.Adapter { - public interface IMqttCommunicationAdapter + public interface IMqttChannelAdapter : IDisposable { IMqttPacketSerializer PacketSerializer { get; } diff --git a/MQTTnet.Core/Adapter/IMqttServerAdapter.cs b/Frameworks/MQTTnet.NetStandard/Adapter/IMqttServerAdapter.cs similarity index 66% rename from MQTTnet.Core/Adapter/IMqttServerAdapter.cs rename to Frameworks/MQTTnet.NetStandard/Adapter/IMqttServerAdapter.cs index 9209185e..bd583f54 100644 --- a/MQTTnet.Core/Adapter/IMqttServerAdapter.cs +++ b/Frameworks/MQTTnet.NetStandard/Adapter/IMqttServerAdapter.cs @@ -1,14 +1,14 @@ using System; using System.Threading.Tasks; -using MQTTnet.Core.Server; +using MQTTnet.Server; -namespace MQTTnet.Core.Adapter +namespace MQTTnet.Adapter { public interface IMqttServerAdapter { event EventHandler ClientAccepted; - Task StartAsync(MqttServerOptions options); + Task StartAsync(IMqttServerOptions options); Task StopAsync(); } } diff --git a/Frameworks/MQTTnet.NetStandard/Adapter/MqttChannelAdapter.cs b/Frameworks/MQTTnet.NetStandard/Adapter/MqttChannelAdapter.cs new file mode 100644 index 00000000..c2f9ba43 --- /dev/null +++ b/Frameworks/MQTTnet.NetStandard/Adapter/MqttChannelAdapter.cs @@ -0,0 +1,255 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Sockets; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using MQTTnet.Channel; +using MQTTnet.Diagnostics; +using MQTTnet.Exceptions; +using MQTTnet.Internal; +using MQTTnet.Packets; +using MQTTnet.Serializer; + +namespace MQTTnet.Adapter +{ + public sealed class MqttChannelAdapter : IMqttChannelAdapter + { + private const uint ErrorOperationAborted = 0x800703E3; + private const int ReadBufferSize = 4096; // TODO: Move buffer size to config + + private bool _isDisposed; + private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); + private readonly IMqttNetLogger _logger; + private readonly IMqttChannel _channel; + + public MqttChannelAdapter(IMqttChannel channel, IMqttPacketSerializer serializer, IMqttNetLogger logger) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _channel = channel ?? throw new ArgumentNullException(nameof(channel)); + PacketSerializer = serializer ?? throw new ArgumentNullException(nameof(serializer)); + } + + public IMqttPacketSerializer PacketSerializer { get; } + + public Task ConnectAsync(TimeSpan timeout) + { + ThrowIfDisposed(); + _logger.Verbose("Connecting [Timeout={0}]", timeout); + + return ExecuteAndWrapExceptionAsync(() => _channel.ConnectAsync().TimeoutAfter(timeout)); + } + + public Task DisconnectAsync(TimeSpan timeout) + { + ThrowIfDisposed(); + _logger.Verbose("Disconnecting [Timeout={0}]", timeout); + + return ExecuteAndWrapExceptionAsync(() => _channel.DisconnectAsync().TimeoutAfter(timeout)); + } + + public Task SendPacketsAsync(TimeSpan timeout, CancellationToken cancellationToken, IEnumerable packets) + { + ThrowIfDisposed(); + + return ExecuteAndWrapExceptionAsync(async () => + { + await _semaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + foreach (var packet in packets) + { + if (cancellationToken.IsCancellationRequested) + { + return; + } + + if (packet == null) + { + continue; + } + + _logger.Verbose("TX >>> {0} [Timeout={1}]", packet, timeout); + + var chunks = PacketSerializer.Serialize(packet); + foreach (var chunk in chunks) + { + if (cancellationToken.IsCancellationRequested) + { + return; + } + + await _channel.SendStream.WriteAsync(chunk.Array, chunk.Offset, chunk.Count, cancellationToken).ConfigureAwait(false); + } + } + + if (cancellationToken.IsCancellationRequested) + { + return; + } + + if (timeout > TimeSpan.Zero) + { + await _channel.SendStream.FlushAsync(cancellationToken).TimeoutAfter(timeout).ConfigureAwait(false); + } + else + { + await _channel.SendStream.FlushAsync(cancellationToken).ConfigureAwait(false); + } + } + finally + { + _semaphore.Release(); + } + }); + } + + public async Task ReceivePacketAsync(TimeSpan timeout, CancellationToken cancellationToken) + { + ThrowIfDisposed(); + + MqttBasePacket packet = null; + await ExecuteAndWrapExceptionAsync(async () => + { + ReceivedMqttPacket receivedMqttPacket = null; + try + { + if (timeout > TimeSpan.Zero) + { + receivedMqttPacket = await ReceiveAsync(_channel.ReceiveStream, cancellationToken).TimeoutAfter(timeout).ConfigureAwait(false); + } + else + { + receivedMqttPacket = await ReceiveAsync(_channel.ReceiveStream, cancellationToken).ConfigureAwait(false); + } + + if (receivedMqttPacket == null || cancellationToken.IsCancellationRequested) + { + throw new TaskCanceledException(); + } + + packet = PacketSerializer.Deserialize(receivedMqttPacket.Header, receivedMqttPacket.Body); + if (packet == null) + { + throw new MqttProtocolViolationException("Received malformed packet."); + } + + _logger.Verbose("RX <<< {0}", packet); + } + finally + { + receivedMqttPacket?.Dispose(); + } + }).ConfigureAwait(false); + + return packet; + } + + private static async Task ReceiveAsync(Stream stream, CancellationToken cancellationToken) + { + var header = await MqttPacketReader.ReadHeaderAsync(stream, cancellationToken).ConfigureAwait(false); + if (header == null) + { + return null; + } + + if (header.BodyLength == 0) + { + return new ReceivedMqttPacket(header, new MemoryStream(new byte[0], false)); + } + + var body = header.BodyLength <= ReadBufferSize ? new MemoryStream(header.BodyLength) : new MemoryStream(); + + var buffer = new byte[ReadBufferSize]; + while (body.Length < header.BodyLength) + { + var bytesLeft = header.BodyLength - (int)body.Length; + if (bytesLeft > buffer.Length) + { + bytesLeft = buffer.Length; + } + + var readBytesCount = await stream.ReadAsync(buffer, 0, bytesLeft, cancellationToken).ConfigureAwait(false); + + // Check if the client closed the connection before sending the full body. + if (readBytesCount == 0) + { + throw new MqttCommunicationException("Connection closed while reading remaining packet body."); + } + + // Here is no need to await because internally only an array is used and no real I/O operation is made. + // Using async here will only generate overhead. + body.Write(buffer, 0, readBytesCount); + } + + body.Seek(0L, SeekOrigin.Begin); + + return new ReceivedMqttPacket(header, body); + } + + private static async Task ExecuteAndWrapExceptionAsync(Func action) + { + try + { + await action().ConfigureAwait(false); + } + catch (TaskCanceledException) + { + throw; + } + catch (OperationCanceledException) + { + throw; + } + catch (MqttCommunicationTimedOutException) + { + throw; + } + catch (MqttCommunicationException) + { + throw; + } + catch (COMException comException) + { + if ((uint)comException.HResult == ErrorOperationAborted) + { + throw new OperationCanceledException(); + } + + throw new MqttCommunicationException(comException); + } + catch (IOException exception) + { + if (exception.InnerException is SocketException socketException) + { + if (socketException.SocketErrorCode == SocketError.ConnectionAborted) + { + throw new OperationCanceledException(); + } + } + + throw new MqttCommunicationException(exception); + } + catch (Exception exception) + { + throw new MqttCommunicationException(exception); + } + } + + public void Dispose() + { + _isDisposed = true; + _semaphore?.Dispose(); + _channel?.Dispose(); + } + + private void ThrowIfDisposed() + { + if (_isDisposed) + { + throw new ObjectDisposedException(nameof(MqttChannelAdapter)); + } + } + } +} diff --git a/MQTTnet.Core/Adapter/MqttConnectingFailedException.cs b/Frameworks/MQTTnet.NetStandard/Adapter/MqttConnectingFailedException.cs similarity index 79% rename from MQTTnet.Core/Adapter/MqttConnectingFailedException.cs rename to Frameworks/MQTTnet.NetStandard/Adapter/MqttConnectingFailedException.cs index 045d0d13..68445cfd 100644 --- a/MQTTnet.Core/Adapter/MqttConnectingFailedException.cs +++ b/Frameworks/MQTTnet.NetStandard/Adapter/MqttConnectingFailedException.cs @@ -1,7 +1,7 @@ -using MQTTnet.Core.Exceptions; -using MQTTnet.Core.Protocol; +using MQTTnet.Exceptions; +using MQTTnet.Protocol; -namespace MQTTnet.Core.Adapter +namespace MQTTnet.Adapter { public class MqttConnectingFailedException : MqttCommunicationException { diff --git a/MQTTnet.Core/Adapter/MqttServerAdapterClientAcceptedEventArgs.cs b/Frameworks/MQTTnet.NetStandard/Adapter/MqttServerAdapterClientAcceptedEventArgs.cs similarity index 61% rename from MQTTnet.Core/Adapter/MqttServerAdapterClientAcceptedEventArgs.cs rename to Frameworks/MQTTnet.NetStandard/Adapter/MqttServerAdapterClientAcceptedEventArgs.cs index 6caca0e9..42852493 100644 --- a/MQTTnet.Core/Adapter/MqttServerAdapterClientAcceptedEventArgs.cs +++ b/Frameworks/MQTTnet.NetStandard/Adapter/MqttServerAdapterClientAcceptedEventArgs.cs @@ -1,16 +1,16 @@ using System; using System.Threading.Tasks; -namespace MQTTnet.Core.Adapter +namespace MQTTnet.Adapter { public class MqttServerAdapterClientAcceptedEventArgs : EventArgs { - public MqttServerAdapterClientAcceptedEventArgs(IMqttCommunicationAdapter client) + public MqttServerAdapterClientAcceptedEventArgs(IMqttChannelAdapter client) { Client = client ?? throw new ArgumentNullException(nameof(client)); } - public IMqttCommunicationAdapter Client { get; } + public IMqttChannelAdapter Client { get; } public Task SessionTask { get; set; } } diff --git a/MQTTnet.Core/Adapter/ReceivedMqttPacket.cs b/Frameworks/MQTTnet.NetStandard/Adapter/ReceivedMqttPacket.cs similarity index 67% rename from MQTTnet.Core/Adapter/ReceivedMqttPacket.cs rename to Frameworks/MQTTnet.NetStandard/Adapter/ReceivedMqttPacket.cs index f8adc2eb..a44fb54e 100644 --- a/MQTTnet.Core/Adapter/ReceivedMqttPacket.cs +++ b/Frameworks/MQTTnet.NetStandard/Adapter/ReceivedMqttPacket.cs @@ -1,10 +1,10 @@ using System; using System.IO; -using MQTTnet.Core.Packets; +using MQTTnet.Packets; -namespace MQTTnet.Core.Adapter +namespace MQTTnet.Adapter { - public class ReceivedMqttPacket + public sealed class ReceivedMqttPacket : IDisposable { public ReceivedMqttPacket(MqttPacketHeader header, MemoryStream body) { @@ -15,5 +15,10 @@ namespace MQTTnet.Core.Adapter public MqttPacketHeader Header { get; } public MemoryStream Body { get; } + + public void Dispose() + { + Body?.Dispose(); + } } } diff --git a/Frameworks/MQTTnet.NetStandard/ApplicationMessagePublisherExtensions.cs b/Frameworks/MQTTnet.NetStandard/ApplicationMessagePublisherExtensions.cs new file mode 100644 index 00000000..b1bf62f5 --- /dev/null +++ b/Frameworks/MQTTnet.NetStandard/ApplicationMessagePublisherExtensions.cs @@ -0,0 +1,41 @@ +using System; +using System.Threading.Tasks; +using MQTTnet.Protocol; + +namespace MQTTnet +{ + public static class ApplicationMessagePublisherExtensions + { + public static Task PublishAsync(this IApplicationMessagePublisher publisher, params MqttApplicationMessage[] applicationMessages) + { + if (publisher == null) throw new ArgumentNullException(nameof(publisher)); + if (applicationMessages == null) throw new ArgumentNullException(nameof(applicationMessages)); + + return publisher.PublishAsync(applicationMessages); + } + + public static Task PublishAsync(this IApplicationMessagePublisher publisher, string topic) + { + if (publisher == null) throw new ArgumentNullException(nameof(publisher)); + if (topic == null) throw new ArgumentNullException(nameof(topic)); + + return publisher.PublishAsync(new MqttApplicationMessageBuilder().WithTopic(topic).Build()); + } + + public static Task PublishAsync(this IApplicationMessagePublisher publisher, string topic, string payload) + { + if (publisher == null) throw new ArgumentNullException(nameof(publisher)); + if (topic == null) throw new ArgumentNullException(nameof(topic)); + + return publisher.PublishAsync(new MqttApplicationMessageBuilder().WithTopic(topic).WithPayload(payload).Build()); + } + + public static Task PublishAsync(this IApplicationMessagePublisher publisher, string topic, string payload, MqttQualityOfServiceLevel qualityOfServiceLevel) + { + if (publisher == null) throw new ArgumentNullException(nameof(publisher)); + if (topic == null) throw new ArgumentNullException(nameof(topic)); + + return publisher.PublishAsync(new MqttApplicationMessageBuilder().WithTopic(topic).WithPayload(payload).WithQualityOfServiceLevel(qualityOfServiceLevel).Build()); + } + } +} diff --git a/MQTTnet.Core/Channel/IMqttCommunicationChannel.cs b/Frameworks/MQTTnet.NetStandard/Channel/IMqttChannel.cs similarity index 52% rename from MQTTnet.Core/Channel/IMqttCommunicationChannel.cs rename to Frameworks/MQTTnet.NetStandard/Channel/IMqttChannel.cs index 8c746d1e..b332884d 100644 --- a/MQTTnet.Core/Channel/IMqttCommunicationChannel.cs +++ b/Frameworks/MQTTnet.NetStandard/Channel/IMqttChannel.cs @@ -1,13 +1,13 @@ -using System.Threading.Tasks; +using System; using System.IO; +using System.Threading.Tasks; -namespace MQTTnet.Core.Channel +namespace MQTTnet.Channel { - public interface IMqttCommunicationChannel + public interface IMqttChannel : IDisposable { Stream SendStream { get; } Stream ReceiveStream { get; } - Stream RawReceiveStream { get; } Task ConnectAsync(); Task DisconnectAsync(); diff --git a/MQTTnet.Core/Client/IMqttClient.cs b/Frameworks/MQTTnet.NetStandard/Client/IMqttClient.cs similarity index 78% rename from MQTTnet.Core/Client/IMqttClient.cs rename to Frameworks/MQTTnet.NetStandard/Client/IMqttClient.cs index 6a39e211..40d1a48a 100644 --- a/MQTTnet.Core/Client/IMqttClient.cs +++ b/Frameworks/MQTTnet.NetStandard/Client/IMqttClient.cs @@ -1,11 +1,10 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using MQTTnet.Core.Packets; -namespace MQTTnet.Core.Client +namespace MQTTnet.Client { - public interface IMqttClient : IApplicationMessageReceiver, IApplicationMessagePublisher + public interface IMqttClient : IApplicationMessageReceiver, IApplicationMessagePublisher, IDisposable { bool IsConnected { get; } @@ -16,6 +15,6 @@ namespace MQTTnet.Core.Client Task DisconnectAsync(); Task> SubscribeAsync(IEnumerable topicFilters); - Task UnsubscribeAsync(IEnumerable topicFilters); + Task UnsubscribeAsync(IEnumerable topics); } } \ No newline at end of file diff --git a/Frameworks/MQTTnet.NetStandard/Client/IMqttClientAdapterFactory.cs b/Frameworks/MQTTnet.NetStandard/Client/IMqttClientAdapterFactory.cs new file mode 100644 index 00000000..c2a7e74b --- /dev/null +++ b/Frameworks/MQTTnet.NetStandard/Client/IMqttClientAdapterFactory.cs @@ -0,0 +1,10 @@ +using MQTTnet.Adapter; +using MQTTnet.Diagnostics; + +namespace MQTTnet.Client +{ + public interface IMqttClientAdapterFactory + { + IMqttChannelAdapter CreateClientAdapter(IMqttClientOptions options, IMqttNetLogger logger); + } +} diff --git a/MQTTnet.Core/Client/IMqttClientChannelOptions.cs b/Frameworks/MQTTnet.NetStandard/Client/IMqttClientChannelOptions.cs similarity index 77% rename from MQTTnet.Core/Client/IMqttClientChannelOptions.cs rename to Frameworks/MQTTnet.NetStandard/Client/IMqttClientChannelOptions.cs index 004430f9..22249632 100644 --- a/MQTTnet.Core/Client/IMqttClientChannelOptions.cs +++ b/Frameworks/MQTTnet.NetStandard/Client/IMqttClientChannelOptions.cs @@ -1,4 +1,4 @@ -namespace MQTTnet.Core.Client +namespace MQTTnet.Client { public interface IMqttClientChannelOptions { diff --git a/MQTTnet.Core/Client/IMqttClientCredentials.cs b/Frameworks/MQTTnet.NetStandard/Client/IMqttClientCredentials.cs similarity index 78% rename from MQTTnet.Core/Client/IMqttClientCredentials.cs rename to Frameworks/MQTTnet.NetStandard/Client/IMqttClientCredentials.cs index c4dfc127..d214d64f 100644 --- a/MQTTnet.Core/Client/IMqttClientCredentials.cs +++ b/Frameworks/MQTTnet.NetStandard/Client/IMqttClientCredentials.cs @@ -1,4 +1,4 @@ -namespace MQTTnet.Core.Client +namespace MQTTnet.Client { public interface IMqttClientCredentials { diff --git a/Frameworks/MQTTnet.NetStandard/Client/IMqttClientFactory.cs b/Frameworks/MQTTnet.NetStandard/Client/IMqttClientFactory.cs new file mode 100644 index 00000000..444c16fc --- /dev/null +++ b/Frameworks/MQTTnet.NetStandard/Client/IMqttClientFactory.cs @@ -0,0 +1,16 @@ +using MQTTnet.Diagnostics; +using MQTTnet.ManagedClient; + +namespace MQTTnet.Client +{ + public interface IMqttClientFactory + { + IMqttClient CreateMqttClient(); + + IMqttClient CreateMqttClient(IMqttNetLogger logger); + + IManagedMqttClient CreateManagedMqttClient(); + + IManagedMqttClient CreateManagedMqttClient(IMqttNetLogger logger); + } +} \ No newline at end of file diff --git a/MQTTnet.Core/Client/IMqttClientOptions.cs b/Frameworks/MQTTnet.NetStandard/Client/IMqttClientOptions.cs similarity index 63% rename from MQTTnet.Core/Client/IMqttClientOptions.cs rename to Frameworks/MQTTnet.NetStandard/Client/IMqttClientOptions.cs index ae771922..647f688b 100644 --- a/MQTTnet.Core/Client/IMqttClientOptions.cs +++ b/Frameworks/MQTTnet.NetStandard/Client/IMqttClientOptions.cs @@ -1,22 +1,20 @@ using System; -using MQTTnet.Core.Serializer; +using MQTTnet.Serializer; -namespace MQTTnet.Core.Client +namespace MQTTnet.Client { public interface IMqttClientOptions { string ClientId { get; } - /// - /// The LogId is used to create a scope to correlate logging. If no value is provided the ClientId is used instead - /// - string LogId { get; } IMqttClientCredentials Credentials { get; } bool CleanSession { get; } MqttApplicationMessage WillMessage { get; } TimeSpan CommunicationTimeout { get; } TimeSpan KeepAlivePeriod { get; } + TimeSpan? KeepAliveSendInterval { get; } + MqttProtocolVersion ProtocolVersion { get; } IMqttClientChannelOptions ChannelOptions { get; } diff --git a/Frameworks/MQTTnet.NetStandard/Client/MqttClient.cs b/Frameworks/MQTTnet.NetStandard/Client/MqttClient.cs new file mode 100644 index 00000000..90790d5a --- /dev/null +++ b/Frameworks/MQTTnet.NetStandard/Client/MqttClient.cs @@ -0,0 +1,516 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MQTTnet.Adapter; +using MQTTnet.Diagnostics; +using MQTTnet.Exceptions; +using MQTTnet.Internal; +using MQTTnet.Packets; +using MQTTnet.Protocol; + +namespace MQTTnet.Client +{ + public class MqttClient : IMqttClient + { + private readonly MqttPacketIdentifierProvider _packetIdentifierProvider = new MqttPacketIdentifierProvider(); + private readonly Stopwatch _sendTracker = new Stopwatch(); + private readonly SemaphoreSlim _disconnectLock = new SemaphoreSlim(1, 1); + private readonly IMqttClientAdapterFactory _adapterFactory; + private readonly MqttPacketDispatcher _packetDispatcher; + private readonly IMqttNetLogger _logger; + + private IMqttClientOptions _options; + private bool _isReceivingPackets; + private CancellationTokenSource _cancellationTokenSource; + private Task _packetReceiverTask; + private Task _keepAliveMessageSenderTask; + private IMqttChannelAdapter _adapter; + + public MqttClient(IMqttClientAdapterFactory channelFactory, IMqttNetLogger logger) + { + _adapterFactory = channelFactory ?? throw new ArgumentNullException(nameof(channelFactory)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + _packetDispatcher = new MqttPacketDispatcher(logger); + } + + public event EventHandler Connected; + public event EventHandler Disconnected; + public event EventHandler ApplicationMessageReceived; + + public bool IsConnected { get; private set; } + + public async Task ConnectAsync(IMqttClientOptions options) + { + if (options == null) throw new ArgumentNullException(nameof(options)); + if (options.ChannelOptions == null) throw new ArgumentException("ChannelOptions are not set."); + + ThrowIfConnected("It is not allowed to connect with a server after the connection is established."); + + try + { + _options = options; + _cancellationTokenSource = new CancellationTokenSource(); + _packetIdentifierProvider.Reset(); + _packetDispatcher.Reset(); + + _adapter = _adapterFactory.CreateClientAdapter(options, _logger); + + _logger.Verbose("Trying to connect with server."); + await _adapter.ConnectAsync(_options.CommunicationTimeout).ConfigureAwait(false); + _logger.Verbose("Connection with server established."); + + await StartReceivingPacketsAsync().ConfigureAwait(false); + + var connectResponse = await AuthenticateAsync(options.WillMessage).ConfigureAwait(false); + _logger.Verbose("MQTT connection with server established."); + + _sendTracker.Restart(); + + if (_options.KeepAlivePeriod != TimeSpan.Zero) + { + StartSendingKeepAliveMessages(); + } + + IsConnected = true; + Connected?.Invoke(this, new MqttClientConnectedEventArgs(connectResponse.IsSessionPresent)); + + _logger.Info("Connected."); + return new MqttClientConnectResult(connectResponse.IsSessionPresent); + } + catch (Exception exception) + { + _logger.Error(exception, "Error while connecting with server."); + await DisconnectInternalAsync(null, exception).ConfigureAwait(false); + + throw; + } + } + + public async Task DisconnectAsync() + { + if (!IsConnected) + { + return; + } + + try + { + if (!_cancellationTokenSource.IsCancellationRequested) + { + await SendAsync(new MqttDisconnectPacket()).ConfigureAwait(false); + } + } + finally + { + await DisconnectInternalAsync(null, null).ConfigureAwait(false); + } + } + + public async Task> SubscribeAsync(IEnumerable topicFilters) + { + if (topicFilters == null) throw new ArgumentNullException(nameof(topicFilters)); + + ThrowIfNotConnected(); + + var subscribePacket = new MqttSubscribePacket + { + PacketIdentifier = _packetIdentifierProvider.GetNewPacketIdentifier(), + TopicFilters = topicFilters.ToList() + }; + + var response = await SendAndReceiveAsync(subscribePacket).ConfigureAwait(false); + + if (response.SubscribeReturnCodes.Count != subscribePacket.TopicFilters.Count) + { + throw new MqttProtocolViolationException("The return codes are not matching the topic filters [MQTT-3.9.3-1]."); + } + + return subscribePacket.TopicFilters.Select((t, i) => new MqttSubscribeResult(t, response.SubscribeReturnCodes[i])).ToList(); + } + + public async Task UnsubscribeAsync(IEnumerable topicFilters) + { + if (topicFilters == null) throw new ArgumentNullException(nameof(topicFilters)); + + ThrowIfNotConnected(); + + var unsubscribePacket = new MqttUnsubscribePacket + { + PacketIdentifier = _packetIdentifierProvider.GetNewPacketIdentifier(), + TopicFilters = topicFilters.ToList() + }; + + await SendAndReceiveAsync(unsubscribePacket).ConfigureAwait(false); + } + + public async Task PublishAsync(IEnumerable applicationMessages) + { + ThrowIfNotConnected(); + + var publishPackets = applicationMessages.Select(m => m.ToPublishPacket()); + var packetGroups = publishPackets.GroupBy(p => p.QualityOfServiceLevel).OrderBy(g => g.Key); + + foreach (var qosGroup in packetGroups) + { + switch (qosGroup.Key) + { + case MqttQualityOfServiceLevel.AtMostOnce: + { + // No packet identifier is used for QoS 0 [3.3.2.2 Packet Identifier] + await SendAsync(qosGroup.Cast().ToArray()).ConfigureAwait(false); + break; + } + case MqttQualityOfServiceLevel.AtLeastOnce: + { + foreach (var publishPacket in qosGroup) + { + publishPacket.PacketIdentifier = _packetIdentifierProvider.GetNewPacketIdentifier(); + await SendAndReceiveAsync(publishPacket).ConfigureAwait(false); + } + + break; + } + case MqttQualityOfServiceLevel.ExactlyOnce: + { + foreach (var publishPacket in qosGroup) + { + publishPacket.PacketIdentifier = _packetIdentifierProvider.GetNewPacketIdentifier(); + + var pubRecPacket = await SendAndReceiveAsync(publishPacket).ConfigureAwait(false); + var pubRelPacket = new MqttPubRelPacket + { + PacketIdentifier = pubRecPacket.PacketIdentifier + }; + + await SendAndReceiveAsync(pubRelPacket).ConfigureAwait(false); + } + + break; + } + default: + { + throw new InvalidOperationException(); + } + } + } + } + + public void Dispose() + { + _cancellationTokenSource?.Dispose(); + _cancellationTokenSource = null; + + _adapter?.Dispose(); + } + + private async Task AuthenticateAsync(MqttApplicationMessage willApplicationMessage) + { + var connectPacket = new MqttConnectPacket + { + ClientId = _options.ClientId, + Username = _options.Credentials?.Username, + Password = _options.Credentials?.Password, + CleanSession = _options.CleanSession, + KeepAlivePeriod = (ushort)_options.KeepAlivePeriod.TotalSeconds, + WillMessage = willApplicationMessage + }; + + var response = await SendAndReceiveAsync(connectPacket).ConfigureAwait(false); + if (response.ConnectReturnCode != MqttConnectReturnCode.ConnectionAccepted) + { + throw new MqttConnectingFailedException(response.ConnectReturnCode); + } + + return response; + } + + private void ThrowIfNotConnected() + { + if (!IsConnected) throw new MqttCommunicationException("The client is not connected."); + } + + private void ThrowIfConnected(string message) + { + if (IsConnected) throw new MqttProtocolViolationException(message); + } + + private async Task DisconnectInternalAsync(Task sender, Exception exception) + { + await _disconnectLock.WaitAsync(); + try + { + if (_cancellationTokenSource == null || _cancellationTokenSource.IsCancellationRequested) + { + return; + } + + _cancellationTokenSource.Cancel(false); + } + catch (Exception adapterException) + { + _logger.Warning(adapterException, "Error while disconnecting from adapter."); + } + finally + { + _disconnectLock.Release(); + } + + var clientWasConnected = IsConnected; + IsConnected = false; + + try + { + if (_packetReceiverTask != null && _packetReceiverTask != sender) + { + _packetReceiverTask.Wait(); + } + + if (_keepAliveMessageSenderTask != null && _keepAliveMessageSenderTask != sender) + { + _keepAliveMessageSenderTask.Wait(); + } + + if (_adapter != null) + { + await _adapter.DisconnectAsync(_options.CommunicationTimeout).ConfigureAwait(false); + } + + _logger.Verbose("Disconnected from adapter."); + } + catch (Exception adapterException) + { + _logger.Warning(adapterException, "Error while disconnecting from adapter."); + } + finally + { + _adapter?.Dispose(); + _adapter = null; + _cancellationTokenSource?.Dispose(); + _cancellationTokenSource = null; + + _logger.Info("Disconnected."); + Disconnected?.Invoke(this, new MqttClientDisconnectedEventArgs(clientWasConnected, exception)); + } + } + + private async Task ProcessReceivedPacketAsync(MqttBasePacket packet) + { + try + { + if (packet is MqttPublishPacket publishPacket) + { + await ProcessReceivedPublishPacketAsync(publishPacket).ConfigureAwait(false); + return; + } + + if (packet is MqttPingReqPacket) + { + await SendAsync(new MqttPingRespPacket()).ConfigureAwait(false); + return; + } + + if (packet is MqttDisconnectPacket) + { + await DisconnectAsync().ConfigureAwait(false); + return; + } + + if (packet is MqttPubRelPacket pubRelPacket) + { + await ProcessReceivedPubRelPacket(pubRelPacket).ConfigureAwait(false); + return; + } + + _packetDispatcher.Dispatch(packet); + } + catch (Exception exception) + { + _logger.Error(exception, "Unhandled exception while processing received packet."); + } + } + + private void FireApplicationMessageReceivedEvent(MqttPublishPacket publishPacket) + { + try + { + var applicationMessage = publishPacket.ToApplicationMessage(); + ApplicationMessageReceived?.Invoke(this, new MqttApplicationMessageReceivedEventArgs(_options.ClientId, applicationMessage)); + } + catch (Exception exception) + { + _logger.Error(exception, "Unhandled exception while handling application message."); + } + } + + private Task ProcessReceivedPublishPacketAsync(MqttPublishPacket publishPacket) + { + if (_cancellationTokenSource.IsCancellationRequested) + { + return Task.FromResult(0); + } + + if (publishPacket.QualityOfServiceLevel == MqttQualityOfServiceLevel.AtMostOnce) + { + FireApplicationMessageReceivedEvent(publishPacket); + return Task.FromResult(0); + } + + if (publishPacket.QualityOfServiceLevel == MqttQualityOfServiceLevel.AtLeastOnce) + { + FireApplicationMessageReceivedEvent(publishPacket); + return SendAsync(new MqttPubAckPacket { PacketIdentifier = publishPacket.PacketIdentifier }); + } + + if (publishPacket.QualityOfServiceLevel == MqttQualityOfServiceLevel.ExactlyOnce) + { + // QoS 2 is implement as method "B" [4.3.3 QoS 2: Exactly once delivery] + FireApplicationMessageReceivedEvent(publishPacket); + return SendAsync(new MqttPubRecPacket { PacketIdentifier = publishPacket.PacketIdentifier }); + } + + throw new MqttCommunicationException("Received a not supported QoS level."); + } + + private Task ProcessReceivedPubRelPacket(MqttPubRelPacket pubRelPacket) + { + var response = new MqttPubCompPacket + { + PacketIdentifier = pubRelPacket.PacketIdentifier + }; + + return SendAsync(response); + } + + private Task SendAsync(params MqttBasePacket[] packets) + { + _sendTracker.Restart(); + return _adapter.SendPacketsAsync(_options.CommunicationTimeout, _cancellationTokenSource.Token, packets); + } + + private async Task SendAndReceiveAsync(MqttBasePacket requestPacket) where TResponsePacket : MqttBasePacket + { + ushort? identifier = null; + if (requestPacket is IMqttPacketWithIdentifier requestPacketWithIdentifier) + { + identifier = requestPacketWithIdentifier.PacketIdentifier; + } + + var packetAwaiter = _packetDispatcher.WaitForPacketAsync(typeof(TResponsePacket), identifier, _options.CommunicationTimeout); + await SendAsync(requestPacket).ConfigureAwait(false); + + return (TResponsePacket)await packetAwaiter.ConfigureAwait(false); + } + + private async Task SendKeepAliveMessagesAsync() + { + _logger.Verbose("Start sending keep alive packets."); + + try + { + while (!_cancellationTokenSource.Token.IsCancellationRequested) + { + var keepAliveSendInterval = TimeSpan.FromSeconds(_options.KeepAlivePeriod.TotalSeconds * 0.75); + if (_options.KeepAliveSendInterval.HasValue) + { + keepAliveSendInterval = _options.KeepAliveSendInterval.Value; + } + + if (_sendTracker.Elapsed > keepAliveSendInterval) + { + await SendAndReceiveAsync(new MqttPingReqPacket()).ConfigureAwait(false); + } + + await Task.Delay(keepAliveSendInterval, _cancellationTokenSource.Token).ConfigureAwait(false); + } + } + catch (Exception exception) + { + if (exception is OperationCanceledException) + { + } + else if (exception is MqttCommunicationException) + { + _logger.Warning(exception, "MQTT communication exception while sending/receiving keep alive packets."); + } + else + { + _logger.Error(exception, "Unhandled exception while sending/receiving keep alive packets."); + } + + await DisconnectInternalAsync(_keepAliveMessageSenderTask, exception).ConfigureAwait(false); + } + finally + { + _logger.Verbose("Stopped sending keep alive packets."); + } + } + + private async Task ReceivePacketsAsync() + { + _logger.Verbose("Start receiving packets."); + + try + { + while (!_cancellationTokenSource.Token.IsCancellationRequested) + { + _isReceivingPackets = true; + + var packet = await _adapter.ReceivePacketAsync(TimeSpan.Zero, _cancellationTokenSource.Token).ConfigureAwait(false); + + if (_cancellationTokenSource.Token.IsCancellationRequested) + { + return; + } + + StartProcessReceivedPacket(packet); + } + } + catch (Exception exception) + { + if (exception is OperationCanceledException) + { + } + else if (exception is MqttCommunicationException) + { + _logger.Warning(exception, "MQTT communication exception while receiving packets."); + } + else + { + _logger.Error(exception, "Unhandled exception while receiving packets."); + } + + await DisconnectInternalAsync(_packetReceiverTask, exception).ConfigureAwait(false); + } + finally + { + _logger.Verbose("Stopped receiving packets."); + } + } + + private void StartProcessReceivedPacket(MqttBasePacket packet) + { + Task.Run(() => ProcessReceivedPacketAsync(packet), _cancellationTokenSource.Token); + } + + private async Task StartReceivingPacketsAsync() + { + _isReceivingPackets = false; + + _packetReceiverTask = Task.Run(ReceivePacketsAsync, _cancellationTokenSource.Token); + + while (!_isReceivingPackets && !_cancellationTokenSource.Token.IsCancellationRequested) + { + await Task.Delay(TimeSpan.FromMilliseconds(100), _cancellationTokenSource.Token).ConfigureAwait(false); + } + } + + private void StartSendingKeepAliveMessages() + { + _keepAliveMessageSenderTask = Task.Run(SendKeepAliveMessagesAsync, _cancellationTokenSource.Token); + } + } +} \ No newline at end of file diff --git a/MQTTnet.Core/Client/MqttClientConnectResult.cs b/Frameworks/MQTTnet.NetStandard/Client/MqttClientConnectResult.cs similarity index 87% rename from MQTTnet.Core/Client/MqttClientConnectResult.cs rename to Frameworks/MQTTnet.NetStandard/Client/MqttClientConnectResult.cs index 57db57df..6044bf50 100644 --- a/MQTTnet.Core/Client/MqttClientConnectResult.cs +++ b/Frameworks/MQTTnet.NetStandard/Client/MqttClientConnectResult.cs @@ -1,4 +1,4 @@ -namespace MQTTnet.Core.Client +namespace MQTTnet.Client { public class MqttClientConnectResult { diff --git a/MQTTnet.Core/Client/MqttClientConnectedEventArgs.cs b/Frameworks/MQTTnet.NetStandard/Client/MqttClientConnectedEventArgs.cs similarity index 90% rename from MQTTnet.Core/Client/MqttClientConnectedEventArgs.cs rename to Frameworks/MQTTnet.NetStandard/Client/MqttClientConnectedEventArgs.cs index 107a2c95..c8c6ce56 100644 --- a/MQTTnet.Core/Client/MqttClientConnectedEventArgs.cs +++ b/Frameworks/MQTTnet.NetStandard/Client/MqttClientConnectedEventArgs.cs @@ -1,6 +1,6 @@ using System; -namespace MQTTnet.Core.Client +namespace MQTTnet.Client { public class MqttClientConnectedEventArgs : EventArgs { diff --git a/MQTTnet.Core/Client/MqttClientCredentials.cs b/Frameworks/MQTTnet.NetStandard/Client/MqttClientCredentials.cs similarity index 83% rename from MQTTnet.Core/Client/MqttClientCredentials.cs rename to Frameworks/MQTTnet.NetStandard/Client/MqttClientCredentials.cs index 085778a3..feaa1e6c 100644 --- a/MQTTnet.Core/Client/MqttClientCredentials.cs +++ b/Frameworks/MQTTnet.NetStandard/Client/MqttClientCredentials.cs @@ -1,4 +1,4 @@ -namespace MQTTnet.Core.Client +namespace MQTTnet.Client { public class MqttClientCredentials : IMqttClientCredentials { diff --git a/MQTTnet.Core/Client/MqttClientDisconnectedEventArgs.cs b/Frameworks/MQTTnet.NetStandard/Client/MqttClientDisconnectedEventArgs.cs similarity index 67% rename from MQTTnet.Core/Client/MqttClientDisconnectedEventArgs.cs rename to Frameworks/MQTTnet.NetStandard/Client/MqttClientDisconnectedEventArgs.cs index 1e9609ef..2161cc26 100644 --- a/MQTTnet.Core/Client/MqttClientDisconnectedEventArgs.cs +++ b/Frameworks/MQTTnet.NetStandard/Client/MqttClientDisconnectedEventArgs.cs @@ -1,14 +1,17 @@ using System; -namespace MQTTnet.Core.Client +namespace MQTTnet.Client { public class MqttClientDisconnectedEventArgs : EventArgs { - public MqttClientDisconnectedEventArgs(bool clientWasConnected) + public MqttClientDisconnectedEventArgs(bool clientWasConnected, Exception exception) { ClientWasConnected = clientWasConnected; + Exception = exception; } public bool ClientWasConnected { get; } + + public Exception Exception { get; } } } diff --git a/Frameworks/MQTTnet.NetStandard/Client/MqttClientExtensions.cs b/Frameworks/MQTTnet.NetStandard/Client/MqttClientExtensions.cs new file mode 100644 index 00000000..a7548dee --- /dev/null +++ b/Frameworks/MQTTnet.NetStandard/Client/MqttClientExtensions.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using MQTTnet.Protocol; + +namespace MQTTnet.Client +{ + public static class MqttClientExtensions + { + public static Task> SubscribeAsync(this IMqttClient client, params TopicFilter[] topicFilters) + { + if (client == null) throw new ArgumentNullException(nameof(client)); + if (topicFilters == null) throw new ArgumentNullException(nameof(topicFilters)); + + return client.SubscribeAsync(topicFilters.ToList()); + } + + public static Task> SubscribeAsync(this IMqttClient client, string topic, MqttQualityOfServiceLevel qualityOfServiceLevel) + { + if (client == null) throw new ArgumentNullException(nameof(client)); + if (topic == null) throw new ArgumentNullException(nameof(topic)); + + return client.SubscribeAsync(new TopicFilterBuilder().WithTopic(topic).WithQualityOfServiceLevel(qualityOfServiceLevel).Build()); + } + + public static Task> SubscribeAsync(this IMqttClient client, string topic) + { + if (client == null) throw new ArgumentNullException(nameof(client)); + if (topic == null) throw new ArgumentNullException(nameof(topic)); + + return client.SubscribeAsync(new TopicFilterBuilder().WithTopic(topic).Build()); + } + + public static Task UnsubscribeAsync(this IMqttClient client, params string[] topicFilters) + { + if (client == null) throw new ArgumentNullException(nameof(client)); + if (topicFilters == null) throw new ArgumentNullException(nameof(topicFilters)); + + return client.UnsubscribeAsync(topicFilters.ToList()); + } + } +} \ No newline at end of file diff --git a/MQTTnet.Core/Client/MqttClientOptions.cs b/Frameworks/MQTTnet.NetStandard/Client/MqttClientOptions.cs similarity index 82% rename from MQTTnet.Core/Client/MqttClientOptions.cs rename to Frameworks/MQTTnet.NetStandard/Client/MqttClientOptions.cs index 1b1c1bb8..9640a474 100644 --- a/MQTTnet.Core/Client/MqttClientOptions.cs +++ b/Frameworks/MQTTnet.NetStandard/Client/MqttClientOptions.cs @@ -1,7 +1,7 @@ using System; -using MQTTnet.Core.Serializer; +using MQTTnet.Serializer; -namespace MQTTnet.Core.Client +namespace MQTTnet.Client { public class MqttClientOptions : IMqttClientOptions { @@ -9,14 +9,13 @@ namespace MQTTnet.Core.Client public string ClientId { get; set; } = Guid.NewGuid().ToString("N"); - /// - public string LogId { get; set; } - public bool CleanSession { get; set; } = true; public IMqttClientCredentials Credentials { get; set; } = new MqttClientCredentials(); - public TimeSpan KeepAlivePeriod { get; set; } = TimeSpan.FromSeconds(5); + public TimeSpan KeepAlivePeriod { get; set; } = TimeSpan.FromSeconds(15); + + public TimeSpan? KeepAliveSendInterval { get; set; } public TimeSpan CommunicationTimeout { get; set; } = TimeSpan.FromSeconds(10); diff --git a/MQTTnet.Core/Client/MqttClientOptionsBuilder.cs b/Frameworks/MQTTnet.NetStandard/Client/MqttClientOptionsBuilder.cs similarity index 66% rename from MQTTnet.Core/Client/MqttClientOptionsBuilder.cs rename to Frameworks/MQTTnet.NetStandard/Client/MqttClientOptionsBuilder.cs index 1eb2a347..ad766c3f 100644 --- a/MQTTnet.Core/Client/MqttClientOptionsBuilder.cs +++ b/Frameworks/MQTTnet.NetStandard/Client/MqttClientOptionsBuilder.cs @@ -1,7 +1,8 @@ using System; -using MQTTnet.Core.Serializer; +using System.Linq; +using MQTTnet.Serializer; -namespace MQTTnet.Core.Client +namespace MQTTnet.Client { public class MqttClientOptionsBuilder { @@ -11,15 +12,15 @@ namespace MQTTnet.Core.Client private MqttClientTlsOptions _tlsOptions; - public MqttClientOptionsBuilder WithProtocolVersion(MqttProtocolVersion protocolVersion) + public MqttClientOptionsBuilder WithProtocolVersion(MqttProtocolVersion value) { - _options.ProtocolVersion = protocolVersion; + _options.ProtocolVersion = value; return this; } - public MqttClientOptionsBuilder WithCommunicationTimeout(TimeSpan communicationTimeout) + public MqttClientOptionsBuilder WithCommunicationTimeout(TimeSpan value) { - _options.CommunicationTimeout = communicationTimeout; + _options.CommunicationTimeout = value; return this; } @@ -29,21 +30,21 @@ namespace MQTTnet.Core.Client return this; } - public MqttClientOptionsBuilder WithKeepAlivePeriod(TimeSpan keepAlivePeriod) + public MqttClientOptionsBuilder WithKeepAlivePeriod(TimeSpan value) { - _options.KeepAlivePeriod = keepAlivePeriod; + _options.KeepAlivePeriod = value; return this; } - public MqttClientOptionsBuilder WithClientId(string clientId) + public MqttClientOptionsBuilder WithClientId(string value) { - _options.ClientId = clientId; + _options.ClientId = value; return this; } - public MqttClientOptionsBuilder WithWillMessage(MqttApplicationMessage applicationMessage) + public MqttClientOptionsBuilder WithWillMessage(MqttApplicationMessage value) { - _options.WillMessage = applicationMessage; + _options.WillMessage = value; return this; } @@ -79,6 +80,24 @@ namespace MQTTnet.Core.Client return this; } + public MqttClientOptionsBuilder WithTls( + bool allowUntrustedCertificates = false, + bool ignoreCertificateChainErrors = false, + bool ignoreCertificateRevocationErrors = false, + params byte[][] certificates) + { + _tlsOptions = new MqttClientTlsOptions + { + UseTls = true, + AllowUntrustedCertificates = allowUntrustedCertificates, + IgnoreCertificateChainErrors = ignoreCertificateChainErrors, + IgnoreCertificateRevocationErrors = ignoreCertificateRevocationErrors, + Certificates = certificates?.ToList() + }; + + return this; + } + public MqttClientOptionsBuilder WithTls() { _tlsOptions = new MqttClientTlsOptions @@ -100,11 +119,11 @@ namespace MQTTnet.Core.Client if (_tcpOptions != null) { - _options.ChannelOptions = _tcpOptions; + _tcpOptions.TlsOptions = _tlsOptions; } - else + else if (_webSocketOptions != null) { - _options.ChannelOptions = _webSocketOptions; + _webSocketOptions.TlsOptions = _tlsOptions; } } diff --git a/MQTTnet.Core/Client/MqttClientTcpOptions.cs b/Frameworks/MQTTnet.NetStandard/Client/MqttClientTcpOptions.cs similarity index 74% rename from MQTTnet.Core/Client/MqttClientTcpOptions.cs rename to Frameworks/MQTTnet.NetStandard/Client/MqttClientTcpOptions.cs index e5cf8d6c..ac96ce63 100644 --- a/MQTTnet.Core/Client/MqttClientTcpOptions.cs +++ b/Frameworks/MQTTnet.NetStandard/Client/MqttClientTcpOptions.cs @@ -1,4 +1,4 @@ -namespace MQTTnet.Core.Client +namespace MQTTnet.Client { public class MqttClientTcpOptions : IMqttClientChannelOptions { @@ -6,6 +6,8 @@ public int? Port { get; set; } + public int BufferSize { get; set; } = 20 * 4096; + public MqttClientTlsOptions TlsOptions { get; set; } = new MqttClientTlsOptions(); } } diff --git a/MQTTnet.Core/Client/MqttClientTcpOptionsExtensions.cs b/Frameworks/MQTTnet.NetStandard/Client/MqttClientTcpOptionsExtensions.cs similarity index 93% rename from MQTTnet.Core/Client/MqttClientTcpOptionsExtensions.cs rename to Frameworks/MQTTnet.NetStandard/Client/MqttClientTcpOptionsExtensions.cs index cada6f8b..460069a7 100644 --- a/MQTTnet.Core/Client/MqttClientTcpOptionsExtensions.cs +++ b/Frameworks/MQTTnet.NetStandard/Client/MqttClientTcpOptionsExtensions.cs @@ -1,6 +1,6 @@ using System; -namespace MQTTnet.Core.Client +namespace MQTTnet.Client { public static class MqttClientTcpOptionsExtensions { diff --git a/MQTTnet.Core/Client/MqttClientTlsOptions.cs b/Frameworks/MQTTnet.NetStandard/Client/MqttClientTlsOptions.cs similarity index 92% rename from MQTTnet.Core/Client/MqttClientTlsOptions.cs rename to Frameworks/MQTTnet.NetStandard/Client/MqttClientTlsOptions.cs index 33cd691f..6478b267 100644 --- a/MQTTnet.Core/Client/MqttClientTlsOptions.cs +++ b/Frameworks/MQTTnet.NetStandard/Client/MqttClientTlsOptions.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace MQTTnet.Core.Client +namespace MQTTnet.Client { public class MqttClientTlsOptions { diff --git a/MQTTnet.Core/Client/MqttClientWebSocketOptions.cs b/Frameworks/MQTTnet.NetStandard/Client/MqttClientWebSocketOptions.cs similarity index 77% rename from MQTTnet.Core/Client/MqttClientWebSocketOptions.cs rename to Frameworks/MQTTnet.NetStandard/Client/MqttClientWebSocketOptions.cs index 25e4ae81..114283d4 100644 --- a/MQTTnet.Core/Client/MqttClientWebSocketOptions.cs +++ b/Frameworks/MQTTnet.NetStandard/Client/MqttClientWebSocketOptions.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Net; -namespace MQTTnet.Core.Client +namespace MQTTnet.Client { public class MqttClientWebSocketOptions : IMqttClientChannelOptions { @@ -9,7 +9,7 @@ namespace MQTTnet.Core.Client public IDictionary RequestHeaders { get; set; } - public ICollection SubProtocols { get; set; } + public ICollection SubProtocols { get; set; } = new List { "mqtt" }; public CookieContainer CookieContainer { get; set; } diff --git a/Frameworks/MQTTnet.NetStandard/Client/MqttPacketDispatcher.cs b/Frameworks/MQTTnet.NetStandard/Client/MqttPacketDispatcher.cs new file mode 100644 index 00000000..8b09fbce --- /dev/null +++ b/Frameworks/MQTTnet.NetStandard/Client/MqttPacketDispatcher.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Concurrent; +using System.Threading.Tasks; +using MQTTnet.Diagnostics; +using MQTTnet.Exceptions; +using MQTTnet.Internal; +using MQTTnet.Packets; + +namespace MQTTnet.Client +{ + public class MqttPacketDispatcher + { + private readonly ConcurrentDictionary>> _awaiters = new ConcurrentDictionary>>(); + private readonly IMqttNetLogger _logger; + + public MqttPacketDispatcher(IMqttNetLogger logger) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public async Task WaitForPacketAsync(Type responseType, ushort? identifier, TimeSpan timeout) + { + var packetAwaiter = AddPacketAwaiter(responseType, identifier); + try + { + return await packetAwaiter.Task.TimeoutAfter(timeout).ConfigureAwait(false); + } + catch (MqttCommunicationTimedOutException) + { + _logger.Warning("Timeout while waiting for packet of type '{0}'.", responseType.Name); + throw; + } + finally + { + RemovePacketAwaiter(responseType, identifier); + } + } + + public void Dispatch(MqttBasePacket packet) + { + if (packet == null) throw new ArgumentNullException(nameof(packet)); + + var type = packet.GetType(); + + if (_awaiters.TryGetValue(type, out var byId)) + { + ushort? identifier = 0; + if (packet is IMqttPacketWithIdentifier packetWithIdentifier) + { + identifier = packetWithIdentifier.PacketIdentifier; + } + + if (byId.TryRemove(identifier.Value, out var tcs)) + { + tcs.TrySetResult(packet); + return; + } + } + + throw new InvalidOperationException($"Packet of type '{type.Name}' not handled or dispatched."); + } + + public void Reset() + { + _awaiters.Clear(); + } + + private TaskCompletionSource AddPacketAwaiter(Type responseType, ushort? identifier) + { + var tcs = new TaskCompletionSource(); + + if (!identifier.HasValue) + { + identifier = 0; + } + + var byId = _awaiters.GetOrAdd(responseType, key => new ConcurrentDictionary>()); + if (!byId.TryAdd(identifier.Value, tcs)) + { + throw new InvalidOperationException($"The packet dispatcher already has an awaiter for packet of type '{responseType}' with identifier {identifier}."); + } + + return tcs; + } + + private void RemovePacketAwaiter(Type responseType, ushort? identifier) + { + if (!identifier.HasValue) + { + identifier = 0; + } + + var byId = _awaiters.GetOrAdd(responseType, key => new ConcurrentDictionary>()); + byId.TryRemove(identifier.Value, out var _); + } + } +} \ No newline at end of file diff --git a/Frameworks/MQTTnet.NetStandard/Client/MqttPacketIdentifierProvider.cs b/Frameworks/MQTTnet.NetStandard/Client/MqttPacketIdentifierProvider.cs new file mode 100644 index 00000000..dc099183 --- /dev/null +++ b/Frameworks/MQTTnet.NetStandard/Client/MqttPacketIdentifierProvider.cs @@ -0,0 +1,32 @@ +namespace MQTTnet.Client +{ + public class MqttPacketIdentifierProvider + { + private readonly object _syncRoot = new object(); + private ushort _value; + + public void Reset() + { + lock (_syncRoot) + { + _value = 0; + } + } + + public ushort GetNewPacketIdentifier() + { + lock (_syncRoot) + { + _value++; + + if (_value == 0) + { + // As per official MQTT documentation the package identifier should never be 0. + _value = 1; + } + + return _value; + } + } + } +} diff --git a/MQTTnet.Core/Client/MqttSubscribeResult.cs b/Frameworks/MQTTnet.NetStandard/Client/MqttSubscribeResult.cs similarity index 79% rename from MQTTnet.Core/Client/MqttSubscribeResult.cs rename to Frameworks/MQTTnet.NetStandard/Client/MqttSubscribeResult.cs index 9dd49449..2b7c46da 100644 --- a/MQTTnet.Core/Client/MqttSubscribeResult.cs +++ b/Frameworks/MQTTnet.NetStandard/Client/MqttSubscribeResult.cs @@ -1,7 +1,6 @@ -using MQTTnet.Core.Packets; -using MQTTnet.Core.Protocol; +using MQTTnet.Protocol; -namespace MQTTnet.Core.Client +namespace MQTTnet.Client { public class MqttSubscribeResult { diff --git a/Frameworks/MQTTnet.NetStandard/Diagnostics/IMqttNetLogger.cs b/Frameworks/MQTTnet.NetStandard/Diagnostics/IMqttNetLogger.cs new file mode 100644 index 00000000..6ed16de2 --- /dev/null +++ b/Frameworks/MQTTnet.NetStandard/Diagnostics/IMqttNetLogger.cs @@ -0,0 +1,21 @@ +using System; + +namespace MQTTnet.Diagnostics +{ + public interface IMqttNetLogger + { + event EventHandler LogMessagePublished; + + void Verbose(string message, params object[] parameters); + + void Info(string message, params object[] parameters); + + void Warning(Exception exception, string message, params object[] parameters); + + void Warning(string message, params object[] parameters); + + void Error(Exception exception, string message, params object[] parameters); + + void Error(string message, params object[] parameters); + } +} diff --git a/Frameworks/MQTTnet.NetStandard/Diagnostics/MqttNetGlobalLogger.cs b/Frameworks/MQTTnet.NetStandard/Diagnostics/MqttNetGlobalLogger.cs new file mode 100644 index 00000000..a42857ea --- /dev/null +++ b/Frameworks/MQTTnet.NetStandard/Diagnostics/MqttNetGlobalLogger.cs @@ -0,0 +1,18 @@ +using System; + +namespace MQTTnet.Diagnostics +{ + public static class MqttNetGlobalLogger + { + public static event EventHandler LogMessagePublished; + + public static bool HasListeners => LogMessagePublished != null; + + public static void Publish(MqttNetLogMessage logMessage) + { + if (logMessage == null) throw new ArgumentNullException(nameof(logMessage)); + + LogMessagePublished?.Invoke(null, new MqttNetLogMessagePublishedEventArgs(logMessage)); + } + } +} diff --git a/Frameworks/MQTTnet.NetStandard/Diagnostics/MqttNetLogLevel.cs b/Frameworks/MQTTnet.NetStandard/Diagnostics/MqttNetLogLevel.cs new file mode 100644 index 00000000..cdc7a78a --- /dev/null +++ b/Frameworks/MQTTnet.NetStandard/Diagnostics/MqttNetLogLevel.cs @@ -0,0 +1,13 @@ +namespace MQTTnet.Diagnostics +{ + public enum MqttNetLogLevel + { + Verbose, + + Info, + + Warning, + + Error + } +} diff --git a/Frameworks/MQTTnet.NetStandard/Diagnostics/MqttNetLogMessage.cs b/Frameworks/MQTTnet.NetStandard/Diagnostics/MqttNetLogMessage.cs new file mode 100644 index 00000000..6035a89c --- /dev/null +++ b/Frameworks/MQTTnet.NetStandard/Diagnostics/MqttNetLogMessage.cs @@ -0,0 +1,43 @@ +using System; + +namespace MQTTnet.Diagnostics +{ + public sealed class MqttNetLogMessage + { + public MqttNetLogMessage(string logId, DateTime timestamp, int threadId, string source, MqttNetLogLevel level, string message, Exception exception) + { + LogId = logId; + Timestamp = timestamp; + ThreadId = threadId; + Source = source; + Level = level; + Message = message; + Exception = exception; + } + + public string LogId { get; } + + public DateTime Timestamp { get; } + + public int ThreadId { get; } + + public string Source { get; } + + public MqttNetLogLevel Level { get; } + + public string Message { get; } + + public Exception Exception { get; } + + public override string ToString() + { + var result = $"[{Timestamp:O}] [{LogId}] [{ThreadId}] [{Source}] [{Level}]: {Message}"; + if (Exception != null) + { + result += Environment.NewLine + Exception; + } + + return result; + } + } +} diff --git a/Frameworks/MQTTnet.NetStandard/Diagnostics/MqttNetLogMessagePublishedEventArgs.cs b/Frameworks/MQTTnet.NetStandard/Diagnostics/MqttNetLogMessagePublishedEventArgs.cs new file mode 100644 index 00000000..2407752b --- /dev/null +++ b/Frameworks/MQTTnet.NetStandard/Diagnostics/MqttNetLogMessagePublishedEventArgs.cs @@ -0,0 +1,14 @@ +using System; + +namespace MQTTnet.Diagnostics +{ + public sealed class MqttNetLogMessagePublishedEventArgs : EventArgs + { + public MqttNetLogMessagePublishedEventArgs(MqttNetLogMessage logMessage) + { + TraceMessage = logMessage ?? throw new ArgumentNullException(nameof(logMessage)); + } + + public MqttNetLogMessage TraceMessage { get; } + } +} diff --git a/Frameworks/MQTTnet.NetStandard/Diagnostics/MqttNetLogger.cs b/Frameworks/MQTTnet.NetStandard/Diagnostics/MqttNetLogger.cs new file mode 100644 index 00000000..53598b2b --- /dev/null +++ b/Frameworks/MQTTnet.NetStandard/Diagnostics/MqttNetLogger.cs @@ -0,0 +1,74 @@ +using System; + +namespace MQTTnet.Diagnostics +{ + public class MqttNetLogger : IMqttNetLogger + { + private readonly string _logId; + + public MqttNetLogger(string logId = null) + { + _logId = logId; + } + + public event EventHandler LogMessagePublished; + + public void Verbose(string message, params object[] parameters) + { + Publish(MqttNetLogLevel.Verbose, null, message, parameters); + } + + public void Info(string message, params object[] parameters) + { + Publish(MqttNetLogLevel.Info, null, message, parameters); + } + + public void Warning(Exception exception, string message, params object[] parameters) + { + Publish(MqttNetLogLevel.Warning, exception, message, parameters); + } + + public void Warning(string message, params object[] parameters) + { + Warning(null, message, parameters); + } + + public void Error(Exception exception, string message, params object[] parameters) + { + Publish(MqttNetLogLevel.Error, exception, message, parameters); + } + + public void Error(string message, params object[] parameters) + { + Warning(null, message, parameters); + } + + private void Publish(MqttNetLogLevel logLevel, Exception exception, string message, object[] parameters) + { + var hasLocalListeners = LogMessagePublished != null; + var hasGlobalListeners = MqttNetGlobalLogger.HasListeners; + + if (!hasLocalListeners && !hasGlobalListeners) + { + return; + } + + if (parameters.Length > 0) + { + message = string.Format(message, parameters); + } + + var traceMessage = new MqttNetLogMessage(_logId, DateTime.Now, Environment.CurrentManagedThreadId, typeof(TSource).Name, logLevel, message, exception); + + if (hasGlobalListeners) + { + MqttNetGlobalLogger.Publish(traceMessage); + } + + if (hasLocalListeners) + { + LogMessagePublished?.Invoke(this, new MqttNetLogMessagePublishedEventArgs(traceMessage)); + } + } + } +} \ No newline at end of file diff --git a/Frameworks/MQTTnet.NetStandard/Diagnostics/TargetFrameworkInfoProvider.cs b/Frameworks/MQTTnet.NetStandard/Diagnostics/TargetFrameworkInfoProvider.cs new file mode 100644 index 00000000..52582769 --- /dev/null +++ b/Frameworks/MQTTnet.NetStandard/Diagnostics/TargetFrameworkInfoProvider.cs @@ -0,0 +1,23 @@ +namespace MQTTnet.Diagnostics +{ + public static class TargetFrameworkInfoProvider + { + public static string TargetFramework + { + get + { +#if NET452 + return "net452"; +#elif NET461 + return "net461"; +#elif NETSTANDARD1_3 + return "netstandard1.3"; +#elif NETSTANDARD2_0 + return "netstandard2.0"; +#elif WINDOWS_UWP + return "uap10.0"; +#endif + } + } + } +} \ No newline at end of file diff --git a/MQTTnet.Core/Exceptions/MqttCommunicationException.cs b/Frameworks/MQTTnet.NetStandard/Exceptions/MqttCommunicationException.cs similarity index 92% rename from MQTTnet.Core/Exceptions/MqttCommunicationException.cs rename to Frameworks/MQTTnet.NetStandard/Exceptions/MqttCommunicationException.cs index e019f4ee..4631de83 100644 --- a/MQTTnet.Core/Exceptions/MqttCommunicationException.cs +++ b/Frameworks/MQTTnet.NetStandard/Exceptions/MqttCommunicationException.cs @@ -1,6 +1,6 @@ using System; -namespace MQTTnet.Core.Exceptions +namespace MQTTnet.Exceptions { public class MqttCommunicationException : Exception { diff --git a/MQTTnet.Core/Exceptions/MqttCommunicationTimedOutException.cs b/Frameworks/MQTTnet.NetStandard/Exceptions/MqttCommunicationTimedOutException.cs similarity index 73% rename from MQTTnet.Core/Exceptions/MqttCommunicationTimedOutException.cs rename to Frameworks/MQTTnet.NetStandard/Exceptions/MqttCommunicationTimedOutException.cs index e7240072..7d0adcd4 100644 --- a/MQTTnet.Core/Exceptions/MqttCommunicationTimedOutException.cs +++ b/Frameworks/MQTTnet.NetStandard/Exceptions/MqttCommunicationTimedOutException.cs @@ -1,4 +1,4 @@ -namespace MQTTnet.Core.Exceptions +namespace MQTTnet.Exceptions { public sealed class MqttCommunicationTimedOutException : MqttCommunicationException { diff --git a/MQTTnet.Core/Exceptions/MqttProtocolViolationException.cs b/Frameworks/MQTTnet.NetStandard/Exceptions/MqttProtocolViolationException.cs similarity index 86% rename from MQTTnet.Core/Exceptions/MqttProtocolViolationException.cs rename to Frameworks/MQTTnet.NetStandard/Exceptions/MqttProtocolViolationException.cs index cef43f5c..8756b3f8 100644 --- a/MQTTnet.Core/Exceptions/MqttProtocolViolationException.cs +++ b/Frameworks/MQTTnet.NetStandard/Exceptions/MqttProtocolViolationException.cs @@ -1,6 +1,6 @@ using System; -namespace MQTTnet.Core.Exceptions +namespace MQTTnet.Exceptions { public sealed class MqttProtocolViolationException : Exception { diff --git a/MQTTnet.Core/IApplicationMessagePublisher.cs b/Frameworks/MQTTnet.NetStandard/IApplicationMessagePublisher.cs similarity index 90% rename from MQTTnet.Core/IApplicationMessagePublisher.cs rename to Frameworks/MQTTnet.NetStandard/IApplicationMessagePublisher.cs index 2f6715b8..e137669e 100644 --- a/MQTTnet.Core/IApplicationMessagePublisher.cs +++ b/Frameworks/MQTTnet.NetStandard/IApplicationMessagePublisher.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Threading.Tasks; -namespace MQTTnet.Core +namespace MQTTnet { public interface IApplicationMessagePublisher { diff --git a/MQTTnet.Core/IApplicationMessageReceiver.cs b/Frameworks/MQTTnet.NetStandard/IApplicationMessageReceiver.cs similarity index 78% rename from MQTTnet.Core/IApplicationMessageReceiver.cs rename to Frameworks/MQTTnet.NetStandard/IApplicationMessageReceiver.cs index 379ba69c..b315067c 100644 --- a/MQTTnet.Core/IApplicationMessageReceiver.cs +++ b/Frameworks/MQTTnet.NetStandard/IApplicationMessageReceiver.cs @@ -1,7 +1,6 @@ using System; -using MQTTnet.Core.Client; -namespace MQTTnet.Core +namespace MQTTnet { public interface IApplicationMessageReceiver { diff --git a/Frameworks/MQTTnet.NetStandard/Implementations/MqttClientAdapterFactory.cs b/Frameworks/MQTTnet.NetStandard/Implementations/MqttClientAdapterFactory.cs new file mode 100644 index 00000000..4692d7ee --- /dev/null +++ b/Frameworks/MQTTnet.NetStandard/Implementations/MqttClientAdapterFactory.cs @@ -0,0 +1,36 @@ +using System; +using MQTTnet.Adapter; +using MQTTnet.Client; +using MQTTnet.Diagnostics; +using MQTTnet.Serializer; + +namespace MQTTnet.Implementations +{ + public class MqttClientAdapterFactory : IMqttClientAdapterFactory + { + public IMqttChannelAdapter CreateClientAdapter(IMqttClientOptions options, IMqttNetLogger logger) + { + if (options == null) throw new ArgumentNullException(nameof(options)); + + var serializer = new MqttPacketSerializer { ProtocolVersion = options.ProtocolVersion }; + + switch (options.ChannelOptions) + { + case MqttClientTcpOptions tcpOptions: + { + return new MqttChannelAdapter(new MqttTcpChannel(tcpOptions), serializer, logger); + } + + case MqttClientWebSocketOptions webSocketOptions: + { + return new MqttChannelAdapter(new MqttWebSocketChannel(webSocketOptions), serializer, logger); + } + + default: + { + throw new NotSupportedException(); + } + } + } + } +} diff --git a/Frameworks/MQTTnet.NetStandard/Implementations/Uap/MqttTcpChannel.cs b/Frameworks/MQTTnet.NetStandard/Implementations/MqttTcpChannel.Uwp.cs similarity index 71% rename from Frameworks/MQTTnet.NetStandard/Implementations/Uap/MqttTcpChannel.cs rename to Frameworks/MQTTnet.NetStandard/Implementations/MqttTcpChannel.Uwp.cs index 6e311d67..a9cd18e7 100644 --- a/Frameworks/MQTTnet.NetStandard/Implementations/Uap/MqttTcpChannel.cs +++ b/Frameworks/MQTTnet.NetStandard/Implementations/MqttTcpChannel.Uwp.cs @@ -1,5 +1,4 @@ -#if NET451 || NETSTANDARD1_3 -#else +#if WINDOWS_UWP using System; using System.Collections.Generic; using System.IO; @@ -9,19 +8,26 @@ using System.Threading.Tasks; using Windows.Networking; using Windows.Networking.Sockets; using Windows.Security.Cryptography.Certificates; -using MQTTnet.Core.Channel; -using MQTTnet.Core.Client; +using MQTTnet.Channel; +using MQTTnet.Client; namespace MQTTnet.Implementations { - public sealed class MqttTcpChannel : IMqttCommunicationChannel, IDisposable + public sealed class MqttTcpChannel : IMqttChannel { + // ReSharper disable once MemberCanBePrivate.Global + // ReSharper disable once AutoPropertyCanBeMadeGetOnly.Global + public static int BufferSize { get; set; } = 4096 * 20; // Can be changed for fine tuning by library user. + + private readonly int _bufferSize = BufferSize; private readonly MqttClientTcpOptions _options; + private StreamSocket _socket; public MqttTcpChannel(MqttClientTcpOptions options) { _options = options ?? throw new ArgumentNullException(nameof(options)); + _bufferSize = _options.BufferSize; } public MqttTcpChannel(StreamSocket socket) @@ -33,7 +39,6 @@ namespace MQTTnet.Implementations public Stream SendStream { get; private set; } public Stream ReceiveStream { get; private set; } - public Stream RawReceiveStream { get; private set; } public static Func> CustomIgnorableServerCertificateErrorsResolver { get; set; } @@ -71,24 +76,56 @@ namespace MQTTnet.Implementations public void Dispose() { - RawReceiveStream?.Dispose(); - RawReceiveStream = null; + try + { + SendStream?.Dispose(); + } + catch (ObjectDisposedException) + { + } + catch (NullReferenceException) + { + } + finally + { + SendStream = null; + } - SendStream?.Dispose(); - SendStream = null; + try + { + ReceiveStream?.Dispose(); + } + catch (ObjectDisposedException) + { + } + catch (NullReferenceException) + { + } + finally + { + ReceiveStream = null; + } - ReceiveStream?.Dispose(); - ReceiveStream = null; - - _socket?.Dispose(); - _socket = null; + try + { + _socket?.Dispose(); + } + catch (ObjectDisposedException) + { + } + catch (NullReferenceException) + { + } + finally + { + _socket = null; + } } private void CreateStreams() { - SendStream = _socket.OutputStream.AsStreamForWrite(); - ReceiveStream = _socket.InputStream.AsStreamForRead(); - RawReceiveStream = ReceiveStream; + SendStream = _socket.OutputStream.AsStreamForWrite(_bufferSize); + ReceiveStream = _socket.InputStream.AsStreamForRead(_bufferSize); } private static Certificate LoadCertificate(MqttClientTcpOptions options) diff --git a/Frameworks/MQTTnet.NetStandard/Implementations/NetStandard/MqttTcpChannel.cs b/Frameworks/MQTTnet.NetStandard/Implementations/MqttTcpChannel.cs similarity index 58% rename from Frameworks/MQTTnet.NetStandard/Implementations/NetStandard/MqttTcpChannel.cs rename to Frameworks/MQTTnet.NetStandard/Implementations/MqttTcpChannel.cs index 62bb8787..8d43d878 100644 --- a/Frameworks/MQTTnet.NetStandard/Implementations/NetStandard/MqttTcpChannel.cs +++ b/Frameworks/MQTTnet.NetStandard/Implementations/MqttTcpChannel.cs @@ -1,29 +1,31 @@ -#if NET451 || NETSTANDARD1_3 - +#if NET452 || NET461 || NETSTANDARD1_3 || NETSTANDARD2_0 using System; using System.Net.Security; using System.Net.Sockets; using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; -using MQTTnet.Core.Channel; -using MQTTnet.Core.Client; using System.IO; using System.Linq; +using MQTTnet.Channel; +using MQTTnet.Client; namespace MQTTnet.Implementations { - public sealed class MqttTcpChannel : IMqttCommunicationChannel, IDisposable + public sealed class MqttTcpChannel : IMqttChannel { - private readonly MqttClientTcpOptions _options; - - //todo: this can be used with min dependency NetStandard1.6 -#if NET45 +#if NET452 || NET461 // ReSharper disable once MemberCanBePrivate.Global // ReSharper disable once AutoPropertyCanBeMadeGetOnly.Global public static int BufferSize { get; set; } = 4096 * 20; // Can be changed for fine tuning by library user. + + private readonly int _bufferSize = BufferSize; +#else + private readonly int _bufferSize = 0; #endif + private readonly MqttClientTcpOptions _options; + private Socket _socket; private SslStream _sslStream; @@ -33,6 +35,7 @@ namespace MQTTnet.Implementations public MqttTcpChannel(MqttClientTcpOptions options) { _options = options ?? throw new ArgumentNullException(nameof(options)); + _bufferSize = options.BufferSize; } /// @@ -44,12 +47,11 @@ namespace MQTTnet.Implementations _socket = socket ?? throw new ArgumentNullException(nameof(socket)); _sslStream = sslStream; - CreateStreams(socket, sslStream); + CreateStreams(); } public Stream SendStream { get; private set; } public Stream ReceiveStream { get; private set; } - public Stream RawReceiveStream { get; private set; } public static Func CustomCertificateValidationCallback { get; set; } @@ -60,8 +62,7 @@ namespace MQTTnet.Implementations _socket = new Socket(SocketType.Stream, ProtocolType.Tcp); } - //todo: else brach can be used with min dependency NET46 -#if NET451 +#if NET452 || NET461 await Task.Factory.FromAsync(_socket.BeginConnect, _socket.EndConnect, _options.Server, _options.GetPort(), null).ConfigureAwait(false); #else await _socket.ConnectAsync(_options.Server, _options.GetPort()).ConfigureAwait(false); @@ -70,10 +71,10 @@ namespace MQTTnet.Implementations if (_options.TlsOptions.UseTls) { _sslStream = new SslStream(new NetworkStream(_socket, true), false, InternalUserCertificateValidationCallback); - await _sslStream.AuthenticateAsClientAsync(_options.Server, LoadCertificates(_options), SslProtocols.Tls12, _options.TlsOptions.IgnoreCertificateRevocationErrors).ConfigureAwait(false); + await _sslStream.AuthenticateAsClientAsync(_options.Server, LoadCertificates(), SslProtocols.Tls12, _options.TlsOptions.IgnoreCertificateRevocationErrors).ConfigureAwait(false); } - - CreateStreams(_socket, _sslStream); + + CreateStreams(); } public Task DisconnectAsync() @@ -84,11 +85,70 @@ namespace MQTTnet.Implementations public void Dispose() { - _socket?.Dispose(); - _socket = null; + var oneStreamIsUsed = SendStream != null && ReceiveStream != null && ReferenceEquals(SendStream, ReceiveStream); - _sslStream?.Dispose(); - _sslStream = null; + try + { + SendStream?.Dispose(); + } + catch (ObjectDisposedException) + { + } + catch (NullReferenceException) + { + } + finally + { + SendStream = null; + } + + try + { + if (!oneStreamIsUsed) + { + ReceiveStream?.Dispose(); + } + } + catch (ObjectDisposedException) + { + } + catch (NullReferenceException) + { + } + finally + { + ReceiveStream = null; + } + + try + { + _sslStream?.Dispose(); + } + catch (ObjectDisposedException) + { + } + catch (NullReferenceException) + { + } + finally + { + _sslStream = null; + } + + try + { + _socket?.Dispose(); + } + catch (ObjectDisposedException) + { + } + catch (NullReferenceException) + { + } + finally + { + _socket = null; + } } private bool InternalUserCertificateValidationCallback(object sender, X509Certificate x509Certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) @@ -122,40 +182,42 @@ namespace MQTTnet.Implementations return _options.TlsOptions.AllowUntrustedCertificates; } - private static X509CertificateCollection LoadCertificates(MqttClientTcpOptions options) + private X509CertificateCollection LoadCertificates() { var certificates = new X509CertificateCollection(); - if (options.TlsOptions.Certificates == null) + if (_options.TlsOptions.Certificates == null) { return certificates; } - foreach (var certificate in options.TlsOptions.Certificates) + foreach (var certificate in _options.TlsOptions.Certificates) { - certificates.Add(new X509Certificate(certificate)); + certificates.Add(new X509Certificate2(certificate)); } return certificates; } - private void CreateStreams(Socket socket, Stream sslStream) + private void CreateStreams() { - RawReceiveStream = sslStream ?? new NetworkStream(socket); - - - //cannot use this as default buffering prevents from receiving the first connect message - //need two streams otherwise read and write have to be synchronized - - //todo: if branch can be used with min dependency NetStandard1.6 -#if NET45 - SendStream = new BufferedStream(RawReceiveStream, BufferSize); - ReceiveStream = new BufferedStream(RawReceiveStream, BufferSize); + Stream stream; + if (_sslStream != null) + { + stream = _sslStream; + } + else + { + stream = new NetworkStream(_socket, true); + } + +#if NET452 || NET461 + SendStream = new BufferedStream(stream, _bufferSize); + ReceiveStream = new BufferedStream(stream, _bufferSize); #else - SendStream = RawReceiveStream; - ReceiveStream = RawReceiveStream; + SendStream = stream; + ReceiveStream = stream; #endif } - } } -#endif \ No newline at end of file +#endif diff --git a/Frameworks/MQTTnet.NetStandard/Implementations/Uap/MqttServerAdapter.cs b/Frameworks/MQTTnet.NetStandard/Implementations/MqttTcpServerAdapter.Uwp.cs similarity index 63% rename from Frameworks/MQTTnet.NetStandard/Implementations/Uap/MqttServerAdapter.cs rename to Frameworks/MQTTnet.NetStandard/Implementations/MqttTcpServerAdapter.Uwp.cs index 7d9d25f1..e71d6fff 100644 --- a/Frameworks/MQTTnet.NetStandard/Implementations/Uap/MqttServerAdapter.cs +++ b/Frameworks/MQTTnet.NetStandard/Implementations/MqttTcpServerAdapter.Uwp.cs @@ -1,29 +1,27 @@ -#if NET451 || NETSTANDARD1_3 -#else +#if WINDOWS_UWP using System; using System.Threading.Tasks; -using MQTTnet.Core.Adapter; -using MQTTnet.Core.Server; using Windows.Networking.Sockets; -using Microsoft.Extensions.Logging; +using MQTTnet.Adapter; +using MQTTnet.Diagnostics; +using MQTTnet.Serializer; +using MQTTnet.Server; namespace MQTTnet.Implementations { - public class MqttServerAdapter : IMqttServerAdapter, IDisposable + public class MqttTcpServerAdapter : IMqttServerAdapter, IDisposable { - private readonly ILogger _logger; - private readonly IMqttCommunicationAdapterFactory _mqttCommunicationAdapterFactory; + private readonly IMqttNetLogger _logger; private StreamSocketListener _defaultEndpointSocket; - public MqttServerAdapter(ILogger logger, IMqttCommunicationAdapterFactory mqttCommunicationAdapterFactory) + public MqttTcpServerAdapter(IMqttNetLogger logger) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _mqttCommunicationAdapterFactory = mqttCommunicationAdapterFactory ?? throw new ArgumentNullException(nameof(mqttCommunicationAdapterFactory)); } public event EventHandler ClientAccepted; - public async Task StartAsync(MqttServerOptions options) + public async Task StartAsync(IMqttServerOptions options) { if (options == null) throw new ArgumentNullException(nameof(options)); @@ -32,7 +30,6 @@ namespace MQTTnet.Implementations if (options.DefaultEndpointOptions.IsEnabled) { _defaultEndpointSocket = new StreamSocketListener(); - _defaultEndpointSocket.Control.NoDelay = true; await _defaultEndpointSocket.BindServiceNameAsync(options.GetDefaultEndpointPort().ToString(), SocketProtectionLevel.PlainSocket); _defaultEndpointSocket.ConnectionReceived += AcceptDefaultEndpointConnectionsAsync; } @@ -45,6 +42,11 @@ namespace MQTTnet.Implementations public Task StopAsync() { + if (_defaultEndpointSocket != null) + { + _defaultEndpointSocket.ConnectionReceived -= AcceptDefaultEndpointConnectionsAsync; + } + _defaultEndpointSocket?.Dispose(); _defaultEndpointSocket = null; @@ -60,14 +62,12 @@ namespace MQTTnet.Implementations { try { - args.Socket.Control.NoDelay = true; - - var clientAdapter = _mqttCommunicationAdapterFactory.CreateServerMqttCommunicationAdapter(new MqttTcpChannel(args.Socket)); + var clientAdapter = new MqttChannelAdapter(new MqttTcpChannel(args.Socket), new MqttPacketSerializer(), _logger); ClientAccepted?.Invoke(this, new MqttServerAdapterClientAcceptedEventArgs(clientAdapter)); } catch (Exception exception) { - _logger.LogError(new EventId(), exception, "Error while accepting connection at default endpoint."); + _logger.Error(exception, "Error while accepting connection at default endpoint."); } } } diff --git a/Frameworks/MQTTnet.NetStandard/Implementations/NetStandard/MqttServerAdapter.cs b/Frameworks/MQTTnet.NetStandard/Implementations/MqttTcpServerAdapter.cs similarity index 64% rename from Frameworks/MQTTnet.NetStandard/Implementations/NetStandard/MqttServerAdapter.cs rename to Frameworks/MQTTnet.NetStandard/Implementations/MqttTcpServerAdapter.cs index 1a6e5b40..dc33a3f2 100644 --- a/Frameworks/MQTTnet.NetStandard/Implementations/NetStandard/MqttServerAdapter.cs +++ b/Frameworks/MQTTnet.NetStandard/Implementations/MqttTcpServerAdapter.cs @@ -1,5 +1,4 @@ -#if NET451 || NETSTANDARD1_3 - +#if NET452 || NET461 || NETSTANDARD1_3 || NETSTANDARD2_0 using System; using System.Net; using System.Net.Security; @@ -8,31 +7,30 @@ using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; -using MQTTnet.Core.Adapter; -using MQTTnet.Core.Server; -using Microsoft.Extensions.Logging; +using MQTTnet.Adapter; +using MQTTnet.Diagnostics; +using MQTTnet.Serializer; +using MQTTnet.Server; namespace MQTTnet.Implementations { - public class MqttServerAdapter : IMqttServerAdapter, IDisposable + public class MqttTcpServerAdapter : IMqttServerAdapter, IDisposable { - private readonly ILogger _logger; - private readonly IMqttCommunicationAdapterFactory _mqttCommunicationAdapterFactory; + private readonly IMqttNetLogger _logger; private CancellationTokenSource _cancellationTokenSource; private Socket _defaultEndpointSocket; private Socket _tlsEndpointSocket; private X509Certificate2 _tlsCertificate; - public MqttServerAdapter(ILogger logger, IMqttCommunicationAdapterFactory mqttCommunicationAdapterFactory) + public MqttTcpServerAdapter(IMqttNetLogger logger) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _mqttCommunicationAdapterFactory = mqttCommunicationAdapterFactory ?? throw new ArgumentNullException(nameof(mqttCommunicationAdapterFactory)); } public event EventHandler ClientAccepted; - public Task StartAsync(MqttServerOptions options) + public Task StartAsync(IMqttServerOptions options) { if (_cancellationTokenSource != null) throw new InvalidOperationException("Server is already started."); @@ -41,10 +39,10 @@ namespace MQTTnet.Implementations if (options.DefaultEndpointOptions.IsEnabled) { _defaultEndpointSocket = new Socket(SocketType.Stream, ProtocolType.Tcp); - _defaultEndpointSocket.Bind(new IPEndPoint(IPAddress.Any, options.GetDefaultEndpointPort())); + _defaultEndpointSocket.Bind(new IPEndPoint(options.DefaultEndpointOptions.BoundIPAddress, options.GetDefaultEndpointPort())); _defaultEndpointSocket.Listen(options.ConnectionBacklog); - Task.Run(() => AcceptDefaultEndpointConnectionsAsync(_cancellationTokenSource.Token), _cancellationTokenSource.Token); + Task.Run(async () => await AcceptDefaultEndpointConnectionsAsync(_cancellationTokenSource.Token).ConfigureAwait(false), _cancellationTokenSource.Token).ConfigureAwait(false); } if (options.TlsEndpointOptions.IsEnabled) @@ -61,10 +59,10 @@ namespace MQTTnet.Implementations } _tlsEndpointSocket = new Socket(SocketType.Stream, ProtocolType.Tcp); - _tlsEndpointSocket.Bind(new IPEndPoint(IPAddress.Any, options.GetTlsEndpointPort())); + _tlsEndpointSocket.Bind(new IPEndPoint(options.TlsEndpointOptions.BoundIPAddress, options.GetTlsEndpointPort())); _tlsEndpointSocket.Listen(options.ConnectionBacklog); - Task.Run(() => AcceptTlsEndpointConnectionsAsync(_cancellationTokenSource.Token), _cancellationTokenSource.Token); + Task.Run(async () => await AcceptTlsEndpointConnectionsAsync(_cancellationTokenSource.Token).ConfigureAwait(false), _cancellationTokenSource.Token).ConfigureAwait(false); } return Task.FromResult(0); @@ -99,19 +97,27 @@ namespace MQTTnet.Implementations try { //todo: else branch can be used with min dependency NET46 -#if NET451 +#if NET452 || NET461 var clientSocket = await Task.Factory.FromAsync(_defaultEndpointSocket.BeginAccept, _defaultEndpointSocket.EndAccept, null).ConfigureAwait(false); #else var clientSocket = await _defaultEndpointSocket.AcceptAsync().ConfigureAwait(false); #endif - var clientAdapter = _mqttCommunicationAdapterFactory.CreateServerMqttCommunicationAdapter(new MqttTcpChannel(clientSocket, null)); + + var clientAdapter = new MqttChannelAdapter(new MqttTcpChannel(clientSocket, null), new MqttPacketSerializer(), _logger); ClientAccepted?.Invoke(this, new MqttServerAdapterClientAcceptedEventArgs(clientAdapter)); } + catch (ObjectDisposedException) + { + // It can happen that the listener socket is accessed after the cancellation token is already set and the listener socket is disposed. + } catch (Exception exception) { - _logger.LogError(new EventId(), exception, "Error while accepting connection at default endpoint."); + if (exception is SocketException s && s.SocketErrorCode == SocketError.OperationAborted) + { + return; + } - //excessive CPU consumed if in endless loop of socket errors + _logger.Error(exception, "Error while accepting connection at default endpoint."); await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false); } } @@ -123,7 +129,7 @@ namespace MQTTnet.Implementations { try { -#if NET451 +#if NET452 || NET461 var clientSocket = await Task.Factory.FromAsync(_tlsEndpointSocket.BeginAccept, _tlsEndpointSocket.EndAccept, null).ConfigureAwait(false); #else var clientSocket = await _tlsEndpointSocket.AcceptAsync().ConfigureAwait(false); @@ -131,15 +137,22 @@ namespace MQTTnet.Implementations var sslStream = new SslStream(new NetworkStream(clientSocket)); await sslStream.AuthenticateAsServerAsync(_tlsCertificate, false, SslProtocols.Tls12, false).ConfigureAwait(false); - - var clientAdapter = _mqttCommunicationAdapterFactory.CreateServerMqttCommunicationAdapter(new MqttTcpChannel(clientSocket, sslStream)); + + var clientAdapter = new MqttChannelAdapter(new MqttTcpChannel(clientSocket, sslStream), new MqttPacketSerializer(), _logger); ClientAccepted?.Invoke(this, new MqttServerAdapterClientAcceptedEventArgs(clientAdapter)); } + catch (ObjectDisposedException) + { + // It can happen that the listener socket is accessed after the cancellation token is already set and the listener socket is disposed. + } catch (Exception exception) { - _logger.LogError(new EventId(), exception, "Error while accepting connection at TLS endpoint."); + if (exception is SocketException s && s.SocketErrorCode == SocketError.OperationAborted) + { + return; + } - //excessive CPU consumed if in endless loop of socket errors + _logger.Error(exception, "Error while accepting connection at TLS endpoint."); await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false); } } diff --git a/Frameworks/MQTTnet.NetStandard/Implementations/MqttWebSocketChannel.cs b/Frameworks/MQTTnet.NetStandard/Implementations/MqttWebSocketChannel.cs index c120775b..057a07e5 100644 --- a/Frameworks/MQTTnet.NetStandard/Implementations/MqttWebSocketChannel.cs +++ b/Frameworks/MQTTnet.NetStandard/Implementations/MqttWebSocketChannel.cs @@ -1,16 +1,20 @@ -using MQTTnet.Core.Channel; -using MQTTnet.Core.Client; -using System; +using System; using System.IO; using System.Net.WebSockets; using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; +using MQTTnet.Channel; +using MQTTnet.Client; namespace MQTTnet.Implementations { - public sealed class MqttWebSocketChannel : IMqttCommunicationChannel, IDisposable + public sealed class MqttWebSocketChannel : IMqttChannel { + // ReSharper disable once MemberCanBePrivate.Global + // ReSharper disable once AutoPropertyCanBeMadeGetOnly.Global + public static int BufferSize { get; set; } = 4096 * 20; // Can be changed for fine tuning by library user. + private readonly MqttClientWebSocketOptions _options; private ClientWebSocket _webSocket; @@ -19,20 +23,26 @@ namespace MQTTnet.Implementations _options = options ?? throw new ArgumentNullException(nameof(options)); } - public Stream SendStream => RawReceiveStream; - public Stream ReceiveStream => RawReceiveStream; - public Stream RawReceiveStream { get; private set; } + public Stream SendStream { get; private set; } + public Stream ReceiveStream { get; private set; } public async Task ConnectAsync() { var uri = _options.Uri; - if (!uri.StartsWith("ws://", StringComparison.OrdinalIgnoreCase)) + if (!uri.StartsWith("ws://", StringComparison.OrdinalIgnoreCase) && !uri.StartsWith("wss://", StringComparison.OrdinalIgnoreCase)) { - uri = "ws://" + uri; + if (_options.TlsOptions?.UseTls == false) + { + uri = "ws://" + uri; + } + else + { + uri = "wss://" + uri; + } } _webSocket = new ClientWebSocket(); - + if (_options.RequestHeaders != null) { foreach (var requestHeader in _options.RequestHeaders) @@ -63,14 +73,14 @@ namespace MQTTnet.Implementations } } - await _webSocket.ConnectAsync(new Uri(uri), CancellationToken.None); - RawReceiveStream = new WebSocketStream(_webSocket); + await _webSocket.ConnectAsync(new Uri(uri), CancellationToken.None).ConfigureAwait(false); + + SendStream = new WebSocketStream(_webSocket); + ReceiveStream = SendStream; } public async Task DisconnectAsync() { - RawReceiveStream = null; - if (_webSocket == null) { return; @@ -80,12 +90,23 @@ namespace MQTTnet.Implementations { await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None).ConfigureAwait(false); } + + Dispose(); } public void Dispose() { - _webSocket?.Dispose(); - _webSocket = null; + try + { + _webSocket?.Dispose(); + } + catch (ObjectDisposedException) + { + } + finally + { + _webSocket = null; + } } } } \ No newline at end of file diff --git a/Frameworks/MQTTnet.NetStandard/Implementations/WebSocketStream.cs b/Frameworks/MQTTnet.NetStandard/Implementations/WebSocketStream.cs index c27e4801..bad03935 100644 --- a/Frameworks/MQTTnet.NetStandard/Implementations/WebSocketStream.cs +++ b/Frameworks/MQTTnet.NetStandard/Implementations/WebSocketStream.cs @@ -1,16 +1,20 @@ using System; +using System.Collections.Generic; using System.IO; +using System.Linq; using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; -using MQTTnet.Core.Exceptions; +using MQTTnet.Exceptions; namespace MQTTnet.Implementations { public class WebSocketStream : Stream { + private readonly byte[] _chunckBuffer = new byte[MqttWebSocketChannel.BufferSize]; + private readonly Queue _buffer = new Queue(MqttWebSocketChannel.BufferSize); private readonly WebSocket _webSocket; - + public WebSocketStream(WebSocket webSocket) { _webSocket = webSocket ?? throw new ArgumentNullException(nameof(webSocket)); @@ -34,33 +38,9 @@ namespace MQTTnet.Implementations { } - public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + public override Task FlushAsync(CancellationToken cancellationToken) { - var currentOffset = offset; - var targetOffset = offset + count; - while (_webSocket.State == WebSocketState.Open && currentOffset < targetOffset) - { - var response = await _webSocket.ReceiveAsync(new ArraySegment(buffer, currentOffset, count), cancellationToken).ConfigureAwait(false); - currentOffset += response.Count; - count -= response.Count; - - if (response.MessageType == WebSocketMessageType.Close) - { - await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, cancellationToken).ConfigureAwait(false); - } - } - - if (_webSocket.State == WebSocketState.Closed) - { - throw new MqttCommunicationException( "connection closed" ); - } - - return currentOffset - offset; - } - - public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - return _webSocket.SendAsync(new ArraySegment(buffer, offset, count), WebSocketMessageType.Binary, true, cancellationToken); + return Task.FromResult(0); } public override int Read(byte[] buffer, int offset, int count) @@ -68,11 +48,61 @@ namespace MQTTnet.Implementations return ReadAsync(buffer, offset, count).GetAwaiter().GetResult(); } + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + var bytesRead = 0; + + // Use existing date from buffer. + while (count > 0 && _buffer.Any()) + { + buffer[offset] = _buffer.Dequeue(); + count--; + bytesRead++; + offset++; + } + + if (count == 0) + { + return bytesRead; + } + + // Fetch new data if the buffer is not full. + while (_webSocket.State == WebSocketState.Open) + { + await FetchChunkAsync(cancellationToken).ConfigureAwait(false); + + while (count > 0 && _buffer.Any()) + { + buffer[offset] = _buffer.Dequeue(); + count--; + bytesRead++; + offset++; + } + + if (count == 0) + { + return bytesRead; + } + } + + if (_webSocket.State == WebSocketState.Closed) + { + throw new MqttCommunicationException("WebSocket connection closed."); + } + + return bytesRead; + } + public override void Write(byte[] buffer, int offset, int count) { WriteAsync(buffer, offset, count).GetAwaiter().GetResult(); } + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return _webSocket.SendAsync(new ArraySegment(buffer, offset, count), WebSocketMessageType.Binary, true, cancellationToken); + } + public override long Seek(long offset, SeekOrigin origin) { throw new NotSupportedException(); @@ -82,5 +112,26 @@ namespace MQTTnet.Implementations { throw new NotSupportedException(); } + + private async Task FetchChunkAsync(CancellationToken cancellationToken) + { + var response = await _webSocket.ReceiveAsync(new ArraySegment(_chunckBuffer, 0, _chunckBuffer.Length), cancellationToken).ConfigureAwait(false); + + if (response.MessageType == WebSocketMessageType.Close) + { + await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, cancellationToken).ConfigureAwait(false); + } + else if (response.MessageType == WebSocketMessageType.Binary) + { + for (var i = 0; i < response.Count; i++) + { + _buffer.Enqueue(_chunckBuffer[i]); + } + } + else if (response.MessageType == WebSocketMessageType.Text) + { + throw new MqttProtocolViolationException("WebSocket channel received TEXT message."); + } + } } } diff --git a/MQTTnet.Core/Internal/MqttApplicationMessageExtensions.cs b/Frameworks/MQTTnet.NetStandard/Internal/MqttApplicationMessageExtensions.cs similarity index 94% rename from MQTTnet.Core/Internal/MqttApplicationMessageExtensions.cs rename to Frameworks/MQTTnet.NetStandard/Internal/MqttApplicationMessageExtensions.cs index 7ca7d005..b1d1a389 100644 --- a/MQTTnet.Core/Internal/MqttApplicationMessageExtensions.cs +++ b/Frameworks/MQTTnet.NetStandard/Internal/MqttApplicationMessageExtensions.cs @@ -1,6 +1,6 @@ -using MQTTnet.Core.Packets; +using MQTTnet.Packets; -namespace MQTTnet.Core.Internal +namespace MQTTnet.Internal { internal static class MqttApplicationMessageExtensions { diff --git a/MQTTnet.Core/Internal/TaskExtensions.cs b/Frameworks/MQTTnet.NetStandard/Internal/TaskExtensions.cs similarity index 74% rename from MQTTnet.Core/Internal/TaskExtensions.cs rename to Frameworks/MQTTnet.NetStandard/Internal/TaskExtensions.cs index 45b3510b..876c5495 100644 --- a/MQTTnet.Core/Internal/TaskExtensions.cs +++ b/Frameworks/MQTTnet.NetStandard/Internal/TaskExtensions.cs @@ -1,21 +1,23 @@ using System; using System.Threading; using System.Threading.Tasks; -using MQTTnet.Core.Exceptions; +using MQTTnet.Exceptions; -namespace MQTTnet.Core.Internal +namespace MQTTnet.Internal { public static class TaskExtensions { public static async Task TimeoutAfter(this Task task, TimeSpan timeout) { - using (var cancellationTokenSource = new CancellationTokenSource()) + if (task == null) throw new ArgumentNullException(nameof(task)); + + using (var timeoutCts = new CancellationTokenSource()) { try { - var timeoutTask = Task.Delay(timeout, cancellationTokenSource.Token); + var timeoutTask = Task.Delay(timeout, timeoutCts.Token); var finishedTask = await Task.WhenAny(timeoutTask, task).ConfigureAwait(false); - + if (finishedTask == timeoutTask) { throw new MqttCommunicationTimedOutException(); @@ -28,23 +30,25 @@ namespace MQTTnet.Core.Internal if (task.IsFaulted) { - throw new MqttCommunicationException(task.Exception.GetBaseException()); + throw new MqttCommunicationException(task.Exception?.GetBaseException()); } } finally { - cancellationTokenSource.Cancel(); + timeoutCts.Cancel(); } } } public static async Task TimeoutAfter(this Task task, TimeSpan timeout) { - using (var cancellationTokenSource = new CancellationTokenSource()) + if (task == null) throw new ArgumentNullException(nameof(task)); + + using (var timeoutCts = new CancellationTokenSource()) { try { - var timeoutTask = Task.Delay(timeout, cancellationTokenSource.Token); + var timeoutTask = Task.Delay(timeout, timeoutCts.Token); var finishedTask = await Task.WhenAny(timeoutTask, task).ConfigureAwait(false); if (finishedTask == timeoutTask) @@ -66,7 +70,7 @@ namespace MQTTnet.Core.Internal } finally { - cancellationTokenSource.Cancel(); + timeoutCts.Cancel(); } } } diff --git a/Frameworks/MQTTnet.NetStandard/MQTTnet.Netstandard.csproj b/Frameworks/MQTTnet.NetStandard/MQTTnet.Netstandard.csproj index 37b7e137..cb154c24 100644 --- a/Frameworks/MQTTnet.NetStandard/MQTTnet.Netstandard.csproj +++ b/Frameworks/MQTTnet.NetStandard/MQTTnet.Netstandard.csproj @@ -1,54 +1,65 @@  - netstandard1.3;net451;uap10.0 + netstandard1.3;netstandard2.0;net452;net461;uap10.0 + netstandard1.3;netstandard2.0 MQTTnet MQTTnet - 2.5.0.0 - 2.5.0.0 + False + Full + 0.0.0.0 + 0.0.0.0 0.0.0.0 + false + false false UAP,Version=v10.0 UAP - 10.0.15063.0 + 10.0.16299.0 10.0.10240.0 .NETCore v5.0 $(DefineConstants);WINDOWS_UWP + en $(MSBuildExtensionsPath)\Microsoft\WindowsXaml\v$(VisualStudioVersion)\Microsoft.Windows.UI.Xaml.CSharp.targets - - - - - + + RELEASE;NETSTANDARD1_3 + - - + + + + - - + + + + + + + \ No newline at end of file diff --git a/Frameworks/MQTTnet.NetStandard/ManagedClient/ApplicationMessageProcessedEventArgs.cs b/Frameworks/MQTTnet.NetStandard/ManagedClient/ApplicationMessageProcessedEventArgs.cs new file mode 100644 index 00000000..fa82c0a4 --- /dev/null +++ b/Frameworks/MQTTnet.NetStandard/ManagedClient/ApplicationMessageProcessedEventArgs.cs @@ -0,0 +1,19 @@ +using System; + +namespace MQTTnet.ManagedClient +{ + public class ApplicationMessageProcessedEventArgs : EventArgs + { + public ApplicationMessageProcessedEventArgs(MqttApplicationMessage applicationMessage, Exception exception) + { + ApplicationMessage = applicationMessage ?? throw new ArgumentNullException(nameof(applicationMessage)); + Exception = exception; + } + + public MqttApplicationMessage ApplicationMessage { get; } + public Exception Exception { get; } + + public bool HasFailed => Exception != null; + public bool HasSucceeded => Exception == null; + } +} diff --git a/MQTTnet.Core/ManagedClient/IManagedMqttClient.cs b/Frameworks/MQTTnet.NetStandard/ManagedClient/IManagedMqttClient.cs similarity index 62% rename from MQTTnet.Core/ManagedClient/IManagedMqttClient.cs rename to Frameworks/MQTTnet.NetStandard/ManagedClient/IManagedMqttClient.cs index 8d2ac8cc..97cdeca9 100644 --- a/MQTTnet.Core/ManagedClient/IManagedMqttClient.cs +++ b/Frameworks/MQTTnet.NetStandard/ManagedClient/IManagedMqttClient.cs @@ -1,21 +1,24 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using MQTTnet.Core.Client; +using MQTTnet.Client; -namespace MQTTnet.Core.ManagedClient +namespace MQTTnet.ManagedClient { - public interface IManagedMqttClient : IApplicationMessageReceiver, IApplicationMessagePublisher + public interface IManagedMqttClient : IApplicationMessageReceiver, IApplicationMessagePublisher, IDisposable { + bool IsStarted { get; } bool IsConnected { get; } event EventHandler Connected; event EventHandler Disconnected; + event EventHandler ApplicationMessageProcessed; + Task StartAsync(IManagedMqttClientOptions options); Task StopAsync(); Task SubscribeAsync(IEnumerable topicFilters); - Task UnsubscribeAsync(IEnumerable topicFilters); + Task UnsubscribeAsync(IEnumerable topics); } } \ No newline at end of file diff --git a/MQTTnet.Core/ManagedClient/IManagedMqttClientOptions.cs b/Frameworks/MQTTnet.NetStandard/ManagedClient/IManagedMqttClientOptions.cs similarity index 77% rename from MQTTnet.Core/ManagedClient/IManagedMqttClientOptions.cs rename to Frameworks/MQTTnet.NetStandard/ManagedClient/IManagedMqttClientOptions.cs index 298de9a1..fbb87b79 100644 --- a/MQTTnet.Core/ManagedClient/IManagedMqttClientOptions.cs +++ b/Frameworks/MQTTnet.NetStandard/ManagedClient/IManagedMqttClientOptions.cs @@ -1,7 +1,7 @@ using System; -using MQTTnet.Core.Client; +using MQTTnet.Client; -namespace MQTTnet.Core.ManagedClient +namespace MQTTnet.ManagedClient { public interface IManagedMqttClientOptions { diff --git a/MQTTnet.Core/ManagedClient/IManagedMqttClientStorage.cs b/Frameworks/MQTTnet.NetStandard/ManagedClient/IManagedMqttClientStorage.cs similarity index 88% rename from MQTTnet.Core/ManagedClient/IManagedMqttClientStorage.cs rename to Frameworks/MQTTnet.NetStandard/ManagedClient/IManagedMqttClientStorage.cs index 39ea89cc..28bf5703 100644 --- a/MQTTnet.Core/ManagedClient/IManagedMqttClientStorage.cs +++ b/Frameworks/MQTTnet.NetStandard/ManagedClient/IManagedMqttClientStorage.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Threading.Tasks; -namespace MQTTnet.Core.ManagedClient +namespace MQTTnet.ManagedClient { public interface IManagedMqttClientStorage { diff --git a/MQTTnet.Core/ManagedClient/ManagedMqttClient.cs b/Frameworks/MQTTnet.NetStandard/ManagedClient/ManagedMqttClient.cs similarity index 50% rename from MQTTnet.Core/ManagedClient/ManagedMqttClient.cs rename to Frameworks/MQTTnet.NetStandard/ManagedClient/ManagedMqttClient.cs index 4eb40b23..f1ce45d9 100644 --- a/MQTTnet.Core/ManagedClient/ManagedMqttClient.cs +++ b/Frameworks/MQTTnet.NetStandard/ManagedClient/ManagedMqttClient.cs @@ -4,29 +4,32 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; -using MQTTnet.Core.Client; -using MQTTnet.Core.Exceptions; -using MQTTnet.Core.Protocol; -using Microsoft.Extensions.Logging; +using MQTTnet.Client; +using MQTTnet.Diagnostics; +using MQTTnet.Exceptions; +using MQTTnet.Protocol; -namespace MQTTnet.Core.ManagedClient +namespace MQTTnet.ManagedClient { public class ManagedMqttClient : IManagedMqttClient { - private readonly ManagedMqttClientStorageManager _storageManager = new ManagedMqttClientStorageManager(); private readonly BlockingCollection _messageQueue = new BlockingCollection(); - private readonly HashSet _subscriptions = new HashSet(); + private readonly Dictionary _subscriptions = new Dictionary(); + private readonly SemaphoreSlim _subscriptionsSemaphore = new SemaphoreSlim(1, 1); + private readonly List _unsubscriptions = new List(); private readonly IMqttClient _mqttClient; - private readonly ILogger _logger; + private readonly IMqttNetLogger _logger; private CancellationTokenSource _connectionCancellationToken; private CancellationTokenSource _publishingCancellationToken; + private ManagedMqttClientStorageManager _storageManager; private IManagedMqttClientOptions _options; + private bool _subscriptionsNotPushed; - - public ManagedMqttClient(ILogger logger, IMqttClient mqttClient) + + public ManagedMqttClient(IMqttClient mqttClient, IMqttNetLogger logger) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _mqttClient = mqttClient ?? throw new ArgumentNullException(nameof(mqttClient)); @@ -37,10 +40,13 @@ namespace MQTTnet.Core.ManagedClient } public bool IsConnected => _mqttClient.IsConnected; + public bool IsStarted => _connectionCancellationToken != null; public event EventHandler Connected; public event EventHandler Disconnected; public event EventHandler ApplicationMessageReceived; + public event EventHandler ApplicationMessageProcessed; + public event EventHandler SynchronizingSubscriptionsFailed; public async Task StartAsync(IManagedMqttClientOptions options) { @@ -52,36 +58,29 @@ namespace MQTTnet.Core.ManagedClient throw new NotSupportedException("The managed client does not support existing sessions."); } - if (_connectionCancellationToken != null) - { - throw new InvalidOperationException("The managed client is already started."); - } + if (_connectionCancellationToken != null) throw new InvalidOperationException("The managed client is already started."); _options = options; - await _storageManager.SetStorageAsync(_options.Storage).ConfigureAwait(false); if (_options.Storage != null) { - var loadedMessages = await _options.Storage.LoadQueuedMessagesAsync().ConfigureAwait(false); - foreach (var loadedMessage in loadedMessages) - { - _messageQueue.Add(loadedMessage); - } + _storageManager = new ManagedMqttClientStorageManager(_options.Storage); + await _storageManager.LoadQueuedMessagesAsync().ConfigureAwait(false); } _connectionCancellationToken = new CancellationTokenSource(); - + #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed - Task.Factory.StartNew(() => MaintainConnectionAsync(_connectionCancellationToken.Token), _connectionCancellationToken.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default).ConfigureAwait(false); + Task.Run(async () => await MaintainConnectionAsync(_connectionCancellationToken.Token).ConfigureAwait(false), _connectionCancellationToken.Token).ConfigureAwait(false); #pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed - _logger.LogInformation("Started"); + _logger.Info("Started"); } public Task StopAsync() { - _connectionCancellationToken?.Cancel(false); - _connectionCancellationToken = null; + StopPublishing(); + StopMaintainingConnection(); while (_messageQueue.Any()) { @@ -97,43 +96,60 @@ namespace MQTTnet.Core.ManagedClient foreach (var applicationMessage in applicationMessages) { - await _storageManager.AddAsync(applicationMessage).ConfigureAwait(false); + if (_storageManager != null) + { + await _storageManager.AddAsync(applicationMessage).ConfigureAwait(false); + } + _messageQueue.Add(applicationMessage); } } - public Task SubscribeAsync(IEnumerable topicFilters) + public async Task SubscribeAsync(IEnumerable topicFilters) { if (topicFilters == null) throw new ArgumentNullException(nameof(topicFilters)); - lock (_subscriptions) + await _subscriptionsSemaphore.WaitAsync().ConfigureAwait(false); + try { foreach (var topicFilter in topicFilters) { - if (_subscriptions.Add(topicFilter)) - { - _subscriptionsNotPushed = true; - } + _subscriptions[topicFilter.Topic] = topicFilter.QualityOfServiceLevel; + _subscriptionsNotPushed = true; } } - - return Task.FromResult(0); + finally + { + _subscriptionsSemaphore.Release(); + } } - public Task UnsubscribeAsync(IEnumerable topicFilters) + public async Task UnsubscribeAsync(IEnumerable topics) { - lock (_subscriptions) + await _subscriptionsSemaphore.WaitAsync().ConfigureAwait(false); + try { - foreach (var topicFilter in topicFilters) + foreach (var topic in topics) { - if (_subscriptions.Remove(topicFilter)) + if (_subscriptions.Remove(topic)) { + _unsubscriptions.Add(topic); _subscriptionsNotPushed = true; } } } + finally + { + _subscriptionsSemaphore.Release(); + } + } - return Task.FromResult(0); + public void Dispose() + { + _messageQueue?.Dispose(); + _subscriptionsSemaphore?.Dispose(); + _connectionCancellationToken?.Dispose(); + _publishingCancellationToken?.Dispose(); } private async Task MaintainConnectionAsync(CancellationToken cancellationToken) @@ -142,33 +158,48 @@ namespace MQTTnet.Core.ManagedClient { while (!cancellationToken.IsCancellationRequested) { - var connectionState = await ReconnectIfRequiredAsync().ConfigureAwait(false); - if (connectionState == ReconnectionResult.NotConnected) - { - _publishingCancellationToken?.Cancel(false); - _publishingCancellationToken = null; + await TryMaintainConnectionAsync(cancellationToken).ConfigureAwait(false); + } + } + catch (OperationCanceledException) + { + } + catch (Exception exception) + { + _logger.Error(exception, "Unhandled exception while maintaining connection."); + } + finally + { + await _mqttClient.DisconnectAsync().ConfigureAwait(false); + _logger.Info("Stopped"); + } + } - await Task.Delay(_options.AutoReconnectDelay, cancellationToken).ConfigureAwait(false); - continue; - } + private async Task TryMaintainConnectionAsync(CancellationToken cancellationToken) + { + try + { + var connectionState = await ReconnectIfRequiredAsync().ConfigureAwait(false); + if (connectionState == ReconnectionResult.NotConnected) + { + StopPublishing(); + await Task.Delay(_options.AutoReconnectDelay, cancellationToken).ConfigureAwait(false); + return; + } - if (connectionState == ReconnectionResult.Reconnected || _subscriptionsNotPushed) - { - await PushSubscriptionsAsync(); + if (connectionState == ReconnectionResult.Reconnected || _subscriptionsNotPushed) + { + await SynchronizeSubscriptionsAsync().ConfigureAwait(false); - _publishingCancellationToken = new CancellationTokenSource(); + StartPublishing(); -#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed - Task.Factory.StartNew(() => PublishQueuedMessagesAsync(_publishingCancellationToken.Token), _publishingCancellationToken.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default).ConfigureAwait(false); -#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed - continue; - } + return; + } - if (connectionState == ReconnectionResult.StillConnected) - { - await Task.Delay(100, _connectionCancellationToken.Token).ConfigureAwait(false); // Consider using the _Disconnected_ event here. (TaskCompletionSource) - } + if (connectionState == ReconnectionResult.StillConnected) + { + await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false); } } catch (OperationCanceledException) @@ -176,16 +207,11 @@ namespace MQTTnet.Core.ManagedClient } catch (MqttCommunicationException exception) { - _logger.LogWarning(new EventId(), exception, "Communication exception while maintaining connection."); + _logger.Warning(exception, "Communication exception while maintaining connection."); } catch (Exception exception) { - _logger.LogError(new EventId(), exception, "Unhandled exception while maintaining connection."); - } - finally - { - await _mqttClient.DisconnectAsync().ConfigureAwait(false); - _logger.LogInformation("Stopped"); + _logger.Error(exception, "Unhandled exception while maintaining connection."); } } @@ -207,27 +233,38 @@ namespace MQTTnet.Core.ManagedClient } await TryPublishQueuedMessageAsync(message).ConfigureAwait(false); - await _storageManager.RemoveAsync(message).ConfigureAwait(false); } } catch (OperationCanceledException) { } + catch (Exception exception) + { + _logger.Error(exception, "Unhandled exception while publishing queued application messages."); + } finally { - _logger.LogInformation("Stopped publishing messages"); + _logger.Verbose("Stopped publishing messages."); } } private async Task TryPublishQueuedMessageAsync(MqttApplicationMessage message) { + Exception transmitException = null; try { await _mqttClient.PublishAsync(message).ConfigureAwait(false); + + if (_storageManager != null) + { + await _storageManager.RemoveAsync(message).ConfigureAwait(false); + } } catch (MqttCommunicationException exception) { - _logger.LogWarning(new EventId(), exception, "Publishing application message failed."); + transmitException = exception; + + _logger.Warning(exception, "Publishing application message failed."); if (message.QualityOfServiceLevel > MqttQualityOfServiceLevel.AtMostOnce) { @@ -236,34 +273,60 @@ namespace MQTTnet.Core.ManagedClient } catch (Exception exception) { - _logger.LogError(new EventId(), exception, "Unhandled exception while publishing queued application message."); + transmitException = exception; + _logger.Error(exception, "Unhandled exception while publishing queued application message."); + } + finally + { + ApplicationMessageProcessed?.Invoke(this, new ApplicationMessageProcessedEventArgs(message, transmitException)); } } - private async Task PushSubscriptionsAsync() + private async Task SynchronizeSubscriptionsAsync() { - _logger.LogInformation(nameof(ManagedMqttClient), "Synchronizing subscriptions"); + _logger.Info(nameof(ManagedMqttClient), "Synchronizing subscriptions"); List subscriptions; - lock (_subscriptions) + List unsubscriptions; + + await _subscriptionsSemaphore.WaitAsync().ConfigureAwait(false); + try { - subscriptions = _subscriptions.ToList(); + subscriptions = _subscriptions.Select(i => new TopicFilter(i.Key, i.Value)).ToList(); + + unsubscriptions = new List(_unsubscriptions); + _unsubscriptions.Clear(); + _subscriptionsNotPushed = false; } - - if (!_subscriptions.Any()) + finally + { + _subscriptionsSemaphore.Release(); + } + + if (!subscriptions.Any() && !unsubscriptions.Any()) { return; } try { - await _mqttClient.SubscribeAsync(subscriptions).ConfigureAwait(false); + if (subscriptions.Any()) + { + await _mqttClient.SubscribeAsync(subscriptions).ConfigureAwait(false); + } + + if (unsubscriptions.Any()) + { + await _mqttClient.UnsubscribeAsync(unsubscriptions).ConfigureAwait(false); + } } catch (Exception exception) { - _logger.LogWarning(new EventId(), exception, "Synchronizing subscriptions failed"); + _logger.Warning(exception, "Synchronizing subscriptions failed."); _subscriptionsNotPushed = true; + + SynchronizingSubscriptionsFailed?.Invoke(this, EventArgs.Empty); } } @@ -299,5 +362,35 @@ namespace MQTTnet.Core.ManagedClient { Connected?.Invoke(this, eventArgs); } + + private void StartPublishing() + { + if (_publishingCancellationToken != null) + { + StopPublishing(); + } + + var cts = new CancellationTokenSource(); + + _publishingCancellationToken = cts; + +#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + Task.Run(async () => await PublishQueuedMessagesAsync(cts.Token).ConfigureAwait(false), cts.Token).ConfigureAwait(false); +#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + } + + private void StopPublishing() + { + _publishingCancellationToken?.Cancel(false); + _publishingCancellationToken?.Dispose(); + _publishingCancellationToken = null; + } + + private void StopMaintainingConnection() + { + _connectionCancellationToken?.Cancel(false); + _connectionCancellationToken?.Dispose(); + _connectionCancellationToken = null; + } } } diff --git a/Frameworks/MQTTnet.NetStandard/ManagedClient/ManagedMqttClientExtensions.cs b/Frameworks/MQTTnet.NetStandard/ManagedClient/ManagedMqttClientExtensions.cs new file mode 100644 index 00000000..84b37858 --- /dev/null +++ b/Frameworks/MQTTnet.NetStandard/ManagedClient/ManagedMqttClientExtensions.cs @@ -0,0 +1,39 @@ +using System; +using System.Threading.Tasks; +using MQTTnet.Protocol; + +namespace MQTTnet.ManagedClient +{ + public static class ManagedMqttClientExtensions + { + public static Task SubscribeAsync(this IManagedMqttClient managedClient, params TopicFilter[] topicFilters) + { + if (managedClient == null) throw new ArgumentNullException(nameof(managedClient)); + + return managedClient.SubscribeAsync(topicFilters); + } + + public static Task SubscribeAsync(this IManagedMqttClient managedClient, string topic, MqttQualityOfServiceLevel qualityOfServiceLevel) + { + if (managedClient == null) throw new ArgumentNullException(nameof(managedClient)); + if (topic == null) throw new ArgumentNullException(nameof(topic)); + + return managedClient.SubscribeAsync(new TopicFilterBuilder().WithTopic(topic).WithQualityOfServiceLevel(qualityOfServiceLevel).Build()); + } + + public static Task SubscribeAsync(this IManagedMqttClient managedClient, string topic) + { + if (managedClient == null) throw new ArgumentNullException(nameof(managedClient)); + if (topic == null) throw new ArgumentNullException(nameof(topic)); + + return managedClient.SubscribeAsync(new TopicFilterBuilder().WithTopic(topic).Build()); + } + + public static Task UnsubscribeAsync(this IManagedMqttClient managedClient, params string[] topicFilters) + { + if (managedClient == null) throw new ArgumentNullException(nameof(managedClient)); + + return managedClient.UnsubscribeAsync(topicFilters); + } + } +} diff --git a/MQTTnet.Core/ManagedClient/ManagedMqttClientOptions.cs b/Frameworks/MQTTnet.NetStandard/ManagedClient/ManagedMqttClientOptions.cs similarity index 83% rename from MQTTnet.Core/ManagedClient/ManagedMqttClientOptions.cs rename to Frameworks/MQTTnet.NetStandard/ManagedClient/ManagedMqttClientOptions.cs index 2e24fd0a..813aee8a 100644 --- a/MQTTnet.Core/ManagedClient/ManagedMqttClientOptions.cs +++ b/Frameworks/MQTTnet.NetStandard/ManagedClient/ManagedMqttClientOptions.cs @@ -1,7 +1,7 @@ using System; -using MQTTnet.Core.Client; +using MQTTnet.Client; -namespace MQTTnet.Core.ManagedClient +namespace MQTTnet.ManagedClient { public class ManagedMqttClientOptions : IManagedMqttClientOptions { diff --git a/Frameworks/MQTTnet.NetStandard/ManagedClient/ManagedMqttClientOptionsBuilder.cs b/Frameworks/MQTTnet.NetStandard/ManagedClient/ManagedMqttClientOptionsBuilder.cs new file mode 100644 index 00000000..4d65bdbb --- /dev/null +++ b/Frameworks/MQTTnet.NetStandard/ManagedClient/ManagedMqttClientOptionsBuilder.cs @@ -0,0 +1,74 @@ +using System; +using MQTTnet.Client; + +namespace MQTTnet.ManagedClient +{ + public class ManagedMqttClientOptionsBuilder + { + private readonly ManagedMqttClientOptions _options = new ManagedMqttClientOptions(); + private MqttClientOptionsBuilder _clientOptionsBuilder; + + public ManagedMqttClientOptionsBuilder WithAutoReconnectDelay(TimeSpan value) + { + _options.AutoReconnectDelay = value; + return this; + } + + public ManagedMqttClientOptionsBuilder WithStorage(IManagedMqttClientStorage value) + { + _options.Storage = value; + return this; + } + + public ManagedMqttClientOptionsBuilder WithClientOptions(IMqttClientOptions value) + { + if (_clientOptionsBuilder != null) + { + throw new InvalidOperationException("Cannot use client options builder and client options at the same time."); + } + + _options.ClientOptions = value ?? throw new ArgumentNullException(nameof(value)); + + return this; + } + + public ManagedMqttClientOptionsBuilder WithClientOptions(MqttClientOptionsBuilder builder) + { + if (_options.ClientOptions != null) + { + throw new InvalidOperationException("Cannot use client options builder and client options at the same time."); + } + + _clientOptionsBuilder = builder; + return this; + } + + public ManagedMqttClientOptionsBuilder WithClientOptions(Action options) + { + if (options == null) throw new ArgumentNullException(nameof(options)); + + if (_clientOptionsBuilder == null) + { + _clientOptionsBuilder = new MqttClientOptionsBuilder(); + } + + options(_clientOptionsBuilder); + return this; + } + + public ManagedMqttClientOptions Build() + { + if (_clientOptionsBuilder != null) + { + _options.ClientOptions = _clientOptionsBuilder.Build(); + } + + if (_options.ClientOptions == null) + { + throw new InvalidOperationException("The ClientOptions cannot be null."); + } + + return _options; + } + } +} diff --git a/MQTTnet.Core/ManagedClient/ManagedMqttClientStorageManager.cs b/Frameworks/MQTTnet.NetStandard/ManagedClient/ManagedMqttClientStorageManager.cs similarity index 70% rename from MQTTnet.Core/ManagedClient/ManagedMqttClientStorageManager.cs rename to Frameworks/MQTTnet.NetStandard/ManagedClient/ManagedMqttClientStorageManager.cs index 0810f36e..0e71776e 100644 --- a/MQTTnet.Core/ManagedClient/ManagedMqttClientStorageManager.cs +++ b/Frameworks/MQTTnet.NetStandard/ManagedClient/ManagedMqttClientStorageManager.cs @@ -1,25 +1,27 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -namespace MQTTnet.Core.ManagedClient +namespace MQTTnet.ManagedClient { public class ManagedMqttClientStorageManager { private readonly List _applicationMessages = new List(); private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); - private IManagedMqttClientStorage _storage; + private readonly IManagedMqttClientStorage _storage; - public async Task SetStorageAsync(IManagedMqttClientStorage storage) + public ManagedMqttClientStorageManager(IManagedMqttClientStorage storage) { - await _semaphore.WaitAsync().ConfigureAwait(false); - try + _storage = storage ?? throw new ArgumentNullException(nameof(storage)); + } + + public async Task LoadQueuedMessagesAsync() + { + var loadedMessages = await _storage.LoadQueuedMessagesAsync().ConfigureAwait(false); + foreach (var loadedMessage in loadedMessages) { - _storage = storage; - } - finally - { - _semaphore.Release(); + _applicationMessages.Add(loadedMessage); } } @@ -28,11 +30,6 @@ namespace MQTTnet.Core.ManagedClient await _semaphore.WaitAsync().ConfigureAwait(false); try { - if (_storage == null) - { - return; - } - _applicationMessages.Add(applicationMessage); await SaveAsync().ConfigureAwait(false); } @@ -47,11 +44,6 @@ namespace MQTTnet.Core.ManagedClient await _semaphore.WaitAsync().ConfigureAwait(false); try { - if (_storage == null) - { - return; - } - var index = _applicationMessages.IndexOf(applicationMessage); if (index == -1) { diff --git a/MQTTnet.Core/ManagedClient/ReconnectionResult.cs b/Frameworks/MQTTnet.NetStandard/ManagedClient/ReconnectionResult.cs similarity index 74% rename from MQTTnet.Core/ManagedClient/ReconnectionResult.cs rename to Frameworks/MQTTnet.NetStandard/ManagedClient/ReconnectionResult.cs index fae07706..42566e3e 100644 --- a/MQTTnet.Core/ManagedClient/ReconnectionResult.cs +++ b/Frameworks/MQTTnet.NetStandard/ManagedClient/ReconnectionResult.cs @@ -1,4 +1,4 @@ -namespace MQTTnet.Core.ManagedClient +namespace MQTTnet.ManagedClient { public enum ReconnectionResult { diff --git a/MQTTnet.Core/MqttApplicationMessage.cs b/Frameworks/MQTTnet.NetStandard/MqttApplicationMessage.cs similarity index 94% rename from MQTTnet.Core/MqttApplicationMessage.cs rename to Frameworks/MQTTnet.NetStandard/MqttApplicationMessage.cs index b07ee18f..387acb21 100644 --- a/MQTTnet.Core/MqttApplicationMessage.cs +++ b/Frameworks/MQTTnet.NetStandard/MqttApplicationMessage.cs @@ -1,7 +1,7 @@ using System; -using MQTTnet.Core.Protocol; +using MQTTnet.Protocol; -namespace MQTTnet.Core +namespace MQTTnet { public sealed class MqttApplicationMessage { diff --git a/MQTTnet.Core/MqttApplicationMessageBuilder.cs b/Frameworks/MQTTnet.NetStandard/MqttApplicationMessageBuilder.cs similarity index 97% rename from MQTTnet.Core/MqttApplicationMessageBuilder.cs rename to Frameworks/MQTTnet.NetStandard/MqttApplicationMessageBuilder.cs index c2e04318..ce949b85 100644 --- a/MQTTnet.Core/MqttApplicationMessageBuilder.cs +++ b/Frameworks/MQTTnet.NetStandard/MqttApplicationMessageBuilder.cs @@ -2,10 +2,10 @@ using System.IO; using System.Linq; using System.Text; -using MQTTnet.Core.Exceptions; -using MQTTnet.Core.Protocol; +using MQTTnet.Exceptions; +using MQTTnet.Protocol; -namespace MQTTnet.Core +namespace MQTTnet { public class MqttApplicationMessageBuilder { diff --git a/Frameworks/MQTTnet.NetStandard/MqttApplicationMessageExtensions.cs b/Frameworks/MQTTnet.NetStandard/MqttApplicationMessageExtensions.cs new file mode 100644 index 00000000..dac9998a --- /dev/null +++ b/Frameworks/MQTTnet.NetStandard/MqttApplicationMessageExtensions.cs @@ -0,0 +1,25 @@ +using System; +using System.Text; + +namespace MQTTnet +{ + public static class MqttApplicationMessageExtensions + { + public static string ConvertPayloadToString(this MqttApplicationMessage applicationMessage) + { + if (applicationMessage == null) throw new ArgumentNullException(nameof(applicationMessage)); + + if (applicationMessage.Payload == null) + { + return null; + } + + if (applicationMessage.Payload.Length == 0) + { + return string.Empty; + } + + return Encoding.UTF8.GetString(applicationMessage.Payload, 0, applicationMessage.Payload.Length); + } + } +} diff --git a/MQTTnet.Core/MqttApplicationMessageReceivedEventArgs.cs b/Frameworks/MQTTnet.NetStandard/MqttApplicationMessageReceivedEventArgs.cs similarity index 95% rename from MQTTnet.Core/MqttApplicationMessageReceivedEventArgs.cs rename to Frameworks/MQTTnet.NetStandard/MqttApplicationMessageReceivedEventArgs.cs index 3de0db54..a16a81d1 100644 --- a/MQTTnet.Core/MqttApplicationMessageReceivedEventArgs.cs +++ b/Frameworks/MQTTnet.NetStandard/MqttApplicationMessageReceivedEventArgs.cs @@ -1,6 +1,6 @@ using System; -namespace MQTTnet.Core +namespace MQTTnet { public sealed class MqttApplicationMessageReceivedEventArgs : EventArgs { diff --git a/Frameworks/MQTTnet.NetStandard/MqttFactory.cs b/Frameworks/MQTTnet.NetStandard/MqttFactory.cs index 50cdddea..e3b3549a 100644 --- a/Frameworks/MQTTnet.NetStandard/MqttFactory.cs +++ b/Frameworks/MQTTnet.NetStandard/MqttFactory.cs @@ -1,130 +1,59 @@ using System; -using MQTTnet.Core.Adapter; -using MQTTnet.Core.Client; -using MQTTnet.Core.Serializer; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; +using System.Collections.Generic; +using MQTTnet.Adapter; +using MQTTnet.Client; +using MQTTnet.Diagnostics; using MQTTnet.Implementations; -using MQTTnet.Core.ManagedClient; -using MQTTnet.Core.Server; -using MQTTnet.Core.Channel; +using MQTTnet.ManagedClient; +using MQTTnet.Server; namespace MQTTnet { - public class MqttFactory : IMqttCommunicationAdapterFactory, IMqttClientSesssionFactory, IMqttClientFactory, IMqttServerFactory + public class MqttFactory : IMqttClientFactory, IMqttServerFactory { - private readonly IServiceProvider _serviceProvider; - - private static IServiceProvider BuildServiceProvider() - { - var serviceProvider = new ServiceCollection() - .AddMqttClient() - .AddMqttServer() - .AddLogging() - .BuildServiceProvider(); - - serviceProvider.GetRequiredService() - .AddMqttTrace(); - - return serviceProvider; - } - - public MqttFactory() - : this(BuildServiceProvider()) - { - } - - public MqttFactory(IServiceProvider serviceProvider) - { - _serviceProvider = serviceProvider; - } - - public ILoggerFactory GetLoggerFactory() - { - return _serviceProvider.GetRequiredService(); - } - - public IMqttCommunicationAdapter CreateClientMqttCommunicationAdapter(IMqttClientOptions options) - { - var logger = _serviceProvider.GetRequiredService>(); - return new MqttChannelCommunicationAdapter(CreateMqttCommunicationChannel(options.ChannelOptions), CreateSerializer(options.ProtocolVersion), logger); - } - - public IMqttCommunicationAdapter CreateServerMqttCommunicationAdapter(IMqttCommunicationChannel channel) - { - var serializer = _serviceProvider.GetRequiredService(); - var logger = _serviceProvider.GetRequiredService>(); - return new MqttChannelCommunicationAdapter(channel, serializer, logger); - } - - public IMqttCommunicationChannel CreateMqttCommunicationChannel(IMqttClientChannelOptions options) - { - if (options == null) throw new ArgumentNullException(nameof(options)); - - switch (options) - { - case MqttClientTcpOptions tcpOptions: - return CreateTcpChannel(tcpOptions); - case MqttClientWebSocketOptions webSocketOptions: - return CreateWebSocketChannel(webSocketOptions); - default: - throw new NotSupportedException(); - } - } - - public MqttTcpChannel CreateTcpChannel(MqttClientTcpOptions tcpOptions) - { - return new MqttTcpChannel(tcpOptions); - } - - public MqttWebSocketChannel CreateWebSocketChannel(MqttClientWebSocketOptions webSocketOptions) - { - return new MqttWebSocketChannel(webSocketOptions); - } - - public MqttPacketSerializer CreateSerializer(MqttProtocolVersion protocolVersion) - { - return new MqttPacketSerializer - { - ProtocolVersion = protocolVersion - }; - } - - public MqttClientSession CreateClientSession(string clientId, MqttClientSessionsManager clientSessionsManager) - { - return new MqttClientSession( - clientId, - _serviceProvider.GetRequiredService>(), - clientSessionsManager, - _serviceProvider.GetRequiredService(), - _serviceProvider.GetRequiredService>(), - _serviceProvider.GetRequiredService>()); - } - public IMqttClient CreateMqttClient() { - return _serviceProvider.GetRequiredService(); + return CreateMqttClient(new MqttNetLogger()); + } + + public IMqttClient CreateMqttClient(IMqttNetLogger logger) + { + if (logger == null) throw new ArgumentNullException(nameof(logger)); + + return new MqttClient(new MqttClientAdapterFactory(), logger); } public IManagedMqttClient CreateManagedMqttClient() { - return _serviceProvider.GetRequiredService(); + return new ManagedMqttClient(CreateMqttClient(), new MqttNetLogger()); + } + + public IManagedMqttClient CreateManagedMqttClient(IMqttNetLogger logger) + { + if (logger == null) throw new ArgumentNullException(nameof(logger)); + + return new ManagedMqttClient(CreateMqttClient(), logger); } public IMqttServer CreateMqttServer() { - return _serviceProvider.GetRequiredService(); + var logger = new MqttNetLogger(); + return CreateMqttServer(logger); } - public IMqttServer CreateMqttServer(Action configure) + public IMqttServer CreateMqttServer(IMqttNetLogger logger) { - if (configure == null) throw new ArgumentNullException(nameof(configure)); + if (logger == null) throw new ArgumentNullException(nameof(logger)); - var options = _serviceProvider.GetRequiredService>(); - configure(options.Value); + return CreateMqttServer(new List { new MqttTcpServerAdapter(logger) }, logger); + } - return _serviceProvider.GetRequiredService(); + public IMqttServer CreateMqttServer(IEnumerable adapters, IMqttNetLogger logger) + { + if (adapters == null) throw new ArgumentNullException(nameof(adapters)); + if (logger == null) throw new ArgumentNullException(nameof(logger)); + + return new MqttServer(adapters, logger); } } } \ No newline at end of file diff --git a/Frameworks/MQTTnet.NetStandard/Packets/IMqttPacketWithIdentifier.cs b/Frameworks/MQTTnet.NetStandard/Packets/IMqttPacketWithIdentifier.cs new file mode 100644 index 00000000..6bbce0cb --- /dev/null +++ b/Frameworks/MQTTnet.NetStandard/Packets/IMqttPacketWithIdentifier.cs @@ -0,0 +1,7 @@ +namespace MQTTnet.Packets +{ + public interface IMqttPacketWithIdentifier + { + ushort? PacketIdentifier { get; set; } + } +} diff --git a/MQTTnet.Core/Packets/MqttBasePacket.cs b/Frameworks/MQTTnet.NetStandard/Packets/MqttBasePacket.cs similarity index 62% rename from MQTTnet.Core/Packets/MqttBasePacket.cs rename to Frameworks/MQTTnet.NetStandard/Packets/MqttBasePacket.cs index 41901e52..d9640568 100644 --- a/MQTTnet.Core/Packets/MqttBasePacket.cs +++ b/Frameworks/MQTTnet.NetStandard/Packets/MqttBasePacket.cs @@ -1,4 +1,4 @@ -namespace MQTTnet.Core.Packets +namespace MQTTnet.Packets { public abstract class MqttBasePacket { diff --git a/MQTTnet.Core/Packets/MqttBasePublishPacket.cs b/Frameworks/MQTTnet.NetStandard/Packets/MqttBasePublishPacket.cs similarity index 53% rename from MQTTnet.Core/Packets/MqttBasePublishPacket.cs rename to Frameworks/MQTTnet.NetStandard/Packets/MqttBasePublishPacket.cs index 67d55b90..ffafc537 100644 --- a/MQTTnet.Core/Packets/MqttBasePublishPacket.cs +++ b/Frameworks/MQTTnet.NetStandard/Packets/MqttBasePublishPacket.cs @@ -1,7 +1,7 @@ -namespace MQTTnet.Core.Packets +namespace MQTTnet.Packets { public class MqttBasePublishPacket : MqttBasePacket, IMqttPacketWithIdentifier { - public ushort PacketIdentifier { get; set; } + public ushort? PacketIdentifier { get; set; } } } diff --git a/MQTTnet.Core/Packets/MqttConnAckPacket.cs b/Frameworks/MQTTnet.NetStandard/Packets/MqttConnAckPacket.cs similarity index 55% rename from MQTTnet.Core/Packets/MqttConnAckPacket.cs rename to Frameworks/MQTTnet.NetStandard/Packets/MqttConnAckPacket.cs index e80a0af5..9cf0adfe 100644 --- a/MQTTnet.Core/Packets/MqttConnAckPacket.cs +++ b/Frameworks/MQTTnet.NetStandard/Packets/MqttConnAckPacket.cs @@ -1,6 +1,6 @@ -using MQTTnet.Core.Protocol; +using MQTTnet.Protocol; -namespace MQTTnet.Core.Packets +namespace MQTTnet.Packets { public sealed class MqttConnAckPacket : MqttBasePacket { @@ -10,7 +10,7 @@ namespace MQTTnet.Core.Packets public override string ToString() { - return nameof(MqttConnAckPacket) + ": [ConnectReturnCode=" + ConnectReturnCode + "] [IsSessionPresent=" + IsSessionPresent + "]"; + return "ConnAck: [ConnectReturnCode=" + ConnectReturnCode + "] [IsSessionPresent=" + IsSessionPresent + "]"; } } } diff --git a/MQTTnet.Core/Packets/MqttConnectPacket.cs b/Frameworks/MQTTnet.NetStandard/Packets/MqttConnectPacket.cs similarity index 64% rename from MQTTnet.Core/Packets/MqttConnectPacket.cs rename to Frameworks/MQTTnet.NetStandard/Packets/MqttConnectPacket.cs index b1d6e3b9..b1943ced 100644 --- a/MQTTnet.Core/Packets/MqttConnectPacket.cs +++ b/Frameworks/MQTTnet.NetStandard/Packets/MqttConnectPacket.cs @@ -1,6 +1,6 @@ -using MQTTnet.Core.Serializer; +using MQTTnet.Serializer; -namespace MQTTnet.Core.Packets +namespace MQTTnet.Packets { public sealed class MqttConnectPacket : MqttBasePacket { @@ -20,7 +20,7 @@ namespace MQTTnet.Core.Packets public override string ToString() { - return nameof(MqttConnectPacket) + ": [ClientId=" + ClientId + "] [Username=" + Username + "] [Password=" + Password + "] [KeepAlivePeriod=" + KeepAlivePeriod + "] [CleanSession=" + CleanSession + "]"; + return "Connect: [ClientId=" + ClientId + "] [Username=" + Username + "] [Password=" + Password + "] [KeepAlivePeriod=" + KeepAlivePeriod + "] [CleanSession=" + CleanSession + "]"; } } } diff --git a/Frameworks/MQTTnet.NetStandard/Packets/MqttDisconnectPacket.cs b/Frameworks/MQTTnet.NetStandard/Packets/MqttDisconnectPacket.cs new file mode 100644 index 00000000..46c3270d --- /dev/null +++ b/Frameworks/MQTTnet.NetStandard/Packets/MqttDisconnectPacket.cs @@ -0,0 +1,10 @@ +namespace MQTTnet.Packets +{ + public sealed class MqttDisconnectPacket : MqttBasePacket + { + public override string ToString() + { + return "Disconnect"; + } + } +} diff --git a/MQTTnet.Core/Packets/MqttPacketHeader.cs b/Frameworks/MQTTnet.NetStandard/Packets/MqttPacketHeader.cs similarity index 77% rename from MQTTnet.Core/Packets/MqttPacketHeader.cs rename to Frameworks/MQTTnet.NetStandard/Packets/MqttPacketHeader.cs index 41646df4..01c26ecd 100644 --- a/MQTTnet.Core/Packets/MqttPacketHeader.cs +++ b/Frameworks/MQTTnet.NetStandard/Packets/MqttPacketHeader.cs @@ -1,6 +1,6 @@ -using MQTTnet.Core.Protocol; +using MQTTnet.Protocol; -namespace MQTTnet.Core.Packets +namespace MQTTnet.Packets { public class MqttPacketHeader { diff --git a/MQTTnet.Core/Packets/MqttPingReqPacket.cs b/Frameworks/MQTTnet.NetStandard/Packets/MqttPingReqPacket.cs similarity index 63% rename from MQTTnet.Core/Packets/MqttPingReqPacket.cs rename to Frameworks/MQTTnet.NetStandard/Packets/MqttPingReqPacket.cs index 3df1c3ae..fee9926c 100644 --- a/MQTTnet.Core/Packets/MqttPingReqPacket.cs +++ b/Frameworks/MQTTnet.NetStandard/Packets/MqttPingReqPacket.cs @@ -1,10 +1,10 @@ -namespace MQTTnet.Core.Packets +namespace MQTTnet.Packets { public sealed class MqttPingReqPacket : MqttBasePacket { public override string ToString() { - return nameof(MqttPingReqPacket); + return "PingReq"; } } } diff --git a/MQTTnet.Core/Packets/MqttPingRespPacket.cs b/Frameworks/MQTTnet.NetStandard/Packets/MqttPingRespPacket.cs similarity index 63% rename from MQTTnet.Core/Packets/MqttPingRespPacket.cs rename to Frameworks/MQTTnet.NetStandard/Packets/MqttPingRespPacket.cs index 753a357d..c5f82a3b 100644 --- a/MQTTnet.Core/Packets/MqttPingRespPacket.cs +++ b/Frameworks/MQTTnet.NetStandard/Packets/MqttPingRespPacket.cs @@ -1,10 +1,10 @@ -namespace MQTTnet.Core.Packets +namespace MQTTnet.Packets { public sealed class MqttPingRespPacket : MqttBasePacket { public override string ToString() { - return nameof(MqttPingRespPacket); + return "PingResp"; } } } diff --git a/Frameworks/MQTTnet.NetStandard/Packets/MqttPubAckPacket.cs b/Frameworks/MQTTnet.NetStandard/Packets/MqttPubAckPacket.cs new file mode 100644 index 00000000..e7fe57b7 --- /dev/null +++ b/Frameworks/MQTTnet.NetStandard/Packets/MqttPubAckPacket.cs @@ -0,0 +1,10 @@ +namespace MQTTnet.Packets +{ + public sealed class MqttPubAckPacket : MqttBasePublishPacket + { + public override string ToString() + { + return "PubAck"; + } + } +} diff --git a/Frameworks/MQTTnet.NetStandard/Packets/MqttPubCompPacket.cs b/Frameworks/MQTTnet.NetStandard/Packets/MqttPubCompPacket.cs new file mode 100644 index 00000000..98dace70 --- /dev/null +++ b/Frameworks/MQTTnet.NetStandard/Packets/MqttPubCompPacket.cs @@ -0,0 +1,10 @@ +namespace MQTTnet.Packets +{ + public sealed class MqttPubCompPacket : MqttBasePublishPacket + { + public override string ToString() + { + return "PubComp"; + } + } +} diff --git a/Frameworks/MQTTnet.NetStandard/Packets/MqttPubRecPacket.cs b/Frameworks/MQTTnet.NetStandard/Packets/MqttPubRecPacket.cs new file mode 100644 index 00000000..86cd4b45 --- /dev/null +++ b/Frameworks/MQTTnet.NetStandard/Packets/MqttPubRecPacket.cs @@ -0,0 +1,10 @@ +namespace MQTTnet.Packets +{ + public sealed class MqttPubRecPacket : MqttBasePublishPacket + { + public override string ToString() + { + return "PubRec"; + } + } +} diff --git a/Frameworks/MQTTnet.NetStandard/Packets/MqttPubRelPacket.cs b/Frameworks/MQTTnet.NetStandard/Packets/MqttPubRelPacket.cs new file mode 100644 index 00000000..ed1d933b --- /dev/null +++ b/Frameworks/MQTTnet.NetStandard/Packets/MqttPubRelPacket.cs @@ -0,0 +1,10 @@ +namespace MQTTnet.Packets +{ + public sealed class MqttPubRelPacket : MqttBasePublishPacket + { + public override string ToString() + { + return "PubRel"; + } + } +} diff --git a/MQTTnet.Core/Packets/MqttPublishPacket.cs b/Frameworks/MQTTnet.NetStandard/Packets/MqttPublishPacket.cs similarity index 71% rename from MQTTnet.Core/Packets/MqttPublishPacket.cs rename to Frameworks/MQTTnet.NetStandard/Packets/MqttPublishPacket.cs index 447aaed7..52fcceed 100644 --- a/MQTTnet.Core/Packets/MqttPublishPacket.cs +++ b/Frameworks/MQTTnet.NetStandard/Packets/MqttPublishPacket.cs @@ -1,7 +1,6 @@ -using System; -using MQTTnet.Core.Protocol; +using MQTTnet.Protocol; -namespace MQTTnet.Core.Packets +namespace MQTTnet.Packets { public sealed class MqttPublishPacket : MqttBasePublishPacket { @@ -17,9 +16,8 @@ namespace MQTTnet.Core.Packets public override string ToString() { - return nameof(MqttPublishPacket) + - ": [Topic=" + Topic + "]" + - " [Payload=" + Convert.ToBase64String(Payload) + "]" + + return "Publish: [Topic=" + Topic + "]" + + " [Payload.Length=" + Payload?.Length + "]" + " [QoSLevel=" + QualityOfServiceLevel + "]" + " [Dup=" + Dup + "]" + " [Retain=" + Retain + "]" + diff --git a/MQTTnet.Core/Packets/MqttSubAckPacket.cs b/Frameworks/MQTTnet.NetStandard/Packets/MqttSubAckPacket.cs similarity index 62% rename from MQTTnet.Core/Packets/MqttSubAckPacket.cs rename to Frameworks/MQTTnet.NetStandard/Packets/MqttSubAckPacket.cs index 5882d4c8..3b8cf7a8 100644 --- a/MQTTnet.Core/Packets/MqttSubAckPacket.cs +++ b/Frameworks/MQTTnet.NetStandard/Packets/MqttSubAckPacket.cs @@ -1,19 +1,19 @@ using System.Collections.Generic; using System.Linq; -using MQTTnet.Core.Protocol; +using MQTTnet.Protocol; -namespace MQTTnet.Core.Packets +namespace MQTTnet.Packets { public sealed class MqttSubAckPacket : MqttBasePacket, IMqttPacketWithIdentifier { - public ushort PacketIdentifier { get; set; } + public ushort? PacketIdentifier { get; set; } public IList SubscribeReturnCodes { get; } = new List(); public override string ToString() { var subscribeReturnCodesText = string.Join(",", SubscribeReturnCodes.Select(f => f.ToString())); - return nameof(MqttSubAckPacket) + ": [PacketIdentifier=" + PacketIdentifier + "] [SubscribeReturnCodes=" + subscribeReturnCodesText + "]"; + return "SubAck: [PacketIdentifier=" + PacketIdentifier + "] [SubscribeReturnCodes=" + subscribeReturnCodesText + "]"; } } } diff --git a/MQTTnet.Core/Packets/MqttSubscribePacket.cs b/Frameworks/MQTTnet.NetStandard/Packets/MqttSubscribePacket.cs similarity index 66% rename from MQTTnet.Core/Packets/MqttSubscribePacket.cs rename to Frameworks/MQTTnet.NetStandard/Packets/MqttSubscribePacket.cs index 5cf1c260..63aede16 100644 --- a/MQTTnet.Core/Packets/MqttSubscribePacket.cs +++ b/Frameworks/MQTTnet.NetStandard/Packets/MqttSubscribePacket.cs @@ -1,18 +1,18 @@ using System.Collections.Generic; using System.Linq; -namespace MQTTnet.Core.Packets +namespace MQTTnet.Packets { public sealed class MqttSubscribePacket : MqttBasePacket, IMqttPacketWithIdentifier { - public ushort PacketIdentifier { get; set; } + public ushort? PacketIdentifier { get; set; } public IList TopicFilters { get; set; } = new List(); public override string ToString() { var topicFiltersText = string.Join(",", TopicFilters.Select(f => f.Topic + "@" + f.QualityOfServiceLevel)); - return nameof(MqttSubscribePacket) + ": [PacketIdentifier=" + PacketIdentifier + "] [TopicFilters=" + topicFiltersText + "]"; + return "Subscribe: [PacketIdentifier=" + PacketIdentifier + "] [TopicFilters=" + topicFiltersText + "]"; } } } diff --git a/Frameworks/MQTTnet.NetStandard/Packets/MqttUnsubAckPacket.cs b/Frameworks/MQTTnet.NetStandard/Packets/MqttUnsubAckPacket.cs new file mode 100644 index 00000000..688195fa --- /dev/null +++ b/Frameworks/MQTTnet.NetStandard/Packets/MqttUnsubAckPacket.cs @@ -0,0 +1,12 @@ +namespace MQTTnet.Packets +{ + public sealed class MqttUnsubAckPacket : MqttBasePacket, IMqttPacketWithIdentifier + { + public ushort? PacketIdentifier { get; set; } + + public override string ToString() + { + return "UnsubAck: [PacketIdentifier=" + PacketIdentifier + "]"; + } + } +} diff --git a/Frameworks/MQTTnet.NetStandard/Packets/MqttUnsubscribe.cs b/Frameworks/MQTTnet.NetStandard/Packets/MqttUnsubscribe.cs new file mode 100644 index 00000000..6c24a477 --- /dev/null +++ b/Frameworks/MQTTnet.NetStandard/Packets/MqttUnsubscribe.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace MQTTnet.Packets +{ + public sealed class MqttUnsubscribePacket : MqttBasePacket, IMqttPacketWithIdentifier + { + public ushort? PacketIdentifier { get; set; } + + public IList TopicFilters { get; set; } = new List(); + + public override string ToString() + { + var topicFiltersText = string.Join(",", TopicFilters); + return "Subscribe: [PacketIdentifier=" + PacketIdentifier + "] [TopicFilters=" + topicFiltersText + "]"; + } + } +} diff --git a/MQTTnet.Core/Protocol/MqttConnectReturnCode.cs b/Frameworks/MQTTnet.NetStandard/Protocol/MqttConnectReturnCode.cs similarity index 91% rename from MQTTnet.Core/Protocol/MqttConnectReturnCode.cs rename to Frameworks/MQTTnet.NetStandard/Protocol/MqttConnectReturnCode.cs index 604cd482..bffc4dcf 100644 --- a/MQTTnet.Core/Protocol/MqttConnectReturnCode.cs +++ b/Frameworks/MQTTnet.NetStandard/Protocol/MqttConnectReturnCode.cs @@ -1,4 +1,4 @@ -namespace MQTTnet.Core.Protocol +namespace MQTTnet.Protocol { public enum MqttConnectReturnCode { diff --git a/MQTTnet.Core/Protocol/MqttControlPacketType.cs b/Frameworks/MQTTnet.NetStandard/Protocol/MqttControlPacketType.cs similarity index 91% rename from MQTTnet.Core/Protocol/MqttControlPacketType.cs rename to Frameworks/MQTTnet.NetStandard/Protocol/MqttControlPacketType.cs index aa46b9df..adb58b66 100644 --- a/MQTTnet.Core/Protocol/MqttControlPacketType.cs +++ b/Frameworks/MQTTnet.NetStandard/Protocol/MqttControlPacketType.cs @@ -1,4 +1,4 @@ -namespace MQTTnet.Core.Protocol +namespace MQTTnet.Protocol { public enum MqttControlPacketType { diff --git a/MQTTnet.Core/Protocol/MqttQualityOfServiceLevel.cs b/Frameworks/MQTTnet.NetStandard/Protocol/MqttQualityOfServiceLevel.cs similarity index 80% rename from MQTTnet.Core/Protocol/MqttQualityOfServiceLevel.cs rename to Frameworks/MQTTnet.NetStandard/Protocol/MqttQualityOfServiceLevel.cs index c19496a8..579f72ef 100644 --- a/MQTTnet.Core/Protocol/MqttQualityOfServiceLevel.cs +++ b/Frameworks/MQTTnet.NetStandard/Protocol/MqttQualityOfServiceLevel.cs @@ -1,4 +1,4 @@ -namespace MQTTnet.Core.Protocol +namespace MQTTnet.Protocol { public enum MqttQualityOfServiceLevel { diff --git a/MQTTnet.Core/Protocol/MqttSubscribeReturnCode.cs b/Frameworks/MQTTnet.NetStandard/Protocol/MqttSubscribeReturnCode.cs similarity index 84% rename from MQTTnet.Core/Protocol/MqttSubscribeReturnCode.cs rename to Frameworks/MQTTnet.NetStandard/Protocol/MqttSubscribeReturnCode.cs index a561423f..b256962b 100644 --- a/MQTTnet.Core/Protocol/MqttSubscribeReturnCode.cs +++ b/Frameworks/MQTTnet.NetStandard/Protocol/MqttSubscribeReturnCode.cs @@ -1,4 +1,4 @@ -namespace MQTTnet.Core.Protocol +namespace MQTTnet.Protocol { public enum MqttSubscribeReturnCode { diff --git a/MQTTnet.Core/Serializer/ByteReader.cs b/Frameworks/MQTTnet.NetStandard/Serializer/ByteReader.cs similarity index 96% rename from MQTTnet.Core/Serializer/ByteReader.cs rename to Frameworks/MQTTnet.NetStandard/Serializer/ByteReader.cs index ce7f709d..ec1e3ea2 100644 --- a/MQTTnet.Core/Serializer/ByteReader.cs +++ b/Frameworks/MQTTnet.NetStandard/Serializer/ByteReader.cs @@ -1,6 +1,6 @@ using System; -namespace MQTTnet.Core.Serializer +namespace MQTTnet.Serializer { public sealed class ByteReader { diff --git a/MQTTnet.Core/Serializer/ByteWriter.cs b/Frameworks/MQTTnet.NetStandard/Serializer/ByteWriter.cs similarity index 95% rename from MQTTnet.Core/Serializer/ByteWriter.cs rename to Frameworks/MQTTnet.NetStandard/Serializer/ByteWriter.cs index d6abec90..af961e1a 100644 --- a/MQTTnet.Core/Serializer/ByteWriter.cs +++ b/Frameworks/MQTTnet.NetStandard/Serializer/ByteWriter.cs @@ -1,6 +1,6 @@ using System; -namespace MQTTnet.Core.Serializer +namespace MQTTnet.Serializer { public sealed class ByteWriter { diff --git a/Frameworks/MQTTnet.NetStandard/Serializer/IMqttPacketSerializer.cs b/Frameworks/MQTTnet.NetStandard/Serializer/IMqttPacketSerializer.cs new file mode 100644 index 00000000..6577b0ad --- /dev/null +++ b/Frameworks/MQTTnet.NetStandard/Serializer/IMqttPacketSerializer.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.IO; +using MQTTnet.Packets; + +namespace MQTTnet.Serializer +{ + public interface IMqttPacketSerializer + { + MqttProtocolVersion ProtocolVersion { get; set; } + + ICollection> Serialize(MqttBasePacket mqttPacket); + + MqttBasePacket Deserialize(MqttPacketHeader header, MemoryStream body); + } +} \ No newline at end of file diff --git a/Frameworks/MQTTnet.NetStandard/Serializer/MqttPacketReader.cs b/Frameworks/MQTTnet.NetStandard/Serializer/MqttPacketReader.cs new file mode 100644 index 00000000..bd43f7d3 --- /dev/null +++ b/Frameworks/MQTTnet.NetStandard/Serializer/MqttPacketReader.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using MQTTnet.Exceptions; +using MQTTnet.Packets; +using MQTTnet.Protocol; + +namespace MQTTnet.Serializer +{ + public sealed class MqttPacketReader : BinaryReader + { + private readonly MqttPacketHeader _header; + + public MqttPacketReader(MqttPacketHeader header, Stream bodyStream) + : base(bodyStream, Encoding.UTF8, true) + { + _header = header; + } + + public bool EndOfRemainingData => BaseStream.Position == _header.BodyLength; + + public static async Task ReadHeaderAsync(Stream stream, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return null; + } + + // Wait for the next package which starts with the header. At this point there will probably + // some large delay and thus the thread should be put back to the pool (await). So ReadByte() + // is not an option here. + var buffer = new byte[1]; + var readCount = await stream.ReadAsync(buffer, 0, 1, cancellationToken).ConfigureAwait(false); + if (readCount <= 0) + { + return null; + } + + var fixedHeader = buffer[0]; + var controlPacketType = (MqttControlPacketType)(fixedHeader >> 4); + var bodyLength = await ReadBodyLengthAsync(stream, cancellationToken).ConfigureAwait(false); + + return new MqttPacketHeader + { + FixedHeader = fixedHeader, + ControlPacketType = controlPacketType, + BodyLength = bodyLength + }; + } + + public override ushort ReadUInt16() + { + var buffer = ReadBytes(2); + + var temp = buffer[0]; + buffer[0] = buffer[1]; + buffer[1] = temp; + + return BitConverter.ToUInt16(buffer, 0); + } + + public string ReadStringWithLengthPrefix() + { + var buffer = ReadWithLengthPrefix(); + if (buffer.Length == 0) + { + return string.Empty; + } + + return Encoding.UTF8.GetString(buffer, 0, buffer.Length); + } + + public byte[] ReadWithLengthPrefix() + { + var length = ReadUInt16(); + if (length == 0) + { + return new byte[0]; + } + + return ReadBytes(length); + } + + public byte[] ReadRemainingData() + { + return ReadBytes(_header.BodyLength - (int)BaseStream.Position); + } + + private static async Task ReadBodyLengthAsync(Stream stream, CancellationToken cancellationToken) + { + // Alorithm taken from https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/errata01/os/mqtt-v3.1.1-errata01-os-complete.html. + var multiplier = 1; + var value = 0; + byte encodedByte; + + var buffer = new byte[1]; + var readBytes = new List(); + do + { + if (cancellationToken.IsCancellationRequested) + { + throw new TaskCanceledException(); + } + + var readCount = await stream.ReadAsync(buffer, 0, 1, cancellationToken).ConfigureAwait(false); + if (readCount <= 0) + { + throw new MqttCommunicationException("Connection closed while reading remaining length data."); + } + + encodedByte = buffer[0]; + readBytes.Add(encodedByte); + + value += (byte)(encodedByte & 127) * multiplier; + if (multiplier > 128 * 128 * 128) + { + throw new MqttProtocolViolationException($"Remaining length is invalid (Data={string.Join(",", readBytes)})."); + } + + multiplier *= 128; + } while ((encodedByte & 128) != 0); + + return value; + } + } +} diff --git a/MQTTnet.Core/Serializer/MqttPacketSerializer.cs b/Frameworks/MQTTnet.NetStandard/Serializer/MqttPacketSerializer.cs similarity index 78% rename from MQTTnet.Core/Serializer/MqttPacketSerializer.cs rename to Frameworks/MQTTnet.NetStandard/Serializer/MqttPacketSerializer.cs index 00906f4a..0904c759 100644 --- a/MQTTnet.Core/Serializer/MqttPacketSerializer.cs +++ b/Frameworks/MQTTnet.NetStandard/Serializer/MqttPacketSerializer.cs @@ -1,14 +1,13 @@ -using System; +using MQTTnet.Exceptions; +using MQTTnet.Packets; +using MQTTnet.Protocol; +using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; -using MQTTnet.Core.Adapter; -using MQTTnet.Core.Exceptions; -using MQTTnet.Core.Packets; -using MQTTnet.Core.Protocol; -namespace MQTTnet.Core.Serializer +namespace MQTTnet.Serializer { public sealed class MqttPacketSerializer : IMqttPacketSerializer { @@ -17,32 +16,40 @@ namespace MQTTnet.Core.Serializer public MqttProtocolVersion ProtocolVersion { get; set; } = MqttProtocolVersion.V311; - public byte[] Serialize(MqttBasePacket packet) + public ICollection> Serialize(MqttBasePacket packet) { if (packet == null) throw new ArgumentNullException(nameof(packet)); - using (var stream = new MemoryStream()) + using (var stream = new MemoryStream(128)) using (var writer = new MqttPacketWriter(stream)) { - var header = new List { SerializePacket(packet, writer) }; - var body = stream.ToArray(); - MqttPacketWriter.BuildLengthHeader(body.Length, header); - var headerArray = header.ToArray(); - var writeBuffer = new byte[header.Count + body.Length]; - Buffer.BlockCopy(headerArray, 0, writeBuffer, 0, headerArray.Length); - Buffer.BlockCopy(body, 0, writeBuffer, headerArray.Length, body.Length); + var fixedHeader = SerializePacket(packet, writer); + var remainingLength = (int)stream.Length; + writer.Write(fixedHeader); + MqttPacketWriter.WriteRemainingLength(remainingLength, writer); + var headerLength = (int)stream.Length - remainingLength; - return writeBuffer; +#if NET461 || NET452 || NETSTANDARD2_0 + var buffer = stream.GetBuffer(); +#else + var buffer = stream.ToArray(); +#endif + return new List> + { + new ArraySegment(buffer, remainingLength, headerLength), + new ArraySegment(buffer, 0, remainingLength) + }; } } - public MqttBasePacket Deserialize(ReceivedMqttPacket receivedMqttPacket) + public MqttBasePacket Deserialize(MqttPacketHeader header, MemoryStream body) { - if (receivedMqttPacket == null) throw new ArgumentNullException(nameof(receivedMqttPacket)); + if (header == null) throw new ArgumentNullException(nameof(header)); + if (body == null) throw new ArgumentNullException(nameof(body)); - using (var reader = new MqttPacketReader(receivedMqttPacket)) + using (var reader = new MqttPacketReader(header, body)) { - return Deserialize(receivedMqttPacket.Header, reader); + return Deserialize(header, reader); } } @@ -68,7 +75,7 @@ namespace MQTTnet.Core.Serializer } } - private static MqttBasePacket Deserialize(MqttPacketHeader header, MqttPacketReader reader) + private MqttBasePacket Deserialize(MqttPacketHeader header, MqttPacketReader reader) { switch (header.ControlPacketType) { @@ -271,20 +278,22 @@ namespace MQTTnet.Core.Serializer return packet; } - private static MqttBasePacket DeserializeConnAck(MqttPacketReader reader) + private MqttBasePacket DeserializeConnAck(MqttPacketReader reader) { - var variableHeader1 = reader.ReadByte(); - var variableHeader2 = reader.ReadByte(); + var packet = new MqttConnAckPacket(); - var packet = new MqttConnAckPacket + var firstByteReader = new ByteReader(reader.ReadByte()); + + if (ProtocolVersion == MqttProtocolVersion.V311) { - IsSessionPresent = new ByteReader(variableHeader1).Read(), - ConnectReturnCode = (MqttConnectReturnCode)variableHeader2 - }; + packet.IsSessionPresent = firstByteReader.Read(); + } + + packet.ConnectReturnCode = (MqttConnectReturnCode)reader.ReadByte(); return packet; } - + private static void ValidateConnectPacket(MqttConnectPacket packet) { if (packet == null) throw new ArgumentNullException(nameof(packet)); @@ -366,14 +375,21 @@ namespace MQTTnet.Core.Serializer private byte Serialize(MqttConnAckPacket packet, MqttPacketWriter writer) { - var connectAcknowledgeFlags = new ByteWriter(); - - if (ProtocolVersion == MqttProtocolVersion.V311) + if (ProtocolVersion == MqttProtocolVersion.V310) { + writer.Write(0); + } + else if (ProtocolVersion == MqttProtocolVersion.V311) + { + var connectAcknowledgeFlags = new ByteWriter(); connectAcknowledgeFlags.Write(packet.IsSessionPresent); + writer.Write(connectAcknowledgeFlags); + } + else + { + throw new MqttProtocolViolationException("Protocol version not supported."); } - writer.Write(connectAcknowledgeFlags); writer.Write((byte)packet.ConnectReturnCode); return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.ConnAck); @@ -381,7 +397,12 @@ namespace MQTTnet.Core.Serializer private static byte Serialize(MqttPubRelPacket packet, MqttPacketWriter writer) { - writer.Write(packet.PacketIdentifier); + if (!packet.PacketIdentifier.HasValue) + { + throw new MqttProtocolViolationException("PubRel packet has no packet identifier."); + } + + writer.Write(packet.PacketIdentifier.Value); return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.PubRel, 0x02); } @@ -394,7 +415,12 @@ namespace MQTTnet.Core.Serializer if (packet.QualityOfServiceLevel > MqttQualityOfServiceLevel.AtMostOnce) { - writer.Write(packet.PacketIdentifier); + if (!packet.PacketIdentifier.HasValue) + { + throw new MqttProtocolViolationException("Publish packet has no packet identifier."); + } + + writer.Write(packet.PacketIdentifier.Value); } else { @@ -428,21 +454,36 @@ namespace MQTTnet.Core.Serializer private static byte Serialize(MqttPubAckPacket packet, MqttPacketWriter writer) { - writer.Write(packet.PacketIdentifier); + if (!packet.PacketIdentifier.HasValue) + { + throw new MqttProtocolViolationException("PubAck packet has no packet identifier."); + } + + writer.Write(packet.PacketIdentifier.Value); return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.PubAck); } private static byte Serialize(MqttPubRecPacket packet, MqttPacketWriter writer) { - writer.Write(packet.PacketIdentifier); + if (!packet.PacketIdentifier.HasValue) + { + throw new MqttProtocolViolationException("PubRec packet has no packet identifier."); + } + + writer.Write(packet.PacketIdentifier.Value); return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.PubRec); } private static byte Serialize(MqttPubCompPacket packet, MqttPacketWriter writer) { - writer.Write(packet.PacketIdentifier); + if (!packet.PacketIdentifier.HasValue) + { + throw new MqttProtocolViolationException("PubComp packet has no packet identifier."); + } + + writer.Write(packet.PacketIdentifier.Value); return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.PubComp); } @@ -451,7 +492,12 @@ namespace MQTTnet.Core.Serializer { if (!packet.TopicFilters.Any()) throw new MqttProtocolViolationException("At least one topic filter must be set [MQTT-3.8.3-3]."); - writer.Write(packet.PacketIdentifier); + if (!packet.PacketIdentifier.HasValue) + { + throw new MqttProtocolViolationException("Subscribe packet has no packet identifier."); + } + + writer.Write(packet.PacketIdentifier.Value); if (packet.TopicFilters?.Count > 0) { @@ -467,7 +513,12 @@ namespace MQTTnet.Core.Serializer private static byte Serialize(MqttSubAckPacket packet, MqttPacketWriter writer) { - writer.Write(packet.PacketIdentifier); + if (!packet.PacketIdentifier.HasValue) + { + throw new MqttProtocolViolationException("SubAck packet has no packet identifier."); + } + + writer.Write(packet.PacketIdentifier.Value); if (packet.SubscribeReturnCodes?.Any() == true) { @@ -484,7 +535,12 @@ namespace MQTTnet.Core.Serializer { if (!packet.TopicFilters.Any()) throw new MqttProtocolViolationException("At least one topic filter must be set [MQTT-3.10.3-2]."); - writer.Write(packet.PacketIdentifier); + if (!packet.PacketIdentifier.HasValue) + { + throw new MqttProtocolViolationException("Unsubscribe packet has no packet identifier."); + } + + writer.Write(packet.PacketIdentifier.Value); if (packet.TopicFilters?.Any() == true) { @@ -497,9 +553,14 @@ namespace MQTTnet.Core.Serializer return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.Unsubscibe, 0x02); } - private static byte Serialize(IMqttPacketWithIdentifier packet, BinaryWriter writer) + private static byte Serialize(MqttUnsubAckPacket packet, BinaryWriter writer) { - writer.Write(packet.PacketIdentifier); + if (!packet.PacketIdentifier.HasValue) + { + throw new MqttProtocolViolationException("UnsubAck packet has no packet identifier."); + } + + writer.Write(packet.PacketIdentifier.Value); return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.UnsubAck); } diff --git a/MQTTnet.Core/Serializer/MqttPacketWriter.cs b/Frameworks/MQTTnet.NetStandard/Serializer/MqttPacketWriter.cs similarity index 86% rename from MQTTnet.Core/Serializer/MqttPacketWriter.cs rename to Frameworks/MQTTnet.NetStandard/Serializer/MqttPacketWriter.cs index d8c997d2..cb3d4589 100644 --- a/MQTTnet.Core/Serializer/MqttPacketWriter.cs +++ b/Frameworks/MQTTnet.NetStandard/Serializer/MqttPacketWriter.cs @@ -1,15 +1,14 @@ using System; using System.IO; using System.Text; -using MQTTnet.Core.Protocol; -using System.Collections.Generic; +using MQTTnet.Protocol; -namespace MQTTnet.Core.Serializer +namespace MQTTnet.Serializer { public sealed class MqttPacketWriter : BinaryWriter { public MqttPacketWriter(Stream stream) - : base(stream) + : base(stream, Encoding.UTF8, true) { } @@ -56,11 +55,11 @@ namespace MQTTnet.Core.Serializer Write(value); } - public static void BuildLengthHeader(int length, List header) + public static void WriteRemainingLength(int length, BinaryWriter target) { if (length == 0) { - header.Add(0); + target.Write((byte)0); return; } @@ -75,7 +74,7 @@ namespace MQTTnet.Core.Serializer encodedByte = encodedByte | 128; } - header.Add((byte)encodedByte); + target.Write((byte)encodedByte); } while (x > 0); } } diff --git a/MQTTnet.Core/Serializer/MqttProtocolVersion.cs b/Frameworks/MQTTnet.NetStandard/Serializer/MqttProtocolVersion.cs similarity index 68% rename from MQTTnet.Core/Serializer/MqttProtocolVersion.cs rename to Frameworks/MQTTnet.NetStandard/Serializer/MqttProtocolVersion.cs index 6b8814a7..15e698d3 100644 --- a/MQTTnet.Core/Serializer/MqttProtocolVersion.cs +++ b/Frameworks/MQTTnet.NetStandard/Serializer/MqttProtocolVersion.cs @@ -1,4 +1,4 @@ -namespace MQTTnet.Core.Serializer +namespace MQTTnet.Serializer { public enum MqttProtocolVersion { diff --git a/Frameworks/MQTTnet.NetStandard/Server/CheckSubscriptionsResult.cs b/Frameworks/MQTTnet.NetStandard/Server/CheckSubscriptionsResult.cs new file mode 100644 index 00000000..9e2d1f0d --- /dev/null +++ b/Frameworks/MQTTnet.NetStandard/Server/CheckSubscriptionsResult.cs @@ -0,0 +1,11 @@ +using MQTTnet.Protocol; + +namespace MQTTnet.Server +{ + public class CheckSubscriptionsResult + { + public bool IsSubscribed { get; set; } + + public MqttQualityOfServiceLevel QualityOfServiceLevel { get; set; } + } +} diff --git a/Frameworks/MQTTnet.NetStandard/Server/ConnectedMqttClient.cs b/Frameworks/MQTTnet.NetStandard/Server/ConnectedMqttClient.cs new file mode 100644 index 00000000..4fa8e4ea --- /dev/null +++ b/Frameworks/MQTTnet.NetStandard/Server/ConnectedMqttClient.cs @@ -0,0 +1,18 @@ +using System; +using MQTTnet.Serializer; + +namespace MQTTnet.Server +{ + public class ConnectedMqttClient + { + public string ClientId { get; set; } + + public MqttProtocolVersion ProtocolVersion { get; set; } + + public TimeSpan LastPacketReceived { get; set; } + + public TimeSpan LastNonKeepAlivePacketReceived { get; set; } + + public int PendingApplicationMessages { get; set; } + } +} diff --git a/MQTTnet.Core/Server/GetOrCreateClientSessionResult.cs b/Frameworks/MQTTnet.NetStandard/Server/GetOrCreateClientSessionResult.cs similarity index 84% rename from MQTTnet.Core/Server/GetOrCreateClientSessionResult.cs rename to Frameworks/MQTTnet.NetStandard/Server/GetOrCreateClientSessionResult.cs index 8f802720..8f5f085e 100644 --- a/MQTTnet.Core/Server/GetOrCreateClientSessionResult.cs +++ b/Frameworks/MQTTnet.NetStandard/Server/GetOrCreateClientSessionResult.cs @@ -1,4 +1,4 @@ -namespace MQTTnet.Core.Server +namespace MQTTnet.Server { public sealed class GetOrCreateClientSessionResult { diff --git a/Frameworks/MQTTnet.NetStandard/Server/IMqttServer.cs b/Frameworks/MQTTnet.NetStandard/Server/IMqttServer.cs new file mode 100644 index 00000000..229e79c5 --- /dev/null +++ b/Frameworks/MQTTnet.NetStandard/Server/IMqttServer.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace MQTTnet.Server +{ + public interface IMqttServer : IApplicationMessageReceiver, IApplicationMessagePublisher + { + event EventHandler Started; + + event EventHandler ClientConnected; + event EventHandler ClientDisconnected; + event EventHandler ClientSubscribedTopic; + event EventHandler ClientUnsubscribedTopic; + + IMqttServerOptions Options { get; } + + Task> GetConnectedClientsAsync(); + + Task SubscribeAsync(string clientId, IList topicFilters); + Task UnsubscribeAsync(string clientId, IList topicFilters); + + Task StartAsync(IMqttServerOptions options); + Task StopAsync(); + } +} \ No newline at end of file diff --git a/Frameworks/MQTTnet.NetStandard/Server/IMqttServerFactory.cs b/Frameworks/MQTTnet.NetStandard/Server/IMqttServerFactory.cs new file mode 100644 index 00000000..6e399cae --- /dev/null +++ b/Frameworks/MQTTnet.NetStandard/Server/IMqttServerFactory.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using MQTTnet.Adapter; +using MQTTnet.Diagnostics; + +namespace MQTTnet.Server +{ + public interface IMqttServerFactory + { + IMqttServer CreateMqttServer(); + + IMqttServer CreateMqttServer(IEnumerable adapters, IMqttNetLogger logger); + } +} \ No newline at end of file diff --git a/Frameworks/MQTTnet.NetStandard/Server/IMqttServerOptions.cs b/Frameworks/MQTTnet.NetStandard/Server/IMqttServerOptions.cs new file mode 100644 index 00000000..cc58eeb8 --- /dev/null +++ b/Frameworks/MQTTnet.NetStandard/Server/IMqttServerOptions.cs @@ -0,0 +1,19 @@ +using System; + +namespace MQTTnet.Server +{ + public interface IMqttServerOptions + { + int ConnectionBacklog { get; } + TimeSpan DefaultCommunicationTimeout { get; } + + Action ConnectionValidator { get; } + Action SubscriptionInterceptor { get; } + Action ApplicationMessageInterceptor { get; } + + MqttServerDefaultEndpointOptions DefaultEndpointOptions { get; } + MqttServerTlsEndpointOptions TlsEndpointOptions { get; } + + IMqttServerStorage Storage { get; } + } +} \ No newline at end of file diff --git a/MQTTnet.Core/Server/IMqttServerStorage.cs b/Frameworks/MQTTnet.NetStandard/Server/IMqttServerStorage.cs similarity index 90% rename from MQTTnet.Core/Server/IMqttServerStorage.cs rename to Frameworks/MQTTnet.NetStandard/Server/IMqttServerStorage.cs index 3cb518c3..da6e6e28 100644 --- a/MQTTnet.Core/Server/IMqttServerStorage.cs +++ b/Frameworks/MQTTnet.NetStandard/Server/IMqttServerStorage.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Threading.Tasks; -namespace MQTTnet.Core.Server +namespace MQTTnet.Server { public interface IMqttServerStorage { diff --git a/Frameworks/MQTTnet.NetStandard/Server/MqttApplicationMessageInterceptorContext.cs b/Frameworks/MQTTnet.NetStandard/Server/MqttApplicationMessageInterceptorContext.cs new file mode 100644 index 00000000..56126015 --- /dev/null +++ b/Frameworks/MQTTnet.NetStandard/Server/MqttApplicationMessageInterceptorContext.cs @@ -0,0 +1,19 @@ +namespace MQTTnet.Server +{ + public class MqttApplicationMessageInterceptorContext + { + public MqttApplicationMessageInterceptorContext(string clientId, MqttApplicationMessage applicationMessage) + { + ClientId = clientId; + ApplicationMessage = applicationMessage; + } + + public string ClientId { get; } + + public MqttApplicationMessage ApplicationMessage { get; set; } + + public bool AcceptPublish { get; set; } = true; + + public bool CloseConnection { get; set; } + } +} diff --git a/MQTTnet.Core/Server/MqttClientConnectedEventArgs.cs b/Frameworks/MQTTnet.NetStandard/Server/MqttClientConnectedEventArgs.cs similarity index 91% rename from MQTTnet.Core/Server/MqttClientConnectedEventArgs.cs rename to Frameworks/MQTTnet.NetStandard/Server/MqttClientConnectedEventArgs.cs index 282e0f8d..1ca3f406 100644 --- a/MQTTnet.Core/Server/MqttClientConnectedEventArgs.cs +++ b/Frameworks/MQTTnet.NetStandard/Server/MqttClientConnectedEventArgs.cs @@ -1,6 +1,6 @@ using System; -namespace MQTTnet.Core.Server +namespace MQTTnet.Server { public class MqttClientConnectedEventArgs : EventArgs { diff --git a/MQTTnet.Core/Server/MqttClientDisconnectedEventArgs.cs b/Frameworks/MQTTnet.NetStandard/Server/MqttClientDisconnectedEventArgs.cs similarity index 66% rename from MQTTnet.Core/Server/MqttClientDisconnectedEventArgs.cs rename to Frameworks/MQTTnet.NetStandard/Server/MqttClientDisconnectedEventArgs.cs index b1e3d15b..6dd505e5 100644 --- a/MQTTnet.Core/Server/MqttClientDisconnectedEventArgs.cs +++ b/Frameworks/MQTTnet.NetStandard/Server/MqttClientDisconnectedEventArgs.cs @@ -1,14 +1,17 @@ using System; -namespace MQTTnet.Core.Server +namespace MQTTnet.Server { public class MqttClientDisconnectedEventArgs : EventArgs { - public MqttClientDisconnectedEventArgs(ConnectedMqttClient client) + public MqttClientDisconnectedEventArgs(ConnectedMqttClient client, bool wasCleanDisconnect) { Client = client ?? throw new ArgumentNullException(nameof(client)); + WasCleanDisconnect = wasCleanDisconnect; } public ConnectedMqttClient Client { get; } + + public bool WasCleanDisconnect { get; } } } diff --git a/Frameworks/MQTTnet.NetStandard/Server/MqttClientKeepAliveMonitor.cs b/Frameworks/MQTTnet.NetStandard/Server/MqttClientKeepAliveMonitor.cs new file mode 100644 index 00000000..3f215369 --- /dev/null +++ b/Frameworks/MQTTnet.NetStandard/Server/MqttClientKeepAliveMonitor.cs @@ -0,0 +1,98 @@ +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using MQTTnet.Diagnostics; +using MQTTnet.Packets; + +namespace MQTTnet.Server +{ + public sealed class MqttClientKeepAliveMonitor + { + private readonly Stopwatch _lastPacketReceivedTracker = new Stopwatch(); + private readonly Stopwatch _lastNonKeepAlivePacketReceivedTracker = new Stopwatch(); + + private readonly string _clientId; + private readonly Func _timeoutCallback; + private readonly IMqttNetLogger _logger; + + private Task _workerTask; + + public MqttClientKeepAliveMonitor(string clientId, Func timeoutCallback, IMqttNetLogger logger) + { + _clientId = clientId; + _timeoutCallback = timeoutCallback; + _logger = logger; + } + + public TimeSpan LastPacketReceived => _lastPacketReceivedTracker.Elapsed; + + public TimeSpan LastNonKeepAlivePacketReceived => _lastNonKeepAlivePacketReceivedTracker.Elapsed; + + public void Start(int keepAlivePeriod, CancellationToken cancellationToken) + { + if (keepAlivePeriod == 0) + { + return; + } + + _workerTask = Task.Run(() => RunAsync(keepAlivePeriod, cancellationToken).ConfigureAwait(false), cancellationToken); + } + + public void WaitForCompletion() + { + if (_workerTask != null) + { + Task.WaitAll(_workerTask); + } + } + + private async Task RunAsync(int keepAlivePeriod, CancellationToken cancellationToken) + { + try + { + _lastPacketReceivedTracker.Restart(); + _lastNonKeepAlivePacketReceivedTracker.Restart(); + + while (!cancellationToken.IsCancellationRequested) + { + // Values described here: [MQTT-3.1.2-24]. + if (_lastPacketReceivedTracker.Elapsed.TotalSeconds > keepAlivePeriod * 1.5D) + { + _logger.Warning("Client '{0}': Did not receive any packet or keep alive signal.", _clientId); + + if (_timeoutCallback != null) + { + await _timeoutCallback().ConfigureAwait(false); + } + + return; + } + + await Task.Delay(keepAlivePeriod, cancellationToken).ConfigureAwait(false); + } + } + catch (OperationCanceledException) + { + } + catch (Exception exception) + { + _logger.Error(exception, "Client '{0}': Unhandled exception while checking keep alive timeouts.", _clientId); + } + finally + { + _logger.Verbose("Client {0}: Stopped checking keep alive timeout.", _clientId); + } + } + + public void PacketReceived(MqttBasePacket packet) + { + _lastPacketReceivedTracker.Restart(); + + if (!(packet is MqttPingReqPacket)) + { + _lastNonKeepAlivePacketReceivedTracker.Restart(); + } + } + } +} diff --git a/Frameworks/MQTTnet.NetStandard/Server/MqttClientPendingMessagesQueue.cs b/Frameworks/MQTTnet.NetStandard/Server/MqttClientPendingMessagesQueue.cs new file mode 100644 index 00000000..99493874 --- /dev/null +++ b/Frameworks/MQTTnet.NetStandard/Server/MqttClientPendingMessagesQueue.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Concurrent; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using MQTTnet.Adapter; +using MQTTnet.Diagnostics; +using MQTTnet.Exceptions; +using MQTTnet.Packets; +using MQTTnet.Protocol; + +namespace MQTTnet.Server +{ + public sealed class MqttClientPendingMessagesQueue : IDisposable + { + private readonly ConcurrentQueue _queue = new ConcurrentQueue(); + private readonly SemaphoreSlim _queueWaitSemaphore = new SemaphoreSlim(0); + private readonly IMqttServerOptions _options; + private readonly MqttClientSession _clientSession; + private readonly IMqttNetLogger _logger; + + private Task _workerTask; + + public MqttClientPendingMessagesQueue(IMqttServerOptions options, MqttClientSession clientSession, IMqttNetLogger logger) + { + _options = options ?? throw new ArgumentNullException(nameof(options)); + _clientSession = clientSession ?? throw new ArgumentNullException(nameof(clientSession)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public int Count => _queue.Count; + + public void Start(IMqttChannelAdapter adapter, CancellationToken cancellationToken) + { + if (adapter == null) throw new ArgumentNullException(nameof(adapter)); + + if (cancellationToken.IsCancellationRequested) + { + return; + } + + _workerTask = Task.Run(() => SendQueuedPacketsAsync(adapter, cancellationToken), cancellationToken); + } + + public void WaitForCompletion() + { + if (_workerTask != null) + { + Task.WaitAll(_workerTask); + } + } + + public void Enqueue(MqttBasePacket packet) + { + if (packet == null) throw new ArgumentNullException(nameof(packet)); + + _queue.Enqueue(packet); + _queueWaitSemaphore.Release(); + + _logger.Verbose("Enqueued packet (ClientId: {0}).", _clientSession.ClientId); + } + + private async Task SendQueuedPacketsAsync(IMqttChannelAdapter adapter, CancellationToken cancellationToken) + { + try + { + while (!cancellationToken.IsCancellationRequested) + { + await SendNextQueuedPacketAsync(adapter, cancellationToken).ConfigureAwait(false); + } + } + catch (OperationCanceledException) + { + } + catch (Exception exception) + { + _logger.Error(exception, "Unhandled exception while sending enqueued packet (ClientId: {0}).", _clientSession.ClientId); + } + } + + private async Task SendNextQueuedPacketAsync(IMqttChannelAdapter adapter, CancellationToken cancellationToken) + { + MqttBasePacket packet = null; + try + { + await _queueWaitSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + if (!_queue.TryDequeue(out packet)) + { + throw new InvalidOperationException(); // should not happen + } + + if (cancellationToken.IsCancellationRequested) + { + return; + } + + await adapter.SendPacketsAsync(_options.DefaultCommunicationTimeout, cancellationToken, new[] { packet }).ConfigureAwait(false); + + _logger.Verbose("Enqueued packet sent (ClientId: {0}).", _clientSession.ClientId); + } + catch (Exception exception) + { + if (exception is MqttCommunicationTimedOutException) + { + _logger.Warning(exception, "Sending publish packet failed due to timeout (ClientId: {0}).", _clientSession.ClientId); + } + else if (exception is MqttCommunicationException) + { + _logger.Warning(exception, "Sending publish packet failed due to communication exception (ClientId: {0}).", _clientSession.ClientId); + } + else if (exception is OperationCanceledException) + { + } + else + { + _logger.Error(exception, "Sending publish packet failed (ClientId: {0}).", _clientSession.ClientId); + } + + if (packet is MqttPublishPacket publishPacket) + { + if (publishPacket.QualityOfServiceLevel > MqttQualityOfServiceLevel.AtMostOnce) + { + publishPacket.Dup = true; + _queue.Enqueue(packet); + _queueWaitSemaphore.Release(); + } + } + + if (!cancellationToken.IsCancellationRequested) + { + await _clientSession.StopAsync().ConfigureAwait(false); + } + } + } + + public void Dispose() + { + _queueWaitSemaphore?.Dispose(); + } + } +} diff --git a/Frameworks/MQTTnet.NetStandard/Server/MqttClientSession.cs b/Frameworks/MQTTnet.NetStandard/Server/MqttClientSession.cs new file mode 100644 index 00000000..8514da99 --- /dev/null +++ b/Frameworks/MQTTnet.NetStandard/Server/MqttClientSession.cs @@ -0,0 +1,346 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MQTTnet.Adapter; +using MQTTnet.Client; +using MQTTnet.Diagnostics; +using MQTTnet.Exceptions; +using MQTTnet.Internal; +using MQTTnet.Packets; +using MQTTnet.Protocol; +using MQTTnet.Serializer; + +namespace MQTTnet.Server +{ + public sealed class MqttClientSession : IDisposable + { + private readonly MqttPacketIdentifierProvider _packetIdentifierProvider = new MqttPacketIdentifierProvider(); + private readonly IMqttServerOptions _options; + private readonly IMqttNetLogger _logger; + private readonly MqttRetainedMessagesManager _retainedMessagesManager; + + private IMqttChannelAdapter _adapter; + private CancellationTokenSource _cancellationTokenSource; + private MqttApplicationMessage _willMessage; + private bool _wasCleanDisconnect; + + public MqttClientSession( + string clientId, + IMqttServerOptions options, + MqttRetainedMessagesManager retainedMessagesManager, + IMqttNetLogger logger) + { + _options = options ?? throw new ArgumentNullException(nameof(options)); + _retainedMessagesManager = retainedMessagesManager ?? throw new ArgumentNullException(nameof(retainedMessagesManager)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + ClientId = clientId; + + KeepAliveMonitor = new MqttClientKeepAliveMonitor(clientId, StopDueToKeepAliveTimeoutAsync, _logger); + SubscriptionsManager = new MqttClientSubscriptionsManager(_options, clientId); + PendingMessagesQueue = new MqttClientPendingMessagesQueue(_options, this, _logger); + } + + public Func ApplicationMessageReceivedCallback { get; set; } + + public MqttClientSubscriptionsManager SubscriptionsManager { get; } + + public MqttClientPendingMessagesQueue PendingMessagesQueue { get; } + + public MqttClientKeepAliveMonitor KeepAliveMonitor { get; } + + public string ClientId { get; } + + public MqttProtocolVersion? ProtocolVersion => _adapter?.PacketSerializer.ProtocolVersion; + + public bool IsConnected => _adapter != null; + + public async Task RunAsync(MqttConnectPacket connectPacket, IMqttChannelAdapter adapter) + { + if (connectPacket == null) throw new ArgumentNullException(nameof(connectPacket)); + if (adapter == null) throw new ArgumentNullException(nameof(adapter)); + + try + { + var cancellationTokenSource = new CancellationTokenSource(); + + _wasCleanDisconnect = false; + _willMessage = connectPacket.WillMessage; + _adapter = adapter; + _cancellationTokenSource = cancellationTokenSource; + + PendingMessagesQueue.Start(adapter, cancellationTokenSource.Token); + KeepAliveMonitor.Start(connectPacket.KeepAlivePeriod, cancellationTokenSource.Token); + + await ReceivePacketsAsync(adapter, cancellationTokenSource.Token).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + } + catch (MqttCommunicationException exception) + { + _logger.Warning(exception, "Client '{0}': Communication exception while processing client packets.", ClientId); + } + catch (Exception exception) + { + _logger.Error(exception, "Client '{0}': Unhandled exception while processing client packets.", ClientId); + } + + return _wasCleanDisconnect; + } + + public async Task StopAsync(bool wasCleanDisconnect = false) + { + try + { + if (_cancellationTokenSource == null) + { + return; + } + + _wasCleanDisconnect = wasCleanDisconnect; + + _cancellationTokenSource?.Cancel(false); + + PendingMessagesQueue.WaitForCompletion(); + KeepAliveMonitor.WaitForCompletion(); + + _cancellationTokenSource?.Dispose(); + _cancellationTokenSource = null; + + _adapter = null; + + _logger.Info("Client '{0}': Session stopped.", ClientId); + } + finally + { + var willMessage = _willMessage; + _willMessage = null; // clear willmessage so it is send just once + + if (willMessage != null && !wasCleanDisconnect) + { + await ApplicationMessageReceivedCallback(this, willMessage).ConfigureAwait(false); + } + } + } + + public async Task EnqueueApplicationMessageAsync(MqttApplicationMessage applicationMessage) + { + if (applicationMessage == null) throw new ArgumentNullException(nameof(applicationMessage)); + + var result = await SubscriptionsManager.CheckSubscriptionsAsync(applicationMessage); + if (!result.IsSubscribed) + { + return; + } + + var publishPacket = applicationMessage.ToPublishPacket(); + publishPacket.QualityOfServiceLevel = result.QualityOfServiceLevel; + + if (publishPacket.QualityOfServiceLevel > 0) + { + publishPacket.PacketIdentifier = _packetIdentifierProvider.GetNewPacketIdentifier(); + } + + PendingMessagesQueue.Enqueue(publishPacket); + } + + public async Task SubscribeAsync(IList topicFilters) + { + if (topicFilters == null) throw new ArgumentNullException(nameof(topicFilters)); + + await SubscriptionsManager.SubscribeAsync(new MqttSubscribePacket + { + TopicFilters = topicFilters + }).ConfigureAwait(false); + + await EnqueueSubscribedRetainedMessagesAsync(topicFilters).ConfigureAwait(false); + } + + public Task UnsubscribeAsync(IList topicFilters) + { + if (topicFilters == null) throw new ArgumentNullException(nameof(topicFilters)); + + return SubscriptionsManager.UnsubscribeAsync(new MqttUnsubscribePacket + { + TopicFilters = topicFilters + }); + } + + public void Dispose() + { + ApplicationMessageReceivedCallback = null; + + SubscriptionsManager?.Dispose(); + PendingMessagesQueue?.Dispose(); + + _cancellationTokenSource?.Dispose(); + } + + private Task StopDueToKeepAliveTimeoutAsync() + { + _logger.Info("Client '{0}': Timeout while waiting for KeepAlive packet.", ClientId); + return StopAsync(); + } + + private async Task ReceivePacketsAsync(IMqttChannelAdapter adapter, CancellationToken cancellationToken) + { + try + { + while (!cancellationToken.IsCancellationRequested) + { + var packet = await adapter.ReceivePacketAsync(TimeSpan.Zero, cancellationToken).ConfigureAwait(false); + KeepAliveMonitor.PacketReceived(packet); + await ProcessReceivedPacketAsync(adapter, packet, cancellationToken).ConfigureAwait(false); + } + } + catch (OperationCanceledException) + { + } + catch (MqttCommunicationException exception) + { + _logger.Warning(exception, "Client '{0}': Communication exception while processing client packets.", ClientId); + await StopAsync().ConfigureAwait(false); + } + catch (Exception exception) + { + _logger.Error(exception, "Client '{0}': Unhandled exception while processing client packets.", ClientId); + await StopAsync().ConfigureAwait(false); + } + } + + private Task ProcessReceivedPacketAsync(IMqttChannelAdapter adapter, MqttBasePacket packet, CancellationToken cancellationToken) + { + if (packet is MqttPublishPacket publishPacket) + { + return HandleIncomingPublishPacketAsync(adapter, publishPacket, cancellationToken); + } + + if (packet is MqttPingReqPacket) + { + return adapter.SendPacketsAsync(_options.DefaultCommunicationTimeout, cancellationToken, new[] { new MqttPingRespPacket() }); + } + + if (packet is MqttPubRelPacket pubRelPacket) + { + return HandleIncomingPubRelPacketAsync(adapter, pubRelPacket, cancellationToken); + } + + if (packet is MqttPubRecPacket pubRecPacket) + { + var responsePacket = new MqttPubRelPacket + { + PacketIdentifier = pubRecPacket.PacketIdentifier + }; + + return adapter.SendPacketsAsync(_options.DefaultCommunicationTimeout, cancellationToken, new[] { responsePacket }); + } + + if (packet is MqttPubAckPacket || packet is MqttPubCompPacket) + { + // Discard message. + return Task.FromResult(0); + } + + if (packet is MqttSubscribePacket subscribePacket) + { + return HandleIncomingSubscribePacketAsync(adapter, subscribePacket, cancellationToken); + } + + if (packet is MqttUnsubscribePacket unsubscribePacket) + { + return HandleIncomingUnsubscribePacketAsync(adapter, unsubscribePacket, cancellationToken); + } + + if (packet is MqttDisconnectPacket) + { + return StopAsync(true); + } + + if (packet is MqttConnectPacket) + { + return StopAsync(); + } + + _logger.Warning("Client '{0}': Received not supported packet ({1}). Closing connection.", ClientId, packet); + return StopAsync(); + } + + private async Task HandleIncomingSubscribePacketAsync(IMqttChannelAdapter adapter, MqttSubscribePacket subscribePacket, CancellationToken cancellationToken) + { + var subscribeResult = await SubscriptionsManager.SubscribeAsync(subscribePacket).ConfigureAwait(false); + await adapter.SendPacketsAsync(_options.DefaultCommunicationTimeout, cancellationToken, new[] { subscribeResult.ResponsePacket }).ConfigureAwait(false); + + if (subscribeResult.CloseConnection) + { + await StopAsync().ConfigureAwait(false); + } + + await EnqueueSubscribedRetainedMessagesAsync(subscribePacket.TopicFilters).ConfigureAwait(false); + } + + private async Task HandleIncomingUnsubscribePacketAsync(IMqttChannelAdapter adapter, MqttUnsubscribePacket unsubscribePacket, CancellationToken cancellationToken) + { + var unsubscribeResult = await SubscriptionsManager.UnsubscribeAsync(unsubscribePacket).ConfigureAwait(false); + await adapter.SendPacketsAsync(_options.DefaultCommunicationTimeout, cancellationToken, new[] { unsubscribeResult }); + } + + private async Task EnqueueSubscribedRetainedMessagesAsync(ICollection topicFilters) + { + var retainedMessages = await _retainedMessagesManager.GetSubscribedMessagesAsync(topicFilters); + foreach (var applicationMessage in retainedMessages) + { + await EnqueueApplicationMessageAsync(applicationMessage).ConfigureAwait(false); + } + } + + private Task HandleIncomingPublishPacketAsync(IMqttChannelAdapter adapter, MqttPublishPacket publishPacket, CancellationToken cancellationToken) + { + var applicationMessage = publishPacket.ToApplicationMessage(); + + switch (applicationMessage.QualityOfServiceLevel) + { + case MqttQualityOfServiceLevel.AtMostOnce: + { + return ApplicationMessageReceivedCallback?.Invoke(this, applicationMessage); + } + case MqttQualityOfServiceLevel.AtLeastOnce: + { + return HandleIncomingPublishPacketWithQoS1(adapter, applicationMessage, publishPacket, cancellationToken); + } + case MqttQualityOfServiceLevel.ExactlyOnce: + { + return HandleIncomingPublishPacketWithQoS2(adapter, applicationMessage, publishPacket, cancellationToken); + } + default: + { + throw new MqttCommunicationException("Received a not supported QoS level."); + } + } + } + + private async Task HandleIncomingPublishPacketWithQoS1(IMqttChannelAdapter adapter, MqttApplicationMessage applicationMessage, MqttPublishPacket publishPacket, CancellationToken cancellationToken) + { + await ApplicationMessageReceivedCallback(this, applicationMessage).ConfigureAwait(false); + + var response = new MqttPubAckPacket { PacketIdentifier = publishPacket.PacketIdentifier }; + await adapter.SendPacketsAsync(_options.DefaultCommunicationTimeout, cancellationToken, new[] { response }).ConfigureAwait(false); + } + + private async Task HandleIncomingPublishPacketWithQoS2(IMqttChannelAdapter adapter, MqttApplicationMessage applicationMessage, MqttPublishPacket publishPacket, CancellationToken cancellationToken) + { + // QoS 2 is implement as method "B" [4.3.3 QoS 2: Exactly once delivery] + await ApplicationMessageReceivedCallback(this, applicationMessage).ConfigureAwait(false); + + var response = new MqttPubRecPacket { PacketIdentifier = publishPacket.PacketIdentifier }; + await adapter.SendPacketsAsync(_options.DefaultCommunicationTimeout, cancellationToken, new[] { response }).ConfigureAwait(false); + } + + private Task HandleIncomingPubRelPacketAsync(IMqttChannelAdapter adapter, MqttPubRelPacket pubRelPacket, CancellationToken cancellationToken) + { + var response = new MqttPubCompPacket { PacketIdentifier = pubRelPacket.PacketIdentifier }; + return adapter.SendPacketsAsync(_options.DefaultCommunicationTimeout, cancellationToken, new[] { response }); + } + } +} diff --git a/Frameworks/MQTTnet.NetStandard/Server/MqttClientSessionsManager.cs b/Frameworks/MQTTnet.NetStandard/Server/MqttClientSessionsManager.cs new file mode 100644 index 00000000..8dd9d062 --- /dev/null +++ b/Frameworks/MQTTnet.NetStandard/Server/MqttClientSessionsManager.cs @@ -0,0 +1,335 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MQTTnet.Adapter; +using MQTTnet.Diagnostics; +using MQTTnet.Exceptions; +using MQTTnet.Packets; +using MQTTnet.Protocol; +using MQTTnet.Serializer; + +namespace MQTTnet.Server +{ + public sealed class MqttClientSessionsManager : IDisposable + { + private readonly Dictionary _sessions = new Dictionary(); + private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); + + private readonly IMqttServerOptions _options; + private readonly MqttRetainedMessagesManager _retainedMessagesManager; + private readonly IMqttNetLogger _logger; + + public MqttClientSessionsManager(IMqttServerOptions options, MqttRetainedMessagesManager retainedMessagesManager, IMqttNetLogger logger) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _options = options ?? throw new ArgumentNullException(nameof(options)); + _retainedMessagesManager = retainedMessagesManager ?? throw new ArgumentNullException(nameof(retainedMessagesManager)); + } + + public Action ClientConnectedCallback { get; set; } + public Action ClientDisconnectedCallback { get; set; } + public Action ClientSubscribedTopicCallback { get; set; } + public Action ClientUnsubscribedTopicCallback { get; set; } + public Action ApplicationMessageReceivedCallback { get; set; } + + public async Task RunSessionAsync(IMqttChannelAdapter clientAdapter, CancellationToken cancellationToken) + { + var clientId = string.Empty; + var wasCleanDisconnect = false; + MqttClientSession clientSession = null; + + try + { + if (!(await clientAdapter.ReceivePacketAsync(_options.DefaultCommunicationTimeout, cancellationToken) + .ConfigureAwait(false) is MqttConnectPacket connectPacket)) + { + throw new MqttProtocolViolationException( + "The first packet from a client must be a 'CONNECT' packet [MQTT-3.1.0-1]."); + } + + clientId = connectPacket.ClientId; + + // Switch to the required protocol version before sending any response. + clientAdapter.PacketSerializer.ProtocolVersion = connectPacket.ProtocolVersion; + + var connectReturnCode = ValidateConnection(connectPacket); + if (connectReturnCode != MqttConnectReturnCode.ConnectionAccepted) + { + await clientAdapter.SendPacketsAsync(_options.DefaultCommunicationTimeout, cancellationToken, new[] + { + new MqttConnAckPacket + { + ConnectReturnCode = connectReturnCode + } + }).ConfigureAwait(false); + + return; + } + + var result = await GetOrCreateClientSessionAsync(connectPacket).ConfigureAwait(false); + clientSession = result.Session; + + await clientAdapter.SendPacketsAsync(_options.DefaultCommunicationTimeout, cancellationToken, new[] + { + new MqttConnAckPacket + { + ConnectReturnCode = connectReturnCode, + IsSessionPresent = result.IsExistingSession + } + }).ConfigureAwait(false); + + ClientConnectedCallback?.Invoke(new ConnectedMqttClient + { + ClientId = clientId, + ProtocolVersion = clientAdapter.PacketSerializer.ProtocolVersion + }); + + wasCleanDisconnect = await clientSession.RunAsync(connectPacket, clientAdapter).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + } + catch (Exception exception) + { + _logger.Error(exception, exception.Message); + } + finally + { + try + { + await clientAdapter.DisconnectAsync(_options.DefaultCommunicationTimeout).ConfigureAwait(false); + clientAdapter.Dispose(); + } + catch (Exception exception) + { + _logger.Error(exception, exception.Message); + } + + ClientDisconnectedCallback?.Invoke(new ConnectedMqttClient + { + ClientId = clientId, + ProtocolVersion = clientAdapter.PacketSerializer.ProtocolVersion, + PendingApplicationMessages = clientSession?.PendingMessagesQueue.Count ?? 0 + }, + wasCleanDisconnect); + } + } + + public async Task StopAsync() + { + await _semaphore.WaitAsync().ConfigureAwait(false); + try + { + foreach (var session in _sessions) + { + await session.Value.StopAsync().ConfigureAwait(false); + } + + _sessions.Clear(); + } + finally + { + _semaphore.Release(); + } + } + + public async Task> GetConnectedClientsAsync() + { + await _semaphore.WaitAsync().ConfigureAwait(false); + try + { + return _sessions.Where(s => s.Value.IsConnected).Select(s => new ConnectedMqttClient + { + ClientId = s.Value.ClientId, + ProtocolVersion = s.Value.ProtocolVersion ?? MqttProtocolVersion.V311, + LastPacketReceived = s.Value.KeepAliveMonitor.LastPacketReceived, + LastNonKeepAlivePacketReceived = s.Value.KeepAliveMonitor.LastNonKeepAlivePacketReceived, + PendingApplicationMessages = s.Value.PendingMessagesQueue.Count + }).ToList(); + } + finally + { + _semaphore.Release(); + } + } + + public async Task DispatchApplicationMessageAsync(MqttClientSession senderClientSession, MqttApplicationMessage applicationMessage) + { + try + { + var interceptorContext = InterceptApplicationMessage(senderClientSession, applicationMessage); + if (interceptorContext.CloseConnection) + { + await senderClientSession.StopAsync().ConfigureAwait(false); + } + + if (interceptorContext.ApplicationMessage == null || !interceptorContext.AcceptPublish) + { + return; + } + + if (applicationMessage.Retain) + { + await _retainedMessagesManager.HandleMessageAsync(senderClientSession?.ClientId, applicationMessage).ConfigureAwait(false); + } + + ApplicationMessageReceivedCallback?.Invoke(senderClientSession?.ClientId, applicationMessage); + } + catch (Exception exception) + { + _logger.Error(exception, "Error while processing application message"); + } + + await _semaphore.WaitAsync().ConfigureAwait(false); + try + { + foreach (var clientSession in _sessions.Values) + { + await clientSession.EnqueueApplicationMessageAsync(applicationMessage); + } + } + finally + { + _semaphore.Release(); + } + } + + public async Task SubscribeAsync(string clientId, IList topicFilters) + { + if (clientId == null) throw new ArgumentNullException(nameof(clientId)); + if (topicFilters == null) throw new ArgumentNullException(nameof(topicFilters)); + + await _semaphore.WaitAsync().ConfigureAwait(false); + try + { + if (!_sessions.TryGetValue(clientId, out var session)) + { + throw new InvalidOperationException($"Client session {clientId} is unknown."); + } + + await session.SubscribeAsync(topicFilters); + } + finally + { + _semaphore.Release(); + } + } + + public async Task UnsubscribeAsync(string clientId, IList topicFilters) + { + if (clientId == null) throw new ArgumentNullException(nameof(clientId)); + if (topicFilters == null) throw new ArgumentNullException(nameof(topicFilters)); + + await _semaphore.WaitAsync().ConfigureAwait(false); + try + { + if (!_sessions.TryGetValue(clientId, out var session)) + { + throw new InvalidOperationException($"Client session {clientId} is unknown."); + } + + await session.UnsubscribeAsync(topicFilters); + } + finally + { + _semaphore.Release(); + } + } + + private MqttApplicationMessageInterceptorContext InterceptApplicationMessage(MqttClientSession senderClientSession, MqttApplicationMessage applicationMessage) + { + var interceptorContext = new MqttApplicationMessageInterceptorContext( + senderClientSession?.ClientId, + applicationMessage); + + var interceptor = _options.ApplicationMessageInterceptor; + if (interceptor == null) + { + return interceptorContext; + } + + interceptor(interceptorContext); + return interceptorContext; + } + + private MqttConnectReturnCode ValidateConnection(MqttConnectPacket connectPacket) + { + if (_options.ConnectionValidator == null) + { + return MqttConnectReturnCode.ConnectionAccepted; + } + + var context = new MqttConnectionValidatorContext( + connectPacket.ClientId, + connectPacket.Username, + connectPacket.Password, + connectPacket.WillMessage); + + _options.ConnectionValidator(context); + return context.ReturnCode; + } + + private async Task GetOrCreateClientSessionAsync(MqttConnectPacket connectPacket) + { + await _semaphore.WaitAsync().ConfigureAwait(false); + try + { + var isSessionPresent = _sessions.TryGetValue(connectPacket.ClientId, out var clientSession); + if (isSessionPresent) + { + if (connectPacket.CleanSession) + { + _sessions.Remove(connectPacket.ClientId); + + await clientSession.StopAsync().ConfigureAwait(false); + clientSession.Dispose(); + clientSession = null; + + _logger.Verbose("Stopped existing session of client '{0}'.", connectPacket.ClientId); + } + else + { + _logger.Verbose("Reusing existing session of client '{0}'.", connectPacket.ClientId); + } + } + + var isExistingSession = true; + if (clientSession == null) + { + isExistingSession = false; + + clientSession = new MqttClientSession(connectPacket.ClientId, _options, _retainedMessagesManager, _logger) + { + ApplicationMessageReceivedCallback = DispatchApplicationMessageAsync + }; + + clientSession.SubscriptionsManager.TopicSubscribedCallback = ClientSubscribedTopicCallback; + clientSession.SubscriptionsManager.TopicUnsubscribedCallback = ClientUnsubscribedTopicCallback; + + _sessions[connectPacket.ClientId] = clientSession; + + _logger.Verbose("Created a new session for client '{0}'.", connectPacket.ClientId); + } + + return new GetOrCreateClientSessionResult { IsExistingSession = isExistingSession, Session = clientSession }; + } + finally + { + _semaphore.Release(); + } + } + + public void Dispose() + { + ClientConnectedCallback = null; + ClientDisconnectedCallback = null; + ClientSubscribedTopicCallback = null; + ClientUnsubscribedTopicCallback = null; + ApplicationMessageReceivedCallback = null; + + _semaphore?.Dispose(); + } + } +} \ No newline at end of file diff --git a/MQTTnet.Core/Server/MqttClientSubscribeResult.cs b/Frameworks/MQTTnet.NetStandard/Server/MqttClientSubscribeResult.cs similarity index 73% rename from MQTTnet.Core/Server/MqttClientSubscribeResult.cs rename to Frameworks/MQTTnet.NetStandard/Server/MqttClientSubscribeResult.cs index 17bfe9a5..fe75c3b2 100644 --- a/MQTTnet.Core/Server/MqttClientSubscribeResult.cs +++ b/Frameworks/MQTTnet.NetStandard/Server/MqttClientSubscribeResult.cs @@ -1,6 +1,6 @@ -using MQTTnet.Core.Packets; +using MQTTnet.Packets; -namespace MQTTnet.Core.Server +namespace MQTTnet.Server { public class MqttClientSubscribeResult { diff --git a/Frameworks/MQTTnet.NetStandard/Server/MqttClientSubscribedTopicEventArgs.cs b/Frameworks/MQTTnet.NetStandard/Server/MqttClientSubscribedTopicEventArgs.cs new file mode 100644 index 00000000..11f5c063 --- /dev/null +++ b/Frameworks/MQTTnet.NetStandard/Server/MqttClientSubscribedTopicEventArgs.cs @@ -0,0 +1,17 @@ +using System; + +namespace MQTTnet.Server +{ + public class MqttClientSubscribedTopicEventArgs : EventArgs + { + public MqttClientSubscribedTopicEventArgs(string clientId, TopicFilter topicFilter) + { + ClientId = clientId ?? throw new ArgumentNullException(nameof(clientId)); + TopicFilter = topicFilter ?? throw new ArgumentNullException(nameof(topicFilter)); + } + + public string ClientId { get; } + + public TopicFilter TopicFilter { get; } + } +} diff --git a/Frameworks/MQTTnet.NetStandard/Server/MqttClientSubscriptionsManager.cs b/Frameworks/MQTTnet.NetStandard/Server/MqttClientSubscriptionsManager.cs new file mode 100644 index 00000000..e4ee9214 --- /dev/null +++ b/Frameworks/MQTTnet.NetStandard/Server/MqttClientSubscriptionsManager.cs @@ -0,0 +1,180 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MQTTnet.Packets; +using MQTTnet.Protocol; + +namespace MQTTnet.Server +{ + public sealed class MqttClientSubscriptionsManager : IDisposable + { + private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); + private readonly Dictionary _subscriptions = new Dictionary(); + private readonly IMqttServerOptions _options; + private readonly string _clientId; + + public MqttClientSubscriptionsManager(IMqttServerOptions options, string clientId) + { + _options = options ?? throw new ArgumentNullException(nameof(options)); + _clientId = clientId ?? throw new ArgumentNullException(nameof(clientId)); + } + + public Action TopicSubscribedCallback { get; set; } + public Action TopicUnsubscribedCallback { get; set; } + + public async Task SubscribeAsync(MqttSubscribePacket subscribePacket) + { + if (subscribePacket == null) throw new ArgumentNullException(nameof(subscribePacket)); + + var result = new MqttClientSubscribeResult + { + ResponsePacket = new MqttSubAckPacket + { + PacketIdentifier = subscribePacket.PacketIdentifier + }, + + CloseConnection = false + }; + + await _semaphore.WaitAsync().ConfigureAwait(false); + try + { + foreach (var topicFilter in subscribePacket.TopicFilters) + { + var interceptorContext = InterceptSubscribe(topicFilter); + if (!interceptorContext.AcceptSubscription) + { + result.ResponsePacket.SubscribeReturnCodes.Add(MqttSubscribeReturnCode.Failure); + } + else + { + result.ResponsePacket.SubscribeReturnCodes.Add(ConvertToMaximumQoS(topicFilter.QualityOfServiceLevel)); + } + + if (interceptorContext.CloseConnection) + { + result.CloseConnection = true; + } + + if (interceptorContext.AcceptSubscription) + { + _subscriptions[topicFilter.Topic] = topicFilter.QualityOfServiceLevel; + TopicSubscribedCallback?.Invoke(_clientId, topicFilter); + } + } + } + finally + { + _semaphore.Release(); + } + + return result; + } + + public async Task UnsubscribeAsync(MqttUnsubscribePacket unsubscribePacket) + { + if (unsubscribePacket == null) throw new ArgumentNullException(nameof(unsubscribePacket)); + + await _semaphore.WaitAsync().ConfigureAwait(false); + try + { + foreach (var topicFilter in unsubscribePacket.TopicFilters) + { + _subscriptions.Remove(topicFilter); + TopicUnsubscribedCallback?.Invoke(_clientId, topicFilter); + } + } + finally + { + _semaphore.Release(); + } + + return new MqttUnsubAckPacket + { + PacketIdentifier = unsubscribePacket.PacketIdentifier + }; + } + + public async Task CheckSubscriptionsAsync(MqttApplicationMessage applicationMessage) + { + if (applicationMessage == null) throw new ArgumentNullException(nameof(applicationMessage)); + + await _semaphore.WaitAsync().ConfigureAwait(false); + try + { + var qosLevels = new HashSet(); + foreach (var subscription in _subscriptions) + { + if (!MqttTopicFilterComparer.IsMatch(applicationMessage.Topic, subscription.Key)) + { + continue; + } + + qosLevels.Add(subscription.Value); + } + + if (qosLevels.Count == 0) + { + return new CheckSubscriptionsResult + { + IsSubscribed = false + }; + } + + return CreateSubscriptionResult(applicationMessage, qosLevels); + } + finally + { + _semaphore.Release(); + } + } + + public void Dispose() + { + _semaphore?.Dispose(); + } + + private static MqttSubscribeReturnCode ConvertToMaximumQoS(MqttQualityOfServiceLevel qualityOfServiceLevel) + { + switch (qualityOfServiceLevel) + { + case MqttQualityOfServiceLevel.AtMostOnce: return MqttSubscribeReturnCode.SuccessMaximumQoS0; + case MqttQualityOfServiceLevel.AtLeastOnce: return MqttSubscribeReturnCode.SuccessMaximumQoS1; + case MqttQualityOfServiceLevel.ExactlyOnce: return MqttSubscribeReturnCode.SuccessMaximumQoS2; + default: return MqttSubscribeReturnCode.Failure; + } + } + + private MqttSubscriptionInterceptorContext InterceptSubscribe(TopicFilter topicFilter) + { + var interceptorContext = new MqttSubscriptionInterceptorContext(_clientId, topicFilter); + _options.SubscriptionInterceptor?.Invoke(interceptorContext); + return interceptorContext; + } + + private static CheckSubscriptionsResult CreateSubscriptionResult(MqttApplicationMessage applicationMessage, HashSet subscribedQoSLevels) + { + MqttQualityOfServiceLevel effectiveQoS; + if (subscribedQoSLevels.Contains(applicationMessage.QualityOfServiceLevel)) + { + effectiveQoS = applicationMessage.QualityOfServiceLevel; + } + else if (subscribedQoSLevels.Count == 1) + { + effectiveQoS = subscribedQoSLevels.First(); + } + else + { + effectiveQoS = subscribedQoSLevels.Max(); + } + + return new CheckSubscriptionsResult + { + IsSubscribed = true, + QualityOfServiceLevel = effectiveQoS + }; + } + } +} diff --git a/Frameworks/MQTTnet.NetStandard/Server/MqttClientUnSubscribedTopicEventArgs.cs b/Frameworks/MQTTnet.NetStandard/Server/MqttClientUnSubscribedTopicEventArgs.cs new file mode 100644 index 00000000..d76347e0 --- /dev/null +++ b/Frameworks/MQTTnet.NetStandard/Server/MqttClientUnSubscribedTopicEventArgs.cs @@ -0,0 +1,17 @@ +using System; + +namespace MQTTnet.Server +{ + public class MqttClientUnsubscribedTopicEventArgs : EventArgs + { + public MqttClientUnsubscribedTopicEventArgs(string clientId, string topicFilter) + { + ClientId = clientId ?? throw new ArgumentNullException(nameof(clientId)); + TopicFilter = topicFilter ?? throw new ArgumentNullException(nameof(topicFilter)); + } + + public string ClientId { get; } + + public string TopicFilter { get; } + } +} diff --git a/Frameworks/MQTTnet.NetStandard/Server/MqttConnectionValidatorContext.cs b/Frameworks/MQTTnet.NetStandard/Server/MqttConnectionValidatorContext.cs new file mode 100644 index 00000000..a24d7955 --- /dev/null +++ b/Frameworks/MQTTnet.NetStandard/Server/MqttConnectionValidatorContext.cs @@ -0,0 +1,25 @@ +using MQTTnet.Protocol; + +namespace MQTTnet.Server +{ + public class MqttConnectionValidatorContext + { + public MqttConnectionValidatorContext(string clientId, string username, string password, MqttApplicationMessage willMessage) + { + ClientId = clientId; + Username = username; + Password = password; + WillMessage = willMessage; + } + + public string ClientId { get; } + + public string Username { get; } + + public string Password { get; } + + public MqttApplicationMessage WillMessage { get; } + + public MqttConnectReturnCode ReturnCode { get; set; } = MqttConnectReturnCode.ConnectionAccepted; + } +} diff --git a/Frameworks/MQTTnet.NetStandard/Server/MqttRetainedMessagesManager.cs b/Frameworks/MQTTnet.NetStandard/Server/MqttRetainedMessagesManager.cs new file mode 100644 index 00000000..f998557e --- /dev/null +++ b/Frameworks/MQTTnet.NetStandard/Server/MqttRetainedMessagesManager.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MQTTnet.Diagnostics; + +namespace MQTTnet.Server +{ + public sealed class MqttRetainedMessagesManager : IDisposable + { + private readonly Dictionary _retainedMessages = new Dictionary(); + private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); + private readonly IMqttNetLogger _logger; + private readonly IMqttServerOptions _options; + + public MqttRetainedMessagesManager(IMqttServerOptions options, IMqttNetLogger logger) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _options = options ?? throw new ArgumentNullException(nameof(options)); + } + + public async Task LoadMessagesAsync() + { + if (_options.Storage == null) + { + return; + } + + await _semaphore.WaitAsync().ConfigureAwait(false); + try + { + var retainedMessages = await _options.Storage.LoadRetainedMessagesAsync(); + + _retainedMessages.Clear(); + foreach (var retainedMessage in retainedMessages) + { + _retainedMessages[retainedMessage.Topic] = retainedMessage; + } + } + catch (Exception exception) + { + _logger.Error(exception, "Unhandled exception while loading retained messages."); + } + finally + { + _semaphore.Release(); + } + } + + public async Task HandleMessageAsync(string clientId, MqttApplicationMessage applicationMessage) + { + if (applicationMessage == null) throw new ArgumentNullException(nameof(applicationMessage)); + + await _semaphore.WaitAsync().ConfigureAwait(false); + try + { + await HandleMessageInternalAsync(clientId, applicationMessage); + } + catch (Exception exception) + { + _logger.Error(exception, "Unhandled exception while handling retained messages."); + } + finally + { + _semaphore.Release(); + } + } + + public async Task> GetSubscribedMessagesAsync(ICollection topicFilters) + { + var retainedMessages = new List(); + + await _semaphore.WaitAsync().ConfigureAwait(false); + try + { + foreach (var retainedMessage in _retainedMessages.Values) + { + foreach (var topicFilter in topicFilters) + { + if (!MqttTopicFilterComparer.IsMatch(retainedMessage.Topic, topicFilter.Topic)) + { + continue; + } + + retainedMessages.Add(retainedMessage); + break; + } + } + } + finally + { + _semaphore.Release(); + } + + return retainedMessages; + } + + public void Dispose() + { + _semaphore?.Dispose(); + } + + private async Task HandleMessageInternalAsync(string clientId, MqttApplicationMessage applicationMessage) + { + var saveIsRequired = false; + + if (applicationMessage.Payload?.Any() == false) + { + saveIsRequired = _retainedMessages.Remove(applicationMessage.Topic); + _logger.Info("Client '{0}' cleared retained message for topic '{1}'.", clientId, applicationMessage.Topic); + } + else + { + if (!_retainedMessages.ContainsKey(applicationMessage.Topic)) + { + _retainedMessages[applicationMessage.Topic] = applicationMessage; + saveIsRequired = true; + } + else + { + var existingMessage = _retainedMessages[applicationMessage.Topic]; + if (existingMessage.QualityOfServiceLevel != applicationMessage.QualityOfServiceLevel || !existingMessage.Payload.SequenceEqual(applicationMessage.Payload ?? new byte[0])) + { + _retainedMessages[applicationMessage.Topic] = applicationMessage; + saveIsRequired = true; + } + } + + _logger.Info("Client '{0}' set retained message for topic '{1}'.", clientId, applicationMessage.Topic); + } + + if (!saveIsRequired) + { + _logger.Verbose("Skipped saving retained messages because no changes were detected."); + } + + if (saveIsRequired && _options.Storage != null) + { + await _options.Storage.SaveRetainedMessagesAsync(_retainedMessages.Values.ToList()); + } + } + } +} diff --git a/Frameworks/MQTTnet.NetStandard/Server/MqttServer.cs b/Frameworks/MQTTnet.NetStandard/Server/MqttServer.cs new file mode 100644 index 00000000..63294420 --- /dev/null +++ b/Frameworks/MQTTnet.NetStandard/Server/MqttServer.cs @@ -0,0 +1,169 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MQTTnet.Adapter; +using MQTTnet.Diagnostics; + +namespace MQTTnet.Server +{ + public class MqttServer : IMqttServer + { + private readonly ICollection _adapters; + private readonly IMqttNetLogger _logger; + + private MqttClientSessionsManager _clientSessionsManager; + private MqttRetainedMessagesManager _retainedMessagesManager; + private CancellationTokenSource _cancellationTokenSource; + + public MqttServer(IEnumerable adapters, IMqttNetLogger logger) + { + if (adapters == null) throw new ArgumentNullException(nameof(adapters)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + _adapters = adapters.ToList(); + } + + public event EventHandler Started; + + public event EventHandler ClientConnected; + public event EventHandler ClientDisconnected; + public event EventHandler ClientSubscribedTopic; + public event EventHandler ClientUnsubscribedTopic; + + public event EventHandler ApplicationMessageReceived; + + public IMqttServerOptions Options { get; private set; } + + public Task> GetConnectedClientsAsync() + { + return _clientSessionsManager.GetConnectedClientsAsync(); + } + + public Task SubscribeAsync(string clientId, IList topicFilters) + { + if (clientId == null) throw new ArgumentNullException(nameof(clientId)); + if (topicFilters == null) throw new ArgumentNullException(nameof(topicFilters)); + + return _clientSessionsManager.SubscribeAsync(clientId, topicFilters); + } + + public Task UnsubscribeAsync(string clientId, IList topicFilters) + { + if (clientId == null) throw new ArgumentNullException(nameof(clientId)); + if (topicFilters == null) throw new ArgumentNullException(nameof(topicFilters)); + + return _clientSessionsManager.UnsubscribeAsync(clientId, topicFilters); + } + + public async Task PublishAsync(IEnumerable applicationMessages) + { + if (applicationMessages == null) throw new ArgumentNullException(nameof(applicationMessages)); + + if (_cancellationTokenSource == null) throw new InvalidOperationException("The server is not started."); + + foreach (var applicationMessage in applicationMessages) + { + await _clientSessionsManager.DispatchApplicationMessageAsync(null, applicationMessage); + } + } + + public async Task StartAsync(IMqttServerOptions options) + { + Options = options ?? throw new ArgumentNullException(nameof(options)); + + if (_cancellationTokenSource != null) throw new InvalidOperationException("The server is already started."); + + _cancellationTokenSource = new CancellationTokenSource(); + + _retainedMessagesManager = new MqttRetainedMessagesManager(Options, _logger); + await _retainedMessagesManager.LoadMessagesAsync(); + + _clientSessionsManager = new MqttClientSessionsManager(Options, _retainedMessagesManager, _logger) + { + ClientConnectedCallback = OnClientConnected, + ClientDisconnectedCallback = OnClientDisconnected, + ClientSubscribedTopicCallback = OnClientSubscribedTopic, + ClientUnsubscribedTopicCallback = OnClientUnsubscribedTopic, + ApplicationMessageReceivedCallback = OnApplicationMessageReceived + }; + + foreach (var adapter in _adapters) + { + adapter.ClientAccepted += OnClientAccepted; + await adapter.StartAsync(Options); + } + + _logger.Info("Started."); + Started?.Invoke(this, new MqttServerStartedEventArgs()); + } + + public async Task StopAsync() + { + try + { + if (_cancellationTokenSource == null) + { + return; + } + + _cancellationTokenSource.Cancel(false); + _cancellationTokenSource.Dispose(); + + foreach (var adapter in _adapters) + { + adapter.ClientAccepted -= OnClientAccepted; + await adapter.StopAsync(); + } + + await _clientSessionsManager.StopAsync(); + + _logger.Info("Stopped."); + } + finally + { + _clientSessionsManager?.Dispose(); + _retainedMessagesManager?.Dispose(); + + _cancellationTokenSource = null; + _retainedMessagesManager = null; + _clientSessionsManager = null; + } + } + + private void OnClientConnected(ConnectedMqttClient client) + { + _logger.Info("Client '{0}': Connected.", client.ClientId); + ClientConnected?.Invoke(this, new MqttClientConnectedEventArgs(client)); + } + + private void OnClientDisconnected(ConnectedMqttClient client, bool wasCleanDisconnect) + { + _logger.Info("Client '{0}': Disconnected (clean={1}).", client.ClientId, wasCleanDisconnect); + ClientDisconnected?.Invoke(this, new MqttClientDisconnectedEventArgs(client, wasCleanDisconnect)); + } + + private void OnClientSubscribedTopic(string clientId, TopicFilter topicFilter) + { + ClientSubscribedTopic?.Invoke(this, new MqttClientSubscribedTopicEventArgs(clientId, topicFilter)); + } + + private void OnClientUnsubscribedTopic(string clientId, string topicFilter) + { + ClientUnsubscribedTopic?.Invoke(this, new MqttClientUnsubscribedTopicEventArgs(clientId, topicFilter)); + } + + private void OnApplicationMessageReceived(string clientId, MqttApplicationMessage applicationMessage) + { + ApplicationMessageReceived?.Invoke(this, new MqttApplicationMessageReceivedEventArgs(clientId, applicationMessage)); + } + + private void OnClientAccepted(object sender, MqttServerAdapterClientAcceptedEventArgs eventArgs) + { + eventArgs.SessionTask = Task.Run( + () => _clientSessionsManager.RunSessionAsync(eventArgs.Client, _cancellationTokenSource.Token), + _cancellationTokenSource.Token); + } + } +} diff --git a/MQTTnet.Core/Server/MqttServerDefaultEndpointOptions.cs b/Frameworks/MQTTnet.NetStandard/Server/MqttServerDefaultEndpointOptions.cs similarity index 58% rename from MQTTnet.Core/Server/MqttServerDefaultEndpointOptions.cs rename to Frameworks/MQTTnet.NetStandard/Server/MqttServerDefaultEndpointOptions.cs index 04c4d732..545499b5 100644 --- a/MQTTnet.Core/Server/MqttServerDefaultEndpointOptions.cs +++ b/Frameworks/MQTTnet.NetStandard/Server/MqttServerDefaultEndpointOptions.cs @@ -1,9 +1,13 @@ -namespace MQTTnet.Core.Server +using System.Net; + +namespace MQTTnet.Server { public sealed class MqttServerDefaultEndpointOptions { public bool IsEnabled { get; set; } = true; public int? Port { get; set; } + + public IPAddress BoundIPAddress { get; set; } = IPAddress.Any; } } diff --git a/MQTTnet.Core/Server/MqttServerOptions.cs b/Frameworks/MQTTnet.NetStandard/Server/MqttServerOptions.cs similarity index 72% rename from MQTTnet.Core/Server/MqttServerOptions.cs rename to Frameworks/MQTTnet.NetStandard/Server/MqttServerOptions.cs index 4b096764..2b0a9a00 100644 --- a/MQTTnet.Core/Server/MqttServerOptions.cs +++ b/Frameworks/MQTTnet.NetStandard/Server/MqttServerOptions.cs @@ -1,24 +1,22 @@ using System; -using MQTTnet.Core.Packets; -using MQTTnet.Core.Protocol; -namespace MQTTnet.Core.Server +namespace MQTTnet.Server { - public class MqttServerOptions + public class MqttServerOptions : IMqttServerOptions { public MqttServerDefaultEndpointOptions DefaultEndpointOptions { get; } = new MqttServerDefaultEndpointOptions(); public MqttServerTlsEndpointOptions TlsEndpointOptions { get; } = new MqttServerTlsEndpointOptions(); public int ConnectionBacklog { get; set; } = 10; - + public TimeSpan DefaultCommunicationTimeout { get; set; } = TimeSpan.FromSeconds(15); - public Func ConnectionValidator { get; set; } + public Action ConnectionValidator { get; set; } public Action ApplicationMessageInterceptor { get; set; } - public Action SubscriptionsInterceptor { get; set; } + public Action SubscriptionInterceptor { get; set; } public IMqttServerStorage Storage { get; set; } } diff --git a/Frameworks/MQTTnet.NetStandard/Server/MqttServerOptionsBuilder.cs b/Frameworks/MQTTnet.NetStandard/Server/MqttServerOptionsBuilder.cs new file mode 100644 index 00000000..18b2de29 --- /dev/null +++ b/Frameworks/MQTTnet.NetStandard/Server/MqttServerOptionsBuilder.cs @@ -0,0 +1,105 @@ +using System; +using System.Net; + +namespace MQTTnet.Server +{ + public class MqttServerOptionsBuilder + { + private readonly MqttServerOptions _options = new MqttServerOptions(); + + public MqttServerOptionsBuilder WithConnectionBacklog(int value) + { + _options.ConnectionBacklog = value; + return this; + } + + public MqttServerOptionsBuilder WithDefaultCommunicationTimeout(TimeSpan value) + { + _options.DefaultCommunicationTimeout = value; + return this; + } + + public MqttServerOptionsBuilder WithDefaultEndpoint() + { + _options.DefaultEndpointOptions.IsEnabled = true; + return this; + } + + public MqttServerOptionsBuilder WithDefaultEndpointPort(int? value) + { + _options.DefaultEndpointOptions.Port = value; + return this; + } + + public MqttServerOptionsBuilder WithDefaultEndpointBoundIPAddress(IPAddress value) + { + _options.DefaultEndpointOptions.BoundIPAddress = value ?? IPAddress.Any; + return this; + } + + public MqttServerOptionsBuilder WithoutDefaultEndpoint() + { + _options.DefaultEndpointOptions.IsEnabled = false; + return this; + } + + public MqttServerOptionsBuilder WithEncryptedEndpoint() + { + _options.TlsEndpointOptions.IsEnabled = true; + return this; + } + + public MqttServerOptionsBuilder WithEncryptedEndpointPort(int? value) + { + _options.TlsEndpointOptions.Port = value; + return this; + } + + public MqttServerOptionsBuilder WithEncryptedEndpointBoundIPAddress(IPAddress value) + { + _options.TlsEndpointOptions.BoundIPAddress = value; + return this; + } + + public MqttServerOptionsBuilder WithEncryptionCertificate(byte[] value) + { + _options.TlsEndpointOptions.Certificate = value; + return this; + } + + public MqttServerOptionsBuilder WithoutEncryptedEndpoint() + { + _options.TlsEndpointOptions.IsEnabled = false; + return this; + } + + public MqttServerOptionsBuilder WithStorage(IMqttServerStorage value) + { + _options.Storage = value; + return this; + } + + public MqttServerOptionsBuilder WithConnectionValidator(Action value) + { + _options.ConnectionValidator = value; + return this; + } + + public MqttServerOptionsBuilder WithApplicationMessageInterceptor(Action value) + { + _options.ApplicationMessageInterceptor = value; + return this; + } + + public MqttServerOptionsBuilder WithSubscriptionInterceptor(Action value) + { + _options.SubscriptionInterceptor = value; + return this; + } + + public IMqttServerOptions Build() + { + return _options; + } + } +} diff --git a/MQTTnet.Core/Server/MqttServerOptionsExtensions.cs b/Frameworks/MQTTnet.NetStandard/Server/MqttServerOptionsExtensions.cs similarity index 77% rename from MQTTnet.Core/Server/MqttServerOptionsExtensions.cs rename to Frameworks/MQTTnet.NetStandard/Server/MqttServerOptionsExtensions.cs index 4917018f..db93ef18 100644 --- a/MQTTnet.Core/Server/MqttServerOptionsExtensions.cs +++ b/Frameworks/MQTTnet.NetStandard/Server/MqttServerOptionsExtensions.cs @@ -1,10 +1,10 @@ using System; -namespace MQTTnet.Core.Server +namespace MQTTnet.Server { public static class MqttServerOptionsExtensions { - public static int GetTlsEndpointPort(this MqttServerOptions options) + public static int GetTlsEndpointPort(this IMqttServerOptions options) { if (options == null) throw new ArgumentNullException(nameof(options)); @@ -16,7 +16,7 @@ namespace MQTTnet.Core.Server return options.TlsEndpointOptions.Port.Value; } - public static int GetDefaultEndpointPort(this MqttServerOptions options) + public static int GetDefaultEndpointPort(this IMqttServerOptions options) { if (options == null) throw new ArgumentNullException(nameof(options)); diff --git a/Frameworks/MQTTnet.NetStandard/Server/MqttServerStartedEventArgs.cs b/Frameworks/MQTTnet.NetStandard/Server/MqttServerStartedEventArgs.cs new file mode 100644 index 00000000..08202994 --- /dev/null +++ b/Frameworks/MQTTnet.NetStandard/Server/MqttServerStartedEventArgs.cs @@ -0,0 +1,8 @@ +using System; + +namespace MQTTnet.Server +{ + public class MqttServerStartedEventArgs : EventArgs + { + } +} diff --git a/MQTTnet.Core/Server/MqttServerTlsEndpointOptions.cs b/Frameworks/MQTTnet.NetStandard/Server/MqttServerTlsEndpointOptions.cs similarity index 62% rename from MQTTnet.Core/Server/MqttServerTlsEndpointOptions.cs rename to Frameworks/MQTTnet.NetStandard/Server/MqttServerTlsEndpointOptions.cs index 0fb3839d..73be93c5 100644 --- a/MQTTnet.Core/Server/MqttServerTlsEndpointOptions.cs +++ b/Frameworks/MQTTnet.NetStandard/Server/MqttServerTlsEndpointOptions.cs @@ -1,4 +1,6 @@ -namespace MQTTnet.Core.Server +using System.Net; + +namespace MQTTnet.Server { public sealed class MqttServerTlsEndpointOptions { @@ -7,5 +9,7 @@ public int? Port { get; set; } public byte[] Certificate { get; set; } + + public IPAddress BoundIPAddress { get; set; } = IPAddress.Any; } } diff --git a/MQTTnet.Core/Server/MqttSubscribeResult.cs b/Frameworks/MQTTnet.NetStandard/Server/MqttSubscribeResult.cs similarity index 73% rename from MQTTnet.Core/Server/MqttSubscribeResult.cs rename to Frameworks/MQTTnet.NetStandard/Server/MqttSubscribeResult.cs index ef80af44..2de40046 100644 --- a/MQTTnet.Core/Server/MqttSubscribeResult.cs +++ b/Frameworks/MQTTnet.NetStandard/Server/MqttSubscribeResult.cs @@ -1,6 +1,6 @@ -using MQTTnet.Core.Packets; +using MQTTnet.Packets; -namespace MQTTnet.Core.Server +namespace MQTTnet.Server { public class MqttSubscribeResult { diff --git a/MQTTnet.Core/Server/MqttSubscriptionInterceptorContext.cs b/Frameworks/MQTTnet.NetStandard/Server/MqttSubscriptionInterceptorContext.cs similarity index 74% rename from MQTTnet.Core/Server/MqttSubscriptionInterceptorContext.cs rename to Frameworks/MQTTnet.NetStandard/Server/MqttSubscriptionInterceptorContext.cs index 93282d3b..07b94bb6 100644 --- a/MQTTnet.Core/Server/MqttSubscriptionInterceptorContext.cs +++ b/Frameworks/MQTTnet.NetStandard/Server/MqttSubscriptionInterceptorContext.cs @@ -1,17 +1,19 @@ -namespace MQTTnet.Core.Server +using System; + +namespace MQTTnet.Server { public class MqttSubscriptionInterceptorContext { public MqttSubscriptionInterceptorContext(string clientId, TopicFilter topicFilter) { ClientId = clientId; - TopicFilter = topicFilter; + TopicFilter = topicFilter ?? throw new ArgumentNullException(nameof(topicFilter)); } public string ClientId { get; } public TopicFilter TopicFilter { get; } - + public bool AcceptSubscription { get; set; } = true; public bool CloseConnection { get; set; } diff --git a/MQTTnet.Core/Server/MqttTopicFilterComparer.cs b/Frameworks/MQTTnet.NetStandard/Server/MqttTopicFilterComparer.cs similarity index 97% rename from MQTTnet.Core/Server/MqttTopicFilterComparer.cs rename to Frameworks/MQTTnet.NetStandard/Server/MqttTopicFilterComparer.cs index 987a71f1..82444159 100644 --- a/MQTTnet.Core/Server/MqttTopicFilterComparer.cs +++ b/Frameworks/MQTTnet.NetStandard/Server/MqttTopicFilterComparer.cs @@ -1,6 +1,6 @@ using System; -namespace MQTTnet.Core.Server +namespace MQTTnet.Server { public static class MqttTopicFilterComparer { diff --git a/Frameworks/MQTTnet.NetStandard/ServiceCollectionExtensions.cs b/Frameworks/MQTTnet.NetStandard/ServiceCollectionExtensions.cs deleted file mode 100644 index aa53e1b8..00000000 --- a/Frameworks/MQTTnet.NetStandard/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,70 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using MQTTnet.Core.Adapter; -using MQTTnet.Core.Client; -using MQTTnet.Core.ManagedClient; -using MQTTnet.Core.Serializer; -using MQTTnet.Core.Server; -using MQTTnet.Implementations; -using System; -using Microsoft.Extensions.Logging; -using MQTTnet.Core.Diagnostics; - -namespace MQTTnet -{ - public static class ServiceCollectionExtensions - { - public static IServiceCollection AddMqttServer(this IServiceCollection services) - { - services.AddMqttServerServices(); - - services.AddSingleton(s => s.GetService()); - services.AddSingleton(); - - return services; - } - - public static IServiceCollection AddMqttServerServices(this IServiceCollection services) - { - services.AddOptions(); - services.AddSingleton(); - services.AddSingleton(s => s.GetService()); - services.AddSingleton(s => s.GetService()); - - services.AddTransient(); - services.AddTransient(); - - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - return services; - } - - public static IServiceCollection AddMqttServer(this IServiceCollection services, Action configureOptions) - { - return services - .AddMqttServer() - .Configure(configureOptions); - } - - public static IServiceCollection AddMqttClient(this IServiceCollection services) - { - services.AddSingleton(); - services.AddSingleton(s => s.GetService()); - - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - - return services; - } - - public static ILoggerFactory AddMqttTrace(this ILoggerFactory factory) - { - factory.AddProvider(new MqttNetTrace()); - return factory; - } - } -} diff --git a/MQTTnet.Core/TopicFilter.cs b/Frameworks/MQTTnet.NetStandard/TopicFilter.cs similarity index 62% rename from MQTTnet.Core/TopicFilter.cs rename to Frameworks/MQTTnet.NetStandard/TopicFilter.cs index 3e95afa0..1dad73d0 100644 --- a/MQTTnet.Core/TopicFilter.cs +++ b/Frameworks/MQTTnet.NetStandard/TopicFilter.cs @@ -1,6 +1,6 @@ -using MQTTnet.Core.Protocol; +using MQTTnet.Protocol; -namespace MQTTnet.Core +namespace MQTTnet { public sealed class TopicFilter { @@ -10,14 +10,9 @@ namespace MQTTnet.Core QualityOfServiceLevel = qualityOfServiceLevel; } - public string Topic { get; } + public string Topic { get; set; } - public MqttQualityOfServiceLevel QualityOfServiceLevel { get; } - - public override int GetHashCode() - { - return QualityOfServiceLevel.GetHashCode() ^ (Topic ?? string.Empty).GetHashCode(); - } + public MqttQualityOfServiceLevel QualityOfServiceLevel { get; set; } public override string ToString() { diff --git a/MQTTnet.Core/TopicFilterBuilder.cs b/Frameworks/MQTTnet.NetStandard/TopicFilterBuilder.cs similarity index 93% rename from MQTTnet.Core/TopicFilterBuilder.cs rename to Frameworks/MQTTnet.NetStandard/TopicFilterBuilder.cs index 90114e7c..38c8fbc2 100644 --- a/MQTTnet.Core/TopicFilterBuilder.cs +++ b/Frameworks/MQTTnet.NetStandard/TopicFilterBuilder.cs @@ -1,12 +1,12 @@ -using MQTTnet.Core.Protocol; -using MQTTnet.Core.Exceptions; +using MQTTnet.Exceptions; +using MQTTnet.Protocol; -namespace MQTTnet.Core +namespace MQTTnet { public class TopicFilterBuilder { - private string _topic; private MqttQualityOfServiceLevel _qualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce; + private string _topic; public TopicFilterBuilder WithTopic(string topic) { diff --git a/Frameworks/MQTTnet.NetStandard/codeSigningKey.pfx b/Frameworks/MQTTnet.NetStandard/codeSigningKey.pfx new file mode 100644 index 00000000..9374a5db Binary files /dev/null and b/Frameworks/MQTTnet.NetStandard/codeSigningKey.pfx differ diff --git a/Frameworks/MQTTnet.Netstandard/MQTTnet.NetStandard.csproj b/Frameworks/MQTTnet.Netstandard/MQTTnet.NetStandard.csproj new file mode 100644 index 00000000..cb154c24 --- /dev/null +++ b/Frameworks/MQTTnet.Netstandard/MQTTnet.NetStandard.csproj @@ -0,0 +1,65 @@ + + + + netstandard1.3;netstandard2.0;net452;net461;uap10.0 + netstandard1.3;netstandard2.0 + MQTTnet + MQTTnet + False + Full + 0.0.0.0 + 0.0.0.0 + 0.0.0.0 + + + + + + false + false + + + + false + UAP,Version=v10.0 + UAP + 10.0.16299.0 + 10.0.10240.0 + .NETCore + v5.0 + $(DefineConstants);WINDOWS_UWP + en + $(MSBuildExtensionsPath)\Microsoft\WindowsXaml\v$(VisualStudioVersion)\Microsoft.Windows.UI.Xaml.CSharp.targets + + + + + + + + RELEASE;NETSTANDARD1_3 + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/MQTTnet.Core/Adapter/IMqttCommunicationAdapterFactory.cs b/MQTTnet.Core/Adapter/IMqttCommunicationAdapterFactory.cs deleted file mode 100644 index ffcfd2c1..00000000 --- a/MQTTnet.Core/Adapter/IMqttCommunicationAdapterFactory.cs +++ /dev/null @@ -1,12 +0,0 @@ -using MQTTnet.Core.Client; -using MQTTnet.Core.Channel; - -namespace MQTTnet.Core.Adapter -{ - public interface IMqttCommunicationAdapterFactory - { - IMqttCommunicationAdapter CreateClientMqttCommunicationAdapter(IMqttClientOptions options); - - IMqttCommunicationAdapter CreateServerMqttCommunicationAdapter(IMqttCommunicationChannel channel); - } -} \ No newline at end of file diff --git a/MQTTnet.Core/Adapter/MqttChannelCommunicationAdapter.cs b/MQTTnet.Core/Adapter/MqttChannelCommunicationAdapter.cs deleted file mode 100644 index d47d4cee..00000000 --- a/MQTTnet.Core/Adapter/MqttChannelCommunicationAdapter.cs +++ /dev/null @@ -1,211 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using MQTTnet.Core.Channel; -using MQTTnet.Core.Exceptions; -using MQTTnet.Core.Internal; -using MQTTnet.Core.Packets; -using MQTTnet.Core.Serializer; -using Microsoft.Extensions.Logging; - -namespace MQTTnet.Core.Adapter -{ - public class MqttChannelCommunicationAdapter : IMqttCommunicationAdapter - { - private readonly ILogger _logger; - private readonly IMqttCommunicationChannel _channel; - private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); - - public MqttChannelCommunicationAdapter(IMqttCommunicationChannel channel, IMqttPacketSerializer serializer, ILogger logger) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _channel = channel ?? throw new ArgumentNullException(nameof(channel)); - PacketSerializer = serializer ?? throw new ArgumentNullException(nameof(serializer)); - } - - public IMqttPacketSerializer PacketSerializer { get; } - - public async Task ConnectAsync(TimeSpan timeout) - { - try - { - await _channel.ConnectAsync().TimeoutAfter(timeout).ConfigureAwait(false); - } - catch (TaskCanceledException) - { - throw; - } - catch (OperationCanceledException) - { - throw; - } - catch (MqttCommunicationTimedOutException) - { - throw; - } - catch (MqttCommunicationException) - { - throw; - } - catch (Exception exception) - { - throw new MqttCommunicationException(exception); - } - } - - public async Task DisconnectAsync(TimeSpan timeout) - { - try - { - await _channel.DisconnectAsync().TimeoutAfter(timeout).ConfigureAwait(false); - } - catch (TaskCanceledException) - { - throw; - } - catch (OperationCanceledException) - { - throw; - } - catch (MqttCommunicationTimedOutException) - { - throw; - } - catch (MqttCommunicationException) - { - throw; - } - catch (Exception exception) - { - throw new MqttCommunicationException(exception); - } - } - - public async Task SendPacketsAsync(TimeSpan timeout, CancellationToken cancellationToken, IEnumerable packets) - { - await _semaphore.WaitAsync(cancellationToken); - - try - { - foreach (var packet in packets) - { - if (packet == null) - { - continue; - } - - _logger.LogInformation("TX >>> {0} [Timeout={1}]", packet, timeout); - - var writeBuffer = PacketSerializer.Serialize(packet); - await _channel.SendStream.WriteAsync(writeBuffer, 0, writeBuffer.Length, cancellationToken).ConfigureAwait(false); - } - - if (timeout > TimeSpan.Zero) - { - await _channel.SendStream.FlushAsync(cancellationToken).TimeoutAfter(timeout).ConfigureAwait(false); - } - else - { - await _channel.SendStream.FlushAsync(cancellationToken).ConfigureAwait(false); - } - } - catch (TaskCanceledException) - { - throw; - } - catch (OperationCanceledException) - { - throw; - } - catch (MqttCommunicationTimedOutException) - { - throw; - } - catch (MqttCommunicationException) - { - throw; - } - catch (Exception exception) - { - throw new MqttCommunicationException(exception); - } - finally - { - _semaphore.Release(); - } - } - - public async Task ReceivePacketAsync(TimeSpan timeout, CancellationToken cancellationToken) - { - try - { - ReceivedMqttPacket receivedMqttPacket; - if (timeout > TimeSpan.Zero) - { - receivedMqttPacket = await ReceiveAsync(_channel.RawReceiveStream, cancellationToken).TimeoutAfter(timeout).ConfigureAwait(false); - } - else - { - receivedMqttPacket = await ReceiveAsync(_channel.ReceiveStream, cancellationToken).ConfigureAwait(false); - } - - if (cancellationToken.IsCancellationRequested) - { - throw new TaskCanceledException(); - } - - var packet = PacketSerializer.Deserialize(receivedMqttPacket); - if (packet == null) - { - throw new MqttProtocolViolationException("Received malformed packet."); - } - - _logger.LogInformation("RX <<< {0}", packet); - return packet; - } - catch (TaskCanceledException) - { - throw; - } - catch (OperationCanceledException) - { - throw; - } - catch (MqttCommunicationTimedOutException) - { - throw; - } - catch (MqttCommunicationException) - { - throw; - } - catch (Exception exception) - { - throw new MqttCommunicationException(exception); - } - } - - private static async Task ReceiveAsync(Stream stream, CancellationToken cancellationToken) - { - var header = MqttPacketReader.ReadHeaderFromSource(stream, cancellationToken); - - if (header.BodyLength == 0) - { - return new ReceivedMqttPacket(header, new MemoryStream(0)); - } - - var body = new byte[header.BodyLength]; - - var offset = 0; - do - { - var readBytesCount = await stream.ReadAsync(body, offset, body.Length - offset, cancellationToken).ConfigureAwait(false); - offset += readBytesCount; - } while (offset < header.BodyLength); - - return new ReceivedMqttPacket(header, new MemoryStream(body, 0, body.Length)); - } - } -} diff --git a/MQTTnet.Core/Adapter/MqttCommunicationAdapterExtensions.cs b/MQTTnet.Core/Adapter/MqttCommunicationAdapterExtensions.cs deleted file mode 100644 index 0fa9ab54..00000000 --- a/MQTTnet.Core/Adapter/MqttCommunicationAdapterExtensions.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using MQTTnet.Core.Packets; - -namespace MQTTnet.Core.Adapter -{ - public static class MqttCommunicationAdapterExtensions - { - public static Task SendPacketsAsync(this IMqttCommunicationAdapter adapter, TimeSpan timeout, CancellationToken cancellationToken, params MqttBasePacket[] packets) - { - return adapter.SendPacketsAsync(timeout, cancellationToken, packets); - } - } -} \ No newline at end of file diff --git a/MQTTnet.Core/ApplicationMessagePublisherExtensions.cs b/MQTTnet.Core/ApplicationMessagePublisherExtensions.cs deleted file mode 100644 index 6893a1b6..00000000 --- a/MQTTnet.Core/ApplicationMessagePublisherExtensions.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace MQTTnet.Core -{ - public static class ApplicationMessagePublisherExtensions - { - public static Task PublishAsync(this IApplicationMessagePublisher client, params MqttApplicationMessage[] applicationMessages) - { - if (client == null) throw new ArgumentNullException(nameof(client)); - if (applicationMessages == null) throw new ArgumentNullException(nameof(applicationMessages)); - - return client.PublishAsync(applicationMessages); - } - } -} diff --git a/MQTTnet.Core/Client/IMqttClientFactory.cs b/MQTTnet.Core/Client/IMqttClientFactory.cs deleted file mode 100644 index c8b5ef3e..00000000 --- a/MQTTnet.Core/Client/IMqttClientFactory.cs +++ /dev/null @@ -1,11 +0,0 @@ -using MQTTnet.Core.ManagedClient; - -namespace MQTTnet.Core.Client -{ - public interface IMqttClientFactory - { - IMqttClient CreateMqttClient(); - - IManagedMqttClient CreateManagedMqttClient(); - } -} \ No newline at end of file diff --git a/MQTTnet.Core/Client/MqttClient.cs b/MQTTnet.Core/Client/MqttClient.cs deleted file mode 100644 index d16575b4..00000000 --- a/MQTTnet.Core/Client/MqttClient.cs +++ /dev/null @@ -1,473 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MQTTnet.Core.Adapter; -using MQTTnet.Core.Exceptions; -using MQTTnet.Core.Internal; -using MQTTnet.Core.Packets; -using MQTTnet.Core.Protocol; -using Microsoft.Extensions.Logging; - -namespace MQTTnet.Core.Client -{ - public class MqttClient : IMqttClient - { - private readonly HashSet _unacknowledgedPublishPackets = new HashSet(); - private readonly MqttPacketDispatcher _packetDispatcher; - private readonly IMqttCommunicationAdapterFactory _communicationAdapterFactory; - private readonly ILogger _logger; - - private IMqttClientOptions _options; - private bool _isReceivingPackets; - private int _latestPacketIdentifier; - private CancellationTokenSource _cancellationTokenSource; - private IMqttCommunicationAdapter _adapter; - private IDisposable _scopeHandle; - - public MqttClient(IMqttCommunicationAdapterFactory communicationAdapterFactory, ILogger logger, MqttPacketDispatcher packetDispatcher) - { - _communicationAdapterFactory = communicationAdapterFactory ?? throw new ArgumentNullException(nameof(communicationAdapterFactory)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _packetDispatcher = packetDispatcher ?? throw new ArgumentNullException(nameof(packetDispatcher)); - } - - public event EventHandler Connected; - public event EventHandler Disconnected; - public event EventHandler ApplicationMessageReceived; - - public bool IsConnected { get; private set; } - - public async Task ConnectAsync(IMqttClientOptions options) - { - if (options == null) throw new ArgumentNullException(nameof(options)); - if (options.ChannelOptions == null) throw new ArgumentException("ChannelOptions are not set."); - - ThrowIfConnected("It is not allowed to connect with a server after the connection is established."); - - try - { - _options = options; - _cancellationTokenSource = new CancellationTokenSource(); - _latestPacketIdentifier = 0; - _packetDispatcher.Reset(); - - _adapter = _communicationAdapterFactory.CreateClientMqttCommunicationAdapter(options); - - _scopeHandle = _logger.BeginScope(options.LogId ?? options.ClientId); - _logger.LogTrace("Trying to connect with server."); - await _adapter.ConnectAsync(_options.CommunicationTimeout).ConfigureAwait(false); - _logger.LogTrace("Connection with server established."); - - await SetupIncomingPacketProcessingAsync(); - var connectResponse = await AuthenticateAsync(options.WillMessage); - - _logger.LogTrace("MQTT connection with server established."); - - if (_options.KeepAlivePeriod != TimeSpan.Zero) - { - StartSendKeepAliveMessages(_cancellationTokenSource.Token); - } - - IsConnected = true; - Connected?.Invoke(this, new MqttClientConnectedEventArgs(connectResponse.IsSessionPresent)); - return new MqttClientConnectResult(connectResponse.IsSessionPresent); - } - catch (Exception exception) - { - _logger.LogError(new EventId(), exception, "Error while connecting with server."); - await DisconnectInternalAsync().ConfigureAwait(false); - throw; - } - } - - public async Task DisconnectAsync() - { - if (!IsConnected) - { - return; - } - - try - { - await SendAsync(new MqttDisconnectPacket()).ConfigureAwait(false); - } - finally - { - await DisconnectInternalAsync().ConfigureAwait(false); - _scopeHandle.Dispose(); - } - } - - public async Task> SubscribeAsync(IEnumerable topicFilters) - { - if (topicFilters == null) throw new ArgumentNullException(nameof(topicFilters)); - - ThrowIfNotConnected(); - - var subscribePacket = new MqttSubscribePacket - { - PacketIdentifier = GetNewPacketIdentifier(), - TopicFilters = topicFilters.ToList() - }; - - var response = await SendAndReceiveAsync(subscribePacket).ConfigureAwait(false); - - if (response.SubscribeReturnCodes.Count != subscribePacket.TopicFilters.Count) - { - throw new MqttProtocolViolationException("The return codes are not matching the topic filters [MQTT-3.9.3-1]."); - } - - return subscribePacket.TopicFilters.Select((t, i) => new MqttSubscribeResult(t, response.SubscribeReturnCodes[i])).ToList(); - } - - public async Task UnsubscribeAsync(IEnumerable topicFilters) - { - if (topicFilters == null) throw new ArgumentNullException(nameof(topicFilters)); - - ThrowIfNotConnected(); - - var unsubscribePacket = new MqttUnsubscribePacket - { - PacketIdentifier = GetNewPacketIdentifier(), - TopicFilters = topicFilters.ToList() - }; - - await SendAndReceiveAsync(unsubscribePacket); - } - - public async Task PublishAsync(IEnumerable applicationMessages) - { - ThrowIfNotConnected(); - - var publishPackets = applicationMessages.Select(m => m.ToPublishPacket()); - - foreach (var qosGroup in publishPackets.GroupBy(p => p.QualityOfServiceLevel)) - { - var qosPackets = qosGroup.ToArray(); - switch (qosGroup.Key) - { - case MqttQualityOfServiceLevel.AtMostOnce: - { - // No packet identifier is used for QoS 0 [3.3.2.2 Packet Identifier] - await _adapter.SendPacketsAsync(_options.CommunicationTimeout, _cancellationTokenSource.Token, qosPackets); - break; - } - case MqttQualityOfServiceLevel.AtLeastOnce: - { - foreach (var publishPacket in qosPackets) - { - publishPacket.PacketIdentifier = GetNewPacketIdentifier(); - await SendAndReceiveAsync(publishPacket); - } - - break; - } - case MqttQualityOfServiceLevel.ExactlyOnce: - { - foreach (var publishPacket in qosPackets) - { - publishPacket.PacketIdentifier = GetNewPacketIdentifier(); - var pubRecPacket = await SendAndReceiveAsync(publishPacket).ConfigureAwait(false); - await SendAndReceiveAsync(pubRecPacket.CreateResponse()).ConfigureAwait(false); - } - - break; - } - default: - { - throw new InvalidOperationException(); - } - } - } - } - - private async Task AuthenticateAsync(MqttApplicationMessage willApplicationMessage) - { - var connectPacket = new MqttConnectPacket - { - ClientId = _options.ClientId, - Username = _options.Credentials?.Username, - Password = _options.Credentials?.Password, - CleanSession = _options.CleanSession, - KeepAlivePeriod = (ushort) _options.KeepAlivePeriod.TotalSeconds, - WillMessage = willApplicationMessage - }; - - var response = await SendAndReceiveAsync(connectPacket).ConfigureAwait(false); - if (response.ConnectReturnCode != MqttConnectReturnCode.ConnectionAccepted) - { - throw new MqttConnectingFailedException(response.ConnectReturnCode); - } - - return response; - } - - private async Task SetupIncomingPacketProcessingAsync() - { - _isReceivingPackets = false; - -#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed - Task.Factory.StartNew( - () => ReceivePackets(_cancellationTokenSource.Token), - _cancellationTokenSource.Token, - TaskCreationOptions.LongRunning, - TaskScheduler.Default).ConfigureAwait(false); -#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed - - while (!_isReceivingPackets && _cancellationTokenSource != null && !_cancellationTokenSource.IsCancellationRequested) - { - await Task.Delay(TimeSpan.FromMilliseconds(100)); - } - } - - private void ThrowIfNotConnected() - { - if (!IsConnected) throw new MqttCommunicationException("The client is not connected."); - } - - private void ThrowIfConnected(string message) - { - if (IsConnected) throw new MqttProtocolViolationException(message); - } - - private async Task DisconnectInternalAsync() - { - var clientWasConnected = IsConnected; - IsConnected = false; - - var cts = _cancellationTokenSource; - if (cts == null || cts.IsCancellationRequested) - { - return; - } - - cts.Cancel(false); - cts.Dispose(); - _cancellationTokenSource = null; - - try - { - await _adapter.DisconnectAsync(_options.CommunicationTimeout).ConfigureAwait(false); - _logger.LogInformation("Disconnected from adapter."); - } - catch (Exception exception) - { - _logger.LogWarning(new EventId(), exception, "Error while disconnecting from adapter."); - } - finally - { - Disconnected?.Invoke(this, new MqttClientDisconnectedEventArgs(clientWasConnected)); - } - } - - private async Task ProcessReceivedPacketAsync(MqttBasePacket packet) - { - try - { - _logger.LogInformation("Received <<< {0}", packet); - - if (packet is MqttPingReqPacket) - { - await SendAsync(new MqttPingRespPacket()); - return; - } - - if (packet is MqttDisconnectPacket) - { - await DisconnectAsync(); - return; - } - - if (packet is MqttPublishPacket publishPacket) - { - await ProcessReceivedPublishPacket(publishPacket); - return; - } - - if (packet is MqttPubRelPacket pubRelPacket) - { - await ProcessReceivedPubRelPacket(pubRelPacket); - return; - } - - _packetDispatcher.Dispatch(packet); - } - catch (Exception exception) - { - _logger.LogError(new EventId(), exception, "Unhandled exception while processing received packet."); - } - } - - private void FireApplicationMessageReceivedEvent(MqttPublishPacket publishPacket) - { - try - { - var applicationMessage = publishPacket.ToApplicationMessage(); - ApplicationMessageReceived?.Invoke(this, new MqttApplicationMessageReceivedEventArgs(_options.ClientId, applicationMessage)); - } - catch (Exception exception) - { - _logger.LogError(new EventId(), exception, "Unhandled exception while handling application message."); - } - } - - private async Task ProcessReceivedPublishPacket(MqttPublishPacket publishPacket) - { - if (publishPacket.QualityOfServiceLevel == MqttQualityOfServiceLevel.AtMostOnce) - { - FireApplicationMessageReceivedEvent(publishPacket); - return; - } - - if (publishPacket.QualityOfServiceLevel == MqttQualityOfServiceLevel.AtLeastOnce) - { - FireApplicationMessageReceivedEvent(publishPacket); - await SendAsync(new MqttPubAckPacket {PacketIdentifier = publishPacket.PacketIdentifier}); - return; - } - - if (publishPacket.QualityOfServiceLevel == MqttQualityOfServiceLevel.ExactlyOnce) - { - // QoS 2 is implement as method "B" [4.3.3 QoS 2: Exactly once delivery] - lock (_unacknowledgedPublishPackets) - { - _unacknowledgedPublishPackets.Add(publishPacket.PacketIdentifier); - } - - FireApplicationMessageReceivedEvent(publishPacket); - await SendAsync(new MqttPubRecPacket {PacketIdentifier = publishPacket.PacketIdentifier}); - return; - } - - throw new MqttCommunicationException("Received a not supported QoS level."); - } - - private Task ProcessReceivedPubRelPacket(MqttPubRelPacket pubRelPacket) - { - lock (_unacknowledgedPublishPackets) - { - _unacknowledgedPublishPackets.Remove(pubRelPacket.PacketIdentifier); - } - - return SendAsync(pubRelPacket.CreateResponse()); - } - - private Task SendAsync(MqttBasePacket packet) - { - return _adapter.SendPacketsAsync(_options.CommunicationTimeout, _cancellationTokenSource.Token, packet); - } - - private async Task SendAndReceiveAsync(MqttBasePacket requestPacket) where TResponsePacket : MqttBasePacket - { - var packetAwaiter = _packetDispatcher.WaitForPacketAsync(requestPacket, typeof(TResponsePacket), _options.CommunicationTimeout); - await _adapter.SendPacketsAsync(_options.CommunicationTimeout, _cancellationTokenSource.Token, requestPacket).ConfigureAwait(false); - return (TResponsePacket) await packetAwaiter.ConfigureAwait(false); - } - - private ushort GetNewPacketIdentifier() - { - return (ushort) Interlocked.Increment(ref _latestPacketIdentifier); - } - - private async Task SendKeepAliveMessagesAsync(CancellationToken cancellationToken) - { - _logger.LogInformation("Start sending keep alive packets."); - - try - { - while (!cancellationToken.IsCancellationRequested) - { - await Task.Delay(_options.KeepAlivePeriod, cancellationToken).ConfigureAwait(false); - if (cancellationToken.IsCancellationRequested) - { - return; - } - - await SendAndReceiveAsync(new MqttPingReqPacket()).ConfigureAwait(false); - } - } - catch (OperationCanceledException) - { - } - catch (MqttCommunicationException exception) - { - if (cancellationToken.IsCancellationRequested) - { - return; - } - - _logger.LogWarning(new EventId(), exception, "MQTT communication exception while sending/receiving keep alive packets."); - await DisconnectInternalAsync().ConfigureAwait(false); - } - catch (Exception exception) - { - _logger.LogWarning(new EventId(), exception, "Unhandled exception while sending/receiving keep alive packets."); - await DisconnectInternalAsync().ConfigureAwait(false); - } - finally - { - _logger.LogInformation("Stopped sending keep alive packets."); - } - } - - private async Task ReceivePackets(CancellationToken cancellationToken) - { - _logger.LogInformation("Start receiving packets."); - - try - { - while (!cancellationToken.IsCancellationRequested) - { - _isReceivingPackets = true; - - var packet = await _adapter.ReceivePacketAsync(TimeSpan.Zero, cancellationToken).ConfigureAwait(false); - if (cancellationToken.IsCancellationRequested) - { - return; - } - - StartProcessReceivedPacket(packet, cancellationToken); - } - } - catch (OperationCanceledException) - { - } - catch (MqttCommunicationException exception) - { - if (cancellationToken.IsCancellationRequested) - { - return; - } - - _logger.LogWarning(new EventId(), exception, "MQTT communication exception while receiving packets."); - await DisconnectInternalAsync().ConfigureAwait(false); - } - catch (Exception exception) - { - _logger.LogError(new EventId(), exception, "Unhandled exception while receiving packets."); - await DisconnectInternalAsync().ConfigureAwait(false); - } - finally - { - _logger.LogInformation(nameof(MqttClient), "Stopped receiving packets."); - } - } - - private void StartProcessReceivedPacket(MqttBasePacket packet, CancellationToken cancellationToken) - { -#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed - Task.Run(() => ProcessReceivedPacketAsync(packet), cancellationToken).ConfigureAwait(false); -#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed - } - - private void StartSendKeepAliveMessages(CancellationToken cancellationToken) - { -#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed - Task.Factory.StartNew(() => SendKeepAliveMessagesAsync(cancellationToken), cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Default) - .ConfigureAwait(false); -#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed - } - } -} \ No newline at end of file diff --git a/MQTTnet.Core/Client/MqttClientExtensions.cs b/MQTTnet.Core/Client/MqttClientExtensions.cs deleted file mode 100644 index 567184d0..00000000 --- a/MQTTnet.Core/Client/MqttClientExtensions.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace MQTTnet.Core.Client -{ - public static class MqttClientExtensions - { - public static Task> SubscribeAsync(this IMqttClient client, params TopicFilter[] topicFilters) - { - if (client == null) throw new ArgumentNullException(nameof(client)); - if (topicFilters == null) throw new ArgumentNullException(nameof(topicFilters)); - - return client.SubscribeAsync(topicFilters.ToList()); - } - - public static Task UnsubscribeAsync(this IMqttClient client, params string[] topicFilters) - { - if (client == null) throw new ArgumentNullException(nameof(client)); - if (topicFilters == null) throw new ArgumentNullException(nameof(topicFilters)); - - return client.UnsubscribeAsync(topicFilters.ToList()); - } - } -} \ No newline at end of file diff --git a/MQTTnet.Core/Client/MqttPacketDispatcher.cs b/MQTTnet.Core/Client/MqttPacketDispatcher.cs deleted file mode 100644 index a999d697..00000000 --- a/MQTTnet.Core/Client/MqttPacketDispatcher.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Threading.Tasks; -using MQTTnet.Core.Exceptions; -using MQTTnet.Core.Internal; -using MQTTnet.Core.Packets; -using Microsoft.Extensions.Logging; - -namespace MQTTnet.Core.Client -{ - public class MqttPacketDispatcher - { - private readonly ConcurrentDictionary> _packetByResponseType = new ConcurrentDictionary>(); - private readonly ConcurrentDictionary>> _packetByResponseTypeAndIdentifier = new ConcurrentDictionary>>(); - private readonly ILogger _logger; - - public MqttPacketDispatcher(ILogger logger) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - public async Task WaitForPacketAsync(MqttBasePacket request, Type responseType, TimeSpan timeout) - { - if (request == null) throw new ArgumentNullException(nameof(request)); - - var packetAwaiter = AddPacketAwaiter(request, responseType); - try - { - return await packetAwaiter.Task.TimeoutAfter(timeout).ConfigureAwait(false); - } - catch (MqttCommunicationTimedOutException) - { - _logger.LogWarning("Timeout while waiting for packet of type '{0}'.", responseType.Name); - throw; - } - finally - { - RemovePacketAwaiter(request, responseType); - } - } - - public void Dispatch(MqttBasePacket packet) - { - if (packet == null) throw new ArgumentNullException(nameof(packet)); - - var type = packet.GetType(); - if (packet is IMqttPacketWithIdentifier withIdentifier) - { - if (_packetByResponseTypeAndIdentifier.TryGetValue(type, out var byid)) - { - if (byid.TryRemove(withIdentifier.PacketIdentifier, out var tcs)) - { - tcs.TrySetResult(packet); - return; - } - } - } - else if (_packetByResponseType.TryRemove(type, out var tcs)) - { - tcs.TrySetResult(packet); - return; - } - - throw new InvalidOperationException($"Packet of type '{type.Name}' not handled or dispatched."); - } - - public void Reset() - { - _packetByResponseTypeAndIdentifier.Clear(); - _packetByResponseType.Clear(); - } - - private TaskCompletionSource AddPacketAwaiter(MqttBasePacket request, Type responseType) - { - var tcs = new TaskCompletionSource(); - - if (request is IMqttPacketWithIdentifier requestWithIdentifier) - { - var byId = _packetByResponseTypeAndIdentifier.GetOrAdd(responseType, key => new ConcurrentDictionary>()); - byId[requestWithIdentifier.PacketIdentifier] = tcs; - } - else - { - _packetByResponseType[responseType] = tcs; - } - - return tcs; - } - - private void RemovePacketAwaiter(MqttBasePacket request, Type responseType) - { - if (request is IMqttPacketWithIdentifier requestWithIdentifier) - { - var byId = _packetByResponseTypeAndIdentifier.GetOrAdd(responseType, key => new ConcurrentDictionary>()); - byId.TryRemove(requestWithIdentifier.PacketIdentifier, out var _); - } - else - { - _packetByResponseType.TryRemove(responseType, out var _); - } - } - } -} \ No newline at end of file diff --git a/MQTTnet.Core/Diagnostics/MqttNetLogger.cs b/MQTTnet.Core/Diagnostics/MqttNetLogger.cs deleted file mode 100644 index 54ab6d44..00000000 --- a/MQTTnet.Core/Diagnostics/MqttNetLogger.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using Microsoft.Extensions.Logging; - -namespace MQTTnet.Core.Diagnostics -{ - public class MqttNetLogger : ILogger - { - private readonly string _categoryName; - private readonly MqttNetTrace _mqttNetTrace; - - public MqttNetLogger(string categoryName, MqttNetTrace mqttNetTrace) - { - _categoryName = categoryName; - _mqttNetTrace = mqttNetTrace; - } - - public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) - { - if (formatter == null) - { - throw new ArgumentNullException(nameof(formatter)); - } - - var message = formatter(state, exception); - - var traceMessage = new MqttNetTraceMessage(DateTime.Now, Environment.CurrentManagedThreadId, _categoryName, logLevel, message, exception); - _mqttNetTrace.Publish(traceMessage); - } - - public bool IsEnabled(LogLevel logLevel) - { - return true; - } - - //not supported: async local requires netstandard1.3 - //for implementation see https://github.com/aspnet/Logging/blob/dev/src/Microsoft.Extensions.Logging.Console/ConsoleLogScope.cs - public IDisposable BeginScope(TState state) - { - return new DisposableScope(); - } - - private class DisposableScope : IDisposable - { - public void Dispose() - { - } - } - } -} \ No newline at end of file diff --git a/MQTTnet.Core/Diagnostics/MqttNetTrace.cs b/MQTTnet.Core/Diagnostics/MqttNetTrace.cs deleted file mode 100644 index c14d4931..00000000 --- a/MQTTnet.Core/Diagnostics/MqttNetTrace.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Collections.Concurrent; -using Microsoft.Extensions.Logging; - -namespace MQTTnet.Core.Diagnostics -{ - public class MqttNetTrace : ILoggerProvider - { - private readonly ConcurrentDictionary _loggers = new ConcurrentDictionary(); - - public static event EventHandler TraceMessagePublished; - - public void Publish(MqttNetTraceMessage msg) - { - TraceMessagePublished?.Invoke(this, new MqttNetTraceMessagePublishedEventArgs(msg)); - } - - public void Dispose() - { - TraceMessagePublished = null; - } - - public ILogger CreateLogger(string categoryName) - { - return _loggers.GetOrAdd(categoryName, CreateLoggerImplementation); - } - - private MqttNetLogger CreateLoggerImplementation(string categoryName) - { - return new MqttNetLogger(categoryName, this); - } - } -} diff --git a/MQTTnet.Core/Diagnostics/MqttNetTraceMessagePublishedEventArgs.cs b/MQTTnet.Core/Diagnostics/MqttNetTraceMessagePublishedEventArgs.cs deleted file mode 100644 index 5b8f796d..00000000 --- a/MQTTnet.Core/Diagnostics/MqttNetTraceMessagePublishedEventArgs.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; - -namespace MQTTnet.Core.Diagnostics -{ - public sealed class MqttNetTraceMessagePublishedEventArgs : EventArgs - { - public MqttNetTraceMessagePublishedEventArgs(MqttNetTraceMessage traceMessage) - { - TraceMessage = traceMessage ?? throw new ArgumentNullException(nameof(traceMessage)); - } - - public MqttNetTraceMessage TraceMessage { get; } - } -} diff --git a/MQTTnet.Core/Diagnostics/MqttTraceMessage.cs b/MQTTnet.Core/Diagnostics/MqttTraceMessage.cs deleted file mode 100644 index 852aae45..00000000 --- a/MQTTnet.Core/Diagnostics/MqttTraceMessage.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using Microsoft.Extensions.Logging; - -namespace MQTTnet.Core.Diagnostics -{ - public sealed class MqttNetTraceMessage - { - public MqttNetTraceMessage(DateTime timestamp, int threadId, string source, LogLevel level, string message, Exception exception) - { - Timestamp = timestamp; - ThreadId = threadId; - Source = source; - Level = level; - Message = message; - Exception = exception; - } - - public DateTime Timestamp { get; } - - public int ThreadId { get; } - - public string Source { get; } - - public LogLevel Level { get; } - - public string Message { get; } - - public Exception Exception { get; } - } -} diff --git a/MQTTnet.Core/MQTTnet.Core.csproj b/MQTTnet.Core/MQTTnet.Core.csproj deleted file mode 100644 index 71a518d5..00000000 --- a/MQTTnet.Core/MQTTnet.Core.csproj +++ /dev/null @@ -1,31 +0,0 @@ - - - - netstandard1.1 - MQTTnet.Core - MQTTnet.Core - False - Full - - - - - 0.0.0.0 - - - - - - - 2.5.0.0 - 2.5.0.0 - - - - - - - - - - \ No newline at end of file diff --git a/MQTTnet.Core/ManagedClient/ManagedMqttClientExtensions.cs b/MQTTnet.Core/ManagedClient/ManagedMqttClientExtensions.cs deleted file mode 100644 index a9984c4a..00000000 --- a/MQTTnet.Core/ManagedClient/ManagedMqttClientExtensions.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace MQTTnet.Core.ManagedClient -{ - public static class ManagedMqttClientExtensions - { - public static Task SubscribeAsync(this IManagedMqttClient managedClient, params TopicFilter[] topicFilters) - { - if (managedClient == null) throw new ArgumentNullException(nameof(managedClient)); - - return managedClient.SubscribeAsync(topicFilters); - } - - public static Task UnsubscribeAsync(this IManagedMqttClient managedClient, params TopicFilter[] topicFilters) - { - if (managedClient == null) throw new ArgumentNullException(nameof(managedClient)); - - return managedClient.UnsubscribeAsync(topicFilters); - } - } -} diff --git a/MQTTnet.Core/Packets/IMqttPacketWithIdentifier.cs b/MQTTnet.Core/Packets/IMqttPacketWithIdentifier.cs deleted file mode 100644 index 420955cd..00000000 --- a/MQTTnet.Core/Packets/IMqttPacketWithIdentifier.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace MQTTnet.Core.Packets -{ - public interface IMqttPacketWithIdentifier - { - ushort PacketIdentifier { get; set; } - } -} diff --git a/MQTTnet.Core/Packets/MqttDisconnectPacket.cs b/MQTTnet.Core/Packets/MqttDisconnectPacket.cs deleted file mode 100644 index 5be8407f..00000000 --- a/MQTTnet.Core/Packets/MqttDisconnectPacket.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace MQTTnet.Core.Packets -{ - public sealed class MqttDisconnectPacket : MqttBasePacket - { - } -} diff --git a/MQTTnet.Core/Packets/MqttPacketExtensions.cs b/MQTTnet.Core/Packets/MqttPacketExtensions.cs deleted file mode 100644 index 6a7e0cea..00000000 --- a/MQTTnet.Core/Packets/MqttPacketExtensions.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; - -namespace MQTTnet.Core.Packets -{ - public static class MqttPacketExtensions - { - public static TResponsePacket CreateResponse(this MqttBasePacket packet) - { - if (packet == null) throw new ArgumentNullException(nameof(packet)); - - var responsePacket = Activator.CreateInstance(); - - if (responsePacket is IMqttPacketWithIdentifier responsePacketWithIdentifier) - { - if (!(packet is IMqttPacketWithIdentifier requestPacketWithIdentifier)) - { - throw new InvalidOperationException("Response packet has PacketIdentifier but request packet does not."); - } - - responsePacketWithIdentifier.PacketIdentifier = requestPacketWithIdentifier.PacketIdentifier; - } - - return responsePacket; - } - } -} diff --git a/MQTTnet.Core/Packets/MqttPubAckPacket.cs b/MQTTnet.Core/Packets/MqttPubAckPacket.cs deleted file mode 100644 index b3e5d3e3..00000000 --- a/MQTTnet.Core/Packets/MqttPubAckPacket.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace MQTTnet.Core.Packets -{ - public sealed class MqttPubAckPacket : MqttBasePublishPacket - { - } -} diff --git a/MQTTnet.Core/Packets/MqttPubCompPacket.cs b/MQTTnet.Core/Packets/MqttPubCompPacket.cs deleted file mode 100644 index cb6b485f..00000000 --- a/MQTTnet.Core/Packets/MqttPubCompPacket.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace MQTTnet.Core.Packets -{ - public sealed class MqttPubCompPacket : MqttBasePublishPacket - { - } -} diff --git a/MQTTnet.Core/Packets/MqttPubRecPacket.cs b/MQTTnet.Core/Packets/MqttPubRecPacket.cs deleted file mode 100644 index a8ed3337..00000000 --- a/MQTTnet.Core/Packets/MqttPubRecPacket.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace MQTTnet.Core.Packets -{ - public sealed class MqttPubRecPacket : MqttBasePublishPacket - { - } -} diff --git a/MQTTnet.Core/Packets/MqttPubRelPacket.cs b/MQTTnet.Core/Packets/MqttPubRelPacket.cs deleted file mode 100644 index c95d16bf..00000000 --- a/MQTTnet.Core/Packets/MqttPubRelPacket.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace MQTTnet.Core.Packets -{ - public sealed class MqttPubRelPacket : MqttBasePublishPacket - { - } -} diff --git a/MQTTnet.Core/Packets/MqttUnsubAckPacket.cs b/MQTTnet.Core/Packets/MqttUnsubAckPacket.cs deleted file mode 100644 index 57a5a7dd..00000000 --- a/MQTTnet.Core/Packets/MqttUnsubAckPacket.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace MQTTnet.Core.Packets -{ - public sealed class MqttUnsubAckPacket : MqttBasePacket, IMqttPacketWithIdentifier - { - public ushort PacketIdentifier { get; set; } - } -} diff --git a/MQTTnet.Core/Packets/MqttUnsubscribe.cs b/MQTTnet.Core/Packets/MqttUnsubscribe.cs deleted file mode 100644 index 269bfcbb..00000000 --- a/MQTTnet.Core/Packets/MqttUnsubscribe.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Collections.Generic; - -namespace MQTTnet.Core.Packets -{ - public sealed class MqttUnsubscribePacket : MqttBasePacket, IMqttPacketWithIdentifier - { - public ushort PacketIdentifier { get; set; } - - public IList TopicFilters { get; set; } = new List(); - } -} diff --git a/MQTTnet.Core/Serializer/IMqttPacketSerializer.cs b/MQTTnet.Core/Serializer/IMqttPacketSerializer.cs deleted file mode 100644 index 5834c539..00000000 --- a/MQTTnet.Core/Serializer/IMqttPacketSerializer.cs +++ /dev/null @@ -1,14 +0,0 @@ -using MQTTnet.Core.Adapter; -using MQTTnet.Core.Packets; - -namespace MQTTnet.Core.Serializer -{ - public interface IMqttPacketSerializer - { - MqttProtocolVersion ProtocolVersion { get; set; } - - byte[] Serialize(MqttBasePacket mqttPacket); - - MqttBasePacket Deserialize(ReceivedMqttPacket receivedMqttPacket); - } -} \ No newline at end of file diff --git a/MQTTnet.Core/Serializer/MqttPacketReader.cs b/MQTTnet.Core/Serializer/MqttPacketReader.cs deleted file mode 100644 index 417c19a8..00000000 --- a/MQTTnet.Core/Serializer/MqttPacketReader.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System; -using System.IO; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using MQTTnet.Core.Adapter; -using MQTTnet.Core.Exceptions; -using MQTTnet.Core.Protocol; -using MQTTnet.Core.Packets; - -namespace MQTTnet.Core.Serializer -{ - public sealed class MqttPacketReader : BinaryReader - { - private readonly ReceivedMqttPacket _receivedMqttPacket; - - public MqttPacketReader(ReceivedMqttPacket receivedMqttPacket) - : base(receivedMqttPacket.Body, Encoding.UTF8, true) - { - _receivedMqttPacket = receivedMqttPacket; - } - - public bool EndOfRemainingData => BaseStream.Position == _receivedMqttPacket.Header.BodyLength; - - public static MqttPacketHeader ReadHeaderFromSource(Stream stream, CancellationToken cancellationToken) - { - var fixedHeader = (byte)stream.ReadByte(); - var controlPacketType = (MqttControlPacketType)(fixedHeader >> 4); - var bodyLength = ReadBodyLengthFromSource(stream, cancellationToken); - - return new MqttPacketHeader - { - FixedHeader = fixedHeader, - ControlPacketType = controlPacketType, - BodyLength = bodyLength - }; - } - - public override ushort ReadUInt16() - { - var buffer = ReadBytes(2); - - var temp = buffer[0]; - buffer[0] = buffer[1]; - buffer[1] = temp; - - return BitConverter.ToUInt16(buffer, 0); - } - - public string ReadStringWithLengthPrefix() - { - var buffer = ReadWithLengthPrefix(); - return Encoding.UTF8.GetString(buffer, 0, buffer.Length); - } - - public byte[] ReadWithLengthPrefix() - { - var length = ReadUInt16(); - return ReadBytes(length); - } - - public byte[] ReadRemainingData() - { - return ReadBytes(_receivedMqttPacket.Header.BodyLength - (int)BaseStream.Position); - } - - private static int ReadBodyLengthFromSource(Stream stream, CancellationToken cancellationToken) - { - // Alorithm taken from http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html. - var multiplier = 1; - var value = 0; - byte encodedByte; - do - { - if (cancellationToken.IsCancellationRequested) - { - throw new TaskCanceledException(); - } - - encodedByte = (byte)stream.ReadByte(); - value += (encodedByte & 127) * multiplier; - multiplier *= 128; - if (multiplier > 128 * 128 * 128) - { - throw new MqttProtocolViolationException("Remaining length is invalid."); - } - } while ((encodedByte & 128) != 0); - return value; - } - } -} diff --git a/MQTTnet.Core/Server/ConnectedMqttClient.cs b/MQTTnet.Core/Server/ConnectedMqttClient.cs deleted file mode 100644 index d3633511..00000000 --- a/MQTTnet.Core/Server/ConnectedMqttClient.cs +++ /dev/null @@ -1,11 +0,0 @@ -using MQTTnet.Core.Serializer; - -namespace MQTTnet.Core.Server -{ - public class ConnectedMqttClient - { - public string ClientId { get; set; } - - public MqttProtocolVersion ProtocolVersion { get; set; } - } -} diff --git a/MQTTnet.Core/Server/IMqttClientSesssionFactory.cs b/MQTTnet.Core/Server/IMqttClientSesssionFactory.cs deleted file mode 100644 index f58dd5e7..00000000 --- a/MQTTnet.Core/Server/IMqttClientSesssionFactory.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace MQTTnet.Core.Server -{ - public interface IMqttClientSesssionFactory - { - MqttClientSession CreateClientSession(string sessionId, MqttClientSessionsManager mqttClientSessionsManager); - } -} diff --git a/MQTTnet.Core/Server/IMqttServer.cs b/MQTTnet.Core/Server/IMqttServer.cs deleted file mode 100644 index 7d76c23c..00000000 --- a/MQTTnet.Core/Server/IMqttServer.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using MQTTnet.Core.Client; - -namespace MQTTnet.Core.Server -{ - public interface IMqttServer : IApplicationMessageReceiver, IApplicationMessagePublisher - { - event EventHandler ClientConnected; - event EventHandler ClientDisconnected; - - IList GetConnectedClients(); - void Publish(IEnumerable applicationMessages); - - Task StartAsync(); - Task StopAsync(); - } -} \ No newline at end of file diff --git a/MQTTnet.Core/Server/IMqttServerFactory.cs b/MQTTnet.Core/Server/IMqttServerFactory.cs deleted file mode 100644 index 500614b0..00000000 --- a/MQTTnet.Core/Server/IMqttServerFactory.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; - -namespace MQTTnet.Core.Server -{ - public interface IMqttServerFactory - { - IMqttServer CreateMqttServer(); - - IMqttServer CreateMqttServer(Action configure); - } -} \ No newline at end of file diff --git a/MQTTnet.Core/Server/MqttApplicationMessageInterceptorContext.cs b/MQTTnet.Core/Server/MqttApplicationMessageInterceptorContext.cs deleted file mode 100644 index 218a02dc..00000000 --- a/MQTTnet.Core/Server/MqttApplicationMessageInterceptorContext.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace MQTTnet.Core.Server -{ - public class MqttApplicationMessageInterceptorContext - { - public MqttApplicationMessage ApplicationMessage { get; set; } - } -} diff --git a/MQTTnet.Core/Server/MqttClientPendingMessagesQueue.cs b/MQTTnet.Core/Server/MqttClientPendingMessagesQueue.cs deleted file mode 100644 index 446e0a86..00000000 --- a/MQTTnet.Core/Server/MqttClientPendingMessagesQueue.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Threading; -using System.Threading.Tasks; -using MQTTnet.Core.Adapter; -using MQTTnet.Core.Exceptions; -using MQTTnet.Core.Packets; -using MQTTnet.Core.Protocol; -using Microsoft.Extensions.Logging; - -namespace MQTTnet.Core.Server -{ - public sealed class MqttClientPendingMessagesQueue - { - private readonly BlockingCollection _pendingPublishPackets = new BlockingCollection(); - private readonly MqttClientSession _session; - private readonly MqttServerOptions _options; - private readonly ILogger _logger; - - public MqttClientPendingMessagesQueue(MqttServerOptions options, MqttClientSession session, ILogger logger) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _session = session ?? throw new ArgumentNullException(nameof(session)); - _options = options ?? throw new ArgumentNullException(nameof(options)); - } - - public void Start(IMqttCommunicationAdapter adapter, CancellationToken cancellationToken) - { - if (adapter == null) throw new ArgumentNullException(nameof(adapter)); - - Task.Factory.StartNew(async () => await SendPendingPublishPacketsAsync(adapter, cancellationToken), cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Default).ConfigureAwait(false); - } - - public void Enqueue(MqttPublishPacket publishPacket) - { - if (publishPacket == null) throw new ArgumentNullException(nameof(publishPacket)); - - _pendingPublishPackets.Add(publishPacket); - } - - private async Task SendPendingPublishPacketsAsync(IMqttCommunicationAdapter adapter, CancellationToken cancellationToken) - { - try - { - while (!cancellationToken.IsCancellationRequested) - { - await SendPendingPublishPacketAsync(adapter, cancellationToken); - } - } - catch (OperationCanceledException) - { - } - catch (Exception exception) - { - _logger.LogError(new EventId(), exception, "Unhandled exception while sending pending publish packets."); - } - } - - private async Task SendPendingPublishPacketAsync(IMqttCommunicationAdapter adapter, CancellationToken cancellationToken) - { - var packet = _pendingPublishPackets.Take(cancellationToken); - - try - { - await adapter.SendPacketsAsync(_options.DefaultCommunicationTimeout, cancellationToken, packet).ConfigureAwait(false); - } - catch (Exception exception) - { - if (exception is MqttCommunicationTimedOutException) - { - _logger.LogWarning(new EventId(), exception, "Sending publish packet failed due to timeout."); - } - else if (exception is MqttCommunicationException) - { - _logger.LogWarning(new EventId(), exception, "Sending publish packet failed due to communication exception."); - } - else if (exception is OperationCanceledException) - { - } - else - { - _logger.LogError(new EventId(), exception, "Sending publish packet failed."); - } - - if (packet.QualityOfServiceLevel > MqttQualityOfServiceLevel.AtMostOnce) - { - packet.Dup = true; - _pendingPublishPackets.Add(packet, cancellationToken); - } - - _session.Stop(); - } - } - } -} diff --git a/MQTTnet.Core/Server/MqttClientRetainedMessagesManager.cs b/MQTTnet.Core/Server/MqttClientRetainedMessagesManager.cs deleted file mode 100644 index 68a7b99b..00000000 --- a/MQTTnet.Core/Server/MqttClientRetainedMessagesManager.cs +++ /dev/null @@ -1,111 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using MQTTnet.Core.Packets; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace MQTTnet.Core.Server -{ - public sealed class MqttClientRetainedMessagesManager - { - private readonly Dictionary _retainedMessages = new Dictionary(); - private readonly ILogger _logger; - private readonly MqttServerOptions _options; - - public MqttClientRetainedMessagesManager(IOptions options, ILogger logger) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _options = options.Value ?? throw new ArgumentNullException(nameof(options)); - } - - public async Task LoadMessagesAsync() - { - if (_options.Storage == null) - { - return; - } - - try - { - var retainedMessages = await _options.Storage.LoadRetainedMessagesAsync(); - lock (_retainedMessages) - { - _retainedMessages.Clear(); - foreach (var retainedMessage in retainedMessages) - { - _retainedMessages[retainedMessage.Topic] = retainedMessage; - } - } - } - catch (Exception exception) - { - _logger.LogError(new EventId(), exception, "Unhandled exception while loading retained messages."); - } - } - - public async Task HandleMessageAsync(string clientId, MqttApplicationMessage applicationMessage) - { - if (applicationMessage == null) throw new ArgumentNullException(nameof(applicationMessage)); - - List allRetainedMessages; - lock (_retainedMessages) - { - if (applicationMessage.Payload?.Any() == false) - { - _retainedMessages.Remove(applicationMessage.Topic); - _logger.LogInformation("Client '{0}' cleared retained message for topic '{1}'.", clientId, applicationMessage.Topic); - } - else - { - _retainedMessages[applicationMessage.Topic] = applicationMessage; - _logger.LogInformation("Client '{0}' updated retained message for topic '{1}'.", clientId, applicationMessage.Topic); - } - - allRetainedMessages = new List(_retainedMessages.Values); - } - - try - { - // ReSharper disable once UseNullPropagation - if (_options.Storage != null) - { - await _options.Storage.SaveRetainedMessagesAsync(allRetainedMessages); - } - } - catch (Exception exception) - { - _logger.LogError(new EventId(), exception, "Unhandled exception while saving retained messages."); - } - } - - public List GetMessages(MqttSubscribePacket subscribePacket) - { - var retainedMessages = new List(); - lock (_retainedMessages) - { - foreach (var retainedMessage in _retainedMessages.Values) - { - foreach (var topicFilter in subscribePacket.TopicFilters) - { - if (retainedMessage.QualityOfServiceLevel < topicFilter.QualityOfServiceLevel) - { - continue; - } - - if (!MqttTopicFilterComparer.IsMatch(retainedMessage.Topic, topicFilter.Topic)) - { - continue; - } - - retainedMessages.Add(retainedMessage); - break; - } - } - } - - return retainedMessages; - } - } -} diff --git a/MQTTnet.Core/Server/MqttClientSession.cs b/MQTTnet.Core/Server/MqttClientSession.cs deleted file mode 100644 index 903b3306..00000000 --- a/MQTTnet.Core/Server/MqttClientSession.cs +++ /dev/null @@ -1,280 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using MQTTnet.Core.Adapter; -using MQTTnet.Core.Exceptions; -using MQTTnet.Core.Internal; -using MQTTnet.Core.Packets; -using MQTTnet.Core.Protocol; -using MQTTnet.Core.Serializer; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace MQTTnet.Core.Server -{ - public sealed class MqttClientSession : IDisposable - { - private readonly HashSet _unacknowledgedPublishPackets = new HashSet(); - - private readonly MqttClientSubscriptionsManager _subscriptionsManager; - private readonly MqttClientSessionsManager _sessionsManager; - private readonly MqttClientPendingMessagesQueue _pendingMessagesQueue; - private readonly MqttServerOptions _options; - private readonly ILogger _logger; - - private IMqttCommunicationAdapter _adapter; - private CancellationTokenSource _cancellationTokenSource; - private MqttApplicationMessage _willMessage; - - public MqttClientSession( - string clientId, - IOptions options, - MqttClientSessionsManager sessionsManager, - MqttClientSubscriptionsManager subscriptionsManager, - ILogger logger, - ILogger messageQueueLogger) - { - _sessionsManager = sessionsManager ?? throw new ArgumentNullException(nameof(sessionsManager)); - _subscriptionsManager = subscriptionsManager ?? throw new ArgumentNullException(nameof(sessionsManager)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - - ClientId = clientId; - - _options = options.Value; - _pendingMessagesQueue = new MqttClientPendingMessagesQueue(_options, this, messageQueueLogger); - } - - public string ClientId { get; } - - public MqttProtocolVersion? ProtocolVersion => _adapter?.PacketSerializer.ProtocolVersion; - - public bool IsConnected => _adapter != null; - - public async Task RunAsync(MqttApplicationMessage willMessage, IMqttCommunicationAdapter adapter) - { - if (adapter == null) throw new ArgumentNullException(nameof(adapter)); - - try - { - _willMessage = willMessage; - _adapter = adapter; - _cancellationTokenSource = new CancellationTokenSource(); - - _pendingMessagesQueue.Start(adapter, _cancellationTokenSource.Token); - await ReceivePacketsAsync(adapter, _cancellationTokenSource.Token); - } - catch (OperationCanceledException) - { - } - catch (MqttCommunicationException exception) - { - _logger.LogWarning(new EventId(), exception, "Client '{0}': Communication exception while processing client packets.", ClientId); - } - catch (Exception exception) - { - _logger.LogError(new EventId(), exception, "Client '{0}': Unhandled exception while processing client packets.", ClientId); - } - } - - public void Stop() - { - try - { - _cancellationTokenSource?.Cancel(false); - _cancellationTokenSource?.Dispose(); - _cancellationTokenSource = null; - - _adapter = null; - - _logger.LogInformation("Client '{0}': Disconnected.", ClientId); - } - finally - { - if (_willMessage != null) - { - _sessionsManager.DispatchApplicationMessage(this, _willMessage); - } - } - } - - public void EnqueuePublishPacket(MqttPublishPacket publishPacket) - { - if (publishPacket == null) throw new ArgumentNullException(nameof(publishPacket)); - - if (!_subscriptionsManager.IsSubscribed(publishPacket)) - { - return; - } - - _pendingMessagesQueue.Enqueue(publishPacket); - _logger.LogTrace("Client '{0}': Enqueued pending publish packet.", ClientId); - } - - public void Dispose() - { - _cancellationTokenSource?.Cancel(); - _cancellationTokenSource?.Dispose(); - } - - private async Task ReceivePacketsAsync(IMqttCommunicationAdapter adapter, CancellationToken cancellationToken) - { - try - { - while (!cancellationToken.IsCancellationRequested) - { - var packet = await adapter.ReceivePacketAsync(TimeSpan.Zero, cancellationToken).ConfigureAwait(false); - await ProcessReceivedPacketAsync(adapter, packet, cancellationToken).ConfigureAwait(false); - } - } - catch (OperationCanceledException) - { - } - catch (MqttCommunicationException exception) - { - _logger.LogWarning(new EventId(), exception, "Client '{0}': Communication exception while processing client packets.", ClientId); - Stop(); - } - catch (Exception exception) - { - _logger.LogError(new EventId(), exception, "Client '{0}': Unhandled exception while processing client packets.", ClientId); - Stop(); - } - } - - private Task ProcessReceivedPacketAsync(IMqttCommunicationAdapter adapter, MqttBasePacket packet, CancellationToken cancellationToken) - { - if (packet is MqttPingReqPacket) - { - return adapter.SendPacketsAsync(_options.DefaultCommunicationTimeout, cancellationToken, new MqttPingRespPacket()); - } - - if (packet is MqttPublishPacket publishPacket) - { - return HandleIncomingPublishPacketAsync(adapter, publishPacket, cancellationToken); - } - - if (packet is MqttPubRelPacket pubRelPacket) - { - return HandleIncomingPubRelPacketAsync(adapter, pubRelPacket, cancellationToken); - } - - if (packet is MqttPubRecPacket pubRecPacket) - { - return adapter.SendPacketsAsync(_options.DefaultCommunicationTimeout, cancellationToken, pubRecPacket.CreateResponse()); - } - - if (packet is MqttPubAckPacket || packet is MqttPubCompPacket) - { - // Discard message. - return Task.FromResult(0); - } - - if (packet is MqttSubscribePacket subscribePacket) - { - return HandleIncomingSubscribePacketAsync(adapter, subscribePacket, cancellationToken); - } - - if (packet is MqttUnsubscribePacket unsubscribePacket) - { - return adapter.SendPacketsAsync(_options.DefaultCommunicationTimeout, cancellationToken, _subscriptionsManager.Unsubscribe(unsubscribePacket)); - } - - if (packet is MqttDisconnectPacket || packet is MqttConnectPacket) - { - Stop(); - return Task.FromResult(0); - } - - _logger.LogWarning("Client '{0}': Received not supported packet ({1}). Closing connection.", ClientId, packet); - Stop(); - - return Task.FromResult(0); - } - - private async Task HandleIncomingSubscribePacketAsync(IMqttCommunicationAdapter adapter, MqttSubscribePacket subscribePacket, CancellationToken cancellationToken) - { - var subscribeResult = _subscriptionsManager.Subscribe(subscribePacket, ClientId); - await adapter.SendPacketsAsync(_options.DefaultCommunicationTimeout, cancellationToken, subscribeResult.ResponsePacket); - EnqueueRetainedMessages(subscribePacket); - - if (subscribeResult.CloseConnection) - { - await adapter.SendPacketsAsync(_options.DefaultCommunicationTimeout, cancellationToken, new MqttDisconnectPacket()); - Stop(); - } - } - - private void EnqueueRetainedMessages(MqttSubscribePacket subscribePacket) - { - var retainedMessages = _sessionsManager.RetainedMessagesManager.GetMessages(subscribePacket); - foreach (var publishPacket in retainedMessages) - { - EnqueuePublishPacket(publishPacket.ToPublishPacket()); - } - } - - private async Task HandleIncomingPublishPacketAsync(IMqttCommunicationAdapter adapter, MqttPublishPacket publishPacket, CancellationToken cancellationToken) - { - var applicationMessage = publishPacket.ToApplicationMessage(); - - var interceptorContext = new MqttApplicationMessageInterceptorContext - { - ApplicationMessage = applicationMessage - }; - - _options.ApplicationMessageInterceptor?.Invoke(interceptorContext); - applicationMessage = interceptorContext.ApplicationMessage; - - if (applicationMessage.Retain) - { - await _sessionsManager.RetainedMessagesManager.HandleMessageAsync(ClientId, applicationMessage); - } - - switch (applicationMessage.QualityOfServiceLevel) - { - case MqttQualityOfServiceLevel.AtMostOnce: - { - _sessionsManager.DispatchApplicationMessage(this, applicationMessage); - return; - } - case MqttQualityOfServiceLevel.AtLeastOnce: - { - _sessionsManager.DispatchApplicationMessage(this, applicationMessage); - - await adapter.SendPacketsAsync(_options.DefaultCommunicationTimeout, cancellationToken, - new MqttPubAckPacket { PacketIdentifier = publishPacket.PacketIdentifier }); - - return; - } - case MqttQualityOfServiceLevel.ExactlyOnce: - { - // QoS 2 is implement as method "B" [4.3.3 QoS 2: Exactly once delivery] - lock (_unacknowledgedPublishPackets) - { - _unacknowledgedPublishPackets.Add(publishPacket.PacketIdentifier); - } - - _sessionsManager.DispatchApplicationMessage(this, applicationMessage); - - await adapter.SendPacketsAsync(_options.DefaultCommunicationTimeout, cancellationToken, - new MqttPubRecPacket { PacketIdentifier = publishPacket.PacketIdentifier }); - - return; - } - default: - throw new MqttCommunicationException("Received a not supported QoS level."); - } - } - - private Task HandleIncomingPubRelPacketAsync(IMqttCommunicationAdapter adapter, MqttPubRelPacket pubRelPacket, CancellationToken cancellationToken) - { - lock (_unacknowledgedPublishPackets) - { - _unacknowledgedPublishPackets.Remove(pubRelPacket.PacketIdentifier); - } - - return adapter.SendPacketsAsync(_options.DefaultCommunicationTimeout, cancellationToken, new MqttPubCompPacket { PacketIdentifier = pubRelPacket.PacketIdentifier }); - } - } -} diff --git a/MQTTnet.Core/Server/MqttClientSessionsManager.cs b/MQTTnet.Core/Server/MqttClientSessionsManager.cs deleted file mode 100644 index 62cc1d82..00000000 --- a/MQTTnet.Core/Server/MqttClientSessionsManager.cs +++ /dev/null @@ -1,197 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MQTTnet.Core.Adapter; -using MQTTnet.Core.Exceptions; -using MQTTnet.Core.Internal; -using MQTTnet.Core.Packets; -using MQTTnet.Core.Protocol; -using MQTTnet.Core.Serializer; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace MQTTnet.Core.Server -{ - public sealed class MqttClientSessionsManager - { - private readonly Dictionary _clientSessions = new Dictionary(); - private readonly ILogger _logger; - private readonly IMqttClientSesssionFactory _clientSesssionFactory; - private readonly MqttServerOptions _options; - - public MqttClientSessionsManager( - IOptions options, - ILogger logger, - MqttClientRetainedMessagesManager retainedMessagesManager, - IMqttClientSesssionFactory clientSesssionFactory) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _options = options.Value ?? throw new ArgumentNullException(nameof(options)); - RetainedMessagesManager = retainedMessagesManager ?? throw new ArgumentNullException(nameof(options)); - _clientSesssionFactory = clientSesssionFactory ?? throw new ArgumentNullException(nameof(clientSesssionFactory)); - } - - public event EventHandler ClientConnected; - public event EventHandler ClientDisconnected; - public event EventHandler ApplicationMessageReceived; - - public MqttClientRetainedMessagesManager RetainedMessagesManager { get; } - - public async Task RunClientSessionAsync(IMqttCommunicationAdapter clientAdapter, CancellationToken cancellationToken) - { - var clientId = string.Empty; - try - { - if (!(await clientAdapter.ReceivePacketAsync(_options.DefaultCommunicationTimeout, cancellationToken).ConfigureAwait(false) is MqttConnectPacket connectPacket)) - { - throw new MqttProtocolViolationException("The first packet from a client must be a 'CONNECT' packet [MQTT-3.1.0-1]."); - } - - clientId = connectPacket.ClientId; - - // Switch to the required protocol version before sending any response. - clientAdapter.PacketSerializer.ProtocolVersion = connectPacket.ProtocolVersion; - - var connectReturnCode = ValidateConnection(connectPacket); - if (connectReturnCode != MqttConnectReturnCode.ConnectionAccepted) - { - await clientAdapter.SendPacketsAsync(_options.DefaultCommunicationTimeout, cancellationToken, new MqttConnAckPacket - { - ConnectReturnCode = connectReturnCode - }).ConfigureAwait(false); - - return; - } - - var clientSession = GetOrCreateClientSession(connectPacket); - - await clientAdapter.SendPacketsAsync(_options.DefaultCommunicationTimeout, cancellationToken, new MqttConnAckPacket - { - ConnectReturnCode = connectReturnCode, - IsSessionPresent = clientSession.IsExistingSession - }).ConfigureAwait(false); - - ClientConnected?.Invoke(this, new MqttClientConnectedEventArgs(new ConnectedMqttClient - { - ClientId = clientId, - ProtocolVersion = clientAdapter.PacketSerializer.ProtocolVersion - })); - - using (_logger.BeginScope(clientId)) - { - await clientSession.Session.RunAsync(connectPacket.WillMessage, clientAdapter).ConfigureAwait(false); - } - } - catch (Exception exception) - { - _logger.LogError(new EventId(), exception, exception.Message); - } - finally - { - try - { - await clientAdapter.DisconnectAsync(_options.DefaultCommunicationTimeout).ConfigureAwait(false); - } - catch (Exception) - { - //ignored - } - - ClientDisconnected?.Invoke(this, new MqttClientDisconnectedEventArgs(new ConnectedMqttClient - { - ClientId = clientId, - ProtocolVersion = clientAdapter.PacketSerializer.ProtocolVersion - })); - } - } - - public void Clear() - { - lock (_clientSessions) - { - _clientSessions.Clear(); - } - } - - public IList GetConnectedClients() - { - lock (_clientSessions) - { - return _clientSessions.Where(s => s.Value.IsConnected).Select(s => new ConnectedMqttClient - { - ClientId = s.Value.ClientId, - ProtocolVersion = s.Value.ProtocolVersion ?? MqttProtocolVersion.V311 - }).ToList(); - } - } - - public void DispatchApplicationMessage(MqttClientSession senderClientSession, MqttApplicationMessage applicationMessage) - { - try - { - var eventArgs = new MqttApplicationMessageReceivedEventArgs(senderClientSession?.ClientId, applicationMessage); - ApplicationMessageReceived?.Invoke(this, eventArgs); - } - catch (Exception exception) - { - _logger.LogError(new EventId(), exception, "Error while processing application message"); - } - - lock (_clientSessions) - { - foreach (var clientSession in _clientSessions.Values.ToList()) - { - clientSession.EnqueuePublishPacket(applicationMessage.ToPublishPacket()); - } - } - } - - private MqttConnectReturnCode ValidateConnection(MqttConnectPacket connectPacket) - { - if (_options.ConnectionValidator != null) - { - return _options.ConnectionValidator(connectPacket); - } - - return MqttConnectReturnCode.ConnectionAccepted; - } - - private GetOrCreateClientSessionResult GetOrCreateClientSession(MqttConnectPacket connectPacket) - { - lock (_clientSessions) - { - var isSessionPresent = _clientSessions.TryGetValue(connectPacket.ClientId, out var clientSession); - if (isSessionPresent) - { - if (connectPacket.CleanSession) - { - _clientSessions.Remove(connectPacket.ClientId); - clientSession.Dispose(); - clientSession = null; - - _logger.LogTrace("Disposed existing session of client '{0}'.", connectPacket.ClientId); - } - else - { - _logger.LogTrace("Reusing existing session of client '{0}'.", connectPacket.ClientId); - } - } - - var isExistingSession = true; - if (clientSession == null) - { - isExistingSession = false; - - clientSession = _clientSesssionFactory.CreateClientSession(connectPacket.ClientId, this); - _clientSessions[connectPacket.ClientId] = clientSession; - - _logger.LogTrace("Created a new session for client '{0}'.", connectPacket.ClientId); - } - - return new GetOrCreateClientSessionResult { IsExistingSession = isExistingSession, Session = clientSession }; - } - } - } -} \ No newline at end of file diff --git a/MQTTnet.Core/Server/MqttClientSubscriptionsManager.cs b/MQTTnet.Core/Server/MqttClientSubscriptionsManager.cs deleted file mode 100644 index 1b0d6c4c..00000000 --- a/MQTTnet.Core/Server/MqttClientSubscriptionsManager.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System; -using System.Collections.Generic; -using Microsoft.Extensions.Options; -using MQTTnet.Core.Packets; -using MQTTnet.Core.Protocol; - -namespace MQTTnet.Core.Server -{ - public sealed class MqttClientSubscriptionsManager - { - private readonly Dictionary _subscribedTopics = new Dictionary(); - private readonly MqttServerOptions _options; - - public MqttClientSubscriptionsManager(IOptions options) - { - _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); - } - - public MqttClientSubscribeResult Subscribe(MqttSubscribePacket subscribePacket, string clientId) - { - if (subscribePacket == null) throw new ArgumentNullException(nameof(subscribePacket)); - - var responsePacket = subscribePacket.CreateResponse(); - var closeConnection = false; - - lock (_subscribedTopics) - { - foreach (var topicFilter in subscribePacket.TopicFilters) - { - var interceptorContext = new MqttSubscriptionInterceptorContext(clientId, topicFilter); - _options.SubscriptionsInterceptor?.Invoke(interceptorContext); - responsePacket.SubscribeReturnCodes.Add(interceptorContext.AcceptSubscription ? MqttSubscribeReturnCode.SuccessMaximumQoS1 : MqttSubscribeReturnCode.Failure); - - if (interceptorContext.CloseConnection) - { - closeConnection = true; - } - - if (interceptorContext.AcceptSubscription) - { - _subscribedTopics[topicFilter.Topic] = topicFilter.QualityOfServiceLevel; - } - } - } - - return new MqttClientSubscribeResult - { - ResponsePacket = responsePacket, - CloseConnection = closeConnection - }; - } - - public MqttUnsubAckPacket Unsubscribe(MqttUnsubscribePacket unsubscribePacket) - { - if (unsubscribePacket == null) throw new ArgumentNullException(nameof(unsubscribePacket)); - - lock (_subscribedTopics) - { - foreach (var topicFilter in unsubscribePacket.TopicFilters) - { - _subscribedTopics.Remove(topicFilter); - } - } - - return unsubscribePacket.CreateResponse(); - } - - public bool IsSubscribed(MqttPublishPacket publishPacket) - { - if (publishPacket == null) throw new ArgumentNullException(nameof(publishPacket)); - - lock (_subscribedTopics) - { - foreach (var subscribedTopic in _subscribedTopics) - { - if (publishPacket.QualityOfServiceLevel > subscribedTopic.Value) - { - continue; - } - - if (!MqttTopicFilterComparer.IsMatch(publishPacket.Topic, subscribedTopic.Key)) - { - continue; - } - - return true; - } - } - - return false; - } - } -} diff --git a/MQTTnet.Core/Server/MqttServer.cs b/MQTTnet.Core/Server/MqttServer.cs deleted file mode 100644 index f1ed791e..00000000 --- a/MQTTnet.Core/Server/MqttServer.cs +++ /dev/null @@ -1,127 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using MQTTnet.Core.Adapter; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using System.Linq; - -namespace MQTTnet.Core.Server -{ - public class MqttServer : IMqttServer - { - private readonly ILogger _logger; - private readonly MqttClientSessionsManager _clientSessionsManager; - private readonly ICollection _adapters; - private readonly MqttServerOptions _options; - - private CancellationTokenSource _cancellationTokenSource; - - public MqttServer(IOptions options, IEnumerable adapters, ILogger logger, MqttClientSessionsManager clientSessionsManager) - { - _options = options.Value ?? throw new ArgumentNullException(nameof(options)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _clientSessionsManager = clientSessionsManager ?? throw new ArgumentNullException(nameof(clientSessionsManager)); - - if (adapters == null) - { - throw new ArgumentNullException(nameof(adapters)); - } - - _adapters = adapters.ToList(); - - _clientSessionsManager.ApplicationMessageReceived += (s, e) => ApplicationMessageReceived?.Invoke(s, e); - _clientSessionsManager.ClientConnected += OnClientConnected; - _clientSessionsManager.ClientDisconnected += OnClientDisconnected; - } - - public IList GetConnectedClients() - { - return _clientSessionsManager.GetConnectedClients(); - } - - public event EventHandler ClientConnected; - public event EventHandler ClientDisconnected; - public event EventHandler ApplicationMessageReceived; - - public void Publish(IEnumerable applicationMessages) - { - if (applicationMessages == null) throw new ArgumentNullException(nameof(applicationMessages)); - - if (_cancellationTokenSource == null) - { - throw new InvalidOperationException("The server is not started."); - } - - foreach (var applicationMessage in applicationMessages) - { - var interceptorContext = new MqttApplicationMessageInterceptorContext - { - ApplicationMessage = applicationMessage - }; - - _options.ApplicationMessageInterceptor?.Invoke(interceptorContext); - - _clientSessionsManager.DispatchApplicationMessage(null, interceptorContext.ApplicationMessage); - } - } - - public Task PublishAsync(IEnumerable applicationMessages) - { - Publish(applicationMessages); - return Task.FromResult(0); - } - - public async Task StartAsync() - { - if (_cancellationTokenSource != null) throw new InvalidOperationException("The MQTT server is already started."); - - _cancellationTokenSource = new CancellationTokenSource(); - - await _clientSessionsManager.RetainedMessagesManager.LoadMessagesAsync(); - - foreach (var adapter in _adapters) - { - adapter.ClientAccepted += OnClientAccepted; - await adapter.StartAsync(_options); - } - - _logger.LogInformation("Started."); - } - - public async Task StopAsync() - { - _cancellationTokenSource?.Cancel(false); - _cancellationTokenSource?.Dispose(); - _cancellationTokenSource = null; - - foreach (var adapter in _adapters) - { - adapter.ClientAccepted -= OnClientAccepted; - await adapter.StopAsync(); - } - - _clientSessionsManager.Clear(); - - _logger.LogInformation("Stopped."); - } - - private void OnClientAccepted(object sender, MqttServerAdapterClientAcceptedEventArgs eventArgs) - { - eventArgs.SessionTask = Task.Run(async () => await _clientSessionsManager.RunClientSessionAsync(eventArgs.Client, _cancellationTokenSource.Token), _cancellationTokenSource.Token); - } - - private void OnClientConnected(object sender, MqttClientConnectedEventArgs eventArgs) - { - _logger.LogInformation("Client '{0}': Connected.", eventArgs.Client.ClientId); - ClientConnected?.Invoke(this, eventArgs); - } - - private void OnClientDisconnected(object sender, MqttClientDisconnectedEventArgs eventArgs) - { - _logger.LogInformation("Client '{0}': Disconnected.", eventArgs.Client.ClientId); - ClientDisconnected?.Invoke(this, eventArgs); - } - } -} diff --git a/MQTTnet.sln b/MQTTnet.sln index 2207c1f8..fab76088 100644 --- a/MQTTnet.sln +++ b/MQTTnet.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.27004.2002 +VisualStudioVersion = 15.0.27004.2010 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Core.Tests", "Tests\MQTTnet.Core.Tests\MQTTnet.Core.Tests.csproj", "{A7FF0C91-25DE-4BA6-B39E-F54E8DADF1CC}" EndProject @@ -9,8 +9,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{9248C2E1 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Frameworks", "Frameworks", "{32A630A7-2598-41D7-B625-204CD906F5FB}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Core", "MQTTnet.Core\MQTTnet.Core.csproj", "{2ECB99E4-72D0-4C23-99BA-93D511D3967D}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MQTTnet.TestApp.UniversalWindows", "Tests\MQTTnet.TestApp.UniversalWindows\MQTTnet.TestApp.UniversalWindows.csproj", "{FF1F72D6-9524-4422-9497-3CC0002216ED}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.NetStandard", "Frameworks\MQTTnet.Netstandard\MQTTnet.NetStandard.csproj", "{3587E506-55A2-4EB3-99C7-DC01E42D25D2}" @@ -20,6 +18,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{67C28AC1 appveyor.yml = appveyor.yml Build\build.ps1 = Build\build.ps1 Build\MQTTnet.AspNetCore.nuspec = Build\MQTTnet.AspNetCore.nuspec + Build\MQTTnet.Extensions.Rpc.nuspec = Build\MQTTnet.Extensions.Rpc.nuspec Build\MQTTnet.nuspec = Build\MQTTnet.nuspec EndProjectSection EndProject @@ -35,6 +34,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.TestApp.AspNetCore2 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.AspNetCore", "Frameworks\MQTTnet.AspnetCore\MQTTnet.AspNetCore.csproj", "{F10C4060-F7EE-4A83-919F-FF723E72F94A}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extensions", "Extensions", "{12816BCC-AF9E-44A9-9AE5-C246AF2A0587}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Extensions.Rpc", "Extensions\MQTTnet.Extensions.Rpc\MQTTnet.Extensions.Rpc.csproj", "{C444E9C8-95FA-430E-9126-274129DE16CD}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -63,22 +66,6 @@ Global {A7FF0C91-25DE-4BA6-B39E-F54E8DADF1CC}.Release|x64.Build.0 = Release|Any CPU {A7FF0C91-25DE-4BA6-B39E-F54E8DADF1CC}.Release|x86.ActiveCfg = Release|Any CPU {A7FF0C91-25DE-4BA6-B39E-F54E8DADF1CC}.Release|x86.Build.0 = Release|Any CPU - {2ECB99E4-72D0-4C23-99BA-93D511D3967D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2ECB99E4-72D0-4C23-99BA-93D511D3967D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2ECB99E4-72D0-4C23-99BA-93D511D3967D}.Debug|ARM.ActiveCfg = Debug|Any CPU - {2ECB99E4-72D0-4C23-99BA-93D511D3967D}.Debug|ARM.Build.0 = Debug|Any CPU - {2ECB99E4-72D0-4C23-99BA-93D511D3967D}.Debug|x64.ActiveCfg = Debug|Any CPU - {2ECB99E4-72D0-4C23-99BA-93D511D3967D}.Debug|x64.Build.0 = Debug|Any CPU - {2ECB99E4-72D0-4C23-99BA-93D511D3967D}.Debug|x86.ActiveCfg = Debug|Any CPU - {2ECB99E4-72D0-4C23-99BA-93D511D3967D}.Debug|x86.Build.0 = Debug|Any CPU - {2ECB99E4-72D0-4C23-99BA-93D511D3967D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2ECB99E4-72D0-4C23-99BA-93D511D3967D}.Release|Any CPU.Build.0 = Release|Any CPU - {2ECB99E4-72D0-4C23-99BA-93D511D3967D}.Release|ARM.ActiveCfg = Release|Any CPU - {2ECB99E4-72D0-4C23-99BA-93D511D3967D}.Release|ARM.Build.0 = Release|Any CPU - {2ECB99E4-72D0-4C23-99BA-93D511D3967D}.Release|x64.ActiveCfg = Release|Any CPU - {2ECB99E4-72D0-4C23-99BA-93D511D3967D}.Release|x64.Build.0 = Release|Any CPU - {2ECB99E4-72D0-4C23-99BA-93D511D3967D}.Release|x86.ActiveCfg = Release|Any CPU - {2ECB99E4-72D0-4C23-99BA-93D511D3967D}.Release|x86.Build.0 = Release|Any CPU {FF1F72D6-9524-4422-9497-3CC0002216ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FF1F72D6-9524-4422-9497-3CC0002216ED}.Debug|Any CPU.Build.0 = Debug|Any CPU {FF1F72D6-9524-4422-9497-3CC0002216ED}.Debug|ARM.ActiveCfg = Debug|ARM @@ -165,6 +152,22 @@ Global {F10C4060-F7EE-4A83-919F-FF723E72F94A}.Release|x64.Build.0 = Release|Any CPU {F10C4060-F7EE-4A83-919F-FF723E72F94A}.Release|x86.ActiveCfg = Release|Any CPU {F10C4060-F7EE-4A83-919F-FF723E72F94A}.Release|x86.Build.0 = Release|Any CPU + {C444E9C8-95FA-430E-9126-274129DE16CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C444E9C8-95FA-430E-9126-274129DE16CD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C444E9C8-95FA-430E-9126-274129DE16CD}.Debug|ARM.ActiveCfg = Debug|Any CPU + {C444E9C8-95FA-430E-9126-274129DE16CD}.Debug|ARM.Build.0 = Debug|Any CPU + {C444E9C8-95FA-430E-9126-274129DE16CD}.Debug|x64.ActiveCfg = Debug|Any CPU + {C444E9C8-95FA-430E-9126-274129DE16CD}.Debug|x64.Build.0 = Debug|Any CPU + {C444E9C8-95FA-430E-9126-274129DE16CD}.Debug|x86.ActiveCfg = Debug|Any CPU + {C444E9C8-95FA-430E-9126-274129DE16CD}.Debug|x86.Build.0 = Debug|Any CPU + {C444E9C8-95FA-430E-9126-274129DE16CD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C444E9C8-95FA-430E-9126-274129DE16CD}.Release|Any CPU.Build.0 = Release|Any CPU + {C444E9C8-95FA-430E-9126-274129DE16CD}.Release|ARM.ActiveCfg = Release|Any CPU + {C444E9C8-95FA-430E-9126-274129DE16CD}.Release|ARM.Build.0 = Release|Any CPU + {C444E9C8-95FA-430E-9126-274129DE16CD}.Release|x64.ActiveCfg = Release|Any CPU + {C444E9C8-95FA-430E-9126-274129DE16CD}.Release|x64.Build.0 = Release|Any CPU + {C444E9C8-95FA-430E-9126-274129DE16CD}.Release|x86.ActiveCfg = Release|Any CPU + {C444E9C8-95FA-430E-9126-274129DE16CD}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -176,6 +179,7 @@ Global {3D283AAD-AAA8-4339-8394-52F80B6304DB} = {9248C2E1-B9D6-40BF-81EC-86004D7765B4} {C6FF8AEA-0855-41EC-A1F3-AC262225BAB9} = {9248C2E1-B9D6-40BF-81EC-86004D7765B4} {F10C4060-F7EE-4A83-919F-FF723E72F94A} = {32A630A7-2598-41D7-B625-204CD906F5FB} + {C444E9C8-95FA-430E-9126-274129DE16CD} = {12816BCC-AF9E-44A9-9AE5-C246AF2A0587} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {07536672-5CBC-4BE3-ACE0-708A431A7894} diff --git a/MQTTnet.sln.DotSettings b/MQTTnet.sln.DotSettings index ef034942..023e3d60 100644 --- a/MQTTnet.sln.DotSettings +++ b/MQTTnet.sln.DotSettings @@ -1,7 +1,13 @@  + NEVER False False False + True + True + True + True True + True True True \ No newline at end of file diff --git a/README.md b/README.md index 6e99af3f..6a3111d8 100644 --- a/README.md +++ b/README.md @@ -3,26 +3,30 @@

[![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) [![BCH compliance](https://bettercodehub.com/edge/badge/chkr1011/MQTTnet?branch=master)](https://bettercodehub.com/) # MQTTnet -MQTTnet is a high performance .NET library for MQTT based communication. It provides a MQTT client and a MQTT server (broker). The implementation is based on the documentation from http://mqtt.org/. -# Features +MQTTnet is a high performance .NET library for MQTT based communication. It provides a MQTT client and a MQTT server (broker). The implementation is based on the documentation from . + +## Features ### General + * Async support * TLS 1.2 support for client and server (but not UWP servers) * Extensible communication channels (i.e. In-Memory, TCP, TCP+TLS, WS) * Lightweight (only the low level implementation of MQTT, no overhead) -* Performance optimized (processing ~27.000 messages / second)* +* Performance optimized (processing ~40.000 messages / second)* * Interfaces included for mocking and testing * Access to internal trace messages * Unit tested (70+ tests) -\* Tested on local machine 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.NetFramework_. +\* 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_. ### Client + * Communication via TCP (+TLS) or WS (WebSocket) supported * Included core _MqttClient_ with low level functionality * Also included _ManagedMqttClient_ which maintains the connection and subscriptions automatically. Also application messages are queued and re-scheduled for higher QoS levels automatically. @@ -30,45 +34,57 @@ MQTTnet is a high performance .NET library for MQTT based communication. It prov * Compatible with Microsoft Azure IoT Hub ### Server (broker) + * List of connected clients available * Supports connected clients with different protocol versions at the same time * Able to publish its own messages (no loopback client required) * Able to receive every message (no loopback client required) * Extensible client credential validation * Retained messages are supported including persisting via interface methods (own implementation required) -* WebSockets supported (via ASP.NET Core 2.0) +* WebSockets supported (via ASP.NET Core 2.0, separate nuget) * 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 + +## Supported frameworks -# Supported frameworks * .NET Standard 1.3+ * .NET Core 1.1+ * .NET Core App 1.1+ +* Universal Windows Platform (UWP) 10.0.10240+ (x86, x64, ARM, AnyCPU, Windows 10 IoT Core) * .NET Framework 4.5.2+ (x86, x64, AnyCPU) -* Universal Windows (UWP) 10.0.10240+ (x86, x64, ARM, AnyCPU) * Mono 5.2+ +* Xamarin.Android 7.5+ +* Xamarin.iOS 10.14+ + +## Supported MQTT versions -# Supported MQTT versions * 3.1.1 * 3.1.0 -# Nuget -This library is available as a nuget package: https://www.nuget.org/packages/MQTTnet/ +## Nuget -# Examples -Please find examples and the documentation at the Wiki of this repository (https://github.com/chkr1011/MQTTnet/wiki). +This library is available as a nuget package: + +## Examples + +Please find examples and the documentation at the Wiki of this repository (). + +## Contributions -# Contributions If you want to contribute to this project just create a pull request. But only pull requests which are matching the code style of this library will be accepted. Before creating a pull request please have a look at the library to get an overview of the required style. -# References +## References + This library is used in the following projects: -* MQTT Client Rx (Wrapper for Reactive Extensions, https://github.com/1iveowl/MQTTClient.rx) -* Wirehome (Open Source Home Automation system for .NET, https://github.com/chkr1011/Wirehome) +* MQTT Client Rx (Wrapper for Reactive Extensions, ) +* MQTT Tester (MQTT client test app for [Android](https://play.google.com/store/apps/details?id=com.liveowl.mqtttester) and [iOS](https://itunes.apple.com/us/app/mqtt-tester/id1278621826?mt=8)) +* Wirehome (Open Source Home Automation system for .NET, ) + If you use this library and want to see your project here please let me know. -# MIT License +## MIT License Copyright (c) 2017 Christian Kratky @@ -88,4 +104,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. diff --git a/Tests/MQTTnet.Core.Tests/ByteReaderTests.cs b/Tests/MQTTnet.Core.Tests/ByteReaderTests.cs index 76ff90b1..e2173cbc 100644 --- a/Tests/MQTTnet.Core.Tests/ByteReaderTests.cs +++ b/Tests/MQTTnet.Core.Tests/ByteReaderTests.cs @@ -1,5 +1,5 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; -using MQTTnet.Core.Serializer; +using MQTTnet.Serializer; namespace MQTTnet.Core.Tests { diff --git a/Tests/MQTTnet.Core.Tests/ByteWriterTests.cs b/Tests/MQTTnet.Core.Tests/ByteWriterTests.cs index 65991931..881df5c5 100644 --- a/Tests/MQTTnet.Core.Tests/ByteWriterTests.cs +++ b/Tests/MQTTnet.Core.Tests/ByteWriterTests.cs @@ -1,5 +1,5 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; -using MQTTnet.Core.Serializer; +using MQTTnet.Serializer; namespace MQTTnet.Core.Tests { diff --git a/Tests/MQTTnet.Core.Tests/ExtensionTests.cs b/Tests/MQTTnet.Core.Tests/ExtensionTests.cs index 2f0966fb..053b0ca3 100644 --- a/Tests/MQTTnet.Core.Tests/ExtensionTests.cs +++ b/Tests/MQTTnet.Core.Tests/ExtensionTests.cs @@ -2,8 +2,8 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; -using MQTTnet.Core.Exceptions; -using MQTTnet.Core.Internal; +using MQTTnet.Exceptions; +using MQTTnet.Internal; namespace MQTTnet.Core.Tests { diff --git a/Tests/MQTTnet.Core.Tests/MQTTnet.Core.Tests.csproj b/Tests/MQTTnet.Core.Tests/MQTTnet.Core.Tests.csproj index 7ba48954..b03d0d11 100644 --- a/Tests/MQTTnet.Core.Tests/MQTTnet.Core.Tests.csproj +++ b/Tests/MQTTnet.Core.Tests/MQTTnet.Core.Tests.csproj @@ -7,15 +7,13 @@ - - - - + + + - \ No newline at end of file diff --git a/Tests/MQTTnet.Core.Tests/MqttApplicationMessageBuilderTests.cs b/Tests/MQTTnet.Core.Tests/MqttApplicationMessageBuilderTests.cs index c6a70174..59afcd68 100644 --- a/Tests/MQTTnet.Core.Tests/MqttApplicationMessageBuilderTests.cs +++ b/Tests/MQTTnet.Core.Tests/MqttApplicationMessageBuilderTests.cs @@ -2,7 +2,7 @@ using System.IO; using System.Text; using Microsoft.VisualStudio.TestTools.UnitTesting; -using MQTTnet.Core.Protocol; +using MQTTnet.Protocol; namespace MQTTnet.Core.Tests { diff --git a/Tests/MQTTnet.Core.Tests/MqttClientTests.cs b/Tests/MQTTnet.Core.Tests/MqttClientTests.cs new file mode 100644 index 00000000..1334a803 --- /dev/null +++ b/Tests/MQTTnet.Core.Tests/MqttClientTests.cs @@ -0,0 +1,36 @@ +using System.Net.Sockets; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Client; +using MQTTnet.Exceptions; + +namespace MQTTnet.Core.Tests +{ + [TestClass] + public class MqttClientTests + { + + [TestMethod] + public async Task ClientDisconnectException() + { + var factory = new MqttFactory(); + var client = factory.CreateMqttClient(); + + var exceptionIsCorrect = false; + client.Disconnected += (s, e) => + { + exceptionIsCorrect = e.Exception is MqttCommunicationException c && c.InnerException is SocketException; + }; + + try + { + await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("wrong-server").Build()); + } + catch + { + } + + Assert.IsTrue(exceptionIsCorrect); + } + } +} diff --git a/Tests/MQTTnet.Core.Tests/MqttCommunicationAdapterFactory.cs b/Tests/MQTTnet.Core.Tests/MqttCommunicationAdapterFactory.cs deleted file mode 100644 index 7e3a7dca..00000000 --- a/Tests/MQTTnet.Core.Tests/MqttCommunicationAdapterFactory.cs +++ /dev/null @@ -1,26 +0,0 @@ -using MQTTnet.Core.Adapter; -using MQTTnet.Core.Channel; -using MQTTnet.Core.Client; - -namespace MQTTnet.Core.Tests -{ - public class MqttCommunicationAdapterFactory : IMqttCommunicationAdapterFactory - { - private readonly IMqttCommunicationAdapter _adapter; - - public MqttCommunicationAdapterFactory(IMqttCommunicationAdapter adapter) - { - _adapter = adapter; - } - - public IMqttCommunicationAdapter CreateClientMqttCommunicationAdapter(IMqttClientOptions options) - { - return _adapter; - } - - public IMqttCommunicationAdapter CreateServerMqttCommunicationAdapter(IMqttCommunicationChannel channel) - { - return _adapter; - } - } -} diff --git a/Tests/MQTTnet.Core.Tests/MqttKeepAliveMonitorTests.cs b/Tests/MQTTnet.Core.Tests/MqttKeepAliveMonitorTests.cs new file mode 100644 index 00000000..206942f4 --- /dev/null +++ b/Tests/MQTTnet.Core.Tests/MqttKeepAliveMonitorTests.cs @@ -0,0 +1,66 @@ +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Diagnostics; +using MQTTnet.Packets; +using MQTTnet.Server; + +namespace MQTTnet.Core.Tests +{ + [TestClass] + public class MqttKeepAliveMonitorTests + { + [TestMethod] + public void KeepAlive_Timeout() + { + var timeoutCalledCount = 0; + + var monitor = new MqttClientKeepAliveMonitor(string.Empty, delegate + { + timeoutCalledCount++; + return Task.FromResult(0); + }, new MqttNetLogger()); + + Assert.AreEqual(0, timeoutCalledCount); + + monitor.Start(1, CancellationToken.None); + + Assert.AreEqual(0, timeoutCalledCount); + + Thread.Sleep(2000); // Internally the keep alive timeout is multiplied with 1.5 as per protocol specification. + + Assert.AreEqual(1, timeoutCalledCount); + } + + [TestMethod] + public void KeepAlive_NoTimeout() + { + var timeoutCalledCount = 0; + + var monitor = new MqttClientKeepAliveMonitor(string.Empty, delegate + { + timeoutCalledCount++; + return Task.FromResult(0); + }, new MqttNetLogger()); + + Assert.AreEqual(0, timeoutCalledCount); + + monitor.Start(1, CancellationToken.None); + + Assert.AreEqual(0, timeoutCalledCount); + + // Simulate traffic. + Thread.Sleep(1000); // Internally the keep alive timeout is multiplied with 1.5 as per protocol specification. + monitor.PacketReceived(new MqttPublishPacket()); + Thread.Sleep(1000); + monitor.PacketReceived(new MqttPublishPacket()); + Thread.Sleep(1000); + + Assert.AreEqual(0, timeoutCalledCount); + + Thread.Sleep(2000); + + Assert.AreEqual(1, timeoutCalledCount); + } + } +} diff --git a/Tests/MQTTnet.Core.Tests/MqttLoggerProviderTest.cs b/Tests/MQTTnet.Core.Tests/MqttLoggerProviderTest.cs deleted file mode 100644 index bda3195e..00000000 --- a/Tests/MQTTnet.Core.Tests/MqttLoggerProviderTest.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using MQTTnet.Core.Diagnostics; - -namespace MQTTnet.Core.Tests -{ - [TestClass] - public class MqttLoggerProviderTest - { - [TestMethod] - public void TestLoggerCallback() - { - var serviceCollection = new ServiceCollection(); - serviceCollection.AddLogging(); - - var serviceProvider = serviceCollection.BuildServiceProvider(); - using ((IDisposable)serviceProvider) - { - var loggerFactory = serviceProvider.GetRequiredService(); - - loggerFactory.AddMqttTrace(); - - var expectedMsg = "Hello World!"; - MqttNetTraceMessage msg = null; - - MqttNetTrace.TraceMessagePublished += (sender, args) => - { - msg = args.TraceMessage; - }; - - var logger = loggerFactory.CreateLogger(); - - logger.LogInformation(expectedMsg); - - Assert.AreEqual(expectedMsg, msg.Message); - - var expectedException = new Exception("bad stuff"); - - logger.LogError(new EventId(), expectedException, expectedException.Message); - - Assert.AreEqual(expectedException, msg.Exception); - Assert.AreEqual(expectedException.Message, msg.Message); - } - } - } -} diff --git a/Tests/MQTTnet.Core.Tests/MqttPacketIdentifierProviderTests.cs b/Tests/MQTTnet.Core.Tests/MqttPacketIdentifierProviderTests.cs new file mode 100644 index 00000000..ab7070ee --- /dev/null +++ b/Tests/MQTTnet.Core.Tests/MqttPacketIdentifierProviderTests.cs @@ -0,0 +1,32 @@ +using Microsoft.VisualStudio.TestTools .UnitTesting; +using MQTTnet.Client; + +namespace MQTTnet.Core.Tests +{ + [TestClass] + public class MqttPacketIdentifierProviderTests + { + [TestMethod] + public void Reset() + { + var p = new MqttPacketIdentifierProvider(); + Assert.AreEqual(1, p.GetNewPacketIdentifier()); + Assert.AreEqual(2, p.GetNewPacketIdentifier()); + p.Reset(); + Assert.AreEqual(1, p.GetNewPacketIdentifier()); + } + + [TestMethod] + public void ReachBoundaries() + { + var p = new MqttPacketIdentifierProvider(); + + for (ushort i = 0; i < ushort.MaxValue; i++) + { + Assert.AreEqual(i + 1, p.GetNewPacketIdentifier()); + } + + Assert.AreEqual(1, p.GetNewPacketIdentifier()); + } + } +} diff --git a/Tests/MQTTnet.Core.Tests/MqttPacketReaderTests.cs b/Tests/MQTTnet.Core.Tests/MqttPacketReaderTests.cs new file mode 100644 index 00000000..4d28035e --- /dev/null +++ b/Tests/MQTTnet.Core.Tests/MqttPacketReaderTests.cs @@ -0,0 +1,20 @@ +using System.IO; +using System.Threading; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Serializer; + +namespace MQTTnet.Core.Tests +{ + [TestClass] + public class MqttPacketReaderTests + { + [TestMethod] + public void MqttPacketReader_EmptyStream() + { + var memStream = new MemoryStream(); + var header = MqttPacketReader.ReadHeaderAsync(memStream, CancellationToken.None).GetAwaiter().GetResult(); + + Assert.IsNull(header); + } + } +} diff --git a/Tests/MQTTnet.Core.Tests/MqttPacketSerializerTests.cs b/Tests/MQTTnet.Core.Tests/MqttPacketSerializerTests.cs index bdd07516..1111d907 100644 --- a/Tests/MQTTnet.Core.Tests/MqttPacketSerializerTests.cs +++ b/Tests/MQTTnet.Core.Tests/MqttPacketSerializerTests.cs @@ -1,12 +1,12 @@ using System; +using System.Collections.Generic; using System.IO; using System.Text; using System.Threading; using Microsoft.VisualStudio.TestTools.UnitTesting; -using MQTTnet.Core.Adapter; -using MQTTnet.Core.Packets; -using MQTTnet.Core.Protocol; -using MQTTnet.Core.Serializer; +using MQTTnet.Packets; +using MQTTnet.Protocol; +using MQTTnet.Serializer; namespace MQTTnet.Core.Tests { @@ -119,10 +119,10 @@ namespace MQTTnet.Core.Tests { var p = new MqttConnAckPacket { - ConnectReturnCode = MqttConnectReturnCode.ConnectionAccepted + ConnectReturnCode = MqttConnectReturnCode.ConnectionRefusedNotAuthorized }; - SerializeAndCompare(p, "IAIAAA==", MqttProtocolVersion.V310); + SerializeAndCompare(p, "IAIABQ==", MqttProtocolVersion.V310); } [TestMethod] @@ -137,6 +137,17 @@ namespace MQTTnet.Core.Tests DeserializeAndCompare(p, "IAIBBQ=="); } + [TestMethod] + public void DeserializeV310_MqttConnAckPacket() + { + var p = new MqttConnAckPacket + { + ConnectReturnCode = MqttConnectReturnCode.ConnectionRefusedNotAuthorized + }; + + DeserializeAndCompare(p, "IAIABQ==", MqttProtocolVersion.V310); + } + [TestMethod] public void SerializeV311_MqttDisconnectPacket() { @@ -392,29 +403,40 @@ namespace MQTTnet.Core.Tests private static void SerializeAndCompare(MqttBasePacket packet, string expectedBase64Value, MqttProtocolVersion protocolVersion = MqttProtocolVersion.V311) { var serializer = new MqttPacketSerializer { ProtocolVersion = protocolVersion }; - var buffer = serializer.Serialize(packet); - - Assert.AreEqual(expectedBase64Value, Convert.ToBase64String(buffer)); + var chunks = serializer.Serialize(packet); + + Assert.AreEqual(expectedBase64Value, Convert.ToBase64String(Join(chunks))); } - private static void DeserializeAndCompare(MqttBasePacket packet, string expectedBase64Value) + private static void DeserializeAndCompare(MqttBasePacket packet, string expectedBase64Value, MqttProtocolVersion protocolVersion = MqttProtocolVersion.V311) { - var serializer = new MqttPacketSerializer(); + var serializer = new MqttPacketSerializer { ProtocolVersion = protocolVersion }; var buffer1 = serializer.Serialize(packet); - using (var headerStream = new MemoryStream(buffer1)) + using (var headerStream = new MemoryStream(Join(buffer1))) { - var header = MqttPacketReader.ReadHeaderFromSource(headerStream, CancellationToken.None); + var header = MqttPacketReader.ReadHeaderAsync(headerStream, CancellationToken.None).GetAwaiter().GetResult(); - using (var bodyStream = new MemoryStream(buffer1, (int)headerStream.Position, header.BodyLength)) + using (var bodyStream = new MemoryStream(Join(buffer1), (int)headerStream.Position, header.BodyLength)) { - var deserializedPacket = serializer.Deserialize(new ReceivedMqttPacket(header, bodyStream)); + var deserializedPacket = serializer.Deserialize(header, bodyStream); var buffer2 = serializer.Serialize(deserializedPacket); - Assert.AreEqual(expectedBase64Value, Convert.ToBase64String(buffer2)); + Assert.AreEqual(expectedBase64Value, Convert.ToBase64String(Join(buffer2))); } } } + + private static byte[] Join(IEnumerable> chunks) + { + var buffer = new MemoryStream(); + foreach (var chunk in chunks) + { + buffer.Write(chunk.Array, chunk.Offset, chunk.Count); + } + + return buffer.ToArray(); + } } } diff --git a/Tests/MQTTnet.Core.Tests/MqttServerTests.cs b/Tests/MQTTnet.Core.Tests/MqttServerTests.cs index 3f5b4cf6..dc2f5251 100644 --- a/Tests/MQTTnet.Core.Tests/MqttServerTests.cs +++ b/Tests/MQTTnet.Core.Tests/MqttServerTests.cs @@ -3,11 +3,10 @@ using System.Collections.Generic; using System.Text; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; -using MQTTnet.Core.Adapter; -using MQTTnet.Core.Client; -using MQTTnet.Core.Protocol; -using MQTTnet.Core.Server; -using Microsoft.Extensions.DependencyInjection; +using MQTTnet.Diagnostics; +using MQTTnet.Protocol; +using MQTTnet.Server; +using MQTTnet.Client; namespace MQTTnet.Core.Tests { @@ -51,53 +50,45 @@ namespace MQTTnet.Core.Tests public async Task MqttServer_WillMessage() { var serverAdapter = new TestMqttServerAdapter(); - var services = new ServiceCollection() - .AddLogging() - .AddMqttServer() - .AddSingleton(serverAdapter) - .BuildServiceProvider(); + var s = new MqttFactory().CreateMqttServer(new[] { serverAdapter }, new MqttNetLogger()); - var s = new MqttFactory(services).CreateMqttServer(); var receivedMessagesCount = 0; try { - await s.StartAsync(); + await s.StartAsync(new MqttServerOptions()); var willMessage = new MqttApplicationMessageBuilder().WithTopic("My/last/will").WithAtMostOnceQoS().Build(); var c1 = await serverAdapter.ConnectTestClient(s, "c1"); var c2 = await serverAdapter.ConnectTestClient(s, "c2", willMessage); c1.ApplicationMessageReceived += (_, __) => receivedMessagesCount++; - await c1.SubscribeAsync(new TopicFilter("#", MqttQualityOfServiceLevel.AtMostOnce)); + await c1.SubscribeAsync(new TopicFilterBuilder().WithTopic("#").Build()); await c2.DisconnectAsync(); await Task.Delay(1000); + + await c1.DisconnectAsync(); } finally { await s.StopAsync(); } - Assert.AreEqual(1, receivedMessagesCount); + + Assert.AreEqual(0, receivedMessagesCount); } [TestMethod] - public async Task MqttServer_Unsubscribe() + public async Task MqttServer_SubscribeUnsubscribe() { var serverAdapter = new TestMqttServerAdapter(); - var services = new ServiceCollection() - .AddLogging() - .AddMqttServer() - .AddSingleton(serverAdapter) - .BuildServiceProvider(); - - var s = new MqttFactory(services).CreateMqttServer(); + var s = new MqttFactory().CreateMqttServer(new[] { serverAdapter }, new MqttNetLogger()); var receivedMessagesCount = 0; try { - await s.StartAsync(); + await s.StartAsync(new MqttServerOptions()); var c1 = await serverAdapter.ConnectTestClient(s, "c1"); var c2 = await serverAdapter.ConnectTestClient(s, "c2"); @@ -108,13 +99,31 @@ namespace MQTTnet.Core.Tests await c2.PublishAsync(message); Assert.AreEqual(0, receivedMessagesCount); + var subscribeEventCalled = false; + s.ClientSubscribedTopic += (_, e) => + { + subscribeEventCalled = e.TopicFilter.Topic == "a" && e.ClientId == "c1"; + }; + await c1.SubscribeAsync(new TopicFilter("a", MqttQualityOfServiceLevel.AtLeastOnce)); + await Task.Delay(100); + Assert.IsTrue(subscribeEventCalled, "Subscribe event not called."); + await c2.PublishAsync(message); await Task.Delay(500); Assert.AreEqual(1, receivedMessagesCount); + var unsubscribeEventCalled = false; + s.ClientUnsubscribedTopic += (_, e) => + { + unsubscribeEventCalled = e.TopicFilter == "a" && e.ClientId == "c1"; + }; + await c1.UnsubscribeAsync("a"); + await Task.Delay(100); + Assert.IsTrue(unsubscribeEventCalled, "Unsubscribe event not called."); + await c2.PublishAsync(message); await Task.Delay(500); @@ -133,18 +142,12 @@ namespace MQTTnet.Core.Tests public async Task MqttServer_Publish() { var serverAdapter = new TestMqttServerAdapter(); - var services = new ServiceCollection() - .AddLogging() - .AddMqttServer() - .AddSingleton(serverAdapter) - .BuildServiceProvider(); - - var s = new MqttFactory(services).CreateMqttServer(); + var s = new MqttFactory().CreateMqttServer(new[] { serverAdapter }, new MqttNetLogger()); var receivedMessagesCount = 0; try { - await s.StartAsync(); + await s.StartAsync(new MqttServerOptions()); var c1 = await serverAdapter.ConnectTestClient(s, "c1"); @@ -153,14 +156,14 @@ namespace MQTTnet.Core.Tests var message = new MqttApplicationMessageBuilder().WithTopic("a").WithAtLeastOnceQoS().Build(); await c1.SubscribeAsync(new TopicFilter("a", MqttQualityOfServiceLevel.AtLeastOnce)); - s.PublishAsync(message).Wait(); + await s.PublishAsync(message); await Task.Delay(500); } finally { await s.StopAsync(); } - + Assert.AreEqual(1, receivedMessagesCount); } @@ -168,18 +171,13 @@ namespace MQTTnet.Core.Tests public async Task MqttServer_NoRetainedMessage() { var serverAdapter = new TestMqttServerAdapter(); - var services = new ServiceCollection() - .AddLogging() - .AddMqttServer() - .AddSingleton(serverAdapter) - .BuildServiceProvider(); + var s = new MqttFactory().CreateMqttServer(new[] { serverAdapter }, new MqttNetLogger()); - var s = new MqttFactory(services).CreateMqttServer(); var receivedMessagesCount = 0; try { - await s.StartAsync(); + await s.StartAsync(new MqttServerOptions()); var c1 = await serverAdapter.ConnectTestClient(s, "c1"); await c1.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("retained").WithPayload(new byte[3]).Build()); @@ -187,7 +185,7 @@ namespace MQTTnet.Core.Tests var c2 = await serverAdapter.ConnectTestClient(s, "c2"); c2.ApplicationMessageReceived += (_, __) => receivedMessagesCount++; - await c2.SubscribeAsync(new TopicFilter("retained", MqttQualityOfServiceLevel.AtMostOnce)); + await c2.SubscribeAsync(new TopicFilterBuilder().WithTopic("retained").Build()); await Task.Delay(500); } @@ -203,26 +201,20 @@ namespace MQTTnet.Core.Tests public async Task MqttServer_RetainedMessage() { var serverAdapter = new TestMqttServerAdapter(); - var services = new ServiceCollection() - .AddLogging() - .AddMqttServer() - .AddSingleton(serverAdapter) - .BuildServiceProvider(); - - var s = new MqttFactory(services).CreateMqttServer(); + var s = new MqttFactory().CreateMqttServer(new[] { serverAdapter }, new MqttNetLogger()); var receivedMessagesCount = 0; try { - await s.StartAsync(); + await s.StartAsync(new MqttServerOptions()); var c1 = await serverAdapter.ConnectTestClient(s, "c1"); - await c1.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("retained").WithPayload(new byte[3]).WithRetainFlag().Build()); + await c1.PublishAndWaitForAsync(s, new MqttApplicationMessageBuilder().WithTopic("retained").WithPayload(new byte[3]).WithRetainFlag().Build()); await c1.DisconnectAsync(); var c2 = await serverAdapter.ConnectTestClient(s, "c2"); c2.ApplicationMessageReceived += (_, __) => receivedMessagesCount++; - await c2.SubscribeAsync(new TopicFilter("retained", MqttQualityOfServiceLevel.AtMostOnce)); + await c2.SubscribeAsync(new TopicFilterBuilder().WithTopic("retained").Build()); await Task.Delay(500); } @@ -230,7 +222,7 @@ namespace MQTTnet.Core.Tests { await s.StopAsync(); } - + Assert.AreEqual(1, receivedMessagesCount); } @@ -238,23 +230,18 @@ namespace MQTTnet.Core.Tests public async Task MqttServer_ClearRetainedMessage() { var serverAdapter = new TestMqttServerAdapter(); - var services = new ServiceCollection() - .AddLogging() - .AddMqttServer() - .AddSingleton(serverAdapter) - .BuildServiceProvider(); + var s = new MqttFactory().CreateMqttServer(new[] { serverAdapter }, new MqttNetLogger()); - var s = new MqttFactory(services).CreateMqttServer(); var receivedMessagesCount = 0; try { - await s.StartAsync(); + await s.StartAsync(new MqttServerOptions()); var c1 = await serverAdapter.ConnectTestClient(s, "c1"); await c1.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("retained").WithPayload(new byte[3]).WithRetainFlag().Build()); await c1.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("retained").WithPayload(new byte[0]).WithRetainFlag().Build()); await c1.DisconnectAsync(); - + var c2 = await serverAdapter.ConnectTestClient(s, "c2"); c2.ApplicationMessageReceived += (_, __) => receivedMessagesCount++; await c2.SubscribeAsync(new TopicFilter("retained", MqttQualityOfServiceLevel.AtMostOnce)); @@ -274,22 +261,18 @@ namespace MQTTnet.Core.Tests public async Task MqttServer_PersistRetainedMessage() { var storage = new TestStorage(); - var serverAdapter = new TestMqttServerAdapter(); - var services = new ServiceCollection() - .AddLogging() - .AddMqttServer() - .AddSingleton(serverAdapter) - .BuildServiceProvider(); - - var s = new MqttFactory(services).CreateMqttServer(options => options.Storage = storage); + var s = new MqttFactory().CreateMqttServer(new[] { serverAdapter }, new MqttNetLogger()); try { - await s.StartAsync(); + var options = new MqttServerOptions { Storage = storage }; + + await s.StartAsync(options); var c1 = await serverAdapter.ConnectTestClient(s, "c1"); - await c1.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("retained").WithPayload(new byte[3]).WithRetainFlag().Build()); + + await c1.PublishAndWaitForAsync(s, new MqttApplicationMessageBuilder().WithTopic("retained").WithPayload(new byte[3]).WithRetainFlag().Build()); await c1.DisconnectAsync(); } finally @@ -297,16 +280,19 @@ namespace MQTTnet.Core.Tests await s.StopAsync(); } - s = services.GetRequiredService(); + Assert.AreEqual(1, storage.Messages.Count); + + s = new MqttFactory().CreateMqttServer(new[] { serverAdapter }, new MqttNetLogger()); var receivedMessagesCount = 0; try { - await s.StartAsync(); + var options = new MqttServerOptions { Storage = storage }; + await s.StartAsync(options); var c2 = await serverAdapter.ConnectTestClient(s, "c2"); c2.ApplicationMessageReceived += (_, __) => receivedMessagesCount++; - await c2.SubscribeAsync(new TopicFilter("retained", MqttQualityOfServiceLevel.AtMostOnce)); + await c2.SubscribeAsync(new TopicFilterBuilder().WithTopic("retained").Build()); await Task.Delay(500); } @@ -327,16 +313,13 @@ namespace MQTTnet.Core.Tests } var serverAdapter = new TestMqttServerAdapter(); - var services = new ServiceCollection() - .AddLogging() - .AddMqttServer() - .AddSingleton(serverAdapter) - .BuildServiceProvider(); + var s = new MqttFactory().CreateMqttServer(new[] { serverAdapter }, new MqttNetLogger()); - var s = new MqttFactory(services).CreateMqttServer(options => options.ApplicationMessageInterceptor = Interceptor); try { - await s.StartAsync(); + var options = new MqttServerOptions { ApplicationMessageInterceptor = Interceptor }; + + await s.StartAsync(options); var c1 = await serverAdapter.ConnectTestClient(s, "c1"); var c2 = await serverAdapter.ConnectTestClient(s, "c2"); @@ -362,19 +345,54 @@ namespace MQTTnet.Core.Tests } } + [TestMethod] + public async Task MqttServer_Body() + { + var serverAdapter = new TestMqttServerAdapter(); + var s = new MqttFactory().CreateMqttServer(new[] { serverAdapter }, new MqttNetLogger()); + + var bodyIsMatching = false; + try + { + await s.StartAsync(new MqttServerOptions()); + + var c1 = await serverAdapter.ConnectTestClient(s, "c1"); + var c2 = await serverAdapter.ConnectTestClient(s, "c2"); + + c1.ApplicationMessageReceived += (_, e) => + { + if (Encoding.UTF8.GetString(e.ApplicationMessage.Payload) == "The body") + { + bodyIsMatching = true; + } + }; + + await c1.SubscribeAsync("A", MqttQualityOfServiceLevel.AtMostOnce); + await c2.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("A").WithPayload(Encoding.UTF8.GetBytes("The body")).Build()); + + await Task.Delay(500); + } + finally + { + await s.StopAsync(); + } + + Assert.IsTrue(bodyIsMatching); + } + private class TestStorage : IMqttServerStorage { - private IList _messages = new List(); + public IList Messages = new List(); public Task SaveRetainedMessagesAsync(IList messages) { - _messages = messages; + Messages = messages; return Task.CompletedTask; } public Task> LoadRetainedMessagesAsync() { - return Task.FromResult(_messages); + return Task.FromResult(Messages); } } @@ -386,17 +404,12 @@ namespace MQTTnet.Core.Tests int expectedReceivedMessagesCount) { var serverAdapter = new TestMqttServerAdapter(); - var services = new ServiceCollection() - .AddMqttServer() - .AddLogging() - .AddSingleton(serverAdapter) - .BuildServiceProvider(); + var s = new MqttFactory().CreateMqttServer(new[] { serverAdapter }, new MqttNetLogger()); - var s = services.GetRequiredService(); var receivedMessagesCount = 0; try { - await s.StartAsync(); + await s.StartAsync(new MqttServerOptions()); var c1 = await serverAdapter.ConnectTestClient(s, "c1"); var c2 = await serverAdapter.ConnectTestClient(s, "c2"); @@ -415,7 +428,7 @@ namespace MQTTnet.Core.Tests { await s.StopAsync(); } - + Assert.AreEqual(expectedReceivedMessagesCount, receivedMessagesCount); } } diff --git a/Tests/MQTTnet.Core.Tests/MqttSubscriptionsManagerTests.cs b/Tests/MQTTnet.Core.Tests/MqttSubscriptionsManagerTests.cs index d46354c7..8b2863af 100644 --- a/Tests/MQTTnet.Core.Tests/MqttSubscriptionsManagerTests.cs +++ b/Tests/MQTTnet.Core.Tests/MqttSubscriptionsManagerTests.cs @@ -1,8 +1,7 @@ -using Microsoft.Extensions.Options; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using MQTTnet.Core.Packets; -using MQTTnet.Core.Protocol; -using MQTTnet.Core.Server; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Packets; +using MQTTnet.Protocol; +using MQTTnet.Server; namespace MQTTnet.Core.Tests { @@ -12,64 +11,109 @@ namespace MQTTnet.Core.Tests [TestMethod] public void MqttSubscriptionsManager_SubscribeSingleSuccess() { - var sm = new MqttClientSubscriptionsManager(new OptionsWrapper(new MqttServerOptions())); + var sm = new MqttClientSubscriptionsManager(new MqttServerOptions(), ""); var sp = new MqttSubscribePacket(); - sp.TopicFilters.Add(new TopicFilter("A/B/C", MqttQualityOfServiceLevel.AtMostOnce)); + sp.TopicFilters.Add(new TopicFilterBuilder().WithTopic("A/B/C").Build()); - sm.Subscribe(sp, ""); + sm.SubscribeAsync(sp).Wait(); - var pp = new MqttPublishPacket + var pp = new MqttApplicationMessage { Topic = "A/B/C", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce }; - Assert.IsTrue(sm.IsSubscribed(pp)); + var result = sm.CheckSubscriptionsAsync(pp).Result; + Assert.IsTrue(result.IsSubscribed); + Assert.AreEqual(result.QualityOfServiceLevel, MqttQualityOfServiceLevel.AtMostOnce); + } + + [TestMethod] + public void MqttSubscriptionsManager_SubscribeDifferentQoSSuccess() + { + var sm = new MqttClientSubscriptionsManager(new MqttServerOptions(), ""); + + var sp = new MqttSubscribePacket(); + sp.TopicFilters.Add(new TopicFilter("A/B/C", MqttQualityOfServiceLevel.AtMostOnce)); + + sm.SubscribeAsync(sp).Wait(); + + var pp = new MqttApplicationMessage + { + Topic = "A/B/C", + QualityOfServiceLevel = MqttQualityOfServiceLevel.ExactlyOnce + }; + + var result = sm.CheckSubscriptionsAsync(pp).Result; + Assert.IsTrue(result.IsSubscribed); + Assert.AreEqual(result.QualityOfServiceLevel, MqttQualityOfServiceLevel.AtMostOnce); + } + + [TestMethod] + public void MqttSubscriptionsManager_SubscribeTwoTimesSuccess() + { + var sm = new MqttClientSubscriptionsManager(new MqttServerOptions(), ""); + + var sp = new MqttSubscribePacket(); + sp.TopicFilters.Add(new TopicFilter("#", MqttQualityOfServiceLevel.AtMostOnce)); + sp.TopicFilters.Add(new TopicFilter("A/B/C", MqttQualityOfServiceLevel.AtLeastOnce)); + + sm.SubscribeAsync(sp).Wait(); + + var pp = new MqttApplicationMessage + { + Topic = "A/B/C", + QualityOfServiceLevel = MqttQualityOfServiceLevel.ExactlyOnce + }; + + var result = sm.CheckSubscriptionsAsync(pp).Result; + Assert.IsTrue(result.IsSubscribed); + Assert.AreEqual(result.QualityOfServiceLevel, MqttQualityOfServiceLevel.AtLeastOnce); } [TestMethod] public void MqttSubscriptionsManager_SubscribeSingleNoSuccess() { - var sm = new MqttClientSubscriptionsManager(new OptionsWrapper(new MqttServerOptions())); + var sm = new MqttClientSubscriptionsManager(new MqttServerOptions(), ""); var sp = new MqttSubscribePacket(); - sp.TopicFilters.Add(new TopicFilter("A/B/C", MqttQualityOfServiceLevel.AtMostOnce)); + sp.TopicFilters.Add(new TopicFilterBuilder().WithTopic("A/B/C").Build()); - sm.Subscribe(sp, ""); + sm.SubscribeAsync(sp).Wait(); - var pp = new MqttPublishPacket + var pp = new MqttApplicationMessage { Topic = "A/B/X", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce }; - Assert.IsFalse(sm.IsSubscribed(pp)); + Assert.IsFalse(sm.CheckSubscriptionsAsync(pp).Result.IsSubscribed); } [TestMethod] public void MqttSubscriptionsManager_SubscribeAndUnsubscribeSingle() { - var sm = new MqttClientSubscriptionsManager(new OptionsWrapper(new MqttServerOptions())); + var sm = new MqttClientSubscriptionsManager(new MqttServerOptions(), ""); var sp = new MqttSubscribePacket(); - sp.TopicFilters.Add(new TopicFilter("A/B/C", MqttQualityOfServiceLevel.AtMostOnce)); + sp.TopicFilters.Add(new TopicFilterBuilder().WithTopic("A/B/C").Build()); - sm.Subscribe(sp, ""); + sm.SubscribeAsync(sp).Wait(); - var pp = new MqttPublishPacket + var pp = new MqttApplicationMessage { Topic = "A/B/C", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce }; - Assert.IsTrue(sm.IsSubscribed(pp)); + Assert.IsTrue(sm.CheckSubscriptionsAsync(pp).Result.IsSubscribed); var up = new MqttUnsubscribePacket(); up.TopicFilters.Add("A/B/C"); - sm.Unsubscribe(up); + sm.UnsubscribeAsync(up).Wait(); - Assert.IsFalse(sm.IsSubscribed(pp)); + Assert.IsFalse(sm.CheckSubscriptionsAsync(pp).Result.IsSubscribed); } } } diff --git a/Tests/MQTTnet.Core.Tests/ServiceCollectionTest.cs b/Tests/MQTTnet.Core.Tests/ServiceCollectionTest.cs deleted file mode 100644 index 0117ed08..00000000 --- a/Tests/MQTTnet.Core.Tests/ServiceCollectionTest.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace MQTTnet.Core.Tests -{ - [TestClass] - public class ServiceCollectionTest - { - [TestMethod] - public void TestCanConstructAllServices() - { - var services = new ServiceCollection() - .AddLogging() - .AddMqttServer() - .AddMqttClient(); - - var serviceProvider = services - .BuildServiceProvider(); - - foreach (var service in services) - { - if (service.ServiceType.IsGenericType) - { - continue; - } - serviceProvider.GetRequiredService(service.ServiceType); - } - } - } -} diff --git a/Tests/MQTTnet.Core.Tests/TestClientSessionFactory.cs b/Tests/MQTTnet.Core.Tests/TestClientSessionFactory.cs deleted file mode 100644 index 638df85d..00000000 --- a/Tests/MQTTnet.Core.Tests/TestClientSessionFactory.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using MQTTnet.Core.Server; - -namespace MQTTnet.Core.Tests -{ - public class TestClientSessionFactory : IMqttClientSesssionFactory - { - public MqttClientSession CreateClientSession(string clientId, MqttClientSessionsManager mqttClientSessionsManager) - { - throw new NotImplementedException(); - //return new MqttClientSession(clientId, mqttClientSessionsManager, new TestLogger(), new TestLogger()); - } - } -} diff --git a/Tests/MQTTnet.Core.Tests/TestLogger.cs b/Tests/MQTTnet.Core.Tests/TestLogger.cs deleted file mode 100644 index f57c877c..00000000 --- a/Tests/MQTTnet.Core.Tests/TestLogger.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Microsoft.Extensions.Logging; -using System; - -namespace MQTTnet.Core.Tests -{ - public class TestLogger : IDisposable, ILogger - { - public IDisposable BeginScope(TState state) - { - return this; - } - - public bool IsEnabled(LogLevel logLevel) - { - return true; - } - - public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) - { - } - - public void Dispose() - { - } - } -} diff --git a/Tests/MQTTnet.Core.Tests/TestMqttCommunicationAdapter.cs b/Tests/MQTTnet.Core.Tests/TestMqttCommunicationAdapter.cs index 9e71f8a4..a898f765 100644 --- a/Tests/MQTTnet.Core.Tests/TestMqttCommunicationAdapter.cs +++ b/Tests/MQTTnet.Core.Tests/TestMqttCommunicationAdapter.cs @@ -3,13 +3,13 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using MQTTnet.Core.Adapter; -using MQTTnet.Core.Packets; -using MQTTnet.Core.Serializer; +using MQTTnet.Adapter; +using MQTTnet.Packets; +using MQTTnet.Serializer; namespace MQTTnet.Core.Tests { - public class TestMqttCommunicationAdapter : IMqttCommunicationAdapter + public class TestMqttCommunicationAdapter : IMqttChannelAdapter { private readonly BlockingCollection _incomingPackets = new BlockingCollection(); @@ -17,6 +17,10 @@ namespace MQTTnet.Core.Tests public IMqttPacketSerializer PacketSerializer { get; } = new MqttPacketSerializer(); + public void Dispose() + { + } + public Task ConnectAsync(TimeSpan timeout) { return Task.FromResult(0); @@ -33,7 +37,7 @@ namespace MQTTnet.Core.Tests foreach (var packet in packets) { - Partner.SendPacketInternal(packet); + Partner.EnqueuePacketInternal(packet); } return Task.FromResult(0); @@ -43,10 +47,20 @@ namespace MQTTnet.Core.Tests { ThrowIfPartnerIsNull(); - return Task.Run(() => _incomingPackets.Take(), cancellationToken); + return Task.Run(() => + { + try + { + return _incomingPackets.Take(cancellationToken); + } + catch + { + return null; + } + }, cancellationToken); } - private void SendPacketInternal(MqttBasePacket packet) + private void EnqueuePacketInternal(MqttBasePacket packet) { if (packet == null) throw new ArgumentNullException(nameof(packet)); diff --git a/Tests/MQTTnet.Core.Tests/TestMqttCommunicationAdapterFactory.cs b/Tests/MQTTnet.Core.Tests/TestMqttCommunicationAdapterFactory.cs new file mode 100644 index 00000000..4fe35373 --- /dev/null +++ b/Tests/MQTTnet.Core.Tests/TestMqttCommunicationAdapterFactory.cs @@ -0,0 +1,21 @@ +using MQTTnet.Adapter; +using MQTTnet.Client; +using MQTTnet.Diagnostics; + +namespace MQTTnet.Core.Tests +{ + public class TestMqttCommunicationAdapterFactory : IMqttClientAdapterFactory + { + private readonly IMqttChannelAdapter _adapter; + + public TestMqttCommunicationAdapterFactory(IMqttChannelAdapter adapter) + { + _adapter = adapter; + } + + public IMqttChannelAdapter CreateClientAdapter(IMqttClientOptions options, IMqttNetLogger logger) + { + return _adapter; + } + } +} diff --git a/Tests/MQTTnet.Core.Tests/TestMqttServerAdapter.cs b/Tests/MQTTnet.Core.Tests/TestMqttServerAdapter.cs index 067d08fd..a2fec80f 100644 --- a/Tests/MQTTnet.Core.Tests/TestMqttServerAdapter.cs +++ b/Tests/MQTTnet.Core.Tests/TestMqttServerAdapter.cs @@ -1,23 +1,27 @@ using System; using System.Threading.Tasks; -using MQTTnet.Core.Adapter; -using MQTTnet.Core.Server; -using MQTTnet.Core.Client; +using MQTTnet.Adapter; +using MQTTnet.Client; +using MQTTnet.Diagnostics; +using MQTTnet.Server; namespace MQTTnet.Core.Tests { public class TestMqttServerAdapter : IMqttServerAdapter { public event EventHandler ClientAccepted; - - public async Task ConnectTestClient(IMqttServer server, string clientId, MqttApplicationMessage willMessage = null) + + public async Task ConnectTestClient(IMqttServer server, string clientId, MqttApplicationMessage willMessage = null) { var adapterA = new TestMqttCommunicationAdapter(); var adapterB = new TestMqttCommunicationAdapter(); adapterA.Partner = adapterB; adapterB.Partner = adapterA; - var client = new MqttClient(new MqttCommunicationAdapterFactory(adapterA), new TestLogger(), new MqttPacketDispatcher(new TestLogger())); + var client = new MqttClient( + new TestMqttCommunicationAdapterFactory(adapterA), + new MqttNetLogger()); + var connected = WaitForClientToConnect(server, clientId); FireClientAcceptedEvent(adapterB); @@ -29,8 +33,6 @@ namespace MQTTnet.Core.Tests ChannelOptions = new MqttClientTcpOptions() }; - options.ChannelOptions = new MqttClientTcpOptions(); - await client.ConnectAsync(options); await connected; @@ -55,12 +57,12 @@ namespace MQTTnet.Core.Tests return tcs.Task; } - private void FireClientAcceptedEvent(IMqttCommunicationAdapter adapter) + private void FireClientAcceptedEvent(IMqttChannelAdapter adapter) { ClientAccepted?.Invoke(this, new MqttServerAdapterClientAcceptedEventArgs(adapter)); } - public Task StartAsync(MqttServerOptions options) + public Task StartAsync(IMqttServerOptions options) { return Task.FromResult(0); } diff --git a/Tests/MQTTnet.Core.Tests/TestServerExtensions.cs b/Tests/MQTTnet.Core.Tests/TestServerExtensions.cs new file mode 100644 index 00000000..cbb278ca --- /dev/null +++ b/Tests/MQTTnet.Core.Tests/TestServerExtensions.cs @@ -0,0 +1,38 @@ +using MQTTnet.Client; +using MQTTnet.Server; +using System; +using System.Threading.Tasks; + +namespace MQTTnet.Core.Tests +{ + public static class TestServerExtensions + { + /// + /// 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) + { + var tcs = new TaskCompletionSource(); + + EventHandler handler = (sender, args) => + { + if (args.ApplicationMessage.Topic == message.Topic) + { + tcs.SetResult(true); + } + }; + server.ApplicationMessageReceived += handler; + + try + { + await client.PublishAsync(message).ConfigureAwait(false); + await tcs.Task.ConfigureAwait(false); + } + finally + { + server.ApplicationMessageReceived -= handler; + } + } + } +} diff --git a/Tests/MQTTnet.Core.Tests/TopicFilterComparerTests.cs b/Tests/MQTTnet.Core.Tests/TopicFilterComparerTests.cs index c65dcaba..be169212 100644 --- a/Tests/MQTTnet.Core.Tests/TopicFilterComparerTests.cs +++ b/Tests/MQTTnet.Core.Tests/TopicFilterComparerTests.cs @@ -1,5 +1,5 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; -using MQTTnet.Core.Server; +using MQTTnet.Server; namespace MQTTnet.Core.Tests { diff --git a/Tests/MQTTnet.TestApp.AspNetCore2/MQTTnet.TestApp.AspNetCore2.csproj b/Tests/MQTTnet.TestApp.AspNetCore2/MQTTnet.TestApp.AspNetCore2.csproj index 7e4e0e3d..0a720ca9 100644 --- a/Tests/MQTTnet.TestApp.AspNetCore2/MQTTnet.TestApp.AspNetCore2.csproj +++ b/Tests/MQTTnet.TestApp.AspNetCore2/MQTTnet.TestApp.AspNetCore2.csproj @@ -10,7 +10,7 @@ - + diff --git a/Tests/MQTTnet.TestApp.AspNetCore2/Startup.cs b/Tests/MQTTnet.TestApp.AspNetCore2/Startup.cs index 18d121cf..591cc87d 100644 --- a/Tests/MQTTnet.TestApp.AspNetCore2/Startup.cs +++ b/Tests/MQTTnet.TestApp.AspNetCore2/Startup.cs @@ -5,35 +5,51 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; -using Microsoft.Extensions.Logging; +using MQTTnet; using MQTTnet.AspNetCore; -using MQTTnet.Core; -using MQTTnet.Core.Client; +using MQTTnet.Server; namespace MQTTnet.TestApp.AspNetCore2 { public class Startup { + // In class _Startup_ of the ASP.NET Core 2.0 project. + public void ConfigureServices(IServiceCollection services) { - services.AddHostedMqttServer(); + var mqttServerOptions = new MqttServerOptionsBuilder().Build(); + services.AddHostedMqttServer(mqttServerOptions); } - public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) + // In class _Startup_ of the ASP.NET Core 2.0 project. + public void Configure(IApplicationBuilder app, IHostingEnvironment env) { app.UseMqttEndpoint(); - app.UseMqttServer(async server => + app.UseMqttServer(server => { - var msg = new MqttApplicationMessageBuilder() - .WithPayload("Mqtt is awesome") - .WithTopic("message"); - - while (true) + server.Started += async (sender, args) => { - server.PublishAsync(msg.Build()).Wait(); - await Task.Delay(TimeSpan.FromSeconds(2)); - msg.WithPayload("Mqtt is still awesome at " + DateTime.Now); - } + var msg = new MqttApplicationMessageBuilder() + .WithPayload("Mqtt is awesome") + .WithTopic("message"); + + while (true) + { + try + { + await server.PublishAsync(msg.Build()); + msg.WithPayload("Mqtt is still awesome at " + DateTime.Now); + } + catch (Exception e) + { + Console.WriteLine(e); + } + finally + { + await Task.Delay(TimeSpan.FromSeconds(2)); + } + } + }; }); app.Use((context, next) => @@ -49,11 +65,11 @@ namespace MQTTnet.TestApp.AspNetCore2 app.UseStaticFiles(); - app.UseStaticFiles( new StaticFileOptions() + app.UseStaticFiles(new StaticFileOptions { RequestPath = "/node_modules", - FileProvider = new PhysicalFileProvider( Path.Combine(env.ContentRootPath, "node_modules" ) ) - } ); + FileProvider = new PhysicalFileProvider(Path.Combine(env.ContentRootPath, "node_modules")) + }); } } } diff --git a/Tests/MQTTnet.TestApp.AspNetCore2/package.json b/Tests/MQTTnet.TestApp.AspNetCore2/package.json index ad48ca10..56c9018c 100644 --- a/Tests/MQTTnet.TestApp.AspNetCore2/package.json +++ b/Tests/MQTTnet.TestApp.AspNetCore2/package.json @@ -6,7 +6,7 @@ }, "dependencies": { - "mqtt": "2.13.1", + "mqtt": "2.15.1", "@types/node": "8.0.46", "systemjs": "0.20.19", "typescript": "2.3.4" diff --git a/Tests/MQTTnet.TestApp.NetCore/ClientTest.cs b/Tests/MQTTnet.TestApp.NetCore/ClientTest.cs index df3cd760..dd4a1bc0 100644 --- a/Tests/MQTTnet.TestApp.NetCore/ClientTest.cs +++ b/Tests/MQTTnet.TestApp.NetCore/ClientTest.cs @@ -1,9 +1,8 @@ using System; using System.Text; using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using MQTTnet.Core; -using MQTTnet.Core.Client; +using MQTTnet.Client; +using MQTTnet.Protocol; namespace MQTTnet.TestApp.NetCore { @@ -19,17 +18,17 @@ namespace MQTTnet.TestApp.NetCore CleanSession = true, ChannelOptions = new MqttClientTcpOptions { - Server = "localhost" + //Server = "localhost", + Server = "192.168.1.174" }, - ////ChannelOptions = new MqttClientWebSocketOptions - ////{ - //// Uri = "localhost" - ////} + //ChannelOptions = new MqttClientWebSocketOptions + //{ + // Uri = "ws://localhost:59690/mqtt" + //} }; var factory = new MqttFactory(); - factory.GetLoggerFactory().AddConsole(); - + var client = factory.CreateMqttClient(); client.ApplicationMessageReceived += (s, e) => @@ -81,6 +80,8 @@ namespace MQTTnet.TestApp.NetCore { Console.ReadLine(); + await client.SubscribeAsync(new TopicFilter("test", MqttQualityOfServiceLevel.AtMostOnce)); + var applicationMessage = new MqttApplicationMessageBuilder() .WithTopic("A/B/C") .WithPayload("Hello World") diff --git a/Tests/MQTTnet.TestApp.NetCore/MQTTnet.TestApp.NetCore.csproj b/Tests/MQTTnet.TestApp.NetCore/MQTTnet.TestApp.NetCore.csproj index 4c0f99d2..1784cc0e 100644 --- a/Tests/MQTTnet.TestApp.NetCore/MQTTnet.TestApp.NetCore.csproj +++ b/Tests/MQTTnet.TestApp.NetCore/MQTTnet.TestApp.NetCore.csproj @@ -3,23 +3,19 @@ Exe Full - netcoreapp2.0;net451 + netcoreapp2.0;net452;net461 + + + + RELEASE;NETCOREAPP2_0 - - - - - - - 1.1.2 - diff --git a/Tests/MQTTnet.TestApp.NetCore/ManagedClientTest.cs b/Tests/MQTTnet.TestApp.NetCore/ManagedClientTest.cs index 7f6d430b..e23e9945 100644 --- a/Tests/MQTTnet.TestApp.NetCore/ManagedClientTest.cs +++ b/Tests/MQTTnet.TestApp.NetCore/ManagedClientTest.cs @@ -1,14 +1,11 @@ using System; using System.Threading.Tasks; -using MQTTnet.Core; -using MQTTnet.Core.Client; -using MQTTnet.Core.ManagedClient; -using MQTTnet.Core.Protocol; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using System.IO; using Newtonsoft.Json; using System.Collections.Generic; +using MQTTnet.Client; +using MQTTnet.ManagedClient; +using MQTTnet.Protocol; namespace MQTTnet.TestApp.NetCore { @@ -16,14 +13,6 @@ namespace MQTTnet.TestApp.NetCore { public static async Task RunAsync() { - var services = new ServiceCollection() - .AddMqttClient() - .AddLogging() - .BuildServiceProvider(); - - services.GetService() - .AddConsole(); - var ms = new ClientRetainedMessageHandler(); var options = new ManagedMqttClientOptions @@ -44,7 +33,7 @@ namespace MQTTnet.TestApp.NetCore try { - var managedClient = services.GetRequiredService(); + var managedClient = new MqttFactory().CreateManagedMqttClient(); managedClient.ApplicationMessageReceived += (s, e) => { Console.WriteLine(">> RECEIVED: " + e.ApplicationMessage.Topic); @@ -56,7 +45,8 @@ namespace MQTTnet.TestApp.NetCore await managedClient.StartAsync(options); await managedClient.SubscribeAsync(new TopicFilter("xyz", MqttQualityOfServiceLevel.AtMostOnce)); - + await managedClient.SubscribeAsync(new TopicFilter("abc", MqttQualityOfServiceLevel.AtMostOnce)); + await managedClient.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("Step").WithPayload("3").Build()); Console.WriteLine("Managed client started."); diff --git a/Tests/MQTTnet.TestApp.NetCore/MqttNetConsoleLogger.cs b/Tests/MQTTnet.TestApp.NetCore/MqttNetConsoleLogger.cs new file mode 100644 index 00000000..2713aa3a --- /dev/null +++ b/Tests/MQTTnet.TestApp.NetCore/MqttNetConsoleLogger.cs @@ -0,0 +1,57 @@ +using System; +using System.Text; +using MQTTnet.Diagnostics; + +namespace MQTTnet.TestApp.NetCore +{ + public static class MqttNetConsoleLogger + { + private static readonly object Lock = new object(); + + public static void ForwardToConsole() + { + MqttNetGlobalLogger.LogMessagePublished -= PrintToConsole; + MqttNetGlobalLogger.LogMessagePublished += PrintToConsole; + } + + public static void PrintToConsole(string message, ConsoleColor color) + { + lock (Lock) + { + var backupColor = Console.ForegroundColor; + Console.ForegroundColor = color; + Console.Write(message); + Console.ForegroundColor = backupColor; + } + } + + private static void PrintToConsole(object sender, MqttNetLogMessagePublishedEventArgs e) + { + var output = new StringBuilder(); + output.AppendLine($">> [{e.TraceMessage.Timestamp:O}] [{e.TraceMessage.ThreadId}] [{e.TraceMessage.Source}] [{e.TraceMessage.Level}]: {e.TraceMessage.Message}"); + if (e.TraceMessage.Exception != null) + { + output.AppendLine(e.TraceMessage.Exception.ToString()); + } + + var color = ConsoleColor.Red; + switch (e.TraceMessage.Level) + { + case MqttNetLogLevel.Error: + color = ConsoleColor.Red; + break; + case MqttNetLogLevel.Warning: + color = ConsoleColor.Yellow; + break; + case MqttNetLogLevel.Info: + color = ConsoleColor.Green; + break; + case MqttNetLogLevel.Verbose: + color = ConsoleColor.Gray; + break; + } + + PrintToConsole(output.ToString(), color); + } + } +} \ No newline at end of file diff --git a/Tests/MQTTnet.TestApp.NetCore/PerformanceTest.cs b/Tests/MQTTnet.TestApp.NetCore/PerformanceTest.cs index 6ef76682..5f2e5adc 100644 --- a/Tests/MQTTnet.TestApp.NetCore/PerformanceTest.cs +++ b/Tests/MQTTnet.TestApp.NetCore/PerformanceTest.cs @@ -1,16 +1,12 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using MQTTnet.Core; -using MQTTnet.Core.Client; -using MQTTnet.Core.Protocol; -using MQTTnet.Core.Server; -using System; -using System.Collections.Generic; +using System; using System.Diagnostics; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; +using MQTTnet.Client; +using MQTTnet.Protocol; +using MQTTnet.Server; namespace MQTTnet.TestApp.NetCore { @@ -18,46 +14,21 @@ namespace MQTTnet.TestApp.NetCore { public static async Task RunAsync() { - var services = new ServiceCollection() - .AddMqttServer(options => - { - - options.ConnectionValidator = p => - { - if (p.ClientId == "SpecialClient") - { - if (p.Username != "USER" || p.Password != "PASS") - { - return MqttConnectReturnCode.ConnectionRefusedBadUsernameOrPassword; - } - } - - return MqttConnectReturnCode.ConnectionAccepted; - }; - - options.DefaultCommunicationTimeout = TimeSpan.FromMinutes(10); - }) - .AddMqttClient() - .AddLogging() - .BuildServiceProvider(); - - services.GetService().AddConsole(LogLevel.Warning, true); - Console.WriteLine("Press 'c' for concurrent sends. Otherwise in one batch."); var concurrent = Console.ReadKey(true).KeyChar == 'c'; - var server = Task.Factory.StartNew(() => RunServerAsync(services), TaskCreationOptions.LongRunning); - var client = Task.Factory.StartNew(() => RunClientAsync(2000, TimeSpan.FromMilliseconds(10), services, concurrent), TaskCreationOptions.LongRunning); + var server = Task.Factory.StartNew(async () => await RunServerAsync(), TaskCreationOptions.LongRunning); + var client = Task.Factory.StartNew(async () => await RunClientAsync(2000, TimeSpan.FromMilliseconds(10), concurrent), TaskCreationOptions.LongRunning); await Task.WhenAll(server, client).ConfigureAwait(false); } - private static Task RunClientsAsync(int msgChunkSize, TimeSpan interval, IServiceProvider serviceProvider, bool concurrent) + private static Task RunClientsAsync(int msgChunkSize, TimeSpan interval, bool concurrent) { - return Task.WhenAll(Enumerable.Range(0, 3).Select(i => Task.Run(() => RunClientAsync(msgChunkSize, interval, serviceProvider, concurrent)))); + return Task.WhenAll(Enumerable.Range(0, 3).Select(i => Task.Run(() => RunClientAsync(msgChunkSize, interval, concurrent)))); } - private static async Task RunClientAsync(int msgChunkSize, TimeSpan interval, IServiceProvider serviceProvider, bool concurrent) + private static async Task RunClientAsync(int msgChunkSize, TimeSpan interval, bool concurrent) { try { @@ -69,34 +40,7 @@ namespace MQTTnet.TestApp.NetCore CommunicationTimeout = TimeSpan.FromMinutes(10) }; - var client = serviceProvider.GetRequiredService(); - - client.Connected += async (s, e) => - { - Console.WriteLine("### CONNECTED WITH SERVER ###"); - - await client.SubscribeAsync(new List - { - new TopicFilter("#", MqttQualityOfServiceLevel.AtMostOnce) - }); - - Console.WriteLine("### SUBSCRIBED ###"); - }; - - client.Disconnected += async (s, e) => - { - Console.WriteLine("### DISCONNECTED FROM SERVER ###"); - await Task.Delay(TimeSpan.FromSeconds(5)); - - try - { - await client.ConnectAsync(options); - } - catch - { - Console.WriteLine("### RECONNECTING FAILED ###"); - } - }; + var client = new MqttFactory().CreateMqttClient(); try { @@ -107,11 +51,31 @@ namespace MQTTnet.TestApp.NetCore Console.WriteLine("### CONNECTING FAILED ###" + Environment.NewLine + exception); } - Console.WriteLine("### WAITING FOR APPLICATION MESSAGES ###"); + var message = CreateMessage(); + var messages = new[] { message }; + + var stopwatch = Stopwatch.StartNew(); + + var sentMessagesCount = 0; + while (stopwatch.ElapsedMilliseconds < 1000) + { + await client.PublishAsync(messages).ConfigureAwait(false); + sentMessagesCount++; + } + + Console.WriteLine($"Sending {sentMessagesCount} messages per second. #1"); + + sentMessagesCount = 0; + stopwatch.Restart(); + while (stopwatch.ElapsedMilliseconds < 1000) + { + await client.PublishAsync(messages).ConfigureAwait(false); + sentMessagesCount++; + } + + Console.WriteLine($"Sending {sentMessagesCount} messages per second. #2"); var testMessageCount = 10000; - var message = CreateMessage(); - var stopwatch = Stopwatch.StartNew(); for (var i = 0; i < testMessageCount; i++) { await client.PublishAsync(message); @@ -120,16 +84,6 @@ namespace MQTTnet.TestApp.NetCore stopwatch.Stop(); Console.WriteLine($"Sent 10.000 messages within {stopwatch.ElapsedMilliseconds} ms ({stopwatch.ElapsedMilliseconds / (float)testMessageCount} ms / message)."); - stopwatch.Restart(); - var sentMessagesCount = 0; - while (stopwatch.ElapsedMilliseconds < 1000) - { - await client.PublishAsync(message); - sentMessagesCount++; - } - - Console.WriteLine($"Sending {sentMessagesCount} messages per second."); - var last = DateTime.Now; var msgCount = 0; @@ -154,9 +108,7 @@ namespace MQTTnet.TestApp.NetCore msgCount += msgs.Count; //send multiple } - - - + var now = DateTime.Now; if (last < now - TimeSpan.FromSeconds(1)) { @@ -180,7 +132,7 @@ namespace MQTTnet.TestApp.NetCore { Topic = "A/B/C", Payload = Encoding.UTF8.GetBytes("Hello World"), - QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce + QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce }; } @@ -190,24 +142,25 @@ namespace MQTTnet.TestApp.NetCore return Task.Run(() => client.PublishAsync(applicationMessage)); } - private static async Task RunServerAsync(IServiceProvider serviceProvider) + private static async Task RunServerAsync() { try { - var mqttServer = serviceProvider.GetRequiredService(); - var msgs = 0; - var stopwatch = Stopwatch.StartNew(); - mqttServer.ApplicationMessageReceived += (sender, args) => - { - msgs++; - if (stopwatch.ElapsedMilliseconds > 1000) - { - Console.WriteLine($"received {msgs}"); - msgs = 0; - stopwatch.Restart(); - } - }; - await mqttServer.StartAsync(); + var mqttServer = new MqttFactory().CreateMqttServer(); + + ////var msgs = 0; + ////var stopwatch = Stopwatch.StartNew(); + ////mqttServer.ApplicationMessageReceived += (sender, args) => + ////{ + //// msgs++; + //// if (stopwatch.ElapsedMilliseconds > 1000) + //// { + //// Console.WriteLine($"received {msgs}"); + //// msgs = 0; + //// stopwatch.Restart(); + //// } + ////}; + await mqttServer.StartAsync(new MqttServerOptions()); Console.WriteLine("Press any key to exit."); Console.ReadLine(); diff --git a/Tests/MQTTnet.TestApp.NetCore/Program.cs b/Tests/MQTTnet.TestApp.NetCore/Program.cs index 2e1d8c90..77b9ea6e 100644 --- a/Tests/MQTTnet.TestApp.NetCore/Program.cs +++ b/Tests/MQTTnet.TestApp.NetCore/Program.cs @@ -1,13 +1,12 @@ -using MQTTnet.Core; -using MQTTnet.Core.Client; -using MQTTnet.Core.Server; -using System; +using System; using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; +using MQTTnet.Client; +using MQTTnet.Diagnostics; +using MQTTnet.Server; using Newtonsoft.Json; -using Microsoft.Extensions.Logging; namespace MQTTnet.TestApp.NetCore { @@ -15,7 +14,7 @@ namespace MQTTnet.TestApp.NetCore { public static void Main() { - Console.WriteLine("MQTTnet - TestApp.NetFramework"); + Console.WriteLine($"MQTTnet - TestApp.{TargetFrameworkInfoProvider.TargetFramework}"); Console.WriteLine("1 = Start client"); Console.WriteLine("2 = Start server"); Console.WriteLine("3 = Start performance test"); @@ -41,7 +40,7 @@ namespace MQTTnet.TestApp.NetCore Thread.Sleep(Timeout.Infinite); } - + // This code is used at the Wiki on GitHub! // ReSharper disable once UnusedMember.Local private static async void WikiCode() @@ -70,10 +69,28 @@ namespace MQTTnet.TestApp.NetCore { var factory = new MqttFactory(); - factory.GetLoggerFactory().AddConsole(); - var client = factory.CreateMqttClient(); } + + { + // Write all trace messages to the console window. + MqttNetGlobalLogger.LogMessagePublished += (s, e) => + { + var trace = $">> [{e.TraceMessage.Timestamp:O}] [{e.TraceMessage.ThreadId}] [{e.TraceMessage.Source}] [{e.TraceMessage.Level}]: {e.TraceMessage.Message}"; + if (e.TraceMessage.Exception != null) + { + trace += Environment.NewLine + e.TraceMessage.Exception.ToString(); + } + + Console.WriteLine(trace); + }; + } + + { + // Use a custom log ID for the logger. + var factory = new MqttFactory(); + var mqttClient = factory.CreateMqttClient(new MqttNetLogger("MyCustomId")); + } } } @@ -83,6 +100,12 @@ namespace MQTTnet.TestApp.NetCore public Task SaveRetainedMessagesAsync(IList messages) { + var directory = Path.GetDirectoryName(Filename); + if (!Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + File.WriteAllText(Filename, JsonConvert.SerializeObject(messages)); return Task.FromResult(0); } @@ -103,4 +126,4 @@ namespace MQTTnet.TestApp.NetCore return Task.FromResult(retainedMessages); } } -} +} \ No newline at end of file diff --git a/Tests/MQTTnet.TestApp.NetCore/ServerTest.cs b/Tests/MQTTnet.TestApp.NetCore/ServerTest.cs index 0a7d5575..80a8e5a4 100644 --- a/Tests/MQTTnet.TestApp.NetCore/ServerTest.cs +++ b/Tests/MQTTnet.TestApp.NetCore/ServerTest.cs @@ -1,53 +1,66 @@ using System; using System.Text; using System.Threading.Tasks; -using MQTTnet.Core.Protocol; -using MQTTnet.Core.Server; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; +using MQTTnet.Protocol; +using MQTTnet.Server; namespace MQTTnet.TestApp.NetCore { public static class ServerTest { - public static Task RunAsync() + public static async Task RunAsync() { try { - var services = new ServiceCollection() - .AddMqttServer() - .AddLogging(); + MqttNetConsoleLogger.ForwardToConsole(); - services.Configure(options => + var options = new MqttServerOptions { - options.ConnectionValidator = p => + ConnectionValidator = p => { if (p.ClientId == "SpecialClient") { if (p.Username != "USER" || p.Password != "PASS") { - return MqttConnectReturnCode.ConnectionRefusedBadUsernameOrPassword; + p.ReturnCode = MqttConnectReturnCode.ConnectionRefusedBadUsernameOrPassword; } } + }, - return MqttConnectReturnCode.ConnectionAccepted; - }; + Storage = new RetainedMessageHandler(), - options.Storage = new RetainedMessageHandler(); - - options.ApplicationMessageInterceptor = context => + ApplicationMessageInterceptor = context => { if (MqttTopicFilterComparer.IsMatch(context.ApplicationMessage.Topic, "/myTopic/WithTimestamp/#")) { // Replace the payload with the timestamp. But also extending a JSON // based payload with the timestamp is a suitable use case. context.ApplicationMessage.Payload = Encoding.UTF8.GetBytes(DateTime.Now.ToString("O")); - } - }; - }); + } - var serviceProvider = services.BuildServiceProvider(); - serviceProvider.GetRequiredService().AddConsole(); + if (context.ApplicationMessage.Topic == "not_allowed_topic") + { + context.AcceptPublish = false; + context.CloseConnection = true; + } + }, + SubscriptionInterceptor = context => + { + if (context.TopicFilter.Topic.StartsWith("admin/foo/bar") && context.ClientId != "theAdmin") + { + context.AcceptSubscription = false; + } + + if (context.TopicFilter.Topic.StartsWith("the/secret/stuff") && context.ClientId != "Imperator") + { + context.AcceptSubscription = false; + context.CloseConnection = true; + } + } + }; + + // Extend the timestamp for all messages from clients. + // Protect several topics from being subscribed from every client. //var certificate = new X509Certificate(@"C:\certs\test\test.cer", ""); //options.TlsEndpointOptions.Certificate = certificate.Export(X509ContentType.Cert); @@ -55,18 +68,48 @@ namespace MQTTnet.TestApp.NetCore //options.DefaultEndpointOptions.IsEnabled = true; //options.TlsEndpointOptions.IsEnabled = false; - var mqttServer = new MqttFactory(serviceProvider).CreateMqttServer(); + var mqttServer = new MqttFactory().CreateMqttServer(); + + mqttServer.ApplicationMessageReceived += (s, e) => + { + MqttNetConsoleLogger.PrintToConsole( + $"'{e.ClientId}' reported '{e.ApplicationMessage.Topic}' > '{Encoding.UTF8.GetString(e.ApplicationMessage.Payload ?? new byte[0])}'", + ConsoleColor.Magenta); + }; + + //options.ApplicationMessageInterceptor = c => + //{ + // if (c.ApplicationMessage.Payload == null || c.ApplicationMessage.Payload.Length == 0) + // { + // return; + // } + + // try + // { + // var content = JObject.Parse(Encoding.UTF8.GetString(c.ApplicationMessage.Payload)); + // var timestampProperty = content.Property("timestamp"); + // if (timestampProperty != null && timestampProperty.Value.Type == JTokenType.Null) + // { + // timestampProperty.Value = DateTime.Now.ToString("O"); + // c.ApplicationMessage.Payload = Encoding.UTF8.GetBytes(content.ToString()); + // } + // } + // catch (Exception) + // { + // } + //}; + mqttServer.ClientDisconnected += (s, e) => { Console.Write("Client disconnected event fired."); }; - mqttServer.StartAsync(); + await mqttServer.StartAsync(options); Console.WriteLine("Press any key to exit."); Console.ReadLine(); - mqttServer.StopAsync(); + await mqttServer.StopAsync(); } catch (Exception e) { @@ -74,7 +117,6 @@ namespace MQTTnet.TestApp.NetCore } Console.ReadLine(); - return Task.FromResult(0); } } } diff --git a/Tests/MQTTnet.TestApp.UniversalWindows/JsonServerStorage.cs b/Tests/MQTTnet.TestApp.UniversalWindows/JsonServerStorage.cs new file mode 100644 index 00000000..fbf5b66d --- /dev/null +++ b/Tests/MQTTnet.TestApp.UniversalWindows/JsonServerStorage.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using MQTTnet.Server; +using Newtonsoft.Json; + +namespace MQTTnet.TestApp.UniversalWindows +{ + public class JsonServerStorage : IMqttServerStorage + { + private readonly string _filename = Path.Combine(Windows.Storage.ApplicationData.Current.LocalFolder.Path, "Retained.json"); + + public async Task SaveRetainedMessagesAsync(IList messages) + { + await Task.CompletedTask; + + var json = JsonConvert.SerializeObject(messages); + File.WriteAllText(_filename, json); + } + + public async Task> LoadRetainedMessagesAsync() + { + await Task.CompletedTask; + + if (!File.Exists(_filename)) + { + return new List(); + } + + try + { + var json = File.ReadAllText(_filename); + return JsonConvert.DeserializeObject>(json); + } + catch + { + return new List(); + } + } + + public void Clear() + { + if (File.Exists(_filename)) + { + File.Delete(_filename); + } + } + } +} diff --git a/Tests/MQTTnet.TestApp.UniversalWindows/MQTTnet.TestApp.UniversalWindows.csproj b/Tests/MQTTnet.TestApp.UniversalWindows/MQTTnet.TestApp.UniversalWindows.csproj index 42b16304..b5d2dbd9 100644 --- a/Tests/MQTTnet.TestApp.UniversalWindows/MQTTnet.TestApp.UniversalWindows.csproj +++ b/Tests/MQTTnet.TestApp.UniversalWindows/MQTTnet.TestApp.UniversalWindows.csproj @@ -11,14 +11,14 @@ MQTTnet.TestApp.UniversalWindows en-US UAP - 10.0.15063.0 - 10.0.15063.0 + 10.0.16299.0 + 10.0.16299.0 14 512 {A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} true MQTTnet.TestApp.UniversalWindows_TemporaryKey.pfx - FA56408F253A1F7FCAC649611411B6B9F42BC4F9 + 05F70CAF1426E296BE2F7396F0B654070D72B930 win10;win10-arm;win10-arm-aot;win10-x86;win10-x86-aot;win10-x64;win10-x64-aot @@ -94,6 +94,7 @@ App.xaml + MainPage.xaml @@ -126,21 +127,21 @@ + + {c444e9c8-95fa-430e-9126-274129de16cd} + MQTTnet.Extensions.Rpc + {3587e506-55a2-4eb3-99c7-dc01e42d25d2} MQTTnet.NetStandard - - {2ecb99e4-72d0-4c23-99ba-93d511d3967d} - MQTTnet.Core - - 5.4.0 + 6.0.8 - 2.0.0 + 2.2.0 @@ -166,7 +167,7 @@ ;2008 true pdbonly - AnyCPU + x86 false prompt MinimumRecommendedRules.ruleset diff --git a/Tests/MQTTnet.TestApp.UniversalWindows/MQTTnet.TestApp.UniversalWindows_TemporaryKey.pfx b/Tests/MQTTnet.TestApp.UniversalWindows/MQTTnet.TestApp.UniversalWindows_TemporaryKey.pfx index adf1e2aa..1adc8878 100644 Binary files a/Tests/MQTTnet.TestApp.UniversalWindows/MQTTnet.TestApp.UniversalWindows_TemporaryKey.pfx and b/Tests/MQTTnet.TestApp.UniversalWindows/MQTTnet.TestApp.UniversalWindows_TemporaryKey.pfx differ diff --git a/Tests/MQTTnet.TestApp.UniversalWindows/MainPage.xaml b/Tests/MQTTnet.TestApp.UniversalWindows/MainPage.xaml index f0031b8b..3698613f 100644 --- a/Tests/MQTTnet.TestApp.UniversalWindows/MainPage.xaml +++ b/Tests/MQTTnet.TestApp.UniversalWindows/MainPage.xaml @@ -9,25 +9,29 @@ mc:Ignorable="d"> - - - + Server: + Port: + User: Password: ClientId: - + Clean session: + + Keep alive interval: + + TCP WS @@ -65,6 +69,40 @@ + + + Method: + + + Payload: + + + Text + Base64 + + + QoS: + + 0 (At most once) + 1 (At least once) + 2 (Exactly once) + + + Responses: + + + + + + + + + + + + + + Topic: @@ -77,9 +115,20 @@ 2 (Exactly once) + Received messages: + Add received messages to list + + + + + + + + - + + @@ -88,21 +137,25 @@ Port: + Persist retained messages in JSON format + Clear previously retained messages on startup + + + + + + - + - - - - diff --git a/Tests/MQTTnet.TestApp.UniversalWindows/MainPage.xaml.cs b/Tests/MQTTnet.TestApp.UniversalWindows/MainPage.xaml.cs index 76c0d073..da0c2f31 100644 --- a/Tests/MQTTnet.TestApp.UniversalWindows/MainPage.xaml.cs +++ b/Tests/MQTTnet.TestApp.UniversalWindows/MainPage.xaml.cs @@ -1,21 +1,27 @@ using System; +using System.Collections.Concurrent; using System.Text; using System.Threading.Tasks; using Windows.Security.Cryptography.Certificates; using Windows.UI.Core; using Windows.UI.Xaml; -using Microsoft.Extensions.DependencyInjection; -using MQTTnet.Core; -using MQTTnet.Core.Client; -using MQTTnet.Core.Diagnostics; -using MQTTnet.Core.Protocol; -using MQTTnet.Core.Server; +using MQTTnet.Client; +using MQTTnet.Diagnostics; +using MQTTnet.Exceptions; +using MQTTnet.Extensions.Rpc; using MQTTnet.Implementations; +using MQTTnet.ManagedClient; +using MQTTnet.Protocol; +using MQTTnet.Server; +using MqttClientConnectedEventArgs = MQTTnet.Client.MqttClientConnectedEventArgs; +using MqttClientDisconnectedEventArgs = MQTTnet.Client.MqttClientDisconnectedEventArgs; namespace MQTTnet.TestApp.UniversalWindows { public sealed partial class MainPage { + private readonly ConcurrentQueue _traceMessages = new ConcurrentQueue(); + private IMqttClient _mqttClient; private IMqttServer _mqttServer; @@ -23,20 +29,43 @@ namespace MQTTnet.TestApp.UniversalWindows { InitializeComponent(); - MqttNetTrace.TraceMessagePublished += OnTraceMessagePublished; + MqttNetGlobalLogger.LogMessagePublished += OnTraceMessagePublished; } - private async void OnTraceMessagePublished(object sender, MqttNetTraceMessagePublishedEventArgs e) + private async void OnTraceMessagePublished(object sender, MqttNetLogMessagePublishedEventArgs e) { - await Trace.Dispatcher.RunAsync(CoreDispatcherPriority.High, () => - { - var text = $"[{e.TraceMessage.Timestamp:yyyy-MM-dd HH:mm:ss.fff}] [{e.TraceMessage.Level}] [{e.TraceMessage.Source}] [{e.TraceMessage.ThreadId}] [{e.TraceMessage.Message}]{Environment.NewLine}"; - if (e.TraceMessage.Exception != null) - { - text += $"{e.TraceMessage.Exception}{Environment.NewLine}"; - } + _traceMessages.Enqueue(e.TraceMessage); + await UpdateLogAsync(); + } - Trace.Text += text; + private async Task UpdateLogAsync() + { + while (_traceMessages.Count > 100) + { + _traceMessages.TryDequeue(out _); + } + + var logText = new StringBuilder(); + foreach (var traceMessage in _traceMessages) + { + logText.AppendFormat( + "[{0:yyyy-MM-dd HH:mm:ss.fff}] [{1}] [{2}] [{3}] [{4}]{5}", + traceMessage.Timestamp, + traceMessage.Level, + traceMessage.Source, + traceMessage.ThreadId, + traceMessage.Message, + Environment.NewLine); + + if (traceMessage.Exception != null) + { + logText.AppendLine(traceMessage.Exception.ToString()); + } + } + + await Trace.Dispatcher.RunAsync(CoreDispatcherPriority.Low, () => + { + Trace.Text = logText.ToString(); }); } @@ -57,6 +86,7 @@ namespace MQTTnet.TestApp.UniversalWindows options.ChannelOptions = new MqttClientTcpOptions { Server = Server.Text, + Port = int.Parse(Port.Text), TlsOptions = tlsOptions }; } @@ -81,15 +111,25 @@ namespace MQTTnet.TestApp.UniversalWindows Password = Password.Text }; + options.CleanSession = CleanSession.IsChecked == true; + options.KeepAlivePeriod = TimeSpan.FromSeconds(double.Parse(KeepAliveInterval.Text)); + try { if (_mqttClient != null) { await _mqttClient.DisconnectAsync(); + _mqttClient.ApplicationMessageReceived -= OnApplicationMessageReceived; + _mqttClient.Connected -= OnConnected; + _mqttClient.Disconnected -= OnDisconnected; } var factory = new MqttFactory(); _mqttClient = factory.CreateMqttClient(); + _mqttClient.ApplicationMessageReceived += OnApplicationMessageReceived; + _mqttClient.Connected += OnConnected; + _mqttClient.Disconnected += OnDisconnected; + await _mqttClient.ConnectAsync(options); } catch (Exception exception) @@ -98,6 +138,35 @@ namespace MQTTnet.TestApp.UniversalWindows } } + private void OnDisconnected(object sender, MqttClientDisconnectedEventArgs e) + { + _traceMessages.Enqueue(new MqttNetLogMessage("", DateTime.Now, -1, + "", MqttNetLogLevel.Info, "! DISCONNECTED EVENT FIRED", null)); + + Task.Run(UpdateLogAsync); + } + + private void OnConnected(object sender, MqttClientConnectedEventArgs e) + { + _traceMessages.Enqueue(new MqttNetLogMessage("", DateTime.Now, -1, + "", MqttNetLogLevel.Info, "! CONNECTED EVENT FIRED", null)); + + Task.Run(UpdateLogAsync); + } + + private async void OnApplicationMessageReceived(object sender, MqttApplicationMessageReceivedEventArgs eventArgs) + { + var item = $"Timestamp: {DateTime.Now:O} | Topic: {eventArgs.ApplicationMessage.Topic} | Payload: {Encoding.UTF8.GetString(eventArgs.ApplicationMessage.Payload)} | QoS: {eventArgs.ApplicationMessage.QualityOfServiceLevel}"; + + await Dispatcher.RunAsync(CoreDispatcherPriority.Low, () => + { + if (AddReceivedMessagesToList.IsChecked == true) + { + ReceivedMessages.Items.Add(item); + } + }); + } + private async void Publish(object sender, RoutedEventArgs e) { if (_mqttClient == null) @@ -156,8 +225,13 @@ namespace MQTTnet.TestApp.UniversalWindows } } - private void Clear(object sender, RoutedEventArgs e) + private void ClearLog(object sender, RoutedEventArgs e) { + while (_traceMessages.Count > 0) + { + _traceMessages.TryDequeue(out _); + } + Trace.Text = string.Empty; } @@ -208,27 +282,155 @@ namespace MQTTnet.TestApp.UniversalWindows // This code is for the Wiki at GitHub! // ReSharper disable once UnusedMember.Local + + private async void StartServer(object sender, RoutedEventArgs e) + { + if (_mqttServer != null) + { + return; + } + + JsonServerStorage storage = null; + if (ServerPersistRetainedMessages.IsChecked == true) + { + storage = new JsonServerStorage(); + + if (ServerClearRetainedMessages.IsChecked == true) + { + storage.Clear(); + } + } + + _mqttServer = new MqttFactory().CreateMqttServer(); + + var options = new MqttServerOptions(); + options.DefaultEndpointOptions.Port = int.Parse(ServerPort.Text); + options.Storage = storage; + + await _mqttServer.StartAsync(options); + } + + private async void StopServer(object sender, RoutedEventArgs e) + { + if (_mqttServer == null) + { + return; + } + + await _mqttServer.StopAsync(); + _mqttServer = null; + } + + private void ClearReceivedMessages(object sender, RoutedEventArgs e) + { + ReceivedMessages.Items.Clear(); + } + + private async void ExecuteRpc(object sender, RoutedEventArgs e) + { + var qos = MqttQualityOfServiceLevel.AtMostOnce; + if (RpcQoS1.IsChecked == true) + { + qos = MqttQualityOfServiceLevel.AtLeastOnce; + } + + if (RpcQoS2.IsChecked == true) + { + qos = MqttQualityOfServiceLevel.ExactlyOnce; + } + + var payload = new byte[0]; + if (RpcText.IsChecked == true) + { + payload = Encoding.UTF8.GetBytes(RpcPayload.Text); + } + + if (RpcBase64.IsChecked == true) + { + payload = Convert.FromBase64String(RpcPayload.Text); + } + + + try + { + var rpcClient = new MqttRpcClient(_mqttClient); + await rpcClient.EnableAsync(); + var response = await rpcClient.ExecuteAsync(TimeSpan.FromSeconds(5), RpcMethod.Text, payload, qos); + await rpcClient.DisableAsync(); + + RpcResponses.Items.Add(RpcMethod.Text + " >>> " + Encoding.UTF8.GetString(response)); + } + catch (MqttCommunicationTimedOutException) + { + RpcResponses.Items.Add(RpcMethod.Text + " >>> [TIMEOUT]"); + } + } + + private void ClearRpcResponses(object sender, RoutedEventArgs e) + { + RpcResponses.Items.Clear(); + } + private async Task WikiCode() { { - // Create a new MQTT client - var services = new ServiceCollection() - .AddMqttClient() - .BuildServiceProvider(); + // Write all trace messages to the console window. + MqttNetGlobalLogger.LogMessagePublished += (s, e) => + { + Console.WriteLine($">> [{e.TraceMessage.Timestamp:O}] [{e.TraceMessage.ThreadId}] [{e.TraceMessage.Source}] [{e.TraceMessage.Level}]: {e.TraceMessage.Message}"); + if (e.TraceMessage.Exception != null) + { + Console.WriteLine(e.TraceMessage.Exception); + } + }; + } - var factory = new MqttFactory(services); - var client = factory.CreateMqttClient(); + { + // Use a custom identifier for the trace messages. + var clientOptions = new MqttClientOptionsBuilder() + .Build(); + } + + { + // Create a new MQTT client. + var factory = new MqttFactory(); + var mqttClient = factory.CreateMqttClient(); { - // Create TCP based options using the builder + // Create TCP based options using the builder. var options = new MqttClientOptionsBuilder() .WithClientId("Client1") .WithTcpServer("broker.hivemq.com") .WithCredentials("bud", "%spencer%") .WithTls() + .WithCleanSession() .Build(); - await client.ConnectAsync(options); + await mqttClient.ConnectAsync(options); + } + + { + // Use TCP connection. + var options = new MqttClientOptionsBuilder() + .WithTcpServer("broker.hivemq.com", 1883) // Port is optional + .Build(); + } + + { + // Use secure TCP connection. + var options = new MqttClientOptionsBuilder() + .WithTcpServer("broker.hivemq.com") + .WithTls() + .Build(); + } + + { + // Use WebSocket connection. + var options = new MqttClientOptionsBuilder() + .WithWebSocketServer("broker.hivemq.com:8000/mqtt") + .Build(); + + await mqttClient.ConnectAsync(options); } { @@ -254,10 +456,10 @@ namespace MQTTnet.TestApp.UniversalWindows { // Subscribe to a topic - await client.SubscribeAsync(new TopicFilterBuilder().WithTopic("my/topic").Build()); + await mqttClient.SubscribeAsync(new TopicFilterBuilder().WithTopic("my/topic").Build()); // Unsubscribe from a topic - await client.UnsubscribeAsync("my/topic"); + await mqttClient.UnsubscribeAsync("my/topic"); // Publish an application message var applicationMessage = new MqttApplicationMessageBuilder() @@ -266,40 +468,40 @@ namespace MQTTnet.TestApp.UniversalWindows .WithAtLeastOnceQoS() .Build(); - await client.PublishAsync(applicationMessage); + await mqttClient.PublishAsync(applicationMessage); } } // ---------------------------------- { - var services = new ServiceCollection() - .AddMqttServer(options => + var options = new MqttServerOptions(); + + options.ConnectionValidator = c => + { + if (c.ClientId.Length < 10) { - options.ConnectionValidator = c => - { - if (c.ClientId.Length < 10) - { - return MqttConnectReturnCode.ConnectionRefusedIdentifierRejected; - } + c.ReturnCode = MqttConnectReturnCode.ConnectionRefusedIdentifierRejected; + return; + } - if (c.Username != "mySecretUser") - { - return MqttConnectReturnCode.ConnectionRefusedBadUsernameOrPassword; - } + if (c.Username != "mySecretUser") + { + c.ReturnCode = MqttConnectReturnCode.ConnectionRefusedBadUsernameOrPassword; + return; + } - if (c.Password != "mySecretPassword") - { - return MqttConnectReturnCode.ConnectionRefusedBadUsernameOrPassword; - } + if (c.Password != "mySecretPassword") + { + c.ReturnCode = MqttConnectReturnCode.ConnectionRefusedBadUsernameOrPassword; + return; + } - return MqttConnectReturnCode.ConnectionAccepted; - }; - }) - .BuildServiceProvider(); + c.ReturnCode = MqttConnectReturnCode.ConnectionAccepted; + }; - var factory = new MqttFactory(services); + var factory = new MqttFactory(); var mqttServer = factory.CreateMqttServer(); - await mqttServer.StartAsync(); + await mqttServer.StartAsync(options); Console.WriteLine("Press any key to exit."); Console.ReadLine(); @@ -320,32 +522,89 @@ namespace MQTTnet.TestApp.UniversalWindows return new ChainValidationResult[0]; }; - } - - private async void StartServer(object sender, RoutedEventArgs e) - { - if (_mqttServer != null) { - return; + // Start a MQTT server. + var mqttServer = new MqttFactory().CreateMqttServer(); + await mqttServer.StartAsync(new MqttServerOptions()); + Console.WriteLine("Press any key to exit."); + Console.ReadLine(); + await mqttServer.StopAsync(); } - _mqttServer = new MqttFactory().CreateMqttServer(o => { - o.DefaultEndpointOptions.Port = int.Parse(ServerPort.Text); - }); + // Configure MQTT server. + var optionsBuilder = new MqttServerOptionsBuilder() + .WithConnectionBacklog(100) + .WithDefaultEndpointPort(1884); - await _mqttServer.StartAsync(); - } + var options = new MqttServerOptions + { + }; - private async void StopServer(object sender, RoutedEventArgs e) - { - if (_mqttServer == null) - { - return; + options.ConnectionValidator = c => + { + if (c.ClientId != "Highlander") + { + c.ReturnCode = MqttConnectReturnCode.ConnectionRefusedIdentifierRejected; + return; + } + + c.ReturnCode = MqttConnectReturnCode.ConnectionAccepted; + }; + + var mqttServer = new MqttFactory().CreateMqttServer(); + await mqttServer.StartAsync(optionsBuilder.Build()); + } + + { + // Setup client validator. + var options = new MqttServerOptions + { + ConnectionValidator = c => + { + if (c.ClientId.Length < 10) + { + c.ReturnCode = MqttConnectReturnCode.ConnectionRefusedIdentifierRejected; + return; + } + + if (c.Username != "mySecretUser") + { + c.ReturnCode = MqttConnectReturnCode.ConnectionRefusedBadUsernameOrPassword; + return; + } + + if (c.Password != "mySecretPassword") + { + c.ReturnCode = MqttConnectReturnCode.ConnectionRefusedBadUsernameOrPassword; + return; + } + + c.ReturnCode = MqttConnectReturnCode.ConnectionAccepted; + } + }; + } + + { + // Create a new MQTT server. + var mqttServer = new MqttFactory().CreateMqttServer(); + } + + { + // Setup and start a managed MQTT client. + var options = new ManagedMqttClientOptionsBuilder() + .WithAutoReconnectDelay(TimeSpan.FromSeconds(5)) + .WithClientOptions(new MqttClientOptionsBuilder() + .WithClientId("Client1") + .WithTcpServer("broker.hivemq.com") + .WithTls().Build()) + .Build(); + + var mqttClient = new MqttFactory().CreateManagedMqttClient(); + await mqttClient.SubscribeAsync(new TopicFilterBuilder().WithTopic("my/topic").Build()); + await mqttClient.StartAsync(options); } - await _mqttServer.StopAsync(); - _mqttServer = null; } } } diff --git a/Tests/MQTTnet.TestApp.UniversalWindows/Package.appxmanifest b/Tests/MQTTnet.TestApp.UniversalWindows/Package.appxmanifest index a3094c70..b677c2aa 100644 --- a/Tests/MQTTnet.TestApp.UniversalWindows/Package.appxmanifest +++ b/Tests/MQTTnet.TestApp.UniversalWindows/Package.appxmanifest @@ -1,10 +1,10 @@  - + MQTTnet.TestApp.UniversalWindows - chris + test Assets\StoreLogo.png diff --git a/_config.yml b/_config.yml new file mode 100644 index 00000000..cc35c1df --- /dev/null +++ b/_config.yml @@ -0,0 +1 @@ +theme: jekyll-theme-modernist \ No newline at end of file diff --git a/appveyor.yml b/appveyor.yml index 97816dd1..253e07d9 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,5 +1,6 @@ version: 1.0.{build} image: Visual Studio 2017 +configuration: Release before_build: - cmd: >- msbuild /t:restore @@ -12,4 +13,4 @@ build: project: MQTTnet.sln verbosity: minimal test_script: -- cmd: vstest.console /logger:Appveyor Tests/MQTTnet.Core.Tests/bin/Release/net461/MQTTnet.Core.Tests.dll +- cmd: dotnet vstest "%APPVEYOR_BUILD_FOLDER%\Tests\MQTTnet.Core.Tests\bin\Release\netcoreapp2.0\MQTTnet.Core.Tests.dll"