diff --git a/Build/MQTTnet.AspNetCore.nuspec b/Build/MQTTnet.AspNetCore.nuspec index 4245077b..c9936f35 100644 --- a/Build/MQTTnet.AspNetCore.nuspec +++ b/Build/MQTTnet.AspNetCore.nuspec @@ -2,7 +2,7 @@ MQTTnet.AspNetCore - 2.7.5 + 0.0.0 Christian Kratky Christian Kratky https://github.com/chkr1011/MQTTnet/blob/master/LICENSE @@ -10,19 +10,20 @@ https://raw.githubusercontent.com/chkr1011/MQTTnet/master/Images/Logo_128x128.png false This is a support library to integrate MQTTnet into AspNetCore. - * Updated to MQTTnet 2.7.5. + * Updated to MQTTnet 2.8.0. 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 + 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.Extensions.ManagedClient.nuspec b/Build/MQTTnet.Extensions.ManagedClient.nuspec new file mode 100644 index 00000000..ba5333f2 --- /dev/null +++ b/Build/MQTTnet.Extensions.ManagedClient.nuspec @@ -0,0 +1,38 @@ + + + + MQTTnet.Extensions.ManagedClient + 0.0.0 + 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 an extension library which provides a managed MQTT client with additional features using MQTTnet. + * Updated to MQTTnet 2.8.0. + + 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/MQTTnet.Extensions.Rpc.nuspec b/Build/MQTTnet.Extensions.Rpc.nuspec index cb31c21b..0df8e7e6 100644 --- a/Build/MQTTnet.Extensions.Rpc.nuspec +++ b/Build/MQTTnet.Extensions.Rpc.nuspec @@ -2,46 +2,37 @@ MQTTnet.Extensions.Rpc - 2.7.5 + 0.0.0 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. + This is an extension library which allows executing synchronous device calls including a response using MQTTnet. + * Updated to MQTTnet 2.8.0. - Copyright Christian Kratky 2016-2017 + 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/MQTTnet.nuspec b/Build/MQTTnet.nuspec index 764a36c5..5cdf3d52 100644 --- a/Build/MQTTnet.nuspec +++ b/Build/MQTTnet.nuspec @@ -2,7 +2,7 @@ MQTTnet - 2.8.0 + 0.0.0 Christian Kratky Christian Kratky https://github.com/chkr1011/MQTTnet/blob/master/LICENSE @@ -10,55 +10,73 @@ 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). - * [Client] Received messages are now processed in the worker thread by default. Added a new setting for switching back to dedicated threads. + ** MQTTnet is now available at Open Collective for donations (https://opencollective.com/mqttnet). ** +* [Core] Performance optimizations. +* [Core] Due to performance reasons the timestamp of log messages is now in UTC format. +* [Core] Added several packet validations. +* [Core] Log messages now contain the complete source path including parent components. +* [Core] The adapter now has an _Endpoint_ definition as string containing remote IP and port. +* [Client] Received messages are now processed completely in the worker thread without creating new Tasks. +* [Client] Fixed wrong calculation for sending keep alive packets (thanks to @cstichlberger) +* [Client] A clean disconnect (via DisconnectAsync) will no longer throw an exception. +* [Client] Added new overloads for quick message publishing. +* [ManagedClient] The managed client is moved to a separate nuget package. +* [ManagedClient] Added an own message format with extended properties like ID (BREAKING CHANGE). +* [ManagedClient] Fixed a loading issue of stored application messages (thanks to @JTrotta). +* [ManagedClient] Added a new event which is fired when a synchronization of the subscriptions has failed. +* [ManagedClient] Added a new event which is fired when a connection attempt has failed. +* [ManagedClient] Exposed a new property which provides the count of not published messages (pending messages count). * [Server] Added support for other WebSocket sub protocol formats like mqttv-3.1.1 (thanks to @israellot). * [Server] The takeover of an existing client sessions is now treated as a _clean_ disconnect of the previous client. +* [Server] The pending messages queue per client is now limited to 250 messages. Overflow strategy and count can be changed via options (thanks to @VladimirAkopyan) +* [Server] Keep alive checking is now suspended while large packages are being received (and thus the client is connected). Keep alive checking continues after a large packet is received completely. +* [Server] Rewritten the _ConnectedClients_ API and added new features for disconnecting and Endpoint information (IP etc.). +* [Server] Added settings for disabling persistent sessions and defining a max pending messages queue size per session. +* [Server] Persistent sessions are disabled by default (BREAKING CHANGE!). +* [Server] Added a new interceptor which is invoked before a new message is added to the client queue. +* [Server] Added support for Linux servers by dividing IPv4 and IPv6 support and adding new options (BREAKING CHANGE!). +* [Server] Gracefully closed connections are no longer reported as warnings. +* [Server] Added new overloads for initializing the ASP.NET Core integration. 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 021ca1d6..63bae548 100644 --- a/Build/build.ps1 +++ b/Build/build.ps1 @@ -3,6 +3,10 @@ param([string]$assemblyVersion, [string]$nugetVersion) if ([string]::IsNullOrEmpty($assemblyVersion)) {$assemblyVersion = "0.0.1"} if ([string]::IsNullOrEmpty($nugetVersion)) {$nugetVersion = "0.0.1"} +Write-Host "Assembly version = $assemblyVersion" +Write-Host "Nuget version = $nugetVersion" +Write-Host + $vswhere = ${Env:\ProgramFiles(x86)} + '\Microsoft Visual Studio\Installer\vswhere' $path = &$vswhere -latest -products * -requires Microsoft.Component.MSBuild -property installationPath @@ -10,26 +14,34 @@ if ($path) { $msbuild = join-path $path 'MSBuild\15.0\Bin\MSBuild.exe' # 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" + &$msbuild ..\Source\MQTTnet\MQTTnet.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 ..\Source\MQTTnet\MQTTnet.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 ..\Source\MQTTnet\MQTTnet.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 ..\Source\MQTTnet\MQTTnet.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 ..\Source\MQTTnet\MQTTnet.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" + &$msbuild ..\Source\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 + &$msbuild ..\Source\MQTTnet.Extensions.Rpc\MQTTnet.Extensions.Rpc.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 ..\Source\MQTTnet.Extensions.Rpc\MQTTnet.Extensions.Rpc.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 ..\Source\MQTTnet.Extensions.Rpc\MQTTnet.Extensions.Rpc.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 ..\Source\MQTTnet.Extensions.Rpc\MQTTnet.Extensions.Rpc.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 ..\Source\MQTTnet.Extensions.Rpc\MQTTnet.Extensions.Rpc.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" - Remove-Item .\NuGet -Force -Recurse -ErrorAction SilentlyContinue + # Build the Managed Client extension + &$msbuild ..\Source\MQTTnet.Extensions.ManagedClient\MQTTnet.Extensions.ManagedClient.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 ..\Source\MQTTnet.Extensions.ManagedClient\MQTTnet.Extensions.ManagedClient.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 ..\Source\MQTTnet.Extensions.ManagedClient\MQTTnet.Extensions.ManagedClient.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 ..\Source\MQTTnet.Extensions.ManagedClient\MQTTnet.Extensions.ManagedClient.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 ..\Source\MQTTnet.Extensions.ManagedClient\MQTTnet.Extensions.ManagedClient.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" + + Remove-Item .\NuGet -Force -Recurse -ErrorAction SilentlyContinue New-Item -ItemType Directory -Force -Path .\NuGet - .\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 + .\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 + .\nuget.exe pack MQTTnet.Extensions.ManagedClient.nuspec -Verbosity detailed -Symbols -OutputDir "NuGet" -Version $nugetVersion } \ No newline at end of file diff --git a/Build/upload.ps1 b/Build/upload.ps1 new file mode 100644 index 00000000..a7cb1728 --- /dev/null +++ b/Build/upload.ps1 @@ -0,0 +1,9 @@ +param([string]$apiKey) + +$files = Get-ChildItem -Path ".\NuGet" -Filter "*.nupkg" +foreach ($file in $files) +{ + Write-Host "Uploading: " $file + + .\nuget.exe push $file.Fullname $apiKey -NoSymbols -Source https://api.nuget.org/v3/index.json +} \ No newline at end of file diff --git a/Extensions/MQTTnet.Extensions.Rpc/MqttRpcClient.cs b/Extensions/MQTTnet.Extensions.Rpc/MqttRpcClient.cs deleted file mode 100644 index 938fcb4c..00000000 --- a/Extensions/MQTTnet.Extensions.Rpc/MqttRpcClient.cs +++ /dev/null @@ -1,101 +0,0 @@ -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 deleted file mode 100644 index 5f282702..00000000 --- a/Extensions/MQTTnet.Extensions.Rpc/SampleCCode.c +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/Frameworks/MQTTnet.AspnetCore/ApplicationBuilderExtensions.cs b/Frameworks/MQTTnet.AspnetCore/ApplicationBuilderExtensions.cs deleted file mode 100644 index 87b2038b..00000000 --- a/Frameworks/MQTTnet.AspnetCore/ApplicationBuilderExtensions.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.DependencyInjection; -using System.Linq; -using MQTTnet.Server; - -namespace MQTTnet.AspNetCore -{ - public static class ApplicationBuilderExtensions - { - public static IApplicationBuilder UseMqttEndpoint(this IApplicationBuilder app, string path = "/mqtt") - { - app.UseWebSockets(); - app.Use(async (context, next) => - { - if (context.Request.Path == path && context.WebSockets.IsWebSocketRequest) - { - string subprotocol = null; - - if (context.Request.Headers.TryGetValue("Sec-WebSocket-Protocol", out var requestedSubProtocolValues)) - { - // Order the protocols to also match "mqtt", "mqttv-3.1", "mqttv-3.11" etc. - subprotocol = requestedSubProtocolValues - .OrderByDescending(p => p.Length) - .FirstOrDefault(p => p.ToLower().StartsWith("mqtt")); - } - - var adapter = app.ApplicationServices.GetRequiredService(); - using (var webSocket = await context.WebSockets.AcceptWebSocketAsync(subprotocol)) - { - await adapter.AcceptWebSocketAsync(webSocket); - } - } - else - { - await next(); - } - }); - - return app; - } - - public static IApplicationBuilder UseMqttServer(this IApplicationBuilder app, Action configure) - { - var server = app.ApplicationServices.GetRequiredService(); - - configure(server); - - return app; - } - } -} diff --git a/Frameworks/MQTTnet.AspnetCore/MqttWebSocketServerChannel.cs b/Frameworks/MQTTnet.AspnetCore/MqttWebSocketServerChannel.cs deleted file mode 100644 index 7325a5f8..00000000 --- a/Frameworks/MQTTnet.AspnetCore/MqttWebSocketServerChannel.cs +++ /dev/null @@ -1,60 +0,0 @@ -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 deleted file mode 100644 index 61354cd9..00000000 --- a/Frameworks/MQTTnet.AspnetCore/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -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, IMqttServerOptions options) - { - 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(s => s.GetService()); - - return services; - } - } -} diff --git a/Frameworks/MQTTnet.NetStandard/Adapter/IMqttChannelAdapter.cs b/Frameworks/MQTTnet.NetStandard/Adapter/IMqttChannelAdapter.cs deleted file mode 100644 index 7a5c02a2..00000000 --- a/Frameworks/MQTTnet.NetStandard/Adapter/IMqttChannelAdapter.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using MQTTnet.Packets; -using MQTTnet.Serializer; - -namespace MQTTnet.Adapter -{ - public interface IMqttChannelAdapter : IDisposable - { - IMqttPacketSerializer PacketSerializer { get; } - - Task ConnectAsync(TimeSpan timeout); - - Task DisconnectAsync(TimeSpan timeout); - - Task SendPacketsAsync(TimeSpan timeout, CancellationToken cancellationToken, IEnumerable packets); - - Task ReceivePacketAsync(TimeSpan timeout, CancellationToken cancellationToken); - } -} diff --git a/Frameworks/MQTTnet.NetStandard/Adapter/MqttChannelAdapter.cs b/Frameworks/MQTTnet.NetStandard/Adapter/MqttChannelAdapter.cs deleted file mode 100644 index c2f9ba43..00000000 --- a/Frameworks/MQTTnet.NetStandard/Adapter/MqttChannelAdapter.cs +++ /dev/null @@ -1,255 +0,0 @@ -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/Frameworks/MQTTnet.NetStandard/Adapter/ReceivedMqttPacket.cs b/Frameworks/MQTTnet.NetStandard/Adapter/ReceivedMqttPacket.cs deleted file mode 100644 index a44fb54e..00000000 --- a/Frameworks/MQTTnet.NetStandard/Adapter/ReceivedMqttPacket.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.IO; -using MQTTnet.Packets; - -namespace MQTTnet.Adapter -{ - public sealed class ReceivedMqttPacket : IDisposable - { - public ReceivedMqttPacket(MqttPacketHeader header, MemoryStream body) - { - Header = header ?? throw new ArgumentNullException(nameof(header)); - Body = body ?? throw new ArgumentNullException(nameof(body)); - } - - 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 deleted file mode 100644 index b1bf62f5..00000000 --- a/Frameworks/MQTTnet.NetStandard/ApplicationMessagePublisherExtensions.cs +++ /dev/null @@ -1,41 +0,0 @@ -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/Frameworks/MQTTnet.NetStandard/Channel/IMqttChannel.cs b/Frameworks/MQTTnet.NetStandard/Channel/IMqttChannel.cs deleted file mode 100644 index b332884d..00000000 --- a/Frameworks/MQTTnet.NetStandard/Channel/IMqttChannel.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.IO; -using System.Threading.Tasks; - -namespace MQTTnet.Channel -{ - public interface IMqttChannel : IDisposable - { - Stream SendStream { get; } - Stream ReceiveStream { get; } - - Task ConnectAsync(); - Task DisconnectAsync(); - } -} diff --git a/Frameworks/MQTTnet.NetStandard/Client/MqttPacketDispatcher.cs b/Frameworks/MQTTnet.NetStandard/Client/MqttPacketDispatcher.cs deleted file mode 100644 index 8b09fbce..00000000 --- a/Frameworks/MQTTnet.NetStandard/Client/MqttPacketDispatcher.cs +++ /dev/null @@ -1,97 +0,0 @@ -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/MqttReceivedApplicationMessageProcessingMode.cs b/Frameworks/MQTTnet.NetStandard/Client/MqttReceivedApplicationMessageProcessingMode.cs deleted file mode 100644 index 651abe5b..00000000 --- a/Frameworks/MQTTnet.NetStandard/Client/MqttReceivedApplicationMessageProcessingMode.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace MQTTnet.Client -{ - public enum MqttReceivedApplicationMessageProcessingMode - { - SingleThread, - DedicatedThread - } -} diff --git a/Frameworks/MQTTnet.NetStandard/Diagnostics/IMqttNetLogger.cs b/Frameworks/MQTTnet.NetStandard/Diagnostics/IMqttNetLogger.cs deleted file mode 100644 index 6ed16de2..00000000 --- a/Frameworks/MQTTnet.NetStandard/Diagnostics/IMqttNetLogger.cs +++ /dev/null @@ -1,21 +0,0 @@ -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/MqttNetLogger.cs b/Frameworks/MQTTnet.NetStandard/Diagnostics/MqttNetLogger.cs deleted file mode 100644 index 53598b2b..00000000 --- a/Frameworks/MQTTnet.NetStandard/Diagnostics/MqttNetLogger.cs +++ /dev/null @@ -1,74 +0,0 @@ -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/Exceptions/MqttCommunicationTimedOutException.cs b/Frameworks/MQTTnet.NetStandard/Exceptions/MqttCommunicationTimedOutException.cs deleted file mode 100644 index 7d0adcd4..00000000 --- a/Frameworks/MQTTnet.NetStandard/Exceptions/MqttCommunicationTimedOutException.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace MQTTnet.Exceptions -{ - public sealed class MqttCommunicationTimedOutException : MqttCommunicationException - { - } -} diff --git a/Frameworks/MQTTnet.NetStandard/IApplicationMessagePublisher.cs b/Frameworks/MQTTnet.NetStandard/IApplicationMessagePublisher.cs deleted file mode 100644 index e137669e..00000000 --- a/Frameworks/MQTTnet.NetStandard/IApplicationMessagePublisher.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace MQTTnet -{ - public interface IApplicationMessagePublisher - { - Task PublishAsync(IEnumerable applicationMessages); - } -} diff --git a/Frameworks/MQTTnet.NetStandard/Implementations/MqttTcpServerAdapter.Uwp.cs b/Frameworks/MQTTnet.NetStandard/Implementations/MqttTcpServerAdapter.Uwp.cs deleted file mode 100644 index e71d6fff..00000000 --- a/Frameworks/MQTTnet.NetStandard/Implementations/MqttTcpServerAdapter.Uwp.cs +++ /dev/null @@ -1,75 +0,0 @@ -#if WINDOWS_UWP -using System; -using System.Threading.Tasks; -using Windows.Networking.Sockets; -using MQTTnet.Adapter; -using MQTTnet.Diagnostics; -using MQTTnet.Serializer; -using MQTTnet.Server; - -namespace MQTTnet.Implementations -{ - public class MqttTcpServerAdapter : IMqttServerAdapter, IDisposable - { - private readonly IMqttNetLogger _logger; - private StreamSocketListener _defaultEndpointSocket; - - public MqttTcpServerAdapter(IMqttNetLogger logger) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - public event EventHandler ClientAccepted; - - public async Task StartAsync(IMqttServerOptions options) - { - if (options == null) throw new ArgumentNullException(nameof(options)); - - if (_defaultEndpointSocket != null) throw new InvalidOperationException("Server is already started."); - - if (options.DefaultEndpointOptions.IsEnabled) - { - _defaultEndpointSocket = new StreamSocketListener(); - await _defaultEndpointSocket.BindServiceNameAsync(options.GetDefaultEndpointPort().ToString(), SocketProtectionLevel.PlainSocket); - _defaultEndpointSocket.ConnectionReceived += AcceptDefaultEndpointConnectionsAsync; - } - - if (options.TlsEndpointOptions.IsEnabled) - { - throw new NotSupportedException("TLS servers are not supported for UWP apps."); - } - } - - public Task StopAsync() - { - if (_defaultEndpointSocket != null) - { - _defaultEndpointSocket.ConnectionReceived -= AcceptDefaultEndpointConnectionsAsync; - } - - _defaultEndpointSocket?.Dispose(); - _defaultEndpointSocket = null; - - return Task.FromResult(0); - } - - public void Dispose() - { - StopAsync(); - } - - private void AcceptDefaultEndpointConnectionsAsync(StreamSocketListener sender, StreamSocketListenerConnectionReceivedEventArgs args) - { - try - { - var clientAdapter = new MqttChannelAdapter(new MqttTcpChannel(args.Socket), new MqttPacketSerializer(), _logger); - ClientAccepted?.Invoke(this, new MqttServerAdapterClientAcceptedEventArgs(clientAdapter)); - } - catch (Exception exception) - { - _logger.Error(exception, "Error while accepting connection at default endpoint."); - } - } - } -} -#endif \ No newline at end of file diff --git a/Frameworks/MQTTnet.NetStandard/Implementations/MqttTcpServerAdapter.cs b/Frameworks/MQTTnet.NetStandard/Implementations/MqttTcpServerAdapter.cs deleted file mode 100644 index dc33a3f2..00000000 --- a/Frameworks/MQTTnet.NetStandard/Implementations/MqttTcpServerAdapter.cs +++ /dev/null @@ -1,162 +0,0 @@ -#if NET452 || NET461 || NETSTANDARD1_3 || NETSTANDARD2_0 -using System; -using System.Net; -using System.Net.Security; -using System.Net.Sockets; -using System.Security.Authentication; -using System.Security.Cryptography.X509Certificates; -using System.Threading; -using System.Threading.Tasks; -using MQTTnet.Adapter; -using MQTTnet.Diagnostics; -using MQTTnet.Serializer; -using MQTTnet.Server; - -namespace MQTTnet.Implementations -{ - public class MqttTcpServerAdapter : IMqttServerAdapter, IDisposable - { - private readonly IMqttNetLogger _logger; - - private CancellationTokenSource _cancellationTokenSource; - private Socket _defaultEndpointSocket; - private Socket _tlsEndpointSocket; - private X509Certificate2 _tlsCertificate; - - public MqttTcpServerAdapter(IMqttNetLogger logger) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - public event EventHandler ClientAccepted; - - public Task StartAsync(IMqttServerOptions options) - { - if (_cancellationTokenSource != null) throw new InvalidOperationException("Server is already started."); - - _cancellationTokenSource = new CancellationTokenSource(); - - if (options.DefaultEndpointOptions.IsEnabled) - { - _defaultEndpointSocket = new Socket(SocketType.Stream, ProtocolType.Tcp); - _defaultEndpointSocket.Bind(new IPEndPoint(options.DefaultEndpointOptions.BoundIPAddress, options.GetDefaultEndpointPort())); - _defaultEndpointSocket.Listen(options.ConnectionBacklog); - - Task.Run(async () => await AcceptDefaultEndpointConnectionsAsync(_cancellationTokenSource.Token).ConfigureAwait(false), _cancellationTokenSource.Token).ConfigureAwait(false); - } - - if (options.TlsEndpointOptions.IsEnabled) - { - if (options.TlsEndpointOptions.Certificate == null) - { - throw new ArgumentException("TLS certificate is not set."); - } - - _tlsCertificate = new X509Certificate2(options.TlsEndpointOptions.Certificate); - if (!_tlsCertificate.HasPrivateKey) - { - throw new InvalidOperationException("The certificate for TLS encryption must contain the private key."); - } - - _tlsEndpointSocket = new Socket(SocketType.Stream, ProtocolType.Tcp); - _tlsEndpointSocket.Bind(new IPEndPoint(options.TlsEndpointOptions.BoundIPAddress, options.GetTlsEndpointPort())); - _tlsEndpointSocket.Listen(options.ConnectionBacklog); - - Task.Run(async () => await AcceptTlsEndpointConnectionsAsync(_cancellationTokenSource.Token).ConfigureAwait(false), _cancellationTokenSource.Token).ConfigureAwait(false); - } - - return Task.FromResult(0); - } - - public Task StopAsync() - { - _cancellationTokenSource?.Cancel(false); - _cancellationTokenSource?.Dispose(); - _cancellationTokenSource = null; - - _defaultEndpointSocket?.Dispose(); - _defaultEndpointSocket = null; - - _tlsCertificate = null; - - _tlsEndpointSocket?.Dispose(); - _tlsEndpointSocket = null; - - return Task.FromResult(0); - } - - public void Dispose() - { - StopAsync(); - } - - private async Task AcceptDefaultEndpointConnectionsAsync(CancellationToken cancellationToken) - { - while (!cancellationToken.IsCancellationRequested) - { - try - { - //todo: else branch can be used with min dependency NET46 -#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 = 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) - { - if (exception is SocketException s && s.SocketErrorCode == SocketError.OperationAborted) - { - return; - } - - _logger.Error(exception, "Error while accepting connection at default endpoint."); - await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false); - } - } - } - - private async Task AcceptTlsEndpointConnectionsAsync(CancellationToken cancellationToken) - { - while (!cancellationToken.IsCancellationRequested) - { - try - { -#if NET452 || NET461 - var clientSocket = await Task.Factory.FromAsync(_tlsEndpointSocket.BeginAccept, _tlsEndpointSocket.EndAccept, null).ConfigureAwait(false); -#else - var clientSocket = await _tlsEndpointSocket.AcceptAsync().ConfigureAwait(false); -#endif - - var sslStream = new SslStream(new NetworkStream(clientSocket)); - await sslStream.AuthenticateAsServerAsync(_tlsCertificate, false, SslProtocols.Tls12, false).ConfigureAwait(false); - - 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) - { - if (exception is SocketException s && s.SocketErrorCode == SocketError.OperationAborted) - { - return; - } - - _logger.Error(exception, "Error while accepting connection at TLS endpoint."); - await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false); - } - } - } - } -} -#endif \ No newline at end of file diff --git a/Frameworks/MQTTnet.NetStandard/Implementations/WebSocketStream.cs b/Frameworks/MQTTnet.NetStandard/Implementations/WebSocketStream.cs deleted file mode 100644 index bad03935..00000000 --- a/Frameworks/MQTTnet.NetStandard/Implementations/WebSocketStream.cs +++ /dev/null @@ -1,137 +0,0 @@ -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.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)); - } - - public override bool CanRead => true; - - public override bool CanSeek => false; - - public override bool CanWrite => true; - - public override long Length => throw new NotSupportedException(); - - public override long Position - { - get => throw new NotSupportedException(); - set => throw new NotSupportedException(); - } - - public override void Flush() - { - } - - public override Task FlushAsync(CancellationToken cancellationToken) - { - return Task.FromResult(0); - } - - public override int Read(byte[] buffer, int offset, int count) - { - 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(); - } - - public override void SetLength(long value) - { - 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/Frameworks/MQTTnet.NetStandard/Internal/TaskExtensions.cs b/Frameworks/MQTTnet.NetStandard/Internal/TaskExtensions.cs deleted file mode 100644 index 876c5495..00000000 --- a/Frameworks/MQTTnet.NetStandard/Internal/TaskExtensions.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using MQTTnet.Exceptions; - -namespace MQTTnet.Internal -{ - public static class TaskExtensions - { - public static async Task TimeoutAfter(this Task task, TimeSpan timeout) - { - if (task == null) throw new ArgumentNullException(nameof(task)); - - using (var timeoutCts = new CancellationTokenSource()) - { - try - { - var timeoutTask = Task.Delay(timeout, timeoutCts.Token); - var finishedTask = await Task.WhenAny(timeoutTask, task).ConfigureAwait(false); - - if (finishedTask == timeoutTask) - { - throw new MqttCommunicationTimedOutException(); - } - - if (task.IsCanceled) - { - throw new TaskCanceledException(); - } - - if (task.IsFaulted) - { - throw new MqttCommunicationException(task.Exception?.GetBaseException()); - } - } - finally - { - timeoutCts.Cancel(); - } - } - } - - public static async Task TimeoutAfter(this Task task, TimeSpan timeout) - { - if (task == null) throw new ArgumentNullException(nameof(task)); - - using (var timeoutCts = new CancellationTokenSource()) - { - try - { - var timeoutTask = Task.Delay(timeout, timeoutCts.Token); - var finishedTask = await Task.WhenAny(timeoutTask, task).ConfigureAwait(false); - - if (finishedTask == timeoutTask) - { - throw new MqttCommunicationTimedOutException(); - } - - if (task.IsCanceled) - { - throw new TaskCanceledException(); - } - - if (task.IsFaulted) - { - throw new MqttCommunicationException(task.Exception.GetBaseException()); - } - - return task.Result; - } - finally - { - timeoutCts.Cancel(); - } - } - } - } -} diff --git a/Frameworks/MQTTnet.NetStandard/ManagedClient/IManagedMqttClientStorage.cs b/Frameworks/MQTTnet.NetStandard/ManagedClient/IManagedMqttClientStorage.cs deleted file mode 100644 index 28bf5703..00000000 --- a/Frameworks/MQTTnet.NetStandard/ManagedClient/IManagedMqttClientStorage.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace MQTTnet.ManagedClient -{ - public interface IManagedMqttClientStorage - { - Task SaveQueuedMessagesAsync(IList messages); - - Task> LoadQueuedMessagesAsync(); - } -} diff --git a/Frameworks/MQTTnet.NetStandard/ManagedClient/ManagedMqttClientStorageManager.cs b/Frameworks/MQTTnet.NetStandard/ManagedClient/ManagedMqttClientStorageManager.cs deleted file mode 100644 index 0e71776e..00000000 --- a/Frameworks/MQTTnet.NetStandard/ManagedClient/ManagedMqttClientStorageManager.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace MQTTnet.ManagedClient -{ - public class ManagedMqttClientStorageManager - { - private readonly List _applicationMessages = new List(); - private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); - private readonly IManagedMqttClientStorage _storage; - - public ManagedMqttClientStorageManager(IManagedMqttClientStorage storage) - { - _storage = storage ?? throw new ArgumentNullException(nameof(storage)); - } - - public async Task LoadQueuedMessagesAsync() - { - var loadedMessages = await _storage.LoadQueuedMessagesAsync().ConfigureAwait(false); - foreach (var loadedMessage in loadedMessages) - { - _applicationMessages.Add(loadedMessage); - } - } - - public async Task AddAsync(MqttApplicationMessage applicationMessage) - { - await _semaphore.WaitAsync().ConfigureAwait(false); - try - { - _applicationMessages.Add(applicationMessage); - await SaveAsync().ConfigureAwait(false); - } - finally - { - _semaphore.Release(); - } - } - - public async Task RemoveAsync(MqttApplicationMessage applicationMessage) - { - await _semaphore.WaitAsync().ConfigureAwait(false); - try - { - var index = _applicationMessages.IndexOf(applicationMessage); - if (index == -1) - { - return; - } - - _applicationMessages.RemoveAt(index); - await SaveAsync().ConfigureAwait(false); - } - finally - { - _semaphore.Release(); - } - } - - private Task SaveAsync() - { - return _storage.SaveQueuedMessagesAsync(_applicationMessages); - } - } -} diff --git a/Frameworks/MQTTnet.NetStandard/MqttApplicationMessage.cs b/Frameworks/MQTTnet.NetStandard/MqttApplicationMessage.cs deleted file mode 100644 index 387acb21..00000000 --- a/Frameworks/MQTTnet.NetStandard/MqttApplicationMessage.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using MQTTnet.Protocol; - -namespace MQTTnet -{ - public sealed class MqttApplicationMessage - { - public MqttApplicationMessage() - { - } - - [Obsolete("Use object initializer or _MqttApplicationMessageBuilder_ instead.")] - public MqttApplicationMessage(string topic, byte[] payload, MqttQualityOfServiceLevel qualityOfServiceLevel, bool retain) - { - Topic = topic ?? throw new ArgumentNullException(nameof(topic)); - Payload = payload ?? throw new ArgumentNullException(nameof(payload)); - QualityOfServiceLevel = qualityOfServiceLevel; - Retain = retain; - } - - public string Topic { get; set; } - - public byte[] Payload { get; set; } - - public MqttQualityOfServiceLevel QualityOfServiceLevel { get; set; } - - public bool Retain { get; set; } - } -} diff --git a/Frameworks/MQTTnet.NetStandard/Packets/MqttPacketHeader.cs b/Frameworks/MQTTnet.NetStandard/Packets/MqttPacketHeader.cs deleted file mode 100644 index 01c26ecd..00000000 --- a/Frameworks/MQTTnet.NetStandard/Packets/MqttPacketHeader.cs +++ /dev/null @@ -1,13 +0,0 @@ -using MQTTnet.Protocol; - -namespace MQTTnet.Packets -{ - public class MqttPacketHeader - { - public MqttControlPacketType ControlPacketType { get; set; } - - public byte FixedHeader { get; set; } - - public int BodyLength { get; set; } - } -} diff --git a/Frameworks/MQTTnet.NetStandard/Packets/MqttPubAckPacket.cs b/Frameworks/MQTTnet.NetStandard/Packets/MqttPubAckPacket.cs deleted file mode 100644 index e7fe57b7..00000000 --- a/Frameworks/MQTTnet.NetStandard/Packets/MqttPubAckPacket.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace MQTTnet.Packets -{ - public sealed class MqttPubAckPacket : MqttBasePublishPacket - { - public override string ToString() - { - return "PubAck"; - } - } -} diff --git a/Frameworks/MQTTnet.NetStandard/Serializer/ByteReader.cs b/Frameworks/MQTTnet.NetStandard/Serializer/ByteReader.cs deleted file mode 100644 index ec1e3ea2..00000000 --- a/Frameworks/MQTTnet.NetStandard/Serializer/ByteReader.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; - -namespace MQTTnet.Serializer -{ - public sealed class ByteReader - { - private readonly int _source; - private int _index; - - public ByteReader(int source) - { - _source = source; - } - - public bool Read() - { - if (_index >= 8) - { - throw new InvalidOperationException("End of byte reached."); - } - - var result = ((1 << _index) & _source) > 0; - _index++; - return result; - } - - public int Read(int count) - { - if (_index + count > 8) - { - throw new InvalidOperationException("End of byte will be reached."); - } - - var result = 0; - for (var i = 0; i < count; i++) - { - if (((1 << _index) & _source) > 0) - { - result |= 1 << i; - } - - _index++; - } - - return result; - } - } -} diff --git a/Frameworks/MQTTnet.NetStandard/Serializer/ByteWriter.cs b/Frameworks/MQTTnet.NetStandard/Serializer/ByteWriter.cs deleted file mode 100644 index af961e1a..00000000 --- a/Frameworks/MQTTnet.NetStandard/Serializer/ByteWriter.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; - -namespace MQTTnet.Serializer -{ - public sealed class ByteWriter - { - private int _index; - private int _byte; - - public byte Value => (byte)_byte; - - public void Write(int @byte, int count) - { - for (var i = 0; i < count; i++) - { - var value = ((1 << i) & @byte) > 0; - Write(value); - } - } - - public void Write(bool bit) - { - if (_index >= 8) - { - throw new InvalidOperationException("End of the byte reached."); - } - - if (bit) - { - _byte |= 1 << _index; - } - - _index++; - } - } -} diff --git a/Frameworks/MQTTnet.NetStandard/Serializer/IMqttPacketSerializer.cs b/Frameworks/MQTTnet.NetStandard/Serializer/IMqttPacketSerializer.cs deleted file mode 100644 index 6577b0ad..00000000 --- a/Frameworks/MQTTnet.NetStandard/Serializer/IMqttPacketSerializer.cs +++ /dev/null @@ -1,16 +0,0 @@ -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 deleted file mode 100644 index bd43f7d3..00000000 --- a/Frameworks/MQTTnet.NetStandard/Serializer/MqttPacketReader.cs +++ /dev/null @@ -1,129 +0,0 @@ -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/Frameworks/MQTTnet.NetStandard/Serializer/MqttPacketWriter.cs b/Frameworks/MQTTnet.NetStandard/Serializer/MqttPacketWriter.cs deleted file mode 100644 index cb3d4589..00000000 --- a/Frameworks/MQTTnet.NetStandard/Serializer/MqttPacketWriter.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System; -using System.IO; -using System.Text; -using MQTTnet.Protocol; - -namespace MQTTnet.Serializer -{ - public sealed class MqttPacketWriter : BinaryWriter - { - public MqttPacketWriter(Stream stream) - : base(stream, Encoding.UTF8, true) - { - } - - public static byte BuildFixedHeader(MqttControlPacketType packetType, byte flags = 0) - { - var fixedHeader = (int)packetType << 4; - fixedHeader |= flags; - return (byte)fixedHeader; - } - - public override void Write(ushort value) - { - var buffer = BitConverter.GetBytes(value); - Write(buffer[1], buffer[0]); - } - - public new void Write(params byte[] values) - { - base.Write(values); - } - - public new void Write(byte value) - { - base.Write(value); - } - - public void Write(ByteWriter value) - { - if (value == null) throw new ArgumentNullException(nameof(value)); - - Write(value.Value); - } - - public void WriteWithLengthPrefix(string value) - { - WriteWithLengthPrefix(Encoding.UTF8.GetBytes(value ?? string.Empty)); - } - - public void WriteWithLengthPrefix(byte[] value) - { - var length = (ushort)value.Length; - - Write(length); - Write(value); - } - - public static void WriteRemainingLength(int length, BinaryWriter target) - { - if (length == 0) - { - target.Write((byte)0); - return; - } - - // Alorithm taken from http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html. - var x = length; - do - { - var encodedByte = x % 128; - x = x / 128; - if (x > 0) - { - encodedByte = encodedByte | 128; - } - - target.Write((byte)encodedByte); - } while (x > 0); - } - } -} diff --git a/Frameworks/MQTTnet.NetStandard/Server/ConnectedMqttClient.cs b/Frameworks/MQTTnet.NetStandard/Server/ConnectedMqttClient.cs deleted file mode 100644 index 4fa8e4ea..00000000 --- a/Frameworks/MQTTnet.NetStandard/Server/ConnectedMqttClient.cs +++ /dev/null @@ -1,18 +0,0 @@ -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/Frameworks/MQTTnet.NetStandard/Server/MqttClientConnectedEventArgs.cs b/Frameworks/MQTTnet.NetStandard/Server/MqttClientConnectedEventArgs.cs deleted file mode 100644 index 1ca3f406..00000000 --- a/Frameworks/MQTTnet.NetStandard/Server/MqttClientConnectedEventArgs.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; - -namespace MQTTnet.Server -{ - public class MqttClientConnectedEventArgs : EventArgs - { - public MqttClientConnectedEventArgs(ConnectedMqttClient client) - { - Client = client ?? throw new ArgumentNullException(nameof(client)); - } - - public ConnectedMqttClient Client { get; } - } -} diff --git a/Frameworks/MQTTnet.NetStandard/Server/MqttClientPendingMessagesQueue.cs b/Frameworks/MQTTnet.NetStandard/Server/MqttClientPendingMessagesQueue.cs deleted file mode 100644 index 1602a4d1..00000000 --- a/Frameworks/MQTTnet.NetStandard/Server/MqttClientPendingMessagesQueue.cs +++ /dev/null @@ -1,141 +0,0 @@ -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(MqttClientDisconnectType.NotClean).ConfigureAwait(false); - } - } - } - - public void Dispose() - { - _queueWaitSemaphore?.Dispose(); - } - } -} diff --git a/Frameworks/MQTTnet.NetStandard/Server/MqttClientSession.cs b/Frameworks/MQTTnet.NetStandard/Server/MqttClientSession.cs deleted file mode 100644 index f86f72a1..00000000 --- a/Frameworks/MQTTnet.NetStandard/Server/MqttClientSession.cs +++ /dev/null @@ -1,349 +0,0 @@ -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(MqttClientDisconnectType type) - { - try - { - if (_cancellationTokenSource == null) - { - return; - } - - _wasCleanDisconnect = type == MqttClientDisconnectType.Clean; - - _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(MqttClientDisconnectType.NotClean); - } - - 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 (Exception exception) - { - if (exception is MqttCommunicationException) - { - _logger.Warning(exception, "Client '{0}': Communication exception while processing client packets.", ClientId); - } - else - { - _logger.Error(exception, "Client '{0}': Unhandled exception while processing client packets.", ClientId); - } - - await StopAsync(MqttClientDisconnectType.NotClean).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(MqttClientDisconnectType.Clean); - } - - if (packet is MqttConnectPacket) - { - return StopAsync(MqttClientDisconnectType.NotClean); - } - - _logger.Warning("Client '{0}': Received not supported packet ({1}). Closing connection.", ClientId, packet); - return StopAsync(MqttClientDisconnectType.NotClean); - } - - 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(MqttClientDisconnectType.NotClean).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 deleted file mode 100644 index 5f6cf15e..00000000 --- a/Frameworks/MQTTnet.NetStandard/Server/MqttClientSessionsManager.cs +++ /dev/null @@ -1,335 +0,0 @@ -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(MqttClientDisconnectType.NotClean).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(MqttClientDisconnectType.NotClean).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(MqttClientDisconnectType.Clean).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/Frameworks/MQTTnet.NetStandard/Server/MqttRetainedMessagesManager.cs b/Frameworks/MQTTnet.NetStandard/Server/MqttRetainedMessagesManager.cs deleted file mode 100644 index f998557e..00000000 --- a/Frameworks/MQTTnet.NetStandard/Server/MqttRetainedMessagesManager.cs +++ /dev/null @@ -1,144 +0,0 @@ -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/MqttServerDefaultEndpointOptions.cs b/Frameworks/MQTTnet.NetStandard/Server/MqttServerDefaultEndpointOptions.cs deleted file mode 100644 index 545499b5..00000000 --- a/Frameworks/MQTTnet.NetStandard/Server/MqttServerDefaultEndpointOptions.cs +++ /dev/null @@ -1,13 +0,0 @@ -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/Frameworks/MQTTnet.NetStandard/Server/MqttServerOptions.cs b/Frameworks/MQTTnet.NetStandard/Server/MqttServerOptions.cs deleted file mode 100644 index 2b0a9a00..00000000 --- a/Frameworks/MQTTnet.NetStandard/Server/MqttServerOptions.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; - -namespace MQTTnet.Server -{ - 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 Action ConnectionValidator { get; set; } - - public Action ApplicationMessageInterceptor { get; set; } - - public Action SubscriptionInterceptor { get; set; } - - public IMqttServerStorage Storage { get; set; } - } -} diff --git a/Frameworks/MQTTnet.NetStandard/Server/MqttServerOptionsExtensions.cs b/Frameworks/MQTTnet.NetStandard/Server/MqttServerOptionsExtensions.cs deleted file mode 100644 index db93ef18..00000000 --- a/Frameworks/MQTTnet.NetStandard/Server/MqttServerOptionsExtensions.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; - -namespace MQTTnet.Server -{ - public static class MqttServerOptionsExtensions - { - public static int GetTlsEndpointPort(this IMqttServerOptions options) - { - if (options == null) throw new ArgumentNullException(nameof(options)); - - if (!options.TlsEndpointOptions.Port.HasValue) - { - return 8883; - } - - return options.TlsEndpointOptions.Port.Value; - } - - public static int GetDefaultEndpointPort(this IMqttServerOptions options) - { - if (options == null) throw new ArgumentNullException(nameof(options)); - - if (!options.DefaultEndpointOptions.Port.HasValue) - { - return 1883; - } - - return options.DefaultEndpointOptions.Port.Value; - } - } -} diff --git a/Frameworks/MQTTnet.NetStandard/Server/MqttServerStartedEventArgs.cs b/Frameworks/MQTTnet.NetStandard/Server/MqttServerStartedEventArgs.cs deleted file mode 100644 index 08202994..00000000 --- a/Frameworks/MQTTnet.NetStandard/Server/MqttServerStartedEventArgs.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System; - -namespace MQTTnet.Server -{ - public class MqttServerStartedEventArgs : EventArgs - { - } -} diff --git a/Frameworks/MQTTnet.NetStandard/Server/MqttServerTlsEndpointOptions.cs b/Frameworks/MQTTnet.NetStandard/Server/MqttServerTlsEndpointOptions.cs deleted file mode 100644 index 73be93c5..00000000 --- a/Frameworks/MQTTnet.NetStandard/Server/MqttServerTlsEndpointOptions.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Net; - -namespace MQTTnet.Server -{ - public sealed class MqttServerTlsEndpointOptions - { - public bool IsEnabled { get; set; } - - public int? Port { get; set; } - - public byte[] Certificate { get; set; } - - public IPAddress BoundIPAddress { get; set; } = IPAddress.Any; - } -} diff --git a/Frameworks/MQTTnet.NetStandard/Server/MqttTopicFilterComparer.cs b/Frameworks/MQTTnet.NetStandard/Server/MqttTopicFilterComparer.cs deleted file mode 100644 index 82444159..00000000 --- a/Frameworks/MQTTnet.NetStandard/Server/MqttTopicFilterComparer.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; - -namespace MQTTnet.Server -{ - public static class MqttTopicFilterComparer - { - private static readonly char[] TopicLevelSeparator = { '/' }; - - public static bool IsMatch(string topic, string filter) - { - if (topic == null) throw new ArgumentNullException(nameof(topic)); - if (filter == null) throw new ArgumentNullException(nameof(filter)); - - if (string.Equals(topic, filter, StringComparison.Ordinal)) - { - return true; - } - - var fragmentsTopic = topic.Split(TopicLevelSeparator, StringSplitOptions.None); - var fragmentsFilter = filter.Split(TopicLevelSeparator, StringSplitOptions.None); - - for (var i = 0; i < fragmentsFilter.Length; i++) - { - switch (fragmentsFilter[i]) - { - case "+": continue; - case "#" when i == fragmentsFilter.Length - 1: return true; - } - - if (i >= fragmentsTopic.Length) - { - return false; - } - - if (!string.Equals(fragmentsFilter[i], fragmentsTopic[i], StringComparison.Ordinal)) - { - return false; - } - } - - return fragmentsTopic.Length <= fragmentsFilter.Length; - } - } -} diff --git a/MQTTnet.sln b/MQTTnet.sln index b60e21d5..be6a0b6f 100644 --- a/MQTTnet.sln +++ b/MQTTnet.sln @@ -7,23 +7,26 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Core.Tests", "Tests EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{9248C2E1-B9D6-40BF-81EC-86004D7765B4}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Frameworks", "Frameworks", "{32A630A7-2598-41D7-B625-204CD906F5FB}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Source", "Source", "{32A630A7-2598-41D7-B625-204CD906F5FB}" 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}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet", "Source\MQTTnet\MQTTnet.csproj", "{3587E506-55A2-4EB3-99C7-DC01E42D25D2}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{67C28AC1-BC3A-420A-BE9C-FA2401431CF9}" ProjectSection(SolutionItems) = preProject - appveyor.yml = appveyor.yml Build\build.ps1 = Build\build.ps1 Build\MQTTnet.AspNetCore.nuspec = Build\MQTTnet.AspNetCore.nuspec + Build\MQTTnet.Extensions.ManagedClient.nuspec = Build\MQTTnet.Extensions.ManagedClient.nuspec Build\MQTTnet.Extensions.Rpc.nuspec = Build\MQTTnet.Extensions.Rpc.nuspec Build\MQTTnet.nuspec = Build\MQTTnet.nuspec + Build\upload.ps1 = Build\upload.ps1 EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{B3F60ECB-45BA-4C66-8903-8BB89CA67998}" ProjectSection(SolutionItems) = preProject + .bettercodehub.yml = .bettercodehub.yml + appveyor.yml = appveyor.yml LICENSE = LICENSE README.md = README.md EndProjectSection @@ -32,13 +35,17 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.TestApp.NetCore", " EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.TestApp.AspNetCore2", "Tests\MQTTnet.TestApp.AspNetCore2\MQTTnet.TestApp.AspNetCore2.csproj", "{C6FF8AEA-0855-41EC-A1F3-AC262225BAB9}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.AspNetCore", "Frameworks\MQTTnet.AspnetCore\MQTTnet.AspNetCore.csproj", "{F10C4060-F7EE-4A83-919F-FF723E72F94A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.AspNetCore", "Source\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}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Extensions.Rpc", "Source\MQTTnet.Extensions.Rpc\MQTTnet.Extensions.Rpc.csproj", "{C444E9C8-95FA-430E-9126-274129DE16CD}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MQTTnet.Benchmarks", "Tests\MQTTnet.Benchmarks\MQTTnet.Benchmarks.csproj", "{998D04DD-7CB0-45F5-A393-E2495C16399E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Benchmarks", "Tests\MQTTnet.Benchmarks\MQTTnet.Benchmarks.csproj", "{998D04DD-7CB0-45F5-A393-E2495C16399E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Extensions.ManagedClient", "Source\MQTTnet.Extensions.ManagedClient\MQTTnet.Extensions.ManagedClient.csproj", "{C400533A-8EBA-4F0B-BF4D-295C3708604B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.AspNetCore.Tests", "Tests\MQTTnet.AspNetCore.Tests\MQTTnet.AspNetCore.Tests.csproj", "{61B62223-F5D0-48E4-BBD6-2CBA9353CB5E}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -70,6 +77,7 @@ Global {A7FF0C91-25DE-4BA6-B39E-F54E8DADF1CC}.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|Any CPU.Deploy.0 = Debug|Any CPU {FF1F72D6-9524-4422-9497-3CC0002216ED}.Debug|ARM.ActiveCfg = Debug|ARM {FF1F72D6-9524-4422-9497-3CC0002216ED}.Debug|ARM.Build.0 = Debug|ARM {FF1F72D6-9524-4422-9497-3CC0002216ED}.Debug|ARM.Deploy.0 = Debug|ARM @@ -186,6 +194,38 @@ Global {998D04DD-7CB0-45F5-A393-E2495C16399E}.Release|x64.Build.0 = Release|Any CPU {998D04DD-7CB0-45F5-A393-E2495C16399E}.Release|x86.ActiveCfg = Release|Any CPU {998D04DD-7CB0-45F5-A393-E2495C16399E}.Release|x86.Build.0 = Release|Any CPU + {C400533A-8EBA-4F0B-BF4D-295C3708604B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C400533A-8EBA-4F0B-BF4D-295C3708604B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C400533A-8EBA-4F0B-BF4D-295C3708604B}.Debug|ARM.ActiveCfg = Debug|Any CPU + {C400533A-8EBA-4F0B-BF4D-295C3708604B}.Debug|ARM.Build.0 = Debug|Any CPU + {C400533A-8EBA-4F0B-BF4D-295C3708604B}.Debug|x64.ActiveCfg = Debug|Any CPU + {C400533A-8EBA-4F0B-BF4D-295C3708604B}.Debug|x64.Build.0 = Debug|Any CPU + {C400533A-8EBA-4F0B-BF4D-295C3708604B}.Debug|x86.ActiveCfg = Debug|Any CPU + {C400533A-8EBA-4F0B-BF4D-295C3708604B}.Debug|x86.Build.0 = Debug|Any CPU + {C400533A-8EBA-4F0B-BF4D-295C3708604B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C400533A-8EBA-4F0B-BF4D-295C3708604B}.Release|Any CPU.Build.0 = Release|Any CPU + {C400533A-8EBA-4F0B-BF4D-295C3708604B}.Release|ARM.ActiveCfg = Release|Any CPU + {C400533A-8EBA-4F0B-BF4D-295C3708604B}.Release|ARM.Build.0 = Release|Any CPU + {C400533A-8EBA-4F0B-BF4D-295C3708604B}.Release|x64.ActiveCfg = Release|Any CPU + {C400533A-8EBA-4F0B-BF4D-295C3708604B}.Release|x64.Build.0 = Release|Any CPU + {C400533A-8EBA-4F0B-BF4D-295C3708604B}.Release|x86.ActiveCfg = Release|Any CPU + {C400533A-8EBA-4F0B-BF4D-295C3708604B}.Release|x86.Build.0 = Release|Any CPU + {61B62223-F5D0-48E4-BBD6-2CBA9353CB5E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {61B62223-F5D0-48E4-BBD6-2CBA9353CB5E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {61B62223-F5D0-48E4-BBD6-2CBA9353CB5E}.Debug|ARM.ActiveCfg = Debug|Any CPU + {61B62223-F5D0-48E4-BBD6-2CBA9353CB5E}.Debug|ARM.Build.0 = Debug|Any CPU + {61B62223-F5D0-48E4-BBD6-2CBA9353CB5E}.Debug|x64.ActiveCfg = Debug|Any CPU + {61B62223-F5D0-48E4-BBD6-2CBA9353CB5E}.Debug|x64.Build.0 = Debug|Any CPU + {61B62223-F5D0-48E4-BBD6-2CBA9353CB5E}.Debug|x86.ActiveCfg = Debug|Any CPU + {61B62223-F5D0-48E4-BBD6-2CBA9353CB5E}.Debug|x86.Build.0 = Debug|Any CPU + {61B62223-F5D0-48E4-BBD6-2CBA9353CB5E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {61B62223-F5D0-48E4-BBD6-2CBA9353CB5E}.Release|Any CPU.Build.0 = Release|Any CPU + {61B62223-F5D0-48E4-BBD6-2CBA9353CB5E}.Release|ARM.ActiveCfg = Release|Any CPU + {61B62223-F5D0-48E4-BBD6-2CBA9353CB5E}.Release|ARM.Build.0 = Release|Any CPU + {61B62223-F5D0-48E4-BBD6-2CBA9353CB5E}.Release|x64.ActiveCfg = Release|Any CPU + {61B62223-F5D0-48E4-BBD6-2CBA9353CB5E}.Release|x64.Build.0 = Release|Any CPU + {61B62223-F5D0-48E4-BBD6-2CBA9353CB5E}.Release|x86.ActiveCfg = Release|Any CPU + {61B62223-F5D0-48E4-BBD6-2CBA9353CB5E}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -199,6 +239,8 @@ Global {F10C4060-F7EE-4A83-919F-FF723E72F94A} = {32A630A7-2598-41D7-B625-204CD906F5FB} {C444E9C8-95FA-430E-9126-274129DE16CD} = {12816BCC-AF9E-44A9-9AE5-C246AF2A0587} {998D04DD-7CB0-45F5-A393-E2495C16399E} = {9248C2E1-B9D6-40BF-81EC-86004D7765B4} + {C400533A-8EBA-4F0B-BF4D-295C3708604B} = {12816BCC-AF9E-44A9-9AE5-C246AF2A0587} + {61B62223-F5D0-48E4-BBD6-2CBA9353CB5E} = {9248C2E1-B9D6-40BF-81EC-86004D7765B4} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {07536672-5CBC-4BE3-ACE0-708A431A7894} diff --git a/README.md b/README.md index 6a3111d8..7181d724 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ [![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/) +[![OpenCollective](https://opencollective.com/mqttnet/backers/badge.svg)](https://opencollective.com/mqttnet) +[![OpenCollective](https://opencollective.com/mqttnet/sponsors/badge.svg)](https://opencollective.com/mqttnet) # MQTTnet @@ -18,10 +20,10 @@ MQTTnet is a high performance .NET library for MQTT based communication. It prov * 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 ~40.000 messages / second)* +* Performance optimized (processing ~70.000 messages / second)* * Interfaces included for mocking and testing * Access to internal trace messages -* Unit tested (70+ tests) +* Unit tested (~90 tests) \* Tested on local machine (Intel i7 8700K) with MQTTnet client and server running in the same process using the TCP channel. The app for verification is part of this repository and stored in _/Tests/MQTTnet.TestApp.NetCore_. @@ -50,14 +52,15 @@ MQTTnet is a high performance .NET library for MQTT based communication. It prov * .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) * Mono 5.2+ +* Universal Windows Platform (UWP) 10.0.10240+ (x86, x64, ARM, AnyCPU, Windows 10 IoT Core) * Xamarin.Android 7.5+ * Xamarin.iOS 10.14+ ## Supported MQTT versions +* 5.0.0 (planned) * 3.1.1 * 3.1.0 @@ -72,6 +75,9 @@ Please find examples and the documentation at the Wiki of this repository () * 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, ) - +* HA4IoT (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 -Copyright (c) 2017 Christian Kratky +Copyright (c) 2017-2018 Christian Kratky Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Source/MQTTnet.AspnetCore/ApplicationBuilderExtensions.cs b/Source/MQTTnet.AspnetCore/ApplicationBuilderExtensions.cs new file mode 100644 index 00000000..22b9e937 --- /dev/null +++ b/Source/MQTTnet.AspnetCore/ApplicationBuilderExtensions.cs @@ -0,0 +1,52 @@ +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using System.Linq; +using MQTTnet.Server; + +namespace MQTTnet.AspNetCore +{ + public static class ApplicationBuilderExtensions + { + public static IApplicationBuilder UseMqttEndpoint(this IApplicationBuilder app, string path = "/mqtt") + { + app.UseWebSockets(); + app.Use(async (context, next) => + { + if (!context.WebSockets.IsWebSocketRequest || context.Request.Path != path) + { + await next(); + return; + } + + string subProtocol = null; + + if (context.Request.Headers.TryGetValue("Sec-WebSocket-Protocol", out var requestedSubProtocolValues)) + { + // Order the protocols to also match "mqtt", "mqttv-3.1", "mqttv-3.11" etc. + subProtocol = requestedSubProtocolValues + .OrderByDescending(p => p.Length) + .FirstOrDefault(p => p.ToLower().StartsWith("mqtt")); + } + + var adapter = app.ApplicationServices.GetRequiredService(); + using (var webSocket = await context.WebSockets.AcceptWebSocketAsync(subProtocol)) + { + var endpoint = $"{context.Connection.RemoteIpAddress}:{context.Connection.RemotePort}"; + await adapter.RunWebSocketConnectionAsync(webSocket, endpoint); + } + }); + + return app; + } + + public static IApplicationBuilder UseMqttServer(this IApplicationBuilder app, Action configure) + { + var server = app.ApplicationServices.GetRequiredService(); + + configure(server); + + return app; + } + } +} diff --git a/Source/MQTTnet.AspnetCore/Client/MqttClientConnectionContextFactory.cs b/Source/MQTTnet.AspnetCore/Client/MqttClientConnectionContextFactory.cs new file mode 100644 index 00000000..e308ccd4 --- /dev/null +++ b/Source/MQTTnet.AspnetCore/Client/MqttClientConnectionContextFactory.cs @@ -0,0 +1,34 @@ +using System; +using System.Net; +using MQTTnet.Adapter; +using MQTTnet.AspNetCore.Client.Tcp; +using MQTTnet.Client; +using MQTTnet.Diagnostics; +using MQTTnet.Serializer; + +namespace MQTTnet.AspNetCore.Client +{ + public class MqttClientConnectionContextFactory : IMqttClientAdapterFactory + { + public IMqttChannelAdapter CreateClientAdapter(IMqttClientOptions options, IMqttNetChildLogger logger) + { + if (options == null) throw new ArgumentNullException(nameof(options)); + + var serializer = new MqttPacketSerializer { ProtocolVersion = options.ProtocolVersion }; + + switch (options.ChannelOptions) + { + case MqttClientTcpOptions tcpOptions: + { + var endpoint = new DnsEndPoint(tcpOptions.Server, tcpOptions.GetPort()); + var tcpConnection = new TcpConnection(endpoint); + return new MqttConnectionContext(serializer, tcpConnection); + } + default: + { + throw new NotSupportedException(); + } + } + } + } +} diff --git a/Source/MQTTnet.AspnetCore/Client/Tcp/BufferExtensions.cs b/Source/MQTTnet.AspnetCore/Client/Tcp/BufferExtensions.cs new file mode 100644 index 00000000..5911a3a1 --- /dev/null +++ b/Source/MQTTnet.AspnetCore/Client/Tcp/BufferExtensions.cs @@ -0,0 +1,22 @@ +using System; +using System.Runtime.InteropServices; + +namespace MQTTnet.AspNetCore.Client.Tcp +{ + public static class BufferExtensions + { + public static ArraySegment GetArray(this Memory memory) + { + return ((ReadOnlyMemory)memory).GetArray(); + } + + public static ArraySegment GetArray(this ReadOnlyMemory memory) + { + if (!MemoryMarshal.TryGetArray(memory, out var result)) + { + throw new InvalidOperationException("Buffer backed by array was expected"); + } + return result; + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet.AspnetCore/Client/Tcp/DuplexPipe.cs b/Source/MQTTnet.AspnetCore/Client/Tcp/DuplexPipe.cs new file mode 100644 index 00000000..e234da55 --- /dev/null +++ b/Source/MQTTnet.AspnetCore/Client/Tcp/DuplexPipe.cs @@ -0,0 +1,41 @@ +using System.IO.Pipelines; + +namespace MQTTnet.AspNetCore.Client.Tcp +{ + public class DuplexPipe : IDuplexPipe + { + public DuplexPipe(PipeReader reader, PipeWriter writer) + { + Input = reader; + Output = writer; + } + + public PipeReader Input { get; } + + public PipeWriter Output { get; } + + public static DuplexPipePair CreateConnectionPair(PipeOptions inputOptions, PipeOptions outputOptions) + { + var input = new Pipe(inputOptions); + var output = new Pipe(outputOptions); + + var transportToApplication = new DuplexPipe(output.Reader, input.Writer); + var applicationToTransport = new DuplexPipe(input.Reader, output.Writer); + + return new DuplexPipePair(applicationToTransport, transportToApplication); + } + + // This class exists to work around issues with value tuple on .NET Framework + public readonly struct DuplexPipePair + { + public IDuplexPipe Transport { get; } + public IDuplexPipe Application { get; } + + public DuplexPipePair(IDuplexPipe transport, IDuplexPipe application) + { + Transport = transport; + Application = application; + } + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet.AspnetCore/Client/Tcp/SocketAwaitable.cs b/Source/MQTTnet.AspnetCore/Client/Tcp/SocketAwaitable.cs new file mode 100644 index 00000000..dbc26120 --- /dev/null +++ b/Source/MQTTnet.AspnetCore/Client/Tcp/SocketAwaitable.cs @@ -0,0 +1,70 @@ +using System; +using System.Diagnostics; +using System.IO.Pipelines; +using System.Net.Sockets; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +namespace MQTTnet.AspNetCore.Client.Tcp +{ + public class SocketAwaitable : ICriticalNotifyCompletion + { + private static readonly Action _callbackCompleted = () => { }; + + private readonly PipeScheduler _ioScheduler; + + private Action _callback; + private int _bytesTransferred; + private SocketError _error; + + public SocketAwaitable(PipeScheduler ioScheduler) + { + _ioScheduler = ioScheduler; + } + + public bool IsCompleted => ReferenceEquals(_callback, _callbackCompleted); + + public SocketAwaitable GetAwaiter() => this; + + public int GetResult() + { + Debug.Assert(ReferenceEquals(_callback, _callbackCompleted)); + + _callback = null; + + if (_error != SocketError.Success) + { + throw new SocketException((int)_error); + } + + return _bytesTransferred; + } + + public void OnCompleted(Action continuation) + { + if (ReferenceEquals(_callback, _callbackCompleted) || + ReferenceEquals(Interlocked.CompareExchange(ref _callback, continuation, null), _callbackCompleted)) + { + Task.Run(continuation); + } + } + + public void UnsafeOnCompleted(Action continuation) + { + OnCompleted(continuation); + } + + public void Complete(int bytesTransferred, SocketError socketError) + { + _error = socketError; + _bytesTransferred = bytesTransferred; + var continuation = Interlocked.Exchange(ref _callback, _callbackCompleted); + + if (continuation != null) + { + _ioScheduler.Schedule(state => ((Action)state)(), continuation); + } + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet.AspnetCore/Client/Tcp/SocketReceiver.cs b/Source/MQTTnet.AspnetCore/Client/Tcp/SocketReceiver.cs new file mode 100644 index 00000000..7d11fa2b --- /dev/null +++ b/Source/MQTTnet.AspnetCore/Client/Tcp/SocketReceiver.cs @@ -0,0 +1,37 @@ +using System; +using System.IO.Pipelines; +using System.Net.Sockets; + +namespace MQTTnet.AspNetCore.Client.Tcp +{ + public class SocketReceiver + { + private readonly Socket _socket; + private readonly SocketAsyncEventArgs _eventArgs = new SocketAsyncEventArgs(); + private readonly SocketAwaitable _awaitable; + + public SocketReceiver(Socket socket, PipeScheduler scheduler) + { + _socket = socket; + _awaitable = new SocketAwaitable(scheduler); + _eventArgs.UserToken = _awaitable; + _eventArgs.Completed += (_, e) => ((SocketAwaitable)e.UserToken).Complete(e.BytesTransferred, e.SocketError); + } + + public SocketAwaitable ReceiveAsync(Memory buffer) + { +#if NETCOREAPP2_1 + _eventArgs.SetBuffer(buffer); +#else + var segment = buffer.GetArray(); + _eventArgs.SetBuffer(segment.Array, segment.Offset, segment.Count); +#endif + if (!_socket.ReceiveAsync(_eventArgs)) + { + _awaitable.Complete(_eventArgs.BytesTransferred, _eventArgs.SocketError); + } + + return _awaitable; + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet.AspnetCore/Client/Tcp/SocketSender.cs b/Source/MQTTnet.AspnetCore/Client/Tcp/SocketSender.cs new file mode 100644 index 00000000..55192d6c --- /dev/null +++ b/Source/MQTTnet.AspnetCore/Client/Tcp/SocketSender.cs @@ -0,0 +1,97 @@ +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO.Pipelines; +using System.Net.Sockets; + +namespace MQTTnet.AspNetCore.Client.Tcp +{ + public class SocketSender + { + private readonly Socket _socket; + private readonly SocketAsyncEventArgs _eventArgs = new SocketAsyncEventArgs(); + private readonly SocketAwaitable _awaitable; + + private List> _bufferList; + + public SocketSender(Socket socket, PipeScheduler scheduler) + { + _socket = socket; + _awaitable = new SocketAwaitable(scheduler); + _eventArgs.UserToken = _awaitable; + _eventArgs.Completed += (_, e) => ((SocketAwaitable)e.UserToken).Complete(e.BytesTransferred, e.SocketError); + } + + public SocketAwaitable SendAsync(in ReadOnlySequence buffers) + { + if (buffers.IsSingleSegment) + { + return SendAsync(buffers.First); + } + +#if NETCOREAPP2_1 + if (!_eventArgs.MemoryBuffer.Equals(Memory.Empty)) +#else + if (_eventArgs.Buffer != null) +#endif + { + _eventArgs.SetBuffer(null, 0, 0); + } + + _eventArgs.BufferList = GetBufferList(buffers); + + if (!_socket.SendAsync(_eventArgs)) + { + _awaitable.Complete(_eventArgs.BytesTransferred, _eventArgs.SocketError); + } + + return _awaitable; + } + + private SocketAwaitable SendAsync(ReadOnlyMemory memory) + { + // The BufferList getter is much less expensive then the setter. + if (_eventArgs.BufferList != null) + { + _eventArgs.BufferList = null; + } + +#if NETCOREAPP2_1 + _eventArgs.SetBuffer(MemoryMarshal.AsMemory(memory)); +#else + var segment = memory.GetArray(); + _eventArgs.SetBuffer(segment.Array, segment.Offset, segment.Count); +#endif + if (!_socket.SendAsync(_eventArgs)) + { + _awaitable.Complete(_eventArgs.BytesTransferred, _eventArgs.SocketError); + } + + return _awaitable; + } + + private List> GetBufferList(in ReadOnlySequence buffer) + { + Debug.Assert(!buffer.IsEmpty); + Debug.Assert(!buffer.IsSingleSegment); + + if (_bufferList == null) + { + _bufferList = new List>(); + } + else + { + // Buffers are pooled, so it's OK to root them until the next multi-buffer write. + _bufferList.Clear(); + } + + foreach (var b in buffer) + { + _bufferList.Add(b.GetArray()); + } + + return _bufferList; + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet.AspnetCore/Client/Tcp/TcpConnection.cs b/Source/MQTTnet.AspnetCore/Client/Tcp/TcpConnection.cs new file mode 100644 index 00000000..37913fe4 --- /dev/null +++ b/Source/MQTTnet.AspnetCore/Client/Tcp/TcpConnection.cs @@ -0,0 +1,258 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Pipelines; +using System.Net; +using System.Net.Sockets; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Http.Features; +using MQTTnet.Exceptions; + +namespace MQTTnet.AspNetCore.Client.Tcp +{ + public class TcpConnection : ConnectionContext + { + private volatile bool _aborted; + private readonly EndPoint _endPoint; + private SocketSender _sender; + private SocketReceiver _receiver; + + private Socket _socket; + private IDuplexPipe _application; + + public bool IsConnected { get; private set; } + public override string ConnectionId { get; set; } + public override IFeatureCollection Features { get; } + public override IDictionary Items { get; set; } + public override IDuplexPipe Transport { get; set; } + + public TcpConnection(EndPoint endPoint) + { + _endPoint = endPoint; + } + + public TcpConnection(Socket socket) + { + _socket = socket; + _endPoint = socket.RemoteEndPoint; + + _sender = new SocketSender(_socket, PipeScheduler.ThreadPool); + _receiver = new SocketReceiver(_socket, PipeScheduler.ThreadPool); + } + + public Task DisposeAsync() + { + IsConnected = false; + + Transport?.Output.Complete(); + Transport?.Input.Complete(); + + _socket?.Dispose(); + + return Task.CompletedTask; + } + + public async Task StartAsync() + { + if (_socket == null) + { + _socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + _sender = new SocketSender(_socket, PipeScheduler.ThreadPool); + _receiver = new SocketReceiver(_socket, PipeScheduler.ThreadPool); + await _socket.ConnectAsync(_endPoint); + } + + var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); + + Transport = pair.Transport; + _application = pair.Application; + + _ = ExecuteAsync(); + + IsConnected = true; + } + + private async Task ExecuteAsync() + { + Exception sendError = null; + try + { + // Spawn send and receive logic + var receiveTask = DoReceive(); + var sendTask = DoSend(); + + // If the sending task completes then close the receive + // We don't need to do this in the other direction because the kestrel + // will trigger the output closing once the input is complete. + if (await Task.WhenAny(receiveTask, sendTask) == sendTask) + { + // Tell the reader it's being aborted + _socket.Dispose(); + } + + // Now wait for both to complete + await receiveTask; + sendError = await sendTask; + + // Dispose the socket(should noop if already called) + _socket.Dispose(); + } + catch (Exception ex) + { + Console.WriteLine($"Unexpected exception in {nameof(TcpConnection)}.{nameof(StartAsync)}: " + ex); + } + finally + { + // Complete the output after disposing the socket + _application.Input.Complete(sendError); + } + } + private async Task DoReceive() + { + Exception error = null; + + try + { + await ProcessReceives(); + } + catch (SocketException ex) when (ex.SocketErrorCode == SocketError.ConnectionReset) + { + error = new MqttCommunicationException(ex); + } + catch (SocketException ex) when (ex.SocketErrorCode == SocketError.OperationAborted || + ex.SocketErrorCode == SocketError.ConnectionAborted || + ex.SocketErrorCode == SocketError.Interrupted || + ex.SocketErrorCode == SocketError.InvalidArgument) + { + if (!_aborted) + { + // Calling Dispose after ReceiveAsync can cause an "InvalidArgument" error on *nix. + error = ConnectionAborted(); + } + } + catch (ObjectDisposedException) + { + if (!_aborted) + { + error = ConnectionAborted(); + } + } + catch (IOException ex) + { + error = ex; + } + catch (Exception ex) + { + error = new IOException(ex.Message, ex); + } + finally + { + if (_aborted) + { + error = error ?? ConnectionAborted(); + } + + _application.Output.Complete(error); + } + } + + private async Task ProcessReceives() + { + while (true) + { + // Ensure we have some reasonable amount of buffer space + var buffer = _application.Output.GetMemory(); + + var bytesReceived = await _receiver.ReceiveAsync(buffer); + + if (bytesReceived == 0) + { + // FIN + break; + } + + _application.Output.Advance(bytesReceived); + + var flushTask = _application.Output.FlushAsync(); + + if (!flushTask.IsCompleted) + { + await flushTask; + } + + var result = flushTask.GetAwaiter().GetResult(); + if (result.IsCompleted) + { + // Pipe consumer is shut down, do we stop writing + break; + } + } + } + + private Exception ConnectionAborted() + { + return new MqttCommunicationException("Connection Aborted"); + } + + private async Task DoSend() + { + Exception error = null; + + try + { + await ProcessSends(); + } + catch (SocketException ex) when (ex.SocketErrorCode == SocketError.OperationAborted) + { + } + catch (ObjectDisposedException) + { + } + catch (IOException ex) + { + error = ex; + } + catch (Exception ex) + { + error = new IOException(ex.Message, ex); + } + finally + { + _aborted = true; + _socket.Shutdown(SocketShutdown.Both); + } + + return error; + } + + private async Task ProcessSends() + { + while (true) + { + // Wait for data to write from the pipe producer + var result = await _application.Input.ReadAsync(); + var buffer = result.Buffer; + + if (result.IsCanceled) + { + break; + } + + var end = buffer.End; + var isCompleted = result.IsCompleted; + if (!buffer.IsEmpty) + { + await _sender.SendAsync(buffer); + } + + _application.Input.AdvanceTo(end); + + if (isCompleted) + { + break; + } + } + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet.AspnetCore/ConnectionBuilderExtensions.cs b/Source/MQTTnet.AspnetCore/ConnectionBuilderExtensions.cs new file mode 100644 index 00000000..262a333f --- /dev/null +++ b/Source/MQTTnet.AspnetCore/ConnectionBuilderExtensions.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Connections; + +namespace MQTTnet.AspNetCore +{ + public static class ConnectionBuilderExtensions + { + public static IConnectionBuilder UseMqtt(this IConnectionBuilder builder) + { + return builder.UseConnectionHandler(); + } + } +} diff --git a/Frameworks/MQTTnet.AspnetCore/MQTTnet.AspnetCore.csproj b/Source/MQTTnet.AspnetCore/MQTTnet.AspNetCore.csproj similarity index 63% rename from Frameworks/MQTTnet.AspnetCore/MQTTnet.AspnetCore.csproj rename to Source/MQTTnet.AspnetCore/MQTTnet.AspNetCore.csproj index a19f2b3b..dd3a4d55 100644 --- a/Frameworks/MQTTnet.AspnetCore/MQTTnet.AspnetCore.csproj +++ b/Source/MQTTnet.AspnetCore/MQTTnet.AspNetCore.csproj @@ -1,14 +1,12 @@ - + netstandard2.0 - 0.0.0.0 - 0.0.0.0 - 0.0.0.0 + 7.2 @@ -16,13 +14,13 @@ - + - + diff --git a/Source/MQTTnet.AspnetCore/MqttConnectionContext.cs b/Source/MQTTnet.AspnetCore/MqttConnectionContext.cs new file mode 100644 index 00000000..8a8469de --- /dev/null +++ b/Source/MQTTnet.AspnetCore/MqttConnectionContext.cs @@ -0,0 +1,117 @@ +using Microsoft.AspNetCore.Connections; +using MQTTnet.Adapter; +using MQTTnet.AspNetCore.Client.Tcp; +using MQTTnet.Packets; +using MQTTnet.Serializer; +using System; +using System.IO.Pipelines; +using System.Threading; +using System.Threading.Tasks; + +namespace MQTTnet.AspNetCore +{ + public class MqttConnectionContext : IMqttChannelAdapter + { + public MqttConnectionContext( + IMqttPacketSerializer packetSerializer, + ConnectionContext connection) + { + PacketSerializer = packetSerializer; + Connection = connection; + } + + public string Endpoint => Connection.ConnectionId; + public ConnectionContext Connection { get; } + public IMqttPacketSerializer PacketSerializer { get; } + public event EventHandler ReadingPacketStarted; + public event EventHandler ReadingPacketCompleted; + + public Task ConnectAsync(TimeSpan timeout, CancellationToken cancellationToken) + { + if (Connection is TcpConnection tcp && !tcp.IsConnected) + { + return tcp.StartAsync(); + } + return Task.CompletedTask; + } + + public Task DisconnectAsync(TimeSpan timeout, CancellationToken cancellationToken) + { + Connection.Transport.Input.Complete(); + Connection.Transport.Output.Complete(); + + return Task.CompletedTask; + } + + public async Task ReceivePacketAsync(TimeSpan timeout, CancellationToken cancellationToken) + { + var input = Connection.Transport.Input; + + try + { + while (!cancellationToken.IsCancellationRequested) + { + ReadResult readResult; + var readTask = input.ReadAsync(cancellationToken); + if (readTask.IsCompleted) + { + readResult = readTask.Result; + } + else + { + readResult = await readTask; + } + + var buffer = readResult.Buffer; + + var consumed = buffer.Start; + var observed = buffer.Start; + + try + { + if (!buffer.IsEmpty) + { + if (PacketSerializer.TryDeserialize(buffer, out var packet, out consumed, out observed)) + { + return packet; + } + else + { + // we did receive something but the message is not yet complete + ReadingPacketStarted?.Invoke(this, EventArgs.Empty); + } + } + else if (readResult.IsCompleted) + { + break; + } + } + finally + { + // The buffer was sliced up to where it was consumed, so we can just advance to the start. + // We mark examined as buffer.End so that if we didn't receive a full frame, we'll wait for more data + // before yielding the read again. + input.AdvanceTo(consumed, observed); + } + } + } + finally + { + ReadingPacketCompleted?.Invoke(this, EventArgs.Empty); + } + + cancellationToken.ThrowIfCancellationRequested(); + return null; + } + + public Task SendPacketAsync(MqttBasePacket packet, CancellationToken cancellationToken) + { + var buffer = PacketSerializer.Serialize(packet); + return Connection.Transport.Output.WriteAsync(buffer.AsMemory(), cancellationToken).AsTask(); + } + + public void Dispose() + { + } + } +} diff --git a/Source/MQTTnet.AspnetCore/MqttConnectionHandler.cs b/Source/MQTTnet.AspnetCore/MqttConnectionHandler.cs new file mode 100644 index 00000000..49a5c098 --- /dev/null +++ b/Source/MQTTnet.AspnetCore/MqttConnectionHandler.cs @@ -0,0 +1,40 @@ +using Microsoft.AspNetCore.Connections; +using MQTTnet.Adapter; +using MQTTnet.Serializer; +using MQTTnet.Server; +using System; +using System.Threading.Tasks; + +namespace MQTTnet.AspNetCore +{ + public class MqttConnectionHandler : ConnectionHandler, IMqttServerAdapter + { + public event EventHandler ClientAccepted; + + public override async Task OnConnectedAsync(ConnectionContext connection) + { + var serializer = new MqttPacketSerializer(); + using (var adapter = new MqttConnectionContext(serializer, connection)) + { + var args = new MqttServerAdapterClientAcceptedEventArgs(adapter); + ClientAccepted?.Invoke(this, args); + + await args.SessionTask; + } + } + + public Task StartAsync(IMqttServerOptions options) + { + return Task.CompletedTask; + } + + public Task StopAsync() + { + return Task.CompletedTask; + } + + public void Dispose() + { + } + } +} diff --git a/Frameworks/MQTTnet.AspnetCore/MqttHostedServer.cs b/Source/MQTTnet.AspnetCore/MqttHostedServer.cs similarity index 84% rename from Frameworks/MQTTnet.AspnetCore/MqttHostedServer.cs rename to Source/MQTTnet.AspnetCore/MqttHostedServer.cs index 8e6761ad..4b6b436b 100644 --- a/Frameworks/MQTTnet.AspnetCore/MqttHostedServer.cs +++ b/Source/MQTTnet.AspnetCore/MqttHostedServer.cs @@ -13,7 +13,8 @@ namespace MQTTnet.AspNetCore { private readonly IMqttServerOptions _options; - public MqttHostedServer(IMqttServerOptions options, IEnumerable adapters, IMqttNetLogger logger) : base(adapters, logger) + public MqttHostedServer(IMqttServerOptions options, IEnumerable adapters, IMqttNetLogger logger) + : base(adapters, logger.CreateChildLogger(nameof(MqttHostedServer))) { _options = options ?? throw new ArgumentNullException(nameof(options)); } diff --git a/Frameworks/MQTTnet.AspnetCore/MqttWebSocketServerAdapter.cs b/Source/MQTTnet.AspnetCore/MqttWebSocketServerAdapter.cs similarity index 58% rename from Frameworks/MQTTnet.AspnetCore/MqttWebSocketServerAdapter.cs rename to Source/MQTTnet.AspnetCore/MqttWebSocketServerAdapter.cs index c84b9be3..dbd93604 100644 --- a/Frameworks/MQTTnet.AspnetCore/MqttWebSocketServerAdapter.cs +++ b/Source/MQTTnet.AspnetCore/MqttWebSocketServerAdapter.cs @@ -3,12 +3,13 @@ using System.Net.WebSockets; using System.Threading.Tasks; using MQTTnet.Adapter; using MQTTnet.Diagnostics; +using MQTTnet.Implementations; using MQTTnet.Serializer; using MQTTnet.Server; namespace MQTTnet.AspNetCore { - public sealed class MqttWebSocketServerAdapter : IMqttServerAdapter, IDisposable + public class MqttWebSocketServerAdapter : IMqttServerAdapter { public event EventHandler ClientAccepted; @@ -22,21 +23,24 @@ namespace MQTTnet.AspNetCore return Task.CompletedTask; } - public Task AcceptWebSocketAsync(WebSocket webSocket) + public async Task RunWebSocketConnectionAsync(WebSocket webSocket, string endpoint) { if (webSocket == null) throw new ArgumentNullException(nameof(webSocket)); - var channel = new MqttWebSocketServerChannel(webSocket); - var clientAdapter = new MqttChannelAdapter(channel, new MqttPacketSerializer(), new MqttNetLogger()); + var clientAdapter = new MqttChannelAdapter(new MqttWebSocketChannel(webSocket, endpoint), new MqttPacketSerializer(), new MqttNetLogger().CreateChildLogger(nameof(MqttWebSocketServerAdapter))); var eventArgs = new MqttServerAdapterClientAcceptedEventArgs(clientAdapter); ClientAccepted?.Invoke(this, eventArgs); - return eventArgs.SessionTask; + + if (eventArgs.SessionTask != null) + { + await eventArgs.SessionTask.ConfigureAwait(false); + } } public void Dispose() { - StopAsync(); + StopAsync().GetAwaiter().GetResult(); } } } \ No newline at end of file diff --git a/Source/MQTTnet.AspnetCore/ReaderExtensions.cs b/Source/MQTTnet.AspnetCore/ReaderExtensions.cs new file mode 100644 index 00000000..b893e43e --- /dev/null +++ b/Source/MQTTnet.AspnetCore/ReaderExtensions.cs @@ -0,0 +1,87 @@ +using System; +using System.Buffers; +using MQTTnet.Adapter; +using MQTTnet.Exceptions; +using MQTTnet.Packets; +using MQTTnet.Serializer; + +namespace MQTTnet.AspNetCore +{ + public static class ReaderExtensions + { + public static bool TryDeserialize(this IMqttPacketSerializer serializer, in ReadOnlySequence input, out MqttBasePacket packet, out SequencePosition consumed, out SequencePosition observed) + { + packet = null; + consumed = input.Start; + observed = input.End; + var copy = input; + if (copy.Length < 2) + { + return false; + } + + var fixedheader = copy.First.Span[0]; + if (!TryReadBodyLength(ref copy, out var bodyLength)) + { + return false; + } + + if (copy.Length < bodyLength) + { + return false; + } + + var bodySlice = copy.Slice(0, bodyLength); + packet = serializer.Deserialize(new ReceivedMqttPacket(fixedheader, new MqttPacketBodyReader(bodySlice.GetArray(), 0))); + consumed = bodySlice.End; + observed = bodySlice.End; + return true; + } + + private static byte[] GetArray(this in ReadOnlySequence input) + { + if (input.IsSingleSegment) + { + return input.First.Span.ToArray(); + } + + // Should be rare + return input.ToArray(); + } + + private static bool TryReadBodyLength(ref ReadOnlySequence input, out int result) + { + // 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 index = 1; + result = 0; + + var temp = input.Slice(0, Math.Min(5, input.Length)).GetArray(); + + do + { + if (index == temp.Length) + { + return false; + } + encodedByte = temp[index]; + index++; + + value += (byte)(encodedByte & 127) * multiplier; + if (multiplier > 128 * 128 * 128) + { + throw new MqttProtocolViolationException($"Remaining length is invalid (Data={string.Join(",", temp.AsSpan(1, index).ToArray())})."); + } + + multiplier *= 128; + } while ((encodedByte & 128) != 0); + + input = input.Slice(index); + + result = value; + return true; + } + } +} diff --git a/Source/MQTTnet.AspnetCore/ServiceCollectionExtensions.cs b/Source/MQTTnet.AspnetCore/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..8b65bbeb --- /dev/null +++ b/Source/MQTTnet.AspnetCore/ServiceCollectionExtensions.cs @@ -0,0 +1,85 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +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, IMqttServerOptions options) + { + if (options == null) throw new ArgumentNullException(nameof(options)); + + services.AddSingleton(options); + + services.AddHostedMqttServer(); + + return services; + } + + public static IServiceCollection AddHostedMqttServer(this IServiceCollection services, Action configure) + { + var builder = new MqttServerOptionsBuilder(); + + configure(builder); + + services.AddSingleton(builder.Build()); + + services.AddHostedMqttServer(); + + return services; + } + + public static IServiceCollection AddHostedMqttServer(this IServiceCollection services) + where TOptions : class, IMqttServerOptions + { + services.AddSingleton(); + + services.AddHostedMqttServer(); + + return services; + } + + public static IServiceCollection AddHostedMqttServer(this IServiceCollection services) + { + var logger = new MqttNetLogger(); + var childLogger = logger.CreateChildLogger(); + + services.AddSingleton(logger); + services.AddSingleton(childLogger); + services.AddSingleton(); + services.AddSingleton(s => s.GetService()); + services.AddSingleton(s => s.GetService()); + + return services; + } + + public static IServiceCollection AddMqttWebSocketServerAdapter(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(s => s.GetService()); + + return services; + } + + public static IServiceCollection AddMqttTcpServerAdapter(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(s => s.GetService()); + + return services; + } + + public static IServiceCollection AddMqttConnectionHandler(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(s => s.GetService()); + + return services; + } + } +} diff --git a/Frameworks/MQTTnet.NetStandard/ManagedClient/ApplicationMessageProcessedEventArgs.cs b/Source/MQTTnet.Extensions.ManagedClient/ApplicationMessageProcessedEventArgs.cs similarity index 63% rename from Frameworks/MQTTnet.NetStandard/ManagedClient/ApplicationMessageProcessedEventArgs.cs rename to Source/MQTTnet.Extensions.ManagedClient/ApplicationMessageProcessedEventArgs.cs index fa82c0a4..6eb97d23 100644 --- a/Frameworks/MQTTnet.NetStandard/ManagedClient/ApplicationMessageProcessedEventArgs.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/ApplicationMessageProcessedEventArgs.cs @@ -1,16 +1,16 @@ using System; -namespace MQTTnet.ManagedClient +namespace MQTTnet.Extensions.ManagedClient { public class ApplicationMessageProcessedEventArgs : EventArgs { - public ApplicationMessageProcessedEventArgs(MqttApplicationMessage applicationMessage, Exception exception) + public ApplicationMessageProcessedEventArgs(ManagedMqttApplicationMessage applicationMessage, Exception exception) { ApplicationMessage = applicationMessage ?? throw new ArgumentNullException(nameof(applicationMessage)); Exception = exception; } - public MqttApplicationMessage ApplicationMessage { get; } + public ManagedMqttApplicationMessage ApplicationMessage { get; } public Exception Exception { get; } public bool HasFailed => Exception != null; diff --git a/Frameworks/MQTTnet.NetStandard/ManagedClient/IManagedMqttClient.cs b/Source/MQTTnet.Extensions.ManagedClient/IManagedMqttClient.cs similarity index 67% rename from Frameworks/MQTTnet.NetStandard/ManagedClient/IManagedMqttClient.cs rename to Source/MQTTnet.Extensions.ManagedClient/IManagedMqttClient.cs index 97cdeca9..b5a6c50b 100644 --- a/Frameworks/MQTTnet.NetStandard/ManagedClient/IManagedMqttClient.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/IManagedMqttClient.cs @@ -3,22 +3,28 @@ using System.Collections.Generic; using System.Threading.Tasks; using MQTTnet.Client; -namespace MQTTnet.ManagedClient +namespace MQTTnet.Extensions.ManagedClient { public interface IManagedMqttClient : IApplicationMessageReceiver, IApplicationMessagePublisher, IDisposable { bool IsStarted { get; } bool IsConnected { get; } + int PendingApplicationMessagesCount { get; } event EventHandler Connected; event EventHandler Disconnected; event EventHandler ApplicationMessageProcessed; + event EventHandler ConnectingFailed; + event EventHandler SynchronizingSubscriptionsFailed; + Task StartAsync(IManagedMqttClientOptions options); Task StopAsync(); Task SubscribeAsync(IEnumerable topicFilters); Task UnsubscribeAsync(IEnumerable topics); + + Task PublishAsync(ManagedMqttApplicationMessage applicationMessages); } } \ No newline at end of file diff --git a/Frameworks/MQTTnet.NetStandard/ManagedClient/IManagedMqttClientOptions.cs b/Source/MQTTnet.Extensions.ManagedClient/IManagedMqttClientOptions.cs similarity index 84% rename from Frameworks/MQTTnet.NetStandard/ManagedClient/IManagedMqttClientOptions.cs rename to Source/MQTTnet.Extensions.ManagedClient/IManagedMqttClientOptions.cs index fbb87b79..77736c44 100644 --- a/Frameworks/MQTTnet.NetStandard/ManagedClient/IManagedMqttClientOptions.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/IManagedMqttClientOptions.cs @@ -1,7 +1,7 @@ using System; using MQTTnet.Client; -namespace MQTTnet.ManagedClient +namespace MQTTnet.Extensions.ManagedClient { public interface IManagedMqttClientOptions { diff --git a/Source/MQTTnet.Extensions.ManagedClient/IManagedMqttClientStorage.cs b/Source/MQTTnet.Extensions.ManagedClient/IManagedMqttClientStorage.cs new file mode 100644 index 00000000..dc44001a --- /dev/null +++ b/Source/MQTTnet.Extensions.ManagedClient/IManagedMqttClientStorage.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace MQTTnet.Extensions.ManagedClient +{ + public interface IManagedMqttClientStorage + { + Task SaveQueuedMessagesAsync(IList messages); + + Task> LoadQueuedMessagesAsync(); + } +} diff --git a/Source/MQTTnet.Extensions.ManagedClient/MQTTnet.Extensions.ManagedClient.csproj b/Source/MQTTnet.Extensions.ManagedClient/MQTTnet.Extensions.ManagedClient.csproj new file mode 100644 index 00000000..1a3d9cc9 --- /dev/null +++ b/Source/MQTTnet.Extensions.ManagedClient/MQTTnet.Extensions.ManagedClient.csproj @@ -0,0 +1,28 @@ + + + + netstandard1.3;netstandard2.0;net452;net461;uap10.0 + + + + + + + + false + UAP,Version=v10.0 + UAP + 10.0.17134.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/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttApplicationMessage.cs b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttApplicationMessage.cs new file mode 100644 index 00000000..bba73b0c --- /dev/null +++ b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttApplicationMessage.cs @@ -0,0 +1,11 @@ +using System; + +namespace MQTTnet.Extensions.ManagedClient +{ + public class ManagedMqttApplicationMessage + { + public Guid Id { get; set; } = Guid.NewGuid(); + + public MqttApplicationMessage ApplicationMessage { get; set; } + } +} diff --git a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttApplicationMessageBuilder.cs b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttApplicationMessageBuilder.cs new file mode 100644 index 00000000..bf67f70e --- /dev/null +++ b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttApplicationMessageBuilder.cs @@ -0,0 +1,47 @@ +using System; + +namespace MQTTnet.Extensions.ManagedClient +{ + public class ManagedMqttApplicationMessageBuilder + { + private Guid _id = Guid.NewGuid(); + private MqttApplicationMessage _applicationMessage; + + public ManagedMqttApplicationMessageBuilder WithId(Guid id) + { + _id = id; + return this; + } + + public ManagedMqttApplicationMessageBuilder WithApplicationMessage(MqttApplicationMessage applicationMessage) + { + _applicationMessage = applicationMessage; + return this; + } + + public ManagedMqttApplicationMessageBuilder WithApplicationMessage(Action builder) + { + if (builder == null) throw new ArgumentNullException(nameof(builder)); + + var internalBuilder = new MqttApplicationMessageBuilder(); + builder(internalBuilder); + + _applicationMessage = internalBuilder.Build(); + return this; + } + + public ManagedMqttApplicationMessage Build() + { + if (_applicationMessage == null) + { + throw new InvalidOperationException("The ApplicationMessage cannot be null."); + } + + return new ManagedMqttApplicationMessage + { + Id = _id, + ApplicationMessage = _applicationMessage + }; + } + } +} diff --git a/Frameworks/MQTTnet.NetStandard/ManagedClient/ManagedMqttClient.cs b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClient.cs similarity index 68% rename from Frameworks/MQTTnet.NetStandard/ManagedClient/ManagedMqttClient.cs rename to Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClient.cs index f1ce45d9..bcd98ae7 100644 --- a/Frameworks/MQTTnet.NetStandard/ManagedClient/ManagedMqttClient.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClient.cs @@ -9,17 +9,16 @@ using MQTTnet.Diagnostics; using MQTTnet.Exceptions; using MQTTnet.Protocol; -namespace MQTTnet.ManagedClient +namespace MQTTnet.Extensions.ManagedClient { public class ManagedMqttClient : IManagedMqttClient { - private readonly BlockingCollection _messageQueue = new BlockingCollection(); + private readonly BlockingCollection _messageQueue = new BlockingCollection(); private readonly Dictionary _subscriptions = new Dictionary(); - private readonly SemaphoreSlim _subscriptionsSemaphore = new SemaphoreSlim(1, 1); - private readonly List _unsubscriptions = new List(); + private readonly HashSet _unsubscriptions = new HashSet(); private readonly IMqttClient _mqttClient; - private readonly IMqttNetLogger _logger; + private readonly IMqttNetChildLogger _logger; private CancellationTokenSource _connectionCancellationToken; private CancellationTokenSource _publishingCancellationToken; @@ -29,24 +28,31 @@ namespace MQTTnet.ManagedClient private bool _subscriptionsNotPushed; - public ManagedMqttClient(IMqttClient mqttClient, IMqttNetLogger logger) + public ManagedMqttClient(IMqttClient mqttClient, IMqttNetChildLogger logger) { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + if (logger == null) throw new ArgumentNullException(nameof(logger)); + _mqttClient = mqttClient ?? throw new ArgumentNullException(nameof(mqttClient)); _mqttClient.Connected += OnConnected; _mqttClient.Disconnected += OnDisconnected; _mqttClient.ApplicationMessageReceived += OnApplicationMessageReceived; + + _logger = logger.CreateChildLogger(nameof(ManagedMqttClient)); } public bool IsConnected => _mqttClient.IsConnected; public bool IsStarted => _connectionCancellationToken != null; + public int PendingApplicationMessagesCount => _messageQueue.Count; public event EventHandler Connected; public event EventHandler Disconnected; + public event EventHandler ApplicationMessageReceived; public event EventHandler ApplicationMessageProcessed; - public event EventHandler SynchronizingSubscriptionsFailed; + + public event EventHandler ConnectingFailed; + public event EventHandler SynchronizingSubscriptionsFailed; public async Task StartAsync(IManagedMqttClientOptions options) { @@ -65,16 +71,21 @@ namespace MQTTnet.ManagedClient if (_options.Storage != null) { _storageManager = new ManagedMqttClientStorageManager(_options.Storage); - await _storageManager.LoadQueuedMessagesAsync().ConfigureAwait(false); + var messages = await _storageManager.LoadQueuedMessagesAsync().ConfigureAwait(false); + + foreach (var message in messages) + { + _messageQueue.Add(message); + } } _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.Run(async () => await MaintainConnectionAsync(_connectionCancellationToken.Token).ConfigureAwait(false), _connectionCancellationToken.Token).ConfigureAwait(false); + Task.Run(() => MaintainConnectionAsync(_connectionCancellationToken.Token), _connectionCancellationToken.Token); #pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed - _logger.Info("Started"); + _logger.Info("Started"); } public Task StopAsync() @@ -90,27 +101,30 @@ namespace MQTTnet.ManagedClient return Task.FromResult(0); } - public async Task PublishAsync(IEnumerable applicationMessages) + public Task PublishAsync(MqttApplicationMessage applicationMessage) { - if (applicationMessages == null) throw new ArgumentNullException(nameof(applicationMessages)); + if (applicationMessage == null) throw new ArgumentNullException(nameof(applicationMessage)); - foreach (var applicationMessage in applicationMessages) - { - if (_storageManager != null) - { - await _storageManager.AddAsync(applicationMessage).ConfigureAwait(false); - } - - _messageQueue.Add(applicationMessage); - } + return PublishAsync(new ManagedMqttApplicationMessageBuilder().WithApplicationMessage(applicationMessage).Build()); } - public async Task SubscribeAsync(IEnumerable topicFilters) + public async Task PublishAsync(ManagedMqttApplicationMessage applicationMessage) + { + if (applicationMessage == null) throw new ArgumentNullException(nameof(applicationMessage)); + + if (_storageManager != null) + { + await _storageManager.AddAsync(applicationMessage).ConfigureAwait(false); + } + + _messageQueue.Add(applicationMessage); + } + + public Task SubscribeAsync(IEnumerable topicFilters) { if (topicFilters == null) throw new ArgumentNullException(nameof(topicFilters)); - await _subscriptionsSemaphore.WaitAsync().ConfigureAwait(false); - try + lock (_subscriptions) { foreach (var topicFilter in topicFilters) { @@ -118,16 +132,15 @@ namespace MQTTnet.ManagedClient _subscriptionsNotPushed = true; } } - finally - { - _subscriptionsSemaphore.Release(); - } + + return Task.FromResult(0); } - public async Task UnsubscribeAsync(IEnumerable topics) + public Task UnsubscribeAsync(IEnumerable topics) { - await _subscriptionsSemaphore.WaitAsync().ConfigureAwait(false); - try + if (topics == null) throw new ArgumentNullException(nameof(topics)); + + lock (_subscriptions) { foreach (var topic in topics) { @@ -138,16 +151,13 @@ namespace MQTTnet.ManagedClient } } } - finally - { - _subscriptionsSemaphore.Release(); - } + + return Task.FromResult(0); } public void Dispose() { _messageQueue?.Dispose(); - _subscriptionsSemaphore?.Dispose(); _connectionCancellationToken?.Dispose(); _publishingCancellationToken?.Dispose(); } @@ -166,12 +176,12 @@ namespace MQTTnet.ManagedClient } catch (Exception exception) { - _logger.Error(exception, "Unhandled exception while maintaining connection."); + _logger.Error(exception, "Unhandled exception while maintaining connection."); } finally { await _mqttClient.DisconnectAsync().ConfigureAwait(false); - _logger.Info("Stopped"); + _logger.Info("Stopped"); } } @@ -190,10 +200,7 @@ namespace MQTTnet.ManagedClient if (connectionState == ReconnectionResult.Reconnected || _subscriptionsNotPushed) { await SynchronizeSubscriptionsAsync().ConfigureAwait(false); - StartPublishing(); - - return; } @@ -207,15 +214,15 @@ namespace MQTTnet.ManagedClient } catch (MqttCommunicationException exception) { - _logger.Warning(exception, "Communication exception while maintaining connection."); + _logger.Warning(exception, "Communication exception while maintaining connection."); } catch (Exception exception) { - _logger.Error(exception, "Unhandled exception while maintaining connection."); + _logger.Error(exception, "Unhandled exception while maintaining connection."); } } - private async Task PublishQueuedMessagesAsync(CancellationToken cancellationToken) + private void PublishQueuedMessages(CancellationToken cancellationToken) { try { @@ -227,12 +234,9 @@ namespace MQTTnet.ManagedClient continue; } - if (cancellationToken.IsCancellationRequested) - { - continue; - } + cancellationToken.ThrowIfCancellationRequested(); - await TryPublishQueuedMessageAsync(message).ConfigureAwait(false); + TryPublishQueuedMessage(message); } } catch (OperationCanceledException) @@ -240,33 +244,29 @@ namespace MQTTnet.ManagedClient } catch (Exception exception) { - _logger.Error(exception, "Unhandled exception while publishing queued application messages."); + _logger.Error(exception, "Unhandled exception while publishing queued application messages."); } finally { - _logger.Verbose("Stopped publishing messages."); + _logger.Verbose("Stopped publishing messages."); } } - private async Task TryPublishQueuedMessageAsync(MqttApplicationMessage message) + private void TryPublishQueuedMessage(ManagedMqttApplicationMessage message) { Exception transmitException = null; try { - await _mqttClient.PublishAsync(message).ConfigureAwait(false); - - if (_storageManager != null) - { - await _storageManager.RemoveAsync(message).ConfigureAwait(false); - } + _mqttClient.PublishAsync(message.ApplicationMessage).GetAwaiter().GetResult(); + _storageManager?.RemoveAsync(message).GetAwaiter().GetResult(); } catch (MqttCommunicationException exception) { transmitException = exception; - _logger.Warning(exception, "Publishing application message failed."); + _logger.Warning(exception, $"Publishing application ({message.Id}) message failed."); - if (message.QualityOfServiceLevel > MqttQualityOfServiceLevel.AtMostOnce) + if (message.ApplicationMessage.QualityOfServiceLevel > MqttQualityOfServiceLevel.AtMostOnce) { _messageQueue.Add(message); } @@ -274,7 +274,7 @@ namespace MQTTnet.ManagedClient catch (Exception exception) { transmitException = exception; - _logger.Error(exception, "Unhandled exception while publishing queued application message."); + _logger.Error(exception, $"Unhandled exception while publishing application message ({message.Id})."); } finally { @@ -284,26 +284,21 @@ namespace MQTTnet.ManagedClient private async Task SynchronizeSubscriptionsAsync() { - _logger.Info(nameof(ManagedMqttClient), "Synchronizing subscriptions"); + _logger.Info(nameof(ManagedMqttClient), "Synchronizing subscriptions"); List subscriptions; - List unsubscriptions; + HashSet unsubscriptions; - await _subscriptionsSemaphore.WaitAsync().ConfigureAwait(false); - try + lock (_subscriptions) { subscriptions = _subscriptions.Select(i => new TopicFilter(i.Key, i.Value)).ToList(); - unsubscriptions = new List(_unsubscriptions); + unsubscriptions = new HashSet(_unsubscriptions); _unsubscriptions.Clear(); _subscriptionsNotPushed = false; } - finally - { - _subscriptionsSemaphore.Release(); - } - + if (!subscriptions.Any() && !unsubscriptions.Any()) { return; @@ -323,10 +318,10 @@ namespace MQTTnet.ManagedClient } catch (Exception exception) { - _logger.Warning(exception, "Synchronizing subscriptions failed."); + _logger.Warning(exception, "Synchronizing subscriptions failed."); _subscriptionsNotPushed = true; - SynchronizingSubscriptionsFailed?.Invoke(this, EventArgs.Empty); + SynchronizingSubscriptionsFailed?.Invoke(this, new MqttManagedProcessFailedEventArgs(exception)); } } @@ -342,8 +337,9 @@ namespace MQTTnet.ManagedClient await _mqttClient.ConnectAsync(_options.ClientOptions).ConfigureAwait(false); return ReconnectionResult.Reconnected; } - catch (Exception) + catch (Exception exception) { + ConnectingFailed?.Invoke(this, new MqttManagedProcessFailedEventArgs(exception)); return ReconnectionResult.NotConnected; } } @@ -374,9 +370,7 @@ namespace MQTTnet.ManagedClient _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 + Task.Factory.StartNew(() => PublishQueuedMessages(cts.Token), cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); } private void StopPublishing() diff --git a/Frameworks/MQTTnet.NetStandard/ManagedClient/ManagedMqttClientExtensions.cs b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientExtensions.cs similarity index 97% rename from Frameworks/MQTTnet.NetStandard/ManagedClient/ManagedMqttClientExtensions.cs rename to Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientExtensions.cs index 84b37858..ba5a16f7 100644 --- a/Frameworks/MQTTnet.NetStandard/ManagedClient/ManagedMqttClientExtensions.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientExtensions.cs @@ -2,7 +2,7 @@ using System.Threading.Tasks; using MQTTnet.Protocol; -namespace MQTTnet.ManagedClient +namespace MQTTnet.Extensions.ManagedClient { public static class ManagedMqttClientExtensions { diff --git a/Frameworks/MQTTnet.NetStandard/ManagedClient/ManagedMqttClientOptions.cs b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientOptions.cs similarity index 88% rename from Frameworks/MQTTnet.NetStandard/ManagedClient/ManagedMqttClientOptions.cs rename to Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientOptions.cs index 813aee8a..e07e2d68 100644 --- a/Frameworks/MQTTnet.NetStandard/ManagedClient/ManagedMqttClientOptions.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientOptions.cs @@ -1,7 +1,7 @@ using System; using MQTTnet.Client; -namespace MQTTnet.ManagedClient +namespace MQTTnet.Extensions.ManagedClient { public class ManagedMqttClientOptions : IManagedMqttClientOptions { diff --git a/Frameworks/MQTTnet.NetStandard/ManagedClient/ManagedMqttClientOptionsBuilder.cs b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientOptionsBuilder.cs similarity index 98% rename from Frameworks/MQTTnet.NetStandard/ManagedClient/ManagedMqttClientOptionsBuilder.cs rename to Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientOptionsBuilder.cs index 4d65bdbb..ef774c59 100644 --- a/Frameworks/MQTTnet.NetStandard/ManagedClient/ManagedMqttClientOptionsBuilder.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientOptionsBuilder.cs @@ -1,7 +1,7 @@ using System; using MQTTnet.Client; -namespace MQTTnet.ManagedClient +namespace MQTTnet.Extensions.ManagedClient { public class ManagedMqttClientOptionsBuilder { diff --git a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientStorageManager.cs b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientStorageManager.cs new file mode 100644 index 00000000..0f4e33e3 --- /dev/null +++ b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientStorageManager.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MQTTnet.Internal; + +namespace MQTTnet.Extensions.ManagedClient +{ + public class ManagedMqttClientStorageManager + { + private readonly List _messages = new List(); + private readonly AsyncLock _messagesLock = new AsyncLock(); + + private readonly IManagedMqttClientStorage _storage; + + public ManagedMqttClientStorageManager(IManagedMqttClientStorage storage) + { + _storage = storage ?? throw new ArgumentNullException(nameof(storage)); + } + + public async Task> LoadQueuedMessagesAsync() + { + var loadedMessages = await _storage.LoadQueuedMessagesAsync().ConfigureAwait(false); + _messages.AddRange(loadedMessages); + + return _messages; + } + + public async Task AddAsync(ManagedMqttApplicationMessage applicationMessage) + { + using (await _messagesLock.LockAsync(CancellationToken.None).ConfigureAwait(false)) + { + _messages.Add(applicationMessage); + await SaveAsync().ConfigureAwait(false); + } + } + + public async Task RemoveAsync(ManagedMqttApplicationMessage applicationMessage) + { + using (await _messagesLock.LockAsync(CancellationToken.None).ConfigureAwait(false)) + { + var index = _messages.IndexOf(applicationMessage); + if (index == -1) + { + return; + } + + _messages.RemoveAt(index); + await SaveAsync().ConfigureAwait(false); + } + } + + private Task SaveAsync() + { + return _storage.SaveQueuedMessagesAsync(_messages); + } + } +} diff --git a/Source/MQTTnet.Extensions.ManagedClient/MqttFactoryExtensions.cs b/Source/MQTTnet.Extensions.ManagedClient/MqttFactoryExtensions.cs new file mode 100644 index 00000000..d2ad020d --- /dev/null +++ b/Source/MQTTnet.Extensions.ManagedClient/MqttFactoryExtensions.cs @@ -0,0 +1,24 @@ +using System; +using MQTTnet.Client; +using MQTTnet.Diagnostics; + +namespace MQTTnet.Extensions.ManagedClient +{ + public static class MqttFactoryExtensions + { + public static IManagedMqttClient CreateManagedMqttClient(this IMqttClientFactory factory) + { + if (factory == null) throw new ArgumentNullException(nameof(factory)); + + return new ManagedMqttClient(factory.CreateMqttClient(), new MqttNetLogger().CreateChildLogger()); + } + + public static IManagedMqttClient CreateManagedMqttClient(this IMqttClientFactory factory, IMqttNetLogger logger) + { + if (factory == null) throw new ArgumentNullException(nameof(factory)); + if (logger == null) throw new ArgumentNullException(nameof(logger)); + + return new ManagedMqttClient(factory.CreateMqttClient(), logger.CreateChildLogger()); + } + } +} diff --git a/Source/MQTTnet.Extensions.ManagedClient/MqttManagedProcessFailedEventArgs.cs b/Source/MQTTnet.Extensions.ManagedClient/MqttManagedProcessFailedEventArgs.cs new file mode 100644 index 00000000..68dc1608 --- /dev/null +++ b/Source/MQTTnet.Extensions.ManagedClient/MqttManagedProcessFailedEventArgs.cs @@ -0,0 +1,14 @@ +using System; + +namespace MQTTnet.Extensions.ManagedClient +{ + public class MqttManagedProcessFailedEventArgs : EventArgs + { + public MqttManagedProcessFailedEventArgs(Exception exception) + { + Exception = exception; + } + + public Exception Exception { get; } + } +} diff --git a/Frameworks/MQTTnet.NetStandard/ManagedClient/ReconnectionResult.cs b/Source/MQTTnet.Extensions.ManagedClient/ReconnectionResult.cs similarity index 71% rename from Frameworks/MQTTnet.NetStandard/ManagedClient/ReconnectionResult.cs rename to Source/MQTTnet.Extensions.ManagedClient/ReconnectionResult.cs index 42566e3e..fa876c36 100644 --- a/Frameworks/MQTTnet.NetStandard/ManagedClient/ReconnectionResult.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/ReconnectionResult.cs @@ -1,4 +1,4 @@ -namespace MQTTnet.ManagedClient +namespace MQTTnet.Extensions.ManagedClient { public enum ReconnectionResult { diff --git a/Extensions/MQTTnet.Extensions.Rpc/MQTTnet.Extensions.Rpc.csproj b/Source/MQTTnet.Extensions.Rpc/MQTTnet.Extensions.Rpc.csproj similarity index 75% rename from Extensions/MQTTnet.Extensions.Rpc/MQTTnet.Extensions.Rpc.csproj rename to Source/MQTTnet.Extensions.Rpc/MQTTnet.Extensions.Rpc.csproj index 3f4128e0..1a3d9cc9 100644 --- a/Extensions/MQTTnet.Extensions.Rpc/MQTTnet.Extensions.Rpc.csproj +++ b/Source/MQTTnet.Extensions.Rpc/MQTTnet.Extensions.Rpc.csproj @@ -1,21 +1,18 @@ - + 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.17134.0 10.0.10240.0 .NETCore v5.0 @@ -25,7 +22,7 @@ - + diff --git a/Source/MQTTnet.Extensions.Rpc/MqttRpcClient.cs b/Source/MQTTnet.Extensions.Rpc/MqttRpcClient.cs new file mode 100644 index 00000000..ebf66ec1 --- /dev/null +++ b/Source/MQTTnet.Extensions.Rpc/MqttRpcClient.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Concurrent; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using MQTTnet.Client; +using MQTTnet.Exceptions; +using MQTTnet.Protocol; + +namespace MQTTnet.Extensions.Rpc +{ + public class MqttRpcClient : IDisposable + { + private readonly ConcurrentDictionary> _waitingCalls = new ConcurrentDictionary>(); + private readonly IMqttClient _mqttClient; + + public MqttRpcClient(IMqttClient mqttClient) + { + _mqttClient = mqttClient ?? throw new ArgumentNullException(nameof(mqttClient)); + + _mqttClient.ApplicationMessageReceived += OnApplicationMessageReceived; + } + + public Task ExecuteAsync(TimeSpan timeout, string methodName, string payload, MqttQualityOfServiceLevel qualityOfServiceLevel) + { + return ExecuteAsync(timeout, methodName, Encoding.UTF8.GetBytes(payload), qualityOfServiceLevel, CancellationToken.None); + } + + public Task ExecuteAsync(TimeSpan timeout, string methodName, string payload, MqttQualityOfServiceLevel qualityOfServiceLevel, CancellationToken cancellationToken) + { + return ExecuteAsync(timeout, methodName, Encoding.UTF8.GetBytes(payload), qualityOfServiceLevel, cancellationToken); + } + + public Task ExecuteAsync(TimeSpan timeout, string methodName, byte[] payload, MqttQualityOfServiceLevel qualityOfServiceLevel) + { + return ExecuteAsync(timeout, methodName, payload, qualityOfServiceLevel, CancellationToken.None); + } + + public async Task ExecuteAsync(TimeSpan timeout, string methodName, byte[] payload, MqttQualityOfServiceLevel qualityOfServiceLevel, CancellationToken cancellationToken) + { + if (methodName == null) throw new ArgumentNullException(nameof(methodName)); + + if (methodName.Contains("/") || methodName.Contains("+") || methodName.Contains("#")) + { + throw new ArgumentException("The method name cannot contain /, + or #."); + } + + 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.SubscribeAsync(responseTopic, qualityOfServiceLevel).ConfigureAwait(false); + await _mqttClient.PublishAsync(requestMessage).ConfigureAwait(false); + + using (var timeoutCts = new CancellationTokenSource(timeout)) + using (var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token)) + { + linkedCts.Token.Register(() => + { + if (!tcs.Task.IsCompleted && !tcs.Task.IsFaulted && !tcs.Task.IsCanceled) + { + tcs.TrySetCanceled(); + } + }); + + try + { + var result = await tcs.Task.ConfigureAwait(false); + timeoutCts.Cancel(false); + return result; + } + catch (OperationCanceledException exception) + { + if (timeoutCts.IsCancellationRequested && !cancellationToken.IsCancellationRequested) + { + throw new MqttCommunicationTimedOutException(exception); + } + else + { + throw; + } + } + } + } + finally + { + _waitingCalls.TryRemove(responseTopic, out _); + await _mqttClient.UnsubscribeAsync(responseTopic).ConfigureAwait(false); + } + } + + private void OnApplicationMessageReceived(object sender, MqttApplicationMessageReceivedEventArgs eventArgs) + { + if (!_waitingCalls.TryRemove(eventArgs.ApplicationMessage.Topic, out var 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/Source/MQTTnet.Extensions.Rpc/SampleCCode.c b/Source/MQTTnet.Extensions.Rpc/SampleCCode.c new file mode 100644 index 00000000..c20aa4af --- /dev/null +++ b/Source/MQTTnet.Extensions.Rpc/SampleCCode.c @@ -0,0 +1,29 @@ +// If using the MQTT client PubSubClient it must be ensured that the request topic for each method is subscribed like the following. +_mqttClient.subscribe("MQTTnet.RPC/+/ping"); +_mqttClient.subscribe("MQTTnet.RPC/+/do_something"); + +// It is not allowed to change the structure of the topic. Otherwise RPC will not work. So method names can be separated using +// an _ or . but no +, # or . If it is required to distinguish between devices own rules can be defined like the following. +_mqttClient.subscribe("MQTTnet.RPC/+/deviceA.ping"); +_mqttClient.subscribe("MQTTnet.RPC/+/deviceB.ping"); +_mqttClient.subscribe("MQTTnet.RPC/+/deviceC.getTemperature"); + +// Within the callback of the MQTT client the topic must be checked if it belongs to MQTTnet RPC. The following code shows one +// possible way of doing this. +void mqtt_Callback(char *topic, byte *payload, unsigned int payloadLength) +{ + String topicString = String(topic); + + if (topicString.startsWith("MQTTnet.RPC/")) { + String responseTopic = topicString + String("/response"); + + if (topicString.endsWith("/deviceA.ping")) { + mqtt_publish(responseTopic, "pong", false); + return; + } + } +} + +// Important notes: +// ! Do not send response message with the _retain_ flag set to true. +// ! All required data for a RPC call and the result must be placed into the payload. \ No newline at end of file diff --git a/Source/MQTTnet/Adapter/IMqttChannelAdapter.cs b/Source/MQTTnet/Adapter/IMqttChannelAdapter.cs new file mode 100644 index 00000000..43e5c8c4 --- /dev/null +++ b/Source/MQTTnet/Adapter/IMqttChannelAdapter.cs @@ -0,0 +1,27 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using MQTTnet.Packets; +using MQTTnet.Serializer; + +namespace MQTTnet.Adapter +{ + public interface IMqttChannelAdapter : IDisposable + { + string Endpoint { get; } + + IMqttPacketSerializer PacketSerializer { get; } + + event EventHandler ReadingPacketStarted; + + event EventHandler ReadingPacketCompleted; + + Task ConnectAsync(TimeSpan timeout, CancellationToken cancellationToken); + + Task DisconnectAsync(TimeSpan timeout, CancellationToken cancellationToken); + + Task SendPacketAsync(MqttBasePacket packet, CancellationToken cancellationToken); + + Task ReceivePacketAsync(TimeSpan timeout, CancellationToken cancellationToken); + } +} diff --git a/Frameworks/MQTTnet.NetStandard/Adapter/IMqttServerAdapter.cs b/Source/MQTTnet/Adapter/IMqttServerAdapter.cs similarity index 83% rename from Frameworks/MQTTnet.NetStandard/Adapter/IMqttServerAdapter.cs rename to Source/MQTTnet/Adapter/IMqttServerAdapter.cs index bd583f54..eff9eab7 100644 --- a/Frameworks/MQTTnet.NetStandard/Adapter/IMqttServerAdapter.cs +++ b/Source/MQTTnet/Adapter/IMqttServerAdapter.cs @@ -4,7 +4,7 @@ using MQTTnet.Server; namespace MQTTnet.Adapter { - public interface IMqttServerAdapter + public interface IMqttServerAdapter : IDisposable { event EventHandler ClientAccepted; diff --git a/Source/MQTTnet/Adapter/MqttChannelAdapter.cs b/Source/MQTTnet/Adapter/MqttChannelAdapter.cs new file mode 100644 index 00000000..0059f201 --- /dev/null +++ b/Source/MQTTnet/Adapter/MqttChannelAdapter.cs @@ -0,0 +1,256 @@ +using System; +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 class MqttChannelAdapter : IMqttChannelAdapter + { + private const uint ErrorOperationAborted = 0x800703E3; + private const int ReadBufferSize = 4096; // TODO: Move buffer size to config + + private readonly SemaphoreSlim _writerSemaphore = new SemaphoreSlim(1, 1); + + private readonly IMqttNetChildLogger _logger; + private readonly IMqttChannel _channel; + + private readonly byte[] _fixedHeaderBuffer = new byte[2]; + + private readonly byte[] _singleByteBuffer = new byte[1]; + + private bool _isDisposed; + + public MqttChannelAdapter(IMqttChannel channel, IMqttPacketSerializer serializer, IMqttNetChildLogger logger) + { + if (logger == null) throw new ArgumentNullException(nameof(logger)); + + _channel = channel ?? throw new ArgumentNullException(nameof(channel)); + PacketSerializer = serializer ?? throw new ArgumentNullException(nameof(serializer)); + + _logger = logger.CreateChildLogger(nameof(MqttChannelAdapter)); + } + + public string Endpoint => _channel.Endpoint; + + public IMqttPacketSerializer PacketSerializer { get; } + + public event EventHandler ReadingPacketStarted; + public event EventHandler ReadingPacketCompleted; + + public async Task ConnectAsync(TimeSpan timeout, CancellationToken cancellationToken) + { + ThrowIfDisposed(); + + try + { + _logger.Verbose("Connecting [Timeout={0}]", timeout); + + await Internal.TaskExtensions + .TimeoutAfterAsync(ct => _channel.ConnectAsync(ct), timeout, cancellationToken) + .ConfigureAwait(false); + } + catch (Exception exception) + { + if (IsWrappedException(exception)) + { + throw; + } + + WrapException(exception); + } + } + + public async Task DisconnectAsync(TimeSpan timeout, CancellationToken cancellationToken) + { + ThrowIfDisposed(); + + try + { + _logger.Verbose("Disconnecting [Timeout={0}]", timeout); + + await Internal.TaskExtensions + .TimeoutAfterAsync(ct => _channel.DisconnectAsync(), timeout, cancellationToken) + .ConfigureAwait(false); + } + catch (Exception exception) + { + if (IsWrappedException(exception)) + { + throw; + } + + WrapException(exception); + } + } + + public async Task SendPacketAsync(MqttBasePacket packet, CancellationToken cancellationToken) + { + await _writerSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + _logger.Verbose("TX >>> {0}", packet); + + var packetData = PacketSerializer.Serialize(packet); + + await _channel.WriteAsync(packetData.Array, packetData.Offset, packetData.Count, cancellationToken).ConfigureAwait(false); + + PacketSerializer.FreeBuffer(); + } + catch (Exception exception) + { + if (IsWrappedException(exception)) + { + throw; + } + + WrapException(exception); + } + finally + { + _writerSemaphore.Release(); + } + } + + public async Task ReceivePacketAsync(TimeSpan timeout, CancellationToken cancellationToken) + { + ThrowIfDisposed(); + + try + { + ReceivedMqttPacket receivedMqttPacket; + + if (timeout > TimeSpan.Zero) + { + receivedMqttPacket = await Internal.TaskExtensions.TimeoutAfterAsync(ct => ReceiveAsync(_channel, ct), timeout, cancellationToken).ConfigureAwait(false); + } + else + { + receivedMqttPacket = await ReceiveAsync(_channel, cancellationToken).ConfigureAwait(false); + } + + if (receivedMqttPacket == null || cancellationToken.IsCancellationRequested) + { + return null; + } + + var packet = PacketSerializer.Deserialize(receivedMqttPacket); + if (packet == null) + { + throw new MqttProtocolViolationException("Received malformed packet."); + } + + _logger.Verbose("RX <<< {0}", packet); + + return packet; + } + catch (Exception exception) + { + if (IsWrappedException(exception)) + { + throw; + } + + WrapException(exception); + } + + return null; + } + + private async Task ReceiveAsync(IMqttChannel channel, CancellationToken cancellationToken) + { + var fixedHeader = await MqttPacketReader.ReadFixedHeaderAsync(channel, _fixedHeaderBuffer, _singleByteBuffer, cancellationToken).ConfigureAwait(false); + + try + { + ReadingPacketStarted?.Invoke(this, EventArgs.Empty); + + if (fixedHeader.RemainingLength == 0) + { + return new ReceivedMqttPacket(fixedHeader.Flags, null); + } + + var body = new byte[fixedHeader.RemainingLength]; + var bodyOffset = 0; + var chunkSize = Math.Min(ReadBufferSize, fixedHeader.RemainingLength); + + do + { + var bytesLeft = body.Length - bodyOffset; + if (chunkSize > bytesLeft) + { + chunkSize = bytesLeft; + } + + // async/await is not used to avoid the overhead of context switches. We assume that the reamining data + // has been sent from the sender directly after the initial bytes. + var readBytes = channel.ReadAsync(body, bodyOffset, chunkSize, cancellationToken).GetAwaiter().GetResult(); + if (readBytes <= 0) + { + ExceptionHelper.ThrowGracefulSocketClose(); + } + + bodyOffset += readBytes; + } while (bodyOffset < body.Length); + + return new ReceivedMqttPacket(fixedHeader.Flags, new MqttPacketBodyReader(body, 0)); + } + finally + { + ReadingPacketCompleted?.Invoke(this, EventArgs.Empty); + } + } + + public void Dispose() + { + _isDisposed = true; + + _channel?.Dispose(); + } + + private void ThrowIfDisposed() + { + if (_isDisposed) + { + throw new ObjectDisposedException(nameof(MqttChannelAdapter)); + } + } + + private static bool IsWrappedException(Exception exception) + { + return exception is TaskCanceledException || + exception is OperationCanceledException || + exception is MqttCommunicationTimedOutException || + exception is MqttCommunicationException; + } + + private static void WrapException(Exception exception) + { + if (exception is IOException && exception.InnerException is SocketException socketException) + { + if (socketException.SocketErrorCode == SocketError.ConnectionAborted) + { + throw new OperationCanceledException(); + } + } + + if (exception is COMException comException) + { + if ((uint)comException.HResult == ErrorOperationAborted) + { + throw new OperationCanceledException(); + } + } + + throw new MqttCommunicationException(exception); + } + } +} diff --git a/Frameworks/MQTTnet.NetStandard/Adapter/MqttConnectingFailedException.cs b/Source/MQTTnet/Adapter/MqttConnectingFailedException.cs similarity index 100% rename from Frameworks/MQTTnet.NetStandard/Adapter/MqttConnectingFailedException.cs rename to Source/MQTTnet/Adapter/MqttConnectingFailedException.cs diff --git a/Frameworks/MQTTnet.NetStandard/Adapter/MqttServerAdapterClientAcceptedEventArgs.cs b/Source/MQTTnet/Adapter/MqttServerAdapterClientAcceptedEventArgs.cs similarity index 100% rename from Frameworks/MQTTnet.NetStandard/Adapter/MqttServerAdapterClientAcceptedEventArgs.cs rename to Source/MQTTnet/Adapter/MqttServerAdapterClientAcceptedEventArgs.cs diff --git a/Source/MQTTnet/Adapter/ReceivedMqttPacket.cs b/Source/MQTTnet/Adapter/ReceivedMqttPacket.cs new file mode 100644 index 00000000..3a92d4de --- /dev/null +++ b/Source/MQTTnet/Adapter/ReceivedMqttPacket.cs @@ -0,0 +1,17 @@ +using MQTTnet.Serializer; + +namespace MQTTnet.Adapter +{ + public class ReceivedMqttPacket + { + public ReceivedMqttPacket(byte fixedHeader, MqttPacketBodyReader body) + { + FixedHeader = fixedHeader; + Body = body; + } + + public byte FixedHeader { get; } + + public MqttPacketBodyReader Body { get; } + } +} diff --git a/Source/MQTTnet/ApplicationMessagePublisherExtensions.cs b/Source/MQTTnet/ApplicationMessagePublisherExtensions.cs new file mode 100644 index 00000000..81dddbf1 --- /dev/null +++ b/Source/MQTTnet/ApplicationMessagePublisherExtensions.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MQTTnet.Protocol; + +namespace MQTTnet +{ + public static class ApplicationMessagePublisherExtensions + { + public static async Task PublishAsync(this IApplicationMessagePublisher publisher, IEnumerable applicationMessages) + { + if (publisher == null) throw new ArgumentNullException(nameof(publisher)); + if (applicationMessages == null) throw new ArgumentNullException(nameof(applicationMessages)); + + foreach (var applicationMessage in applicationMessages) + { + await publisher.PublishAsync(applicationMessage).ConfigureAwait(false); + } + } + + public static async Task PublishAsync(this IApplicationMessagePublisher publisher, params MqttApplicationMessage[] applicationMessages) + { + if (publisher == null) throw new ArgumentNullException(nameof(publisher)); + if (applicationMessages == null) throw new ArgumentNullException(nameof(applicationMessages)); + + foreach (var applicationMessage in applicationMessages) + { + await publisher.PublishAsync(applicationMessage).ConfigureAwait(false); + } + } + + 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(builder => builder + .WithTopic(topic)); + } + + 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(builder => builder + .WithTopic(topic) + .WithPayload(payload)); + } + + 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(builder => builder + .WithTopic(topic) + .WithPayload(payload) + .WithQualityOfServiceLevel(qualityOfServiceLevel)); + } + + public static Task PublishAsync(this IApplicationMessagePublisher publisher, string topic, string payload, MqttQualityOfServiceLevel qualityOfServiceLevel, bool retain) + { + if (publisher == null) throw new ArgumentNullException(nameof(publisher)); + if (topic == null) throw new ArgumentNullException(nameof(topic)); + + return publisher.PublishAsync(builder => builder + .WithTopic(topic) + .WithPayload(payload) + .WithQualityOfServiceLevel(qualityOfServiceLevel) + .WithRetainFlag(retain)); + } + + public static Task PublishAsync(this IApplicationMessagePublisher publisher, Func builder) + { + var message = builder(new MqttApplicationMessageBuilder()).Build(); + return publisher.PublishAsync(message); + } + } +} diff --git a/Source/MQTTnet/Channel/IMqttChannel.cs b/Source/MQTTnet/Channel/IMqttChannel.cs new file mode 100644 index 00000000..6050bcb1 --- /dev/null +++ b/Source/MQTTnet/Channel/IMqttChannel.cs @@ -0,0 +1,17 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace MQTTnet.Channel +{ + public interface IMqttChannel : IDisposable + { + string Endpoint { get; } + + Task ConnectAsync(CancellationToken cancellationToken); + Task DisconnectAsync(); + + Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken); + Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken); + } +} diff --git a/Frameworks/MQTTnet.NetStandard/Client/IMqttClient.cs b/Source/MQTTnet/Client/IMqttClient.cs similarity index 100% rename from Frameworks/MQTTnet.NetStandard/Client/IMqttClient.cs rename to Source/MQTTnet/Client/IMqttClient.cs diff --git a/Frameworks/MQTTnet.NetStandard/Client/IMqttClientAdapterFactory.cs b/Source/MQTTnet/Client/IMqttClientAdapterFactory.cs similarity index 83% rename from Frameworks/MQTTnet.NetStandard/Client/IMqttClientAdapterFactory.cs rename to Source/MQTTnet/Client/IMqttClientAdapterFactory.cs index c2a7e74b..197858b8 100644 --- a/Frameworks/MQTTnet.NetStandard/Client/IMqttClientAdapterFactory.cs +++ b/Source/MQTTnet/Client/IMqttClientAdapterFactory.cs @@ -5,6 +5,6 @@ namespace MQTTnet.Client { public interface IMqttClientAdapterFactory { - IMqttChannelAdapter CreateClientAdapter(IMqttClientOptions options, IMqttNetLogger logger); + IMqttChannelAdapter CreateClientAdapter(IMqttClientOptions options, IMqttNetChildLogger logger); } } diff --git a/Frameworks/MQTTnet.NetStandard/Client/IMqttClientChannelOptions.cs b/Source/MQTTnet/Client/IMqttClientChannelOptions.cs similarity index 100% rename from Frameworks/MQTTnet.NetStandard/Client/IMqttClientChannelOptions.cs rename to Source/MQTTnet/Client/IMqttClientChannelOptions.cs diff --git a/Frameworks/MQTTnet.NetStandard/Client/IMqttClientCredentials.cs b/Source/MQTTnet/Client/IMqttClientCredentials.cs similarity index 100% rename from Frameworks/MQTTnet.NetStandard/Client/IMqttClientCredentials.cs rename to Source/MQTTnet/Client/IMqttClientCredentials.cs diff --git a/Frameworks/MQTTnet.NetStandard/Client/IMqttClientFactory.cs b/Source/MQTTnet/Client/IMqttClientFactory.cs similarity index 55% rename from Frameworks/MQTTnet.NetStandard/Client/IMqttClientFactory.cs rename to Source/MQTTnet/Client/IMqttClientFactory.cs index 444c16fc..a15b499b 100644 --- a/Frameworks/MQTTnet.NetStandard/Client/IMqttClientFactory.cs +++ b/Source/MQTTnet/Client/IMqttClientFactory.cs @@ -1,5 +1,4 @@ using MQTTnet.Diagnostics; -using MQTTnet.ManagedClient; namespace MQTTnet.Client { @@ -8,9 +7,5 @@ namespace MQTTnet.Client IMqttClient CreateMqttClient(); IMqttClient CreateMqttClient(IMqttNetLogger logger); - - IManagedMqttClient CreateManagedMqttClient(); - - IManagedMqttClient CreateManagedMqttClient(IMqttNetLogger logger); } } \ No newline at end of file diff --git a/Frameworks/MQTTnet.NetStandard/Client/IMqttClientOptions.cs b/Source/MQTTnet/Client/IMqttClientOptions.cs similarity index 84% rename from Frameworks/MQTTnet.NetStandard/Client/IMqttClientOptions.cs rename to Source/MQTTnet/Client/IMqttClientOptions.cs index aa873af0..176f5e43 100644 --- a/Frameworks/MQTTnet.NetStandard/Client/IMqttClientOptions.cs +++ b/Source/MQTTnet/Client/IMqttClientOptions.cs @@ -14,8 +14,7 @@ namespace MQTTnet.Client TimeSpan CommunicationTimeout { get; } TimeSpan KeepAlivePeriod { get; } TimeSpan? KeepAliveSendInterval { get; } - MqttReceivedApplicationMessageProcessingMode ReceivedApplicationMessageProcessingMode { get; } - + MqttApplicationMessage WillMessage { get; } } } \ No newline at end of file diff --git a/Frameworks/MQTTnet.NetStandard/Client/MqttClient.cs b/Source/MQTTnet/Client/MqttClient.cs similarity index 61% rename from Frameworks/MQTTnet.NetStandard/Client/MqttClient.cs rename to Source/MQTTnet/Client/MqttClient.cs index ec863563..ccedb5ae 100644 --- a/Frameworks/MQTTnet.NetStandard/Client/MqttClient.cs +++ b/Source/MQTTnet/Client/MqttClient.cs @@ -17,23 +17,25 @@ namespace MQTTnet.Client { private readonly MqttPacketIdentifierProvider _packetIdentifierProvider = new MqttPacketIdentifierProvider(); private readonly Stopwatch _sendTracker = new Stopwatch(); - private readonly SemaphoreSlim _disconnectLock = new SemaphoreSlim(1, 1); + private readonly object _disconnectLock = new object(); + private readonly MqttPacketDispatcher _packetDispatcher = new MqttPacketDispatcher(); + private readonly IMqttClientAdapterFactory _adapterFactory; - private readonly MqttPacketDispatcher _packetDispatcher; - private readonly IMqttNetLogger _logger; + private readonly IMqttNetChildLogger _logger; private IMqttClientOptions _options; private CancellationTokenSource _cancellationTokenSource; private Task _packetReceiverTask; private Task _keepAliveMessageSenderTask; private IMqttChannelAdapter _adapter; + private bool _cleanDisconnectInitiated; public MqttClient(IMqttClientAdapterFactory channelFactory, IMqttNetLogger logger) { - _adapterFactory = channelFactory ?? throw new ArgumentNullException(nameof(channelFactory)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + if (logger == null) throw new ArgumentNullException(nameof(logger)); - _packetDispatcher = new MqttPacketDispatcher(logger); + _adapterFactory = channelFactory ?? throw new ArgumentNullException(nameof(channelFactory)); + _logger = logger.CreateChildLogger(nameof(MqttClient)); } public event EventHandler Connected; @@ -51,21 +53,21 @@ namespace MQTTnet.Client try { - _options = options; _cancellationTokenSource = new CancellationTokenSource(); + _options = options; _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."); + _logger.Verbose($"Trying to connect with server ({_options.ChannelOptions})."); + await _adapter.ConnectAsync(_options.CommunicationTimeout, _cancellationTokenSource.Token).ConfigureAwait(false); + _logger.Verbose("Connection with server established."); StartReceivingPackets(_cancellationTokenSource.Token); var connectResponse = await AuthenticateAsync(options.WillMessage, _cancellationTokenSource.Token).ConfigureAwait(false); - _logger.Verbose("MQTT connection with server established."); + _logger.Verbose("MQTT connection with server established."); _sendTracker.Restart(); @@ -77,12 +79,12 @@ namespace MQTTnet.Client IsConnected = true; Connected?.Invoke(this, new MqttClientConnectedEventArgs(connectResponse.IsSessionPresent)); - _logger.Info("Connected."); + _logger.Info("Connected."); return new MqttClientConnectResult(connectResponse.IsSessionPresent); } catch (Exception exception) { - _logger.Error(exception, "Error while connecting with server."); + _logger.Error(exception, "Error while connecting with server."); await DisconnectInternalAsync(null, exception).ConfigureAwait(false); throw; @@ -91,14 +93,11 @@ namespace MQTTnet.Client public async Task DisconnectAsync() { - if (!IsConnected) - { - return; - } - try { - if (!_cancellationTokenSource.IsCancellationRequested) + _cleanDisconnectInitiated = true; + + if (IsConnected && !_cancellationTokenSource.IsCancellationRequested) { await SendAsync(new MqttDisconnectPacket(), _cancellationTokenSource.Token).ConfigureAwait(false); } @@ -146,55 +145,32 @@ namespace MQTTnet.Client await SendAndReceiveAsync(unsubscribePacket, _cancellationTokenSource.Token).ConfigureAwait(false); } - public async Task PublishAsync(IEnumerable applicationMessages) + public Task PublishAsync(MqttApplicationMessage applicationMessage) { ThrowIfNotConnected(); - var publishPackets = applicationMessages.Select(m => m.ToPublishPacket()); - var packetGroups = publishPackets.GroupBy(p => p.QualityOfServiceLevel).OrderBy(g => g.Key); + var publishPacket = applicationMessage.ToPublishPacket(); - foreach (var qosGroup in packetGroups) + switch (applicationMessage.QualityOfServiceLevel) { - switch (qosGroup.Key) - { - case MqttQualityOfServiceLevel.AtMostOnce: - { - // No packet identifier is used for QoS 0 [3.3.2.2 Packet Identifier] - await SendAsync(qosGroup, _cancellationTokenSource.Token).ConfigureAwait(false); - break; - } - case MqttQualityOfServiceLevel.AtLeastOnce: - { - foreach (var publishPacket in qosGroup) - { - publishPacket.PacketIdentifier = _packetIdentifierProvider.GetNewPacketIdentifier(); - await SendAndReceiveAsync(publishPacket, _cancellationTokenSource.Token).ConfigureAwait(false); - } - - break; - } - case MqttQualityOfServiceLevel.ExactlyOnce: - { - foreach (var publishPacket in qosGroup) - { - publishPacket.PacketIdentifier = _packetIdentifierProvider.GetNewPacketIdentifier(); - - var pubRecPacket = await SendAndReceiveAsync(publishPacket, _cancellationTokenSource.Token).ConfigureAwait(false); - var pubRelPacket = new MqttPubRelPacket - { - PacketIdentifier = pubRecPacket.PacketIdentifier - }; - - await SendAndReceiveAsync(pubRelPacket, _cancellationTokenSource.Token).ConfigureAwait(false); - } - - break; - } - default: - { - throw new InvalidOperationException(); - } - } + case MqttQualityOfServiceLevel.AtMostOnce: + { + // No packet identifier is used for QoS 0 [3.3.2.2 Packet Identifier] + return SendAsync(publishPacket, _cancellationTokenSource.Token); + } + case MqttQualityOfServiceLevel.AtLeastOnce: + { + publishPacket.PacketIdentifier = _packetIdentifierProvider.GetNewPacketIdentifier(); + return SendAndReceiveAsync(publishPacket, _cancellationTokenSource.Token); + } + case MqttQualityOfServiceLevel.ExactlyOnce: + { + return PublishExactlyOnce(publishPacket, _cancellationTokenSource.Token); + } + default: + { + throw new NotSupportedException(); + } } } @@ -218,7 +194,7 @@ namespace MQTTnet.Client WillMessage = willApplicationMessage }; - var response = await SendAndReceiveAsync(connectPacket, _cancellationTokenSource.Token).ConfigureAwait(false); + var response = await SendAndReceiveAsync(connectPacket, cancellationToken).ConfigureAwait(false); if (response.ConnectReturnCode != MqttConnectReturnCode.ConnectionAccepted) { throw new MqttConnectingFailedException(response.ConnectReturnCode); @@ -239,24 +215,7 @@ namespace MQTTnet.Client 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(); - } + InitiateDisconnect(); var clientWasConnected = IsConnected; IsConnected = false; @@ -266,21 +225,16 @@ namespace MQTTnet.Client await WaitForTaskAsync(_packetReceiverTask, sender).ConfigureAwait(false); await WaitForTaskAsync(_keepAliveMessageSenderTask, sender).ConfigureAwait(false); - if (_keepAliveMessageSenderTask != null && _keepAliveMessageSenderTask != sender) - { - await _keepAliveMessageSenderTask.ConfigureAwait(false); - } - if (_adapter != null) { - await _adapter.DisconnectAsync(_options.CommunicationTimeout).ConfigureAwait(false); + await _adapter.DisconnectAsync(_options.CommunicationTimeout, CancellationToken.None).ConfigureAwait(false); } - _logger.Verbose("Disconnected from adapter."); + _logger.Verbose("Disconnected from adapter."); } catch (Exception adapterException) { - _logger.Warning(adapterException, "Error while disconnecting from adapter."); + _logger.Warning(adapterException, "Error while disconnecting from adapter."); } finally { @@ -288,41 +242,76 @@ namespace MQTTnet.Client _adapter = null; _cancellationTokenSource?.Dispose(); _cancellationTokenSource = null; + _cleanDisconnectInitiated = false; - _logger.Info("Disconnected."); + _logger.Info("Disconnected."); Disconnected?.Invoke(this, new MqttClientDisconnectedEventArgs(clientWasConnected, exception)); } } + private void InitiateDisconnect() + { + lock (_disconnectLock) + { + try + { + if (_cancellationTokenSource == null || _cancellationTokenSource.IsCancellationRequested) + { + return; + } + + _cancellationTokenSource.Cancel(false); + } + catch (Exception adapterException) + { + _logger.Warning(adapterException, "Error while initiating disconnect."); + } + } + } + private Task SendAsync(MqttBasePacket packet, CancellationToken cancellationToken) { - _sendTracker.Restart(); - return _adapter.SendPacketsAsync(_options.CommunicationTimeout, cancellationToken, new[] { packet }); - } + cancellationToken.ThrowIfCancellationRequested(); - private Task SendAsync(IEnumerable packets, CancellationToken cancellationToken) - { _sendTracker.Restart(); - return _adapter.SendPacketsAsync(_options.CommunicationTimeout, cancellationToken, packets); + + return _adapter.SendPacketAsync(packet, cancellationToken); } private async Task SendAndReceiveAsync(MqttBasePacket requestPacket, CancellationToken cancellationToken) where TResponsePacket : MqttBasePacket { - ushort? identifier = null; - if (requestPacket is IMqttPacketWithIdentifier requestPacketWithIdentifier) + cancellationToken.ThrowIfCancellationRequested(); + + _sendTracker.Restart(); + + ushort identifier = 0; + if (requestPacket is IMqttPacketWithIdentifier packetWithIdentifier && packetWithIdentifier.PacketIdentifier.HasValue) { - identifier = requestPacketWithIdentifier.PacketIdentifier; + identifier = packetWithIdentifier.PacketIdentifier.Value; } - var packetAwaiter = _packetDispatcher.WaitForPacketAsync(typeof(TResponsePacket), identifier, _options.CommunicationTimeout); - await SendAsync(requestPacket, cancellationToken).ConfigureAwait(false); + var packetAwaiter = _packetDispatcher.AddPacketAwaiter(identifier); + try + { + await _adapter.SendPacketAsync(requestPacket, cancellationToken).ConfigureAwait(false); + var respone = await Internal.TaskExtensions.TimeoutAfterAsync(ct => packetAwaiter.Task, _options.CommunicationTimeout, cancellationToken).ConfigureAwait(false); - return (TResponsePacket)await packetAwaiter.ConfigureAwait(false); + return (TResponsePacket)respone; + } + catch (MqttCommunicationTimedOutException) + { + _logger.Warning(null, "Timeout while waiting for packet of type '{0}'.", typeof(TResponsePacket).Namespace); + throw; + } + finally + { + _packetDispatcher.RemovePacketAwaiter(identifier); + } } private async Task SendKeepAliveMessagesAsync(CancellationToken cancellationToken) { - _logger.Verbose("Start sending keep alive packets."); + _logger.Verbose("Start sending keep alive packets."); try { @@ -334,122 +323,110 @@ namespace MQTTnet.Client keepAliveSendInterval = _options.KeepAliveSendInterval.Value; } - if (_sendTracker.Elapsed > keepAliveSendInterval) + var waitTime = keepAliveSendInterval - _sendTracker.Elapsed; + if (waitTime <= TimeSpan.Zero) { await SendAndReceiveAsync(new MqttPingReqPacket(), cancellationToken).ConfigureAwait(false); + waitTime = keepAliveSendInterval; } - await Task.Delay(keepAliveSendInterval, cancellationToken).ConfigureAwait(false); + await Task.Delay(waitTime, cancellationToken).ConfigureAwait(false); } } catch (Exception exception) { + if (_cleanDisconnectInitiated) + { + return; + } + if (exception is OperationCanceledException) { } else if (exception is MqttCommunicationException) { - _logger.Warning(exception, "MQTT communication exception while sending/receiving keep alive packets."); + _logger.Warning(exception, "MQTT communication exception while sending/receiving keep alive packets."); } else { - _logger.Error(exception, "Unhandled exception while sending/receiving keep alive packets."); + _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."); + _logger.Verbose("Stopped sending keep alive packets."); } } private async Task ReceivePacketsAsync(CancellationToken cancellationToken) { - _logger.Verbose("Start receiving packets."); + _logger.Verbose("Start receiving packets."); try { while (!cancellationToken.IsCancellationRequested) { var packet = await _adapter.ReceivePacketAsync(TimeSpan.Zero, cancellationToken).ConfigureAwait(false); - - if (cancellationToken.IsCancellationRequested) - { - return; - } - - if (packet == null) - { - continue; - } - - if (_options.ReceivedApplicationMessageProcessingMode == MqttReceivedApplicationMessageProcessingMode.SingleThread) + if (packet != null) { await ProcessReceivedPacketAsync(packet, cancellationToken).ConfigureAwait(false); } - else if (_options.ReceivedApplicationMessageProcessingMode == MqttReceivedApplicationMessageProcessingMode.DedicatedThread) - { - StartProcessReceivedPacketAsync(packet, cancellationToken); - } } } catch (Exception exception) { + if (_cleanDisconnectInitiated) + { + return; + } + if (exception is OperationCanceledException) { } else if (exception is MqttCommunicationException) { - _logger.Warning(exception, "MQTT communication exception while receiving packets."); + _logger.Warning(exception, "MQTT communication exception while receiving packets."); } else { - _logger.Error(exception, "Unhandled exception while receiving packets."); + _logger.Error(exception, "Unhandled exception while receiving packets."); } + _packetDispatcher.Dispatch(exception); await DisconnectInternalAsync(_packetReceiverTask, exception).ConfigureAwait(false); } finally { - _logger.Verbose("Stopped receiving packets."); + _logger.Verbose("Stopped receiving packets."); } } - private async Task ProcessReceivedPacketAsync(MqttBasePacket packet, CancellationToken cancellationToken) + private Task ProcessReceivedPacketAsync(MqttBasePacket packet, CancellationToken cancellationToken) { - try + if (packet is MqttPublishPacket publishPacket) { - if (packet is MqttPublishPacket publishPacket) - { - await ProcessReceivedPublishPacketAsync(publishPacket, cancellationToken).ConfigureAwait(false); - return; - } - - if (packet is MqttPingReqPacket) - { - await SendAsync(new MqttPingRespPacket(), cancellationToken).ConfigureAwait(false); - return; - } - - if (packet is MqttDisconnectPacket) - { - await DisconnectAsync().ConfigureAwait(false); - return; - } - - if (packet is MqttPubRelPacket pubRelPacket) - { - await ProcessReceivedPubRelPacket(pubRelPacket, cancellationToken).ConfigureAwait(false); - return; - } - - _packetDispatcher.Dispatch(packet); + return ProcessReceivedPublishPacketAsync(publishPacket, cancellationToken); } - catch (Exception exception) + + if (packet is MqttPingReqPacket) { - _logger.Error(exception, "Unhandled exception while processing received packet."); + return SendAsync(new MqttPingRespPacket(), cancellationToken); } + + if (packet is MqttDisconnectPacket) + { + return DisconnectAsync(); + } + + if (packet is MqttPubRelPacket pubRelPacket) + { + return ProcessReceivedPubRelPacket(pubRelPacket, cancellationToken); + } + + _packetDispatcher.Dispatch(packet); + return Task.FromResult(0); } private Task ProcessReceivedPublishPacketAsync(MqttPublishPacket publishPacket, CancellationToken cancellationToken) @@ -468,7 +445,7 @@ namespace MQTTnet.Client if (publishPacket.QualityOfServiceLevel == MqttQualityOfServiceLevel.ExactlyOnce) { - // QoS 2 is implement as method "B" [4.3.3 QoS 2: Exactly once delivery] + // QoS 2 is implement as method "B" (4.3.3 QoS 2: Exactly once delivery) FireApplicationMessageReceivedEvent(publishPacket); return SendAsync(new MqttPubRecPacket { PacketIdentifier = publishPacket.PacketIdentifier }, cancellationToken); } @@ -486,21 +463,31 @@ namespace MQTTnet.Client return SendAsync(response, cancellationToken); } + private async Task PublishExactlyOnce(MqttPublishPacket publishPacket, CancellationToken cancellationToken) + { + publishPacket.PacketIdentifier = _packetIdentifierProvider.GetNewPacketIdentifier(); + + var pubRecPacket = await SendAndReceiveAsync(publishPacket, cancellationToken).ConfigureAwait(false); + var pubRelPacket = new MqttPubRelPacket + { + PacketIdentifier = pubRecPacket.PacketIdentifier + }; + + await SendAndReceiveAsync(pubRelPacket, cancellationToken).ConfigureAwait(false); + } + private void StartReceivingPackets(CancellationToken cancellationToken) { - _packetReceiverTask = Task.Run(() => ReceivePacketsAsync(cancellationToken), cancellationToken); + _packetReceiverTask = Task.Run( + () => ReceivePacketsAsync(cancellationToken), + cancellationToken); } private void StartSendingKeepAliveMessages(CancellationToken cancellationToken) { - _keepAliveMessageSenderTask = Task.Run(() => SendKeepAliveMessagesAsync(cancellationToken), cancellationToken); - } - - private void StartProcessReceivedPacketAsync(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), cancellationToken); -#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + _keepAliveMessageSenderTask = Task.Run( + () => SendKeepAliveMessagesAsync(cancellationToken), + cancellationToken); } private void FireApplicationMessageReceivedEvent(MqttPublishPacket publishPacket) @@ -512,13 +499,13 @@ namespace MQTTnet.Client } catch (Exception exception) { - _logger.Error(exception, "Unhandled exception while handling application message."); + _logger.Error(exception, "Unhandled exception while handling application message."); } } private static async Task WaitForTaskAsync(Task task, Task sender) { - if (task == sender) + if (task == sender || task == null) { return; } @@ -532,7 +519,7 @@ namespace MQTTnet.Client { await task.ConfigureAwait(false); } - catch (TaskCanceledException) + catch (OperationCanceledException) { } } diff --git a/Frameworks/MQTTnet.NetStandard/Client/MqttClientConnectResult.cs b/Source/MQTTnet/Client/MqttClientConnectResult.cs similarity index 100% rename from Frameworks/MQTTnet.NetStandard/Client/MqttClientConnectResult.cs rename to Source/MQTTnet/Client/MqttClientConnectResult.cs diff --git a/Frameworks/MQTTnet.NetStandard/Client/MqttClientConnectedEventArgs.cs b/Source/MQTTnet/Client/MqttClientConnectedEventArgs.cs similarity index 100% rename from Frameworks/MQTTnet.NetStandard/Client/MqttClientConnectedEventArgs.cs rename to Source/MQTTnet/Client/MqttClientConnectedEventArgs.cs diff --git a/Frameworks/MQTTnet.NetStandard/Client/MqttClientCredentials.cs b/Source/MQTTnet/Client/MqttClientCredentials.cs similarity index 100% rename from Frameworks/MQTTnet.NetStandard/Client/MqttClientCredentials.cs rename to Source/MQTTnet/Client/MqttClientCredentials.cs diff --git a/Frameworks/MQTTnet.NetStandard/Client/MqttClientDisconnectedEventArgs.cs b/Source/MQTTnet/Client/MqttClientDisconnectedEventArgs.cs similarity index 100% rename from Frameworks/MQTTnet.NetStandard/Client/MqttClientDisconnectedEventArgs.cs rename to Source/MQTTnet/Client/MqttClientDisconnectedEventArgs.cs diff --git a/Frameworks/MQTTnet.NetStandard/Client/MqttClientExtensions.cs b/Source/MQTTnet/Client/MqttClientExtensions.cs similarity index 100% rename from Frameworks/MQTTnet.NetStandard/Client/MqttClientExtensions.cs rename to Source/MQTTnet/Client/MqttClientExtensions.cs diff --git a/Frameworks/MQTTnet.NetStandard/Client/MqttClientOptions.cs b/Source/MQTTnet/Client/MqttClientOptions.cs similarity index 82% rename from Frameworks/MQTTnet.NetStandard/Client/MqttClientOptions.cs rename to Source/MQTTnet/Client/MqttClientOptions.cs index ae0517a6..7d3ad5d8 100644 --- a/Frameworks/MQTTnet.NetStandard/Client/MqttClientOptions.cs +++ b/Source/MQTTnet/Client/MqttClientOptions.cs @@ -14,8 +14,7 @@ namespace MQTTnet.Client public TimeSpan CommunicationTimeout { get; set; } = TimeSpan.FromSeconds(10); public TimeSpan KeepAlivePeriod { get; set; } = TimeSpan.FromSeconds(15); public TimeSpan? KeepAliveSendInterval { get; set; } - public MqttReceivedApplicationMessageProcessingMode ReceivedApplicationMessageProcessingMode { get; set; } = MqttReceivedApplicationMessageProcessingMode.SingleThread; - + public MqttApplicationMessage WillMessage { get; set; } } } diff --git a/Frameworks/MQTTnet.NetStandard/Client/MqttClientOptionsBuilder.cs b/Source/MQTTnet/Client/MqttClientOptionsBuilder.cs similarity index 94% rename from Frameworks/MQTTnet.NetStandard/Client/MqttClientOptionsBuilder.cs rename to Source/MQTTnet/Client/MqttClientOptionsBuilder.cs index bc6aa9f6..985cda7c 100644 --- a/Frameworks/MQTTnet.NetStandard/Client/MqttClientOptionsBuilder.cs +++ b/Source/MQTTnet/Client/MqttClientOptionsBuilder.cs @@ -36,6 +36,12 @@ namespace MQTTnet.Client return this; } + public MqttClientOptionsBuilder WithKeepAliveSendInterval(TimeSpan value) + { + _options.KeepAliveSendInterval = value; + return this; + } + public MqttClientOptionsBuilder WithClientId(string value) { _options.ClientId = value; @@ -108,13 +114,6 @@ namespace MQTTnet.Client return this; } - public MqttClientOptionsBuilder WithReceivedApplicationMessageProcessingMode( - MqttReceivedApplicationMessageProcessingMode mode) - { - _options.ReceivedApplicationMessageProcessingMode = mode; - return this; - } - public IMqttClientOptions Build() { if (_tlsOptions != null) diff --git a/Frameworks/MQTTnet.NetStandard/Client/MqttClientTcpOptions.cs b/Source/MQTTnet/Client/MqttClientTcpOptions.cs similarity index 63% rename from Frameworks/MQTTnet.NetStandard/Client/MqttClientTcpOptions.cs rename to Source/MQTTnet/Client/MqttClientTcpOptions.cs index ac96ce63..4569be33 100644 --- a/Frameworks/MQTTnet.NetStandard/Client/MqttClientTcpOptions.cs +++ b/Source/MQTTnet/Client/MqttClientTcpOptions.cs @@ -6,8 +6,13 @@ public int? Port { get; set; } - public int BufferSize { get; set; } = 20 * 4096; + public int BufferSize { get; set; } = 4096; public MqttClientTlsOptions TlsOptions { get; set; } = new MqttClientTlsOptions(); + + public override string ToString() + { + return Server + ":" + this.GetPort(); + } } } diff --git a/Frameworks/MQTTnet.NetStandard/Client/MqttClientTcpOptionsExtensions.cs b/Source/MQTTnet/Client/MqttClientTcpOptionsExtensions.cs similarity index 100% rename from Frameworks/MQTTnet.NetStandard/Client/MqttClientTcpOptionsExtensions.cs rename to Source/MQTTnet/Client/MqttClientTcpOptionsExtensions.cs diff --git a/Frameworks/MQTTnet.NetStandard/Client/MqttClientTlsOptions.cs b/Source/MQTTnet/Client/MqttClientTlsOptions.cs similarity index 58% rename from Frameworks/MQTTnet.NetStandard/Client/MqttClientTlsOptions.cs rename to Source/MQTTnet/Client/MqttClientTlsOptions.cs index 6478b267..2536b231 100644 --- a/Frameworks/MQTTnet.NetStandard/Client/MqttClientTlsOptions.cs +++ b/Source/MQTTnet/Client/MqttClientTlsOptions.cs @@ -1,4 +1,7 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; namespace MQTTnet.Client { @@ -13,5 +16,7 @@ namespace MQTTnet.Client public bool AllowUntrustedCertificates { get; set; } public List Certificates { get; set; } + + public Func CertificateValidationCallback { get; set; } } } diff --git a/Frameworks/MQTTnet.NetStandard/Client/MqttClientWebSocketOptions.cs b/Source/MQTTnet/Client/MqttClientWebSocketOptions.cs similarity index 85% rename from Frameworks/MQTTnet.NetStandard/Client/MqttClientWebSocketOptions.cs rename to Source/MQTTnet/Client/MqttClientWebSocketOptions.cs index 114283d4..a4dd0d5c 100644 --- a/Frameworks/MQTTnet.NetStandard/Client/MqttClientWebSocketOptions.cs +++ b/Source/MQTTnet/Client/MqttClientWebSocketOptions.cs @@ -14,5 +14,10 @@ namespace MQTTnet.Client public CookieContainer CookieContainer { get; set; } public MqttClientTlsOptions TlsOptions { get; set; } = new MqttClientTlsOptions(); + + public override string ToString() + { + return Uri; + } } } diff --git a/Source/MQTTnet/Client/MqttPacketDispatcher.cs b/Source/MQTTnet/Client/MqttPacketDispatcher.cs new file mode 100644 index 00000000..fd205c51 --- /dev/null +++ b/Source/MQTTnet/Client/MqttPacketDispatcher.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Concurrent; +using System.Threading.Tasks; +using MQTTnet.Packets; + +namespace MQTTnet.Client +{ + public class MqttPacketDispatcher + { + private readonly ConcurrentDictionary, TaskCompletionSource> _awaiters = new ConcurrentDictionary, TaskCompletionSource>(); + + public void Dispatch(Exception exception) + { + foreach (var awaiter in _awaiters) + { + Task.Run(() => awaiter.Value.TrySetException(exception)); // Task.Run fixes a dead lock. Without this the client only receives one message. + } + + _awaiters.Clear(); + } + + public void Dispatch(MqttBasePacket packet) + { + if (packet == null) throw new ArgumentNullException(nameof(packet)); + + ushort identifier = 0; + if (packet is IMqttPacketWithIdentifier packetWithIdentifier && packetWithIdentifier.PacketIdentifier.HasValue) + { + identifier = packetWithIdentifier.PacketIdentifier.Value; + } + + var type = packet.GetType(); + var key = new Tuple(identifier, type); + + if (_awaiters.TryRemove(key, out var awaiter)) + { + Task.Run(() => awaiter.TrySetResult(packet)); // Task.Run fixes a dead lock. Without this the client only receives one message. + return; + } + + throw new InvalidOperationException($"Packet of type '{type.Name}' not handled or dispatched."); + } + + public void Reset() + { + _awaiters.Clear(); + } + + public TaskCompletionSource AddPacketAwaiter(ushort? identifier) where TResponsePacket : MqttBasePacket + { + var tcs = new TaskCompletionSource(); + + if (!identifier.HasValue) + { + identifier = 0; + } + + var key = new Tuple(identifier ?? 0, typeof(TResponsePacket)); + if (!_awaiters.TryAdd(key, tcs)) + { + throw new InvalidOperationException($"The packet dispatcher already has an awaiter for packet of type '{key.Item2.Name}' with identifier {key.Item1}."); + } + + return tcs; + } + + public void RemovePacketAwaiter(ushort? identifier) where TResponsePacket : MqttBasePacket + { + if (!identifier.HasValue) + { + identifier = 0; + } + + var key = new Tuple(identifier ?? 0, typeof(TResponsePacket)); + _awaiters.TryRemove(key, out _); + } + } +} \ No newline at end of file diff --git a/Frameworks/MQTTnet.NetStandard/Client/MqttPacketIdentifierProvider.cs b/Source/MQTTnet/Client/MqttPacketIdentifierProvider.cs similarity index 100% rename from Frameworks/MQTTnet.NetStandard/Client/MqttPacketIdentifierProvider.cs rename to Source/MQTTnet/Client/MqttPacketIdentifierProvider.cs diff --git a/Frameworks/MQTTnet.NetStandard/Client/MqttSubscribeResult.cs b/Source/MQTTnet/Client/MqttSubscribeResult.cs similarity index 100% rename from Frameworks/MQTTnet.NetStandard/Client/MqttSubscribeResult.cs rename to Source/MQTTnet/Client/MqttSubscribeResult.cs diff --git a/Source/MQTTnet/Diagnostics/IMqttNetChildLogger.cs b/Source/MQTTnet/Diagnostics/IMqttNetChildLogger.cs new file mode 100644 index 00000000..df99f443 --- /dev/null +++ b/Source/MQTTnet/Diagnostics/IMqttNetChildLogger.cs @@ -0,0 +1,17 @@ +using System; + +namespace MQTTnet.Diagnostics +{ + public interface IMqttNetChildLogger + { + IMqttNetChildLogger CreateChildLogger(string source = null); + + void Verbose(string message, params object[] parameters); + + void Info(string message, params object[] parameters); + + void Warning(Exception exception, string message, params object[] parameters); + + void Error(Exception exception, string message, params object[] parameters); + } +} diff --git a/Source/MQTTnet/Diagnostics/IMqttNetLogger.cs b/Source/MQTTnet/Diagnostics/IMqttNetLogger.cs new file mode 100644 index 00000000..73b6fcbd --- /dev/null +++ b/Source/MQTTnet/Diagnostics/IMqttNetLogger.cs @@ -0,0 +1,13 @@ +using System; + +namespace MQTTnet.Diagnostics +{ + public interface IMqttNetLogger + { + event EventHandler LogMessagePublished; + + IMqttNetChildLogger CreateChildLogger(string source = null); + + void Publish(MqttNetLogLevel logLevel, string source, string message, object[] parameters, Exception exception); + } +} diff --git a/Source/MQTTnet/Diagnostics/MqttNetChildLogger.cs b/Source/MQTTnet/Diagnostics/MqttNetChildLogger.cs new file mode 100644 index 00000000..37334543 --- /dev/null +++ b/Source/MQTTnet/Diagnostics/MqttNetChildLogger.cs @@ -0,0 +1,51 @@ +using System; + +namespace MQTTnet.Diagnostics +{ + public class MqttNetChildLogger : IMqttNetChildLogger + { + private readonly IMqttNetLogger _logger; + private readonly string _source; + + public MqttNetChildLogger(IMqttNetLogger logger, string source) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _source = source; + } + + public IMqttNetChildLogger CreateChildLogger(string source) + { + string childSource; + if (!string.IsNullOrEmpty(_source)) + { + childSource = _source + "." + source; + } + else + { + childSource = source; + } + + return new MqttNetChildLogger(_logger, childSource); + } + + public void Verbose(string message, params object[] parameters) + { + _logger.Publish(MqttNetLogLevel.Verbose, _source, message, parameters, null); + } + + public void Info(string message, params object[] parameters) + { + _logger.Publish(MqttNetLogLevel.Info, _source, message, parameters, null); + } + + public void Warning(Exception exception, string message, params object[] parameters) + { + _logger.Publish(MqttNetLogLevel.Warning, _source, message, parameters, exception); + } + + public void Error(Exception exception, string message, params object[] parameters) + { + _logger.Publish(MqttNetLogLevel.Error, _source, message, parameters, exception); + } + } +} diff --git a/Frameworks/MQTTnet.NetStandard/Diagnostics/MqttNetGlobalLogger.cs b/Source/MQTTnet/Diagnostics/MqttNetGlobalLogger.cs similarity index 100% rename from Frameworks/MQTTnet.NetStandard/Diagnostics/MqttNetGlobalLogger.cs rename to Source/MQTTnet/Diagnostics/MqttNetGlobalLogger.cs diff --git a/Frameworks/MQTTnet.NetStandard/Diagnostics/MqttNetLogLevel.cs b/Source/MQTTnet/Diagnostics/MqttNetLogLevel.cs similarity index 100% rename from Frameworks/MQTTnet.NetStandard/Diagnostics/MqttNetLogLevel.cs rename to Source/MQTTnet/Diagnostics/MqttNetLogLevel.cs diff --git a/Frameworks/MQTTnet.NetStandard/Diagnostics/MqttNetLogMessage.cs b/Source/MQTTnet/Diagnostics/MqttNetLogMessage.cs similarity index 96% rename from Frameworks/MQTTnet.NetStandard/Diagnostics/MqttNetLogMessage.cs rename to Source/MQTTnet/Diagnostics/MqttNetLogMessage.cs index 6035a89c..107d8f34 100644 --- a/Frameworks/MQTTnet.NetStandard/Diagnostics/MqttNetLogMessage.cs +++ b/Source/MQTTnet/Diagnostics/MqttNetLogMessage.cs @@ -2,7 +2,7 @@ namespace MQTTnet.Diagnostics { - public sealed class MqttNetLogMessage + public class MqttNetLogMessage { public MqttNetLogMessage(string logId, DateTime timestamp, int threadId, string source, MqttNetLogLevel level, string message, Exception exception) { diff --git a/Frameworks/MQTTnet.NetStandard/Diagnostics/MqttNetLogMessagePublishedEventArgs.cs b/Source/MQTTnet/Diagnostics/MqttNetLogMessagePublishedEventArgs.cs similarity index 81% rename from Frameworks/MQTTnet.NetStandard/Diagnostics/MqttNetLogMessagePublishedEventArgs.cs rename to Source/MQTTnet/Diagnostics/MqttNetLogMessagePublishedEventArgs.cs index 2407752b..8a0c0fcb 100644 --- a/Frameworks/MQTTnet.NetStandard/Diagnostics/MqttNetLogMessagePublishedEventArgs.cs +++ b/Source/MQTTnet/Diagnostics/MqttNetLogMessagePublishedEventArgs.cs @@ -2,7 +2,7 @@ namespace MQTTnet.Diagnostics { - public sealed class MqttNetLogMessagePublishedEventArgs : EventArgs + public class MqttNetLogMessagePublishedEventArgs : EventArgs { public MqttNetLogMessagePublishedEventArgs(MqttNetLogMessage logMessage) { diff --git a/Source/MQTTnet/Diagnostics/MqttNetLogger.cs b/Source/MQTTnet/Diagnostics/MqttNetLogger.cs new file mode 100644 index 00000000..56a664e4 --- /dev/null +++ b/Source/MQTTnet/Diagnostics/MqttNetLogger.cs @@ -0,0 +1,56 @@ +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 IMqttNetChildLogger CreateChildLogger(string source = null) + { + return new MqttNetChildLogger(this, source); + } + + public void Publish(MqttNetLogLevel logLevel, string source, string message, object[] parameters, Exception exception) + { + var hasLocalListeners = LogMessagePublished != null; + var hasGlobalListeners = MqttNetGlobalLogger.HasListeners; + + if (!hasLocalListeners && !hasGlobalListeners) + { + return; + } + + if (parameters?.Length > 0) + { + try + { + message = string.Format(message, parameters); + } + catch + { + message = "MESSAGE FORMAT INVALID: " + message; + } + } + + var traceMessage = new MqttNetLogMessage(_logId, DateTime.UtcNow, Environment.CurrentManagedThreadId, source, 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/Source/MQTTnet/Diagnostics/TargetFrameworkInfoProvider.cs similarity index 91% rename from Frameworks/MQTTnet.NetStandard/Diagnostics/TargetFrameworkInfoProvider.cs rename to Source/MQTTnet/Diagnostics/TargetFrameworkInfoProvider.cs index 52582769..efbf08b8 100644 --- a/Frameworks/MQTTnet.NetStandard/Diagnostics/TargetFrameworkInfoProvider.cs +++ b/Source/MQTTnet/Diagnostics/TargetFrameworkInfoProvider.cs @@ -10,6 +10,8 @@ return "net452"; #elif NET461 return "net461"; +#elif NET472 + return "net472"; #elif NETSTANDARD1_3 return "netstandard1.3"; #elif NETSTANDARD2_0 diff --git a/Source/MQTTnet/Exceptions/MqttCommunicationClosedGracefullyException.cs b/Source/MQTTnet/Exceptions/MqttCommunicationClosedGracefullyException.cs new file mode 100644 index 00000000..21a66b90 --- /dev/null +++ b/Source/MQTTnet/Exceptions/MqttCommunicationClosedGracefullyException.cs @@ -0,0 +1,6 @@ +namespace MQTTnet.Exceptions +{ + public class MqttCommunicationClosedGracefullyException : MqttCommunicationException + { + } +} diff --git a/Frameworks/MQTTnet.NetStandard/Exceptions/MqttCommunicationException.cs b/Source/MQTTnet/Exceptions/MqttCommunicationException.cs similarity index 100% rename from Frameworks/MQTTnet.NetStandard/Exceptions/MqttCommunicationException.cs rename to Source/MQTTnet/Exceptions/MqttCommunicationException.cs diff --git a/Source/MQTTnet/Exceptions/MqttCommunicationTimedOutException.cs b/Source/MQTTnet/Exceptions/MqttCommunicationTimedOutException.cs new file mode 100644 index 00000000..df3c4255 --- /dev/null +++ b/Source/MQTTnet/Exceptions/MqttCommunicationTimedOutException.cs @@ -0,0 +1,15 @@ +using System; + +namespace MQTTnet.Exceptions +{ + public class MqttCommunicationTimedOutException : MqttCommunicationException + { + public MqttCommunicationTimedOutException() + { + } + + public MqttCommunicationTimedOutException(Exception innerException) : base(innerException) + { + } + } +} diff --git a/Frameworks/MQTTnet.NetStandard/Exceptions/MqttProtocolViolationException.cs b/Source/MQTTnet/Exceptions/MqttProtocolViolationException.cs similarity index 72% rename from Frameworks/MQTTnet.NetStandard/Exceptions/MqttProtocolViolationException.cs rename to Source/MQTTnet/Exceptions/MqttProtocolViolationException.cs index 8756b3f8..262f4afb 100644 --- a/Frameworks/MQTTnet.NetStandard/Exceptions/MqttProtocolViolationException.cs +++ b/Source/MQTTnet/Exceptions/MqttProtocolViolationException.cs @@ -2,7 +2,7 @@ namespace MQTTnet.Exceptions { - public sealed class MqttProtocolViolationException : Exception + public class MqttProtocolViolationException : Exception { public MqttProtocolViolationException(string message) : base(message) diff --git a/Source/MQTTnet/IApplicationMessagePublisher.cs b/Source/MQTTnet/IApplicationMessagePublisher.cs new file mode 100644 index 00000000..c47893af --- /dev/null +++ b/Source/MQTTnet/IApplicationMessagePublisher.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace MQTTnet +{ + public interface IApplicationMessagePublisher + { + Task PublishAsync(MqttApplicationMessage applicationMessage); + } +} diff --git a/Frameworks/MQTTnet.NetStandard/IApplicationMessageReceiver.cs b/Source/MQTTnet/IApplicationMessageReceiver.cs similarity index 100% rename from Frameworks/MQTTnet.NetStandard/IApplicationMessageReceiver.cs rename to Source/MQTTnet/IApplicationMessageReceiver.cs diff --git a/Frameworks/MQTTnet.NetStandard/Implementations/MqttClientAdapterFactory.cs b/Source/MQTTnet/Implementations/MqttClientAdapterFactory.cs similarity index 88% rename from Frameworks/MQTTnet.NetStandard/Implementations/MqttClientAdapterFactory.cs rename to Source/MQTTnet/Implementations/MqttClientAdapterFactory.cs index 4692d7ee..dd06c31a 100644 --- a/Frameworks/MQTTnet.NetStandard/Implementations/MqttClientAdapterFactory.cs +++ b/Source/MQTTnet/Implementations/MqttClientAdapterFactory.cs @@ -8,7 +8,7 @@ namespace MQTTnet.Implementations { public class MqttClientAdapterFactory : IMqttClientAdapterFactory { - public IMqttChannelAdapter CreateClientAdapter(IMqttClientOptions options, IMqttNetLogger logger) + public IMqttChannelAdapter CreateClientAdapter(IMqttClientOptions options, IMqttNetChildLogger logger) { if (options == null) throw new ArgumentNullException(nameof(options)); @@ -16,9 +16,9 @@ namespace MQTTnet.Implementations switch (options.ChannelOptions) { - case MqttClientTcpOptions tcpOptions: + case MqttClientTcpOptions _: { - return new MqttChannelAdapter(new MqttTcpChannel(tcpOptions), serializer, logger); + return new MqttChannelAdapter(new MqttTcpChannel(options), serializer, logger); } case MqttClientWebSocketOptions webSocketOptions: diff --git a/Frameworks/MQTTnet.NetStandard/Implementations/MqttTcpChannel.Uwp.cs b/Source/MQTTnet/Implementations/MqttTcpChannel.Uwp.cs similarity index 62% rename from Frameworks/MQTTnet.NetStandard/Implementations/MqttTcpChannel.Uwp.cs rename to Source/MQTTnet/Implementations/MqttTcpChannel.Uwp.cs index a9cd18e7..2ccc7d97 100644 --- a/Frameworks/MQTTnet.NetStandard/Implementations/MqttTcpChannel.Uwp.cs +++ b/Source/MQTTnet/Implementations/MqttTcpChannel.Uwp.cs @@ -4,49 +4,62 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.InteropServices.WindowsRuntime; +using System.Threading; using System.Threading.Tasks; using Windows.Networking; using Windows.Networking.Sockets; using Windows.Security.Cryptography.Certificates; using MQTTnet.Channel; using MQTTnet.Client; +using MQTTnet.Server; namespace MQTTnet.Implementations { - public sealed class MqttTcpChannel : IMqttChannel + public 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 readonly int _bufferSize; private StreamSocket _socket; + private Stream _readStream; + private Stream _writeStream; - public MqttTcpChannel(MqttClientTcpOptions options) + public MqttTcpChannel(IMqttClientOptions clientOptions) { - _options = options ?? throw new ArgumentNullException(nameof(options)); + _options = (MqttClientTcpOptions)clientOptions.ChannelOptions; _bufferSize = _options.BufferSize; } - public MqttTcpChannel(StreamSocket socket) + public MqttTcpChannel(StreamSocket socket, IMqttServerOptions serverOptions) { _socket = socket ?? throw new ArgumentNullException(nameof(socket)); + _bufferSize = serverOptions.DefaultEndpointOptions.BufferSize; CreateStreams(); } - public Stream SendStream { get; private set; } - public Stream ReceiveStream { get; private set; } - public static Func> CustomIgnorableServerCertificateErrorsResolver { get; set; } - public async Task ConnectAsync() + public string Endpoint + { + get + { + if (_socket?.Information != null) + { + return _socket.Information.RemoteAddress + ":" + _socket.Information.RemotePort; + } + + return null; + } + } + + public async Task ConnectAsync(CancellationToken cancellationToken) { if (_socket == null) { _socket = new StreamSocket(); + _socket.Control.NoDelay = true; + _socket.Control.KeepAlive = true; } if (!_options.TlsOptions.UseTls) @@ -74,61 +87,28 @@ namespace MQTTnet.Implementations return Task.FromResult(0); } + public Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return _readStream.ReadAsync(buffer, offset, count, cancellationToken); + } + + public Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + // In the write method only the internal buffer will be filled. So here is no + // async/await required. The real network transmit is done when calling the + // Flush method. + _writeStream.Write(buffer, offset, count); + return _writeStream.FlushAsync(cancellationToken); + } + public void Dispose() { - try - { - SendStream?.Dispose(); - } - catch (ObjectDisposedException) - { - } - catch (NullReferenceException) - { - } - finally - { - SendStream = null; - } - - try - { - ReceiveStream?.Dispose(); - } - catch (ObjectDisposedException) - { - } - catch (NullReferenceException) - { - } - finally - { - ReceiveStream = null; - } - - try - { - _socket?.Dispose(); - } - catch (ObjectDisposedException) - { - } - catch (NullReferenceException) - { - } - finally - { - _socket = null; - } + TryDispose(_readStream, () => _readStream = null); + TryDispose(_writeStream, () => _writeStream = null); + TryDispose(_socket, () => _socket = null); } - private void CreateStreams() - { - SendStream = _socket.OutputStream.AsStreamForWrite(_bufferSize); - ReceiveStream = _socket.InputStream.AsStreamForRead(_bufferSize); - } - - private static Certificate LoadCertificate(MqttClientTcpOptions options) + private static Certificate LoadCertificate(IMqttClientChannelOptions options) { if (options.TlsOptions.Certificates == null || !options.TlsOptions.Certificates.Any()) { @@ -171,6 +151,30 @@ namespace MQTTnet.Implementations return result; } + + private void CreateStreams() + { + _readStream = _socket.InputStream.AsStreamForRead(_bufferSize); + _writeStream = _socket.OutputStream.AsStreamForWrite(_bufferSize); + } + + private static void TryDispose(IDisposable disposable, Action afterDispose) + { + try + { + disposable?.Dispose(); + } + catch (ObjectDisposedException) + { + } + catch (NullReferenceException) + { + } + finally + { + afterDispose(); + } + } } } #endif \ No newline at end of file diff --git a/Frameworks/MQTTnet.NetStandard/Implementations/MqttTcpChannel.cs b/Source/MQTTnet/Implementations/MqttTcpChannel.cs similarity index 55% rename from Frameworks/MQTTnet.NetStandard/Implementations/MqttTcpChannel.cs rename to Source/MQTTnet/Implementations/MqttTcpChannel.cs index 8d43d878..c48a06f6 100644 --- a/Frameworks/MQTTnet.NetStandard/Implementations/MqttTcpChannel.cs +++ b/Source/MQTTnet/Implementations/MqttTcpChannel.cs @@ -1,4 +1,4 @@ -#if NET452 || NET461 || NETSTANDARD1_3 || NETSTANDARD2_0 +#if !WINDOWS_UWP using System; using System.Net.Security; using System.Net.Sockets; @@ -7,35 +7,27 @@ using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; using System.IO; using System.Linq; +using System.Threading; using MQTTnet.Channel; using MQTTnet.Client; namespace MQTTnet.Implementations { - public sealed class MqttTcpChannel : IMqttChannel + public class MqttTcpChannel : IMqttChannel { -#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 IMqttClientOptions _clientOptions; private readonly MqttClientTcpOptions _options; - + private Socket _socket; - private SslStream _sslStream; + private Stream _stream; /// /// called on client sockets are created in connect /// - public MqttTcpChannel(MqttClientTcpOptions options) + public MqttTcpChannel(IMqttClientOptions clientOptions) { - _options = options ?? throw new ArgumentNullException(nameof(options)); - _bufferSize = options.BufferSize; + _clientOptions = clientOptions ?? throw new ArgumentNullException(nameof(clientOptions)); + _options = (MqttClientTcpOptions)clientOptions.ChannelOptions; } /// @@ -45,21 +37,20 @@ namespace MQTTnet.Implementations public MqttTcpChannel(Socket socket, SslStream sslStream) { _socket = socket ?? throw new ArgumentNullException(nameof(socket)); - _sslStream = sslStream; - CreateStreams(); + CreateStream(sslStream); } - public Stream SendStream { get; private set; } - public Stream ReceiveStream { get; private set; } - + [Obsolete("There is a new callback at the TLS options. This one will be deleted soon.")] public static Func CustomCertificateValidationCallback { get; set; } - public async Task ConnectAsync() + public string Endpoint => _socket?.RemoteEndPoint?.ToString(); + + public async Task ConnectAsync(CancellationToken cancellationToken) { if (_socket == null) { - _socket = new Socket(SocketType.Stream, ProtocolType.Tcp); + _socket = new Socket(SocketType.Stream, ProtocolType.Tcp) { NoDelay = true }; } #if NET452 || NET461 @@ -68,13 +59,14 @@ namespace MQTTnet.Implementations await _socket.ConnectAsync(_options.Server, _options.GetPort()).ConfigureAwait(false); #endif + SslStream sslStream = null; if (_options.TlsOptions.UseTls) { - _sslStream = new SslStream(new NetworkStream(_socket, true), false, InternalUserCertificateValidationCallback); - await _sslStream.AuthenticateAsClientAsync(_options.Server, LoadCertificates(), SslProtocols.Tls12, _options.TlsOptions.IgnoreCertificateRevocationErrors).ConfigureAwait(false); + sslStream = new SslStream(new NetworkStream(_socket, true), false, InternalUserCertificateValidationCallback); + await sslStream.AuthenticateAsClientAsync(_options.Server, LoadCertificates(), SslProtocols.Tls12, _options.TlsOptions.IgnoreCertificateRevocationErrors).ConfigureAwait(false); } - - CreateStreams(); + + CreateStream(sslStream); } public Task DisconnectAsync() @@ -83,76 +75,31 @@ namespace MQTTnet.Implementations return Task.FromResult(0); } + public Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return _stream.ReadAsync(buffer, offset, count, cancellationToken); + } + + public Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return _stream.WriteAsync(buffer, offset, count, cancellationToken); + } + public void Dispose() { - var oneStreamIsUsed = SendStream != null && ReceiveStream != null && ReferenceEquals(SendStream, ReceiveStream); - - 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; - } + TryDispose(_stream, () => _stream = null); + TryDispose(_socket, () => _socket = null); } private bool InternalUserCertificateValidationCallback(object sender, X509Certificate x509Certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) { + // Try the instance callback. + if (_options.TlsOptions.CertificateValidationCallback != null) + { + return _options.TlsOptions.CertificateValidationCallback(x509Certificate, chain, sslPolicyErrors,_clientOptions); + } + + // Try static callback. if (CustomCertificateValidationCallback != null) { return CustomCertificateValidationCallback(x509Certificate, chain, sslPolicyErrors, _options); @@ -198,25 +145,34 @@ namespace MQTTnet.Implementations return certificates; } - private void CreateStreams() + private void CreateStream(Stream stream) { - Stream stream; - if (_sslStream != null) + if (stream != null) { - stream = _sslStream; + _stream = stream; } else { - stream = new NetworkStream(_socket, true); + _stream = new NetworkStream(_socket, true); + } + } + + private static void TryDispose(IDisposable disposable, Action afterDispose) + { + try + { + disposable?.Dispose(); + } + catch (ObjectDisposedException) + { + } + catch (NullReferenceException) + { + } + finally + { + afterDispose(); } - -#if NET452 || NET461 - SendStream = new BufferedStream(stream, _bufferSize); - ReceiveStream = new BufferedStream(stream, _bufferSize); -#else - SendStream = stream; - ReceiveStream = stream; -#endif } } } diff --git a/Source/MQTTnet/Implementations/MqttTcpServerAdapter.Uwp.cs b/Source/MQTTnet/Implementations/MqttTcpServerAdapter.Uwp.cs new file mode 100644 index 00000000..c42db2b6 --- /dev/null +++ b/Source/MQTTnet/Implementations/MqttTcpServerAdapter.Uwp.cs @@ -0,0 +1,85 @@ +#if WINDOWS_UWP +using System; +using System.Threading.Tasks; +using Windows.Networking.Sockets; +using MQTTnet.Adapter; +using MQTTnet.Diagnostics; +using MQTTnet.Serializer; +using MQTTnet.Server; + +namespace MQTTnet.Implementations +{ + public class MqttTcpServerAdapter : IMqttServerAdapter + { + private readonly IMqttNetChildLogger _logger; + + private IMqttServerOptions _options; + private StreamSocketListener _listener; + + public MqttTcpServerAdapter(IMqttNetChildLogger logger) + { + if (logger == null) throw new ArgumentNullException(nameof(logger)); + + _logger = logger.CreateChildLogger(nameof(MqttTcpServerAdapter)); + } + + public event EventHandler ClientAccepted; + + public async Task StartAsync(IMqttServerOptions options) + { + _options = options ?? throw new ArgumentNullException(nameof(options)); + + if (_listener != null) throw new InvalidOperationException("Server is already started."); + + if (options.DefaultEndpointOptions.IsEnabled) + { + _listener = new StreamSocketListener(); + + // This also affects the client sockets. + _listener.Control.NoDelay = true; + _listener.Control.KeepAlive = true; + _listener.Control.QualityOfService = SocketQualityOfService.LowLatency; + _listener.ConnectionReceived += AcceptDefaultEndpointConnectionsAsync; + + await _listener.BindServiceNameAsync(options.DefaultEndpointOptions.Port.ToString(), SocketProtectionLevel.PlainSocket); + } + + if (options.TlsEndpointOptions.IsEnabled) + { + throw new NotSupportedException("TLS servers are not supported for UWP apps."); + } + } + + public Task StopAsync() + { + if (_listener != null) + { + _listener.ConnectionReceived -= AcceptDefaultEndpointConnectionsAsync; + } + + _listener?.Dispose(); + _listener = null; + + return Task.FromResult(0); + } + + public void Dispose() + { + StopAsync().GetAwaiter().GetResult(); + } + + private void AcceptDefaultEndpointConnectionsAsync(StreamSocketListener sender, StreamSocketListenerConnectionReceivedEventArgs args) + { + try + { + var clientAdapter = new MqttChannelAdapter(new MqttTcpChannel(args.Socket, _options), new MqttPacketSerializer(), _logger); + ClientAccepted?.Invoke(this, new MqttServerAdapterClientAcceptedEventArgs(clientAdapter)); + } + catch (Exception exception) + { + _logger.Error(exception, "Error while accepting connection at default endpoint."); + } + } + } +} +#endif \ No newline at end of file diff --git a/Source/MQTTnet/Implementations/MqttTcpServerAdapter.cs b/Source/MQTTnet/Implementations/MqttTcpServerAdapter.cs new file mode 100644 index 00000000..eeefb344 --- /dev/null +++ b/Source/MQTTnet/Implementations/MqttTcpServerAdapter.cs @@ -0,0 +1,111 @@ +#if !WINDOWS_UWP +using System; +using System.Collections.Generic; +using System.Net.Sockets; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; +using MQTTnet.Adapter; +using MQTTnet.Diagnostics; +using MQTTnet.Server; + +namespace MQTTnet.Implementations +{ + public class MqttTcpServerAdapter : IMqttServerAdapter + { + private readonly List _listeners = new List(); + private readonly IMqttNetChildLogger _logger; + + private CancellationTokenSource _cancellationTokenSource; + + public MqttTcpServerAdapter(IMqttNetChildLogger logger) + { + if (logger == null) throw new ArgumentNullException(nameof(logger)); + + _logger = logger.CreateChildLogger(nameof(MqttTcpServerAdapter)); + } + + public event EventHandler ClientAccepted; + + public Task StartAsync(IMqttServerOptions options) + { + if (_cancellationTokenSource != null) throw new InvalidOperationException("Server is already started."); + + _cancellationTokenSource = new CancellationTokenSource(); + + if (options.DefaultEndpointOptions.IsEnabled) + { + RegisterListeners(options.DefaultEndpointOptions, null); + } + + if (options.TlsEndpointOptions.IsEnabled) + { + if (options.TlsEndpointOptions.Certificate == null) + { + throw new ArgumentException("TLS certificate is not set."); + } + + var tlsCertificate = new X509Certificate2(options.TlsEndpointOptions.Certificate); + if (!tlsCertificate.HasPrivateKey) + { + throw new InvalidOperationException("The certificate for TLS encryption must contain the private key."); + } + + RegisterListeners(options.TlsEndpointOptions, tlsCertificate); + } + + return Task.FromResult(0); + } + + public Task StopAsync() + { + Dispose(); + return Task.FromResult(0); + } + + public void Dispose() + { + _cancellationTokenSource?.Cancel(false); + _cancellationTokenSource?.Dispose(); + _cancellationTokenSource = null; + + foreach (var listener in _listeners) + { + listener.Dispose(); + } + + _listeners.Clear(); + } + + private void RegisterListeners(MqttServerTcpEndpointBaseOptions options, X509Certificate2 tlsCertificate) + { + var listenerV4 = new MqttTcpServerListener( + AddressFamily.InterNetwork, + options, + tlsCertificate, + _cancellationTokenSource.Token, + _logger); + + listenerV4.ClientAccepted += OnClientAccepted; + listenerV4.Start(); + _listeners.Add(listenerV4); + + var listenerV6 = new MqttTcpServerListener( + AddressFamily.InterNetworkV6, + options, + tlsCertificate, + _cancellationTokenSource.Token, + _logger); + + listenerV6.ClientAccepted += OnClientAccepted; + listenerV6.Start(); + _listeners.Add(listenerV6); + } + + private void OnClientAccepted(object sender, MqttServerAdapterClientAcceptedEventArgs e) + { + ClientAccepted?.Invoke(this, e); + } + } +} +#endif \ No newline at end of file diff --git a/Source/MQTTnet/Implementations/MqttTcpServerListener.cs b/Source/MQTTnet/Implementations/MqttTcpServerListener.cs new file mode 100644 index 00000000..7f9ffbd3 --- /dev/null +++ b/Source/MQTTnet/Implementations/MqttTcpServerListener.cs @@ -0,0 +1,114 @@ +#if !WINDOWS_UWP +using System; +using System.Net; +using System.Net.Security; +using System.Net.Sockets; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; +using MQTTnet.Adapter; +using MQTTnet.Diagnostics; +using MQTTnet.Serializer; +using MQTTnet.Server; + +namespace MQTTnet.Implementations +{ + public class MqttTcpServerListener : IDisposable + { + private readonly IMqttNetChildLogger _logger; + private readonly CancellationToken _cancellationToken; + private readonly AddressFamily _addressFamily; + private readonly MqttServerTcpEndpointBaseOptions _options; + private readonly X509Certificate2 _tlsCertificate; + + private Socket _socket; + + public MqttTcpServerListener( + AddressFamily addressFamily, + MqttServerTcpEndpointBaseOptions options, + X509Certificate2 tlsCertificate, + CancellationToken cancellationToken, + IMqttNetChildLogger logger) + { + _addressFamily = addressFamily; + _options = options; + _tlsCertificate = tlsCertificate; + _cancellationToken = cancellationToken; + _logger = logger.CreateChildLogger(nameof(MqttTcpServerListener)); + } + + public event EventHandler ClientAccepted; + + public void Start() + { + var boundIp = _options.BoundInterNetworkAddress; + if (_addressFamily == AddressFamily.InterNetworkV6) + { + boundIp = _options.BoundInterNetworkV6Address; + } + + _socket = new Socket(_addressFamily, SocketType.Stream, ProtocolType.Tcp); + _socket.Bind(new IPEndPoint(boundIp, _options.Port)); + + _logger.Info($"Starting TCP listener for {_socket.LocalEndPoint} TLS={_tlsCertificate != null}."); + + _socket.Listen(_options.ConnectionBacklog); + Task.Run(AcceptClientConnectionsAsync, _cancellationToken); + } + + private async Task AcceptClientConnectionsAsync() + { + while (!_cancellationToken.IsCancellationRequested) + { + try + { +#if NET452 || NET461 + var clientSocket = await Task.Factory.FromAsync(_socket.BeginAccept, _socket.EndAccept, null).ConfigureAwait(false); +#else + var clientSocket = await _socket.AcceptAsync().ConfigureAwait(false); +#endif + clientSocket.NoDelay = true; + + SslStream sslStream = null; + + if (_tlsCertificate != null) + { + sslStream = new SslStream(new NetworkStream(clientSocket), false); + await sslStream.AuthenticateAsServerAsync(_tlsCertificate, false, SslProtocols.Tls12, false).ConfigureAwait(false); + } + + var protocol = _addressFamily == AddressFamily.InterNetwork ? "ipv4" : "ipv6"; + _logger.Verbose($"Client '{clientSocket.RemoteEndPoint}' accepted by TCP listener '{_socket.LocalEndPoint}, {protocol}'."); + + 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) + { + if (exception is SocketException s && s.SocketErrorCode == SocketError.OperationAborted) + { + return; + } + + _logger.Error(exception, $"Error while accepting connection at TCP listener {_socket.LocalEndPoint} TLS={_tlsCertificate != null}."); + await Task.Delay(TimeSpan.FromSeconds(1), _cancellationToken).ConfigureAwait(false); + } + } + } + + public void Dispose() + { + _socket?.Dispose(); + +#if NETSTANDARD1_3 || NETSTANDARD2_0 || NET461 || NET472 + _tlsCertificate?.Dispose(); +#endif + } + } +} +#endif \ No newline at end of file diff --git a/Frameworks/MQTTnet.NetStandard/Implementations/MqttWebSocketChannel.cs b/Source/MQTTnet/Implementations/MqttWebSocketChannel.cs similarity index 50% rename from Frameworks/MQTTnet.NetStandard/Implementations/MqttWebSocketChannel.cs rename to Source/MQTTnet/Implementations/MqttWebSocketChannel.cs index 057a07e5..3243cd76 100644 --- a/Frameworks/MQTTnet.NetStandard/Implementations/MqttWebSocketChannel.cs +++ b/Source/MQTTnet/Implementations/MqttWebSocketChannel.cs @@ -1,5 +1,4 @@ using System; -using System.IO; using System.Net.WebSockets; using System.Security.Cryptography.X509Certificates; using System.Threading; @@ -9,24 +8,27 @@ using MQTTnet.Client; namespace MQTTnet.Implementations { - public sealed class MqttWebSocketChannel : IMqttChannel + public 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 SemaphoreSlim _sendLock = new SemaphoreSlim(1, 1); private readonly MqttClientWebSocketOptions _options; - private ClientWebSocket _webSocket; + private WebSocket _webSocket; + public MqttWebSocketChannel(MqttClientWebSocketOptions options) { _options = options ?? throw new ArgumentNullException(nameof(options)); } - public Stream SendStream { get; private set; } - public Stream ReceiveStream { get; private set; } + public MqttWebSocketChannel(WebSocket webSocket, string endpoint) + { + _webSocket = webSocket ?? throw new ArgumentNullException(nameof(webSocket)); + Endpoint = endpoint; + } - public async Task ConnectAsync() + public string Endpoint { get; } + + public async Task ConnectAsync(CancellationToken cancellationToken) { var uri = _options.Uri; if (!uri.StartsWith("ws://", StringComparison.OrdinalIgnoreCase) && !uri.StartsWith("wss://", StringComparison.OrdinalIgnoreCase)) @@ -41,42 +43,40 @@ namespace MQTTnet.Implementations } } - _webSocket = new ClientWebSocket(); - + var clientWebSocket = new ClientWebSocket(); + if (_options.RequestHeaders != null) { foreach (var requestHeader in _options.RequestHeaders) { - _webSocket.Options.SetRequestHeader(requestHeader.Key, requestHeader.Value); - } + clientWebSocket.Options.SetRequestHeader(requestHeader.Key, requestHeader.Value); + } } if (_options.SubProtocols != null) { foreach (var subProtocol in _options.SubProtocols) { - _webSocket.Options.AddSubProtocol(subProtocol); + clientWebSocket.Options.AddSubProtocol(subProtocol); } } if (_options.CookieContainer != null) { - _webSocket.Options.Cookies = _options.CookieContainer; + clientWebSocket.Options.Cookies = _options.CookieContainer; } if (_options.TlsOptions?.UseTls == true && _options.TlsOptions?.Certificates != null) { - _webSocket.Options.ClientCertificates = new X509CertificateCollection(); + clientWebSocket.Options.ClientCertificates = new X509CertificateCollection(); foreach (var certificate in _options.TlsOptions.Certificates) { - _webSocket.Options.ClientCertificates.Add(new X509Certificate(certificate)); + clientWebSocket.Options.ClientCertificates.Add(new X509Certificate(certificate)); } } - await _webSocket.ConnectAsync(new Uri(uri), CancellationToken.None).ConfigureAwait(false); - - SendStream = new WebSocketStream(_webSocket); - ReceiveStream = SendStream; + await clientWebSocket.ConnectAsync(new Uri(uri), cancellationToken).ConfigureAwait(false); + _webSocket = clientWebSocket; } public async Task DisconnectAsync() @@ -94,8 +94,32 @@ namespace MQTTnet.Implementations Dispose(); } + public async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + var response = await _webSocket.ReceiveAsync(new ArraySegment(buffer, offset, count), cancellationToken).ConfigureAwait(false); + return response.Count; + } + + public async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + // This lock is required because the client will throw an exception if _SendAsync_ is + // called from multiple threads at the same time. But this issue only happens with several + // framework versions. + await _sendLock.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + await _webSocket.SendAsync(new ArraySegment(buffer, offset, count), WebSocketMessageType.Binary, true, cancellationToken).ConfigureAwait(false); + } + finally + { + _sendLock.Release(); + } + } + public void Dispose() { + _sendLock?.Dispose(); + try { _webSocket?.Dispose(); @@ -106,7 +130,7 @@ namespace MQTTnet.Implementations finally { _webSocket = null; - } + } } } } \ No newline at end of file diff --git a/Source/MQTTnet/Internal/AsyncAutoResetEvent.cs b/Source/MQTTnet/Internal/AsyncAutoResetEvent.cs new file mode 100644 index 00000000..a90b98e4 --- /dev/null +++ b/Source/MQTTnet/Internal/AsyncAutoResetEvent.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace MQTTnet.Internal +{ + // Inspired from Stephen Toub (https://blogs.msdn.microsoft.com/pfxteam/2012/02/11/building-async-coordination-primitives-part-2-asyncautoresetevent/) and Chris Gillum (https://stackoverflow.com/a/43012490) + public class AsyncAutoResetEvent + { + private readonly LinkedList> _waiters = new LinkedList>(); + private bool _isSignaled; + + public AsyncAutoResetEvent() + : this(false) + { + } + + public AsyncAutoResetEvent(bool signaled) + { + _isSignaled = signaled; + } + + public Task WaitOneAsync() + { + return WaitOneAsync(CancellationToken.None); + } + + public Task WaitOneAsync(TimeSpan timeout) + { + return WaitOneAsync(timeout, CancellationToken.None); + } + + public Task WaitOneAsync(CancellationToken cancellationToken) + { + return WaitOneAsync(Timeout.InfiniteTimeSpan, cancellationToken); + } + + public async Task WaitOneAsync(TimeSpan timeout, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + TaskCompletionSource tcs; + + lock (_waiters) + { + if (_isSignaled) + { + _isSignaled = false; + return true; + } + + if (timeout == TimeSpan.Zero) + { + return _isSignaled; + } + + tcs = new TaskCompletionSource(); + _waiters.AddLast(tcs); + } + + var winner = await Task.WhenAny(tcs.Task, Task.Delay(timeout, cancellationToken)).ConfigureAwait(false); + var taskWasSignaled = winner == tcs.Task; + if (taskWasSignaled) + { + return true; + } + + // We timed-out; remove our reference to the task. + // This is an O(n) operation since waiters is a LinkedList. + lock (_waiters) + { + _waiters.Remove(tcs); + + if (winner.Status == TaskStatus.Canceled) + { + throw new OperationCanceledException(cancellationToken); + } + + throw new TimeoutException(); + } + } + + public void Set() + { + TaskCompletionSource toRelease = null; + + lock (_waiters) + { + if (_waiters.Count > 0) + { + // Signal the first task in the waiters list. + toRelease = _waiters.First.Value; + _waiters.RemoveFirst(); + } + else if (!_isSignaled) + { + // No tasks are pending + _isSignaled = true; + } + } + + toRelease?.SetResult(true); + } + } +} diff --git a/Source/MQTTnet/Internal/AsyncLock.cs b/Source/MQTTnet/Internal/AsyncLock.cs new file mode 100644 index 00000000..87571c2a --- /dev/null +++ b/Source/MQTTnet/Internal/AsyncLock.cs @@ -0,0 +1,48 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace MQTTnet.Internal +{ + // From Stephen Toub (https://blogs.msdn.microsoft.com/pfxteam/2012/02/12/building-async-coordination-primitives-part-6-asynclock/) + public class AsyncLock : IDisposable + { + private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); + private readonly Task _releaser; + + public AsyncLock() + { + _releaser = Task.FromResult((IDisposable)new Releaser(this)); + } + + public Task LockAsync(CancellationToken cancellationToken) + { + var wait = _semaphore.WaitAsync(cancellationToken); + return wait.IsCompleted ? + _releaser : + wait.ContinueWith((_, state) => (IDisposable)state, + _releaser.Result, cancellationToken, + TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); + } + + public void Dispose() + { + _semaphore?.Dispose(); + } + + private class Releaser : IDisposable + { + private readonly AsyncLock _toRelease; + + internal Releaser(AsyncLock toRelease) + { + _toRelease = toRelease; + } + + public void Dispose() + { + _toRelease._semaphore.Release(); + } + } + } +} diff --git a/Source/MQTTnet/Internal/ExceptionHelper.cs b/Source/MQTTnet/Internal/ExceptionHelper.cs new file mode 100644 index 00000000..f3cadc91 --- /dev/null +++ b/Source/MQTTnet/Internal/ExceptionHelper.cs @@ -0,0 +1,12 @@ +using MQTTnet.Exceptions; + +namespace MQTTnet.Internal +{ + public static class ExceptionHelper + { + public static void ThrowGracefulSocketClose() + { + throw new MqttCommunicationClosedGracefullyException(); + } + } +} diff --git a/Frameworks/MQTTnet.NetStandard/Internal/MqttApplicationMessageExtensions.cs b/Source/MQTTnet/Internal/MqttApplicationMessageExtensions.cs similarity index 94% rename from Frameworks/MQTTnet.NetStandard/Internal/MqttApplicationMessageExtensions.cs rename to Source/MQTTnet/Internal/MqttApplicationMessageExtensions.cs index b1d1a389..79eb2c43 100644 --- a/Frameworks/MQTTnet.NetStandard/Internal/MqttApplicationMessageExtensions.cs +++ b/Source/MQTTnet/Internal/MqttApplicationMessageExtensions.cs @@ -2,7 +2,7 @@ namespace MQTTnet.Internal { - internal static class MqttApplicationMessageExtensions + public static class MqttApplicationMessageExtensions { public static MqttApplicationMessage ToApplicationMessage(this MqttPublishPacket publishPacket) { diff --git a/Source/MQTTnet/Internal/TaskExtensions.cs b/Source/MQTTnet/Internal/TaskExtensions.cs new file mode 100644 index 00000000..1356d97d --- /dev/null +++ b/Source/MQTTnet/Internal/TaskExtensions.cs @@ -0,0 +1,58 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using MQTTnet.Exceptions; + +namespace MQTTnet.Internal +{ + public static class TaskExtensions + { + public static async Task TimeoutAfterAsync(Func action, TimeSpan timeout, CancellationToken cancellationToken) + { + if (action == null) throw new ArgumentNullException(nameof(action)); + + using (var timeoutCts = new CancellationTokenSource(timeout)) + using (var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(timeoutCts.Token, cancellationToken)) + { + try + { + await action(linkedCts.Token).ConfigureAwait(false); + } + catch (OperationCanceledException exception) + { + var timeoutReached = timeoutCts.IsCancellationRequested && !cancellationToken.IsCancellationRequested; + if (timeoutReached) + { + throw new MqttCommunicationTimedOutException(exception); + } + + throw; + } + } + } + + public static async Task TimeoutAfterAsync(Func> action, TimeSpan timeout, CancellationToken cancellationToken) + { + if (action == null) throw new ArgumentNullException(nameof(action)); + + using (var timeoutCts = new CancellationTokenSource(timeout)) + using (var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(timeoutCts.Token, cancellationToken)) + { + try + { + return await action(linkedCts.Token).ConfigureAwait(false); + } + catch (OperationCanceledException exception) + { + var timeoutReached = timeoutCts.IsCancellationRequested && !cancellationToken.IsCancellationRequested; + if (timeoutReached) + { + throw new MqttCommunicationTimedOutException(exception); + } + + throw; + } + } + } + } +} diff --git a/Source/MQTTnet/Internal/TestMqttChannel.cs b/Source/MQTTnet/Internal/TestMqttChannel.cs new file mode 100644 index 00000000..e48b8ede --- /dev/null +++ b/Source/MQTTnet/Internal/TestMqttChannel.cs @@ -0,0 +1,43 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using MQTTnet.Channel; + +namespace MQTTnet.Internal +{ + public class TestMqttChannel : IMqttChannel + { + private readonly MemoryStream _stream; + + public TestMqttChannel(MemoryStream stream) + { + _stream = stream; + } + + public void Dispose() + { + } + + public string Endpoint { get; } + + public Task ConnectAsync(CancellationToken cancellationToken) + { + return Task.FromResult(0); + } + + public Task DisconnectAsync() + { + return Task.FromResult(0); + } + + public Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return _stream.ReadAsync(buffer, offset, count, cancellationToken); + } + + public Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return _stream.WriteAsync(buffer, offset, count, cancellationToken); + } + } +} diff --git a/Frameworks/MQTTnet.NetStandard/MQTTnet.Netstandard.csproj b/Source/MQTTnet/MQTTnet.csproj similarity index 88% rename from Frameworks/MQTTnet.NetStandard/MQTTnet.Netstandard.csproj rename to Source/MQTTnet/MQTTnet.csproj index cb154c24..26bf0029 100644 --- a/Frameworks/MQTTnet.NetStandard/MQTTnet.Netstandard.csproj +++ b/Source/MQTTnet/MQTTnet.csproj @@ -6,10 +6,6 @@ MQTTnet MQTTnet False - Full - 0.0.0.0 - 0.0.0.0 - 0.0.0.0 @@ -19,11 +15,11 @@ false - + false UAP,Version=v10.0 UAP - 10.0.16299.0 + 10.0.17134.0 10.0.10240.0 .NETCore v5.0 @@ -32,6 +28,10 @@ $(MSBuildExtensionsPath)\Microsoft\WindowsXaml\v$(VisualStudioVersion)\Microsoft.Windows.UI.Xaml.CSharp.targets + + Full + + @@ -43,17 +43,17 @@ - + - + - + diff --git a/Source/MQTTnet/MqttApplicationMessage.cs b/Source/MQTTnet/MqttApplicationMessage.cs new file mode 100644 index 00000000..334dfb40 --- /dev/null +++ b/Source/MQTTnet/MqttApplicationMessage.cs @@ -0,0 +1,15 @@ +using MQTTnet.Protocol; + +namespace MQTTnet +{ + public class MqttApplicationMessage + { + public string Topic { get; set; } + + public byte[] Payload { get; set; } + + public MqttQualityOfServiceLevel QualityOfServiceLevel { get; set; } + + public bool Retain { get; set; } + } +} diff --git a/Frameworks/MQTTnet.NetStandard/MqttApplicationMessageBuilder.cs b/Source/MQTTnet/MqttApplicationMessageBuilder.cs similarity index 90% rename from Frameworks/MQTTnet.NetStandard/MqttApplicationMessageBuilder.cs rename to Source/MQTTnet/MqttApplicationMessageBuilder.cs index ce949b85..79c65ece 100644 --- a/Frameworks/MQTTnet.NetStandard/MqttApplicationMessageBuilder.cs +++ b/Source/MQTTnet/MqttApplicationMessageBuilder.cs @@ -32,7 +32,12 @@ namespace MQTTnet return this; } - public MqttApplicationMessageBuilder WithPayload(MemoryStream payload) + public MqttApplicationMessageBuilder WithPayload(Stream payload) + { + return WithPayload(payload, payload.Length - payload.Position); + } + + public MqttApplicationMessageBuilder WithPayload(Stream payload, long length) { if (payload == null) { @@ -46,7 +51,7 @@ namespace MQTTnet } else { - _payload = new byte[payload.Length - payload.Position]; + _payload = new byte[length]; payload.Read(_payload, 0, _payload.Length); } diff --git a/Frameworks/MQTTnet.NetStandard/MqttApplicationMessageExtensions.cs b/Source/MQTTnet/MqttApplicationMessageExtensions.cs similarity index 100% rename from Frameworks/MQTTnet.NetStandard/MqttApplicationMessageExtensions.cs rename to Source/MQTTnet/MqttApplicationMessageExtensions.cs diff --git a/Frameworks/MQTTnet.NetStandard/MqttApplicationMessageReceivedEventArgs.cs b/Source/MQTTnet/MqttApplicationMessageReceivedEventArgs.cs similarity index 85% rename from Frameworks/MQTTnet.NetStandard/MqttApplicationMessageReceivedEventArgs.cs rename to Source/MQTTnet/MqttApplicationMessageReceivedEventArgs.cs index a16a81d1..e81c69a9 100644 --- a/Frameworks/MQTTnet.NetStandard/MqttApplicationMessageReceivedEventArgs.cs +++ b/Source/MQTTnet/MqttApplicationMessageReceivedEventArgs.cs @@ -2,7 +2,7 @@ namespace MQTTnet { - public sealed class MqttApplicationMessageReceivedEventArgs : EventArgs + public class MqttApplicationMessageReceivedEventArgs : EventArgs { public MqttApplicationMessageReceivedEventArgs(string clientId, MqttApplicationMessage applicationMessage) { diff --git a/Frameworks/MQTTnet.NetStandard/MqttFactory.cs b/Source/MQTTnet/MqttFactory.cs similarity index 76% rename from Frameworks/MQTTnet.NetStandard/MqttFactory.cs rename to Source/MQTTnet/MqttFactory.cs index e3b3549a..e830ad20 100644 --- a/Frameworks/MQTTnet.NetStandard/MqttFactory.cs +++ b/Source/MQTTnet/MqttFactory.cs @@ -4,7 +4,6 @@ using MQTTnet.Adapter; using MQTTnet.Client; using MQTTnet.Diagnostics; using MQTTnet.Implementations; -using MQTTnet.ManagedClient; using MQTTnet.Server; namespace MQTTnet @@ -23,16 +22,12 @@ namespace MQTTnet return new MqttClient(new MqttClientAdapterFactory(), logger); } - public IManagedMqttClient CreateManagedMqttClient() - { - return new ManagedMqttClient(CreateMqttClient(), new MqttNetLogger()); - } - - public IManagedMqttClient CreateManagedMqttClient(IMqttNetLogger logger) + public IMqttClient CreateMqttClient(IMqttNetLogger logger, IMqttClientAdapterFactory mqttClientAdapterFactory) { if (logger == null) throw new ArgumentNullException(nameof(logger)); + if (mqttClientAdapterFactory == null) throw new ArgumentNullException(nameof(mqttClientAdapterFactory)); - return new ManagedMqttClient(CreateMqttClient(), logger); + return new MqttClient(mqttClientAdapterFactory, logger); } public IMqttServer CreateMqttServer() @@ -45,7 +40,7 @@ namespace MQTTnet { if (logger == null) throw new ArgumentNullException(nameof(logger)); - return CreateMqttServer(new List { new MqttTcpServerAdapter(logger) }, logger); + return CreateMqttServer(new List { new MqttTcpServerAdapter(logger.CreateChildLogger()) }, logger); } public IMqttServer CreateMqttServer(IEnumerable adapters, IMqttNetLogger logger) @@ -53,7 +48,7 @@ namespace MQTTnet if (adapters == null) throw new ArgumentNullException(nameof(adapters)); if (logger == null) throw new ArgumentNullException(nameof(logger)); - return new MqttServer(adapters, logger); + return new MqttServer(adapters, logger.CreateChildLogger()); } } } \ No newline at end of file diff --git a/Frameworks/MQTTnet.NetStandard/Packets/IMqttPacketWithIdentifier.cs b/Source/MQTTnet/Packets/IMqttPacketWithIdentifier.cs similarity index 100% rename from Frameworks/MQTTnet.NetStandard/Packets/IMqttPacketWithIdentifier.cs rename to Source/MQTTnet/Packets/IMqttPacketWithIdentifier.cs diff --git a/Frameworks/MQTTnet.NetStandard/Packets/MqttBasePacket.cs b/Source/MQTTnet/Packets/MqttBasePacket.cs similarity index 100% rename from Frameworks/MQTTnet.NetStandard/Packets/MqttBasePacket.cs rename to Source/MQTTnet/Packets/MqttBasePacket.cs diff --git a/Frameworks/MQTTnet.NetStandard/Packets/MqttBasePublishPacket.cs b/Source/MQTTnet/Packets/MqttBasePublishPacket.cs similarity index 100% rename from Frameworks/MQTTnet.NetStandard/Packets/MqttBasePublishPacket.cs rename to Source/MQTTnet/Packets/MqttBasePublishPacket.cs diff --git a/Frameworks/MQTTnet.NetStandard/Packets/MqttConnAckPacket.cs b/Source/MQTTnet/Packets/MqttConnAckPacket.cs similarity index 86% rename from Frameworks/MQTTnet.NetStandard/Packets/MqttConnAckPacket.cs rename to Source/MQTTnet/Packets/MqttConnAckPacket.cs index 9cf0adfe..dbdeea89 100644 --- a/Frameworks/MQTTnet.NetStandard/Packets/MqttConnAckPacket.cs +++ b/Source/MQTTnet/Packets/MqttConnAckPacket.cs @@ -2,7 +2,7 @@ namespace MQTTnet.Packets { - public sealed class MqttConnAckPacket : MqttBasePacket + public class MqttConnAckPacket : MqttBasePacket { public bool IsSessionPresent { get; set; } diff --git a/Frameworks/MQTTnet.NetStandard/Packets/MqttConnectPacket.cs b/Source/MQTTnet/Packets/MqttConnectPacket.cs similarity index 92% rename from Frameworks/MQTTnet.NetStandard/Packets/MqttConnectPacket.cs rename to Source/MQTTnet/Packets/MqttConnectPacket.cs index b1943ced..2134f4bb 100644 --- a/Frameworks/MQTTnet.NetStandard/Packets/MqttConnectPacket.cs +++ b/Source/MQTTnet/Packets/MqttConnectPacket.cs @@ -2,7 +2,7 @@ namespace MQTTnet.Packets { - public sealed class MqttConnectPacket : MqttBasePacket + public class MqttConnectPacket : MqttBasePacket { public MqttProtocolVersion ProtocolVersion { get; set; } diff --git a/Frameworks/MQTTnet.NetStandard/Packets/MqttDisconnectPacket.cs b/Source/MQTTnet/Packets/MqttDisconnectPacket.cs similarity index 69% rename from Frameworks/MQTTnet.NetStandard/Packets/MqttDisconnectPacket.cs rename to Source/MQTTnet/Packets/MqttDisconnectPacket.cs index 46c3270d..621829a2 100644 --- a/Frameworks/MQTTnet.NetStandard/Packets/MqttDisconnectPacket.cs +++ b/Source/MQTTnet/Packets/MqttDisconnectPacket.cs @@ -1,6 +1,6 @@ namespace MQTTnet.Packets { - public sealed class MqttDisconnectPacket : MqttBasePacket + public class MqttDisconnectPacket : MqttBasePacket { public override string ToString() { diff --git a/Frameworks/MQTTnet.NetStandard/Packets/MqttPingReqPacket.cs b/Source/MQTTnet/Packets/MqttPingReqPacket.cs similarity index 69% rename from Frameworks/MQTTnet.NetStandard/Packets/MqttPingReqPacket.cs rename to Source/MQTTnet/Packets/MqttPingReqPacket.cs index fee9926c..5c65da1d 100644 --- a/Frameworks/MQTTnet.NetStandard/Packets/MqttPingReqPacket.cs +++ b/Source/MQTTnet/Packets/MqttPingReqPacket.cs @@ -1,6 +1,6 @@ namespace MQTTnet.Packets { - public sealed class MqttPingReqPacket : MqttBasePacket + public class MqttPingReqPacket : MqttBasePacket { public override string ToString() { diff --git a/Frameworks/MQTTnet.NetStandard/Packets/MqttPingRespPacket.cs b/Source/MQTTnet/Packets/MqttPingRespPacket.cs similarity index 69% rename from Frameworks/MQTTnet.NetStandard/Packets/MqttPingRespPacket.cs rename to Source/MQTTnet/Packets/MqttPingRespPacket.cs index c5f82a3b..b6f9bc3c 100644 --- a/Frameworks/MQTTnet.NetStandard/Packets/MqttPingRespPacket.cs +++ b/Source/MQTTnet/Packets/MqttPingRespPacket.cs @@ -1,6 +1,6 @@ namespace MQTTnet.Packets { - public sealed class MqttPingRespPacket : MqttBasePacket + public class MqttPingRespPacket : MqttBasePacket { public override string ToString() { diff --git a/Source/MQTTnet/Packets/MqttPubAckPacket.cs b/Source/MQTTnet/Packets/MqttPubAckPacket.cs new file mode 100644 index 00000000..5811849b --- /dev/null +++ b/Source/MQTTnet/Packets/MqttPubAckPacket.cs @@ -0,0 +1,10 @@ +namespace MQTTnet.Packets +{ + public class MqttPubAckPacket : MqttBasePublishPacket + { + public override string ToString() + { + return $"PubAck [PacketIdentifier={PacketIdentifier}]"; + } + } +} diff --git a/Frameworks/MQTTnet.NetStandard/Packets/MqttPubCompPacket.cs b/Source/MQTTnet/Packets/MqttPubCompPacket.cs similarity index 67% rename from Frameworks/MQTTnet.NetStandard/Packets/MqttPubCompPacket.cs rename to Source/MQTTnet/Packets/MqttPubCompPacket.cs index 98dace70..e9ea18eb 100644 --- a/Frameworks/MQTTnet.NetStandard/Packets/MqttPubCompPacket.cs +++ b/Source/MQTTnet/Packets/MqttPubCompPacket.cs @@ -1,6 +1,6 @@ namespace MQTTnet.Packets { - public sealed class MqttPubCompPacket : MqttBasePublishPacket + public class MqttPubCompPacket : MqttBasePublishPacket { public override string ToString() { diff --git a/Frameworks/MQTTnet.NetStandard/Packets/MqttPubRecPacket.cs b/Source/MQTTnet/Packets/MqttPubRecPacket.cs similarity index 67% rename from Frameworks/MQTTnet.NetStandard/Packets/MqttPubRecPacket.cs rename to Source/MQTTnet/Packets/MqttPubRecPacket.cs index 86cd4b45..0184d6b8 100644 --- a/Frameworks/MQTTnet.NetStandard/Packets/MqttPubRecPacket.cs +++ b/Source/MQTTnet/Packets/MqttPubRecPacket.cs @@ -1,6 +1,6 @@ namespace MQTTnet.Packets { - public sealed class MqttPubRecPacket : MqttBasePublishPacket + public class MqttPubRecPacket : MqttBasePublishPacket { public override string ToString() { diff --git a/Frameworks/MQTTnet.NetStandard/Packets/MqttPubRelPacket.cs b/Source/MQTTnet/Packets/MqttPubRelPacket.cs similarity index 67% rename from Frameworks/MQTTnet.NetStandard/Packets/MqttPubRelPacket.cs rename to Source/MQTTnet/Packets/MqttPubRelPacket.cs index ed1d933b..bb37655c 100644 --- a/Frameworks/MQTTnet.NetStandard/Packets/MqttPubRelPacket.cs +++ b/Source/MQTTnet/Packets/MqttPubRelPacket.cs @@ -1,6 +1,6 @@ namespace MQTTnet.Packets { - public sealed class MqttPubRelPacket : MqttBasePublishPacket + public class MqttPubRelPacket : MqttBasePublishPacket { public override string ToString() { diff --git a/Frameworks/MQTTnet.NetStandard/Packets/MqttPublishPacket.cs b/Source/MQTTnet/Packets/MqttPublishPacket.cs similarity index 91% rename from Frameworks/MQTTnet.NetStandard/Packets/MqttPublishPacket.cs rename to Source/MQTTnet/Packets/MqttPublishPacket.cs index 52fcceed..55144a9e 100644 --- a/Frameworks/MQTTnet.NetStandard/Packets/MqttPublishPacket.cs +++ b/Source/MQTTnet/Packets/MqttPublishPacket.cs @@ -2,7 +2,7 @@ namespace MQTTnet.Packets { - public sealed class MqttPublishPacket : MqttBasePublishPacket + public class MqttPublishPacket : MqttBasePublishPacket { public bool Retain { get; set; } diff --git a/Frameworks/MQTTnet.NetStandard/Packets/MqttSubAckPacket.cs b/Source/MQTTnet/Packets/MqttSubAckPacket.cs similarity index 87% rename from Frameworks/MQTTnet.NetStandard/Packets/MqttSubAckPacket.cs rename to Source/MQTTnet/Packets/MqttSubAckPacket.cs index 3b8cf7a8..05e18b2b 100644 --- a/Frameworks/MQTTnet.NetStandard/Packets/MqttSubAckPacket.cs +++ b/Source/MQTTnet/Packets/MqttSubAckPacket.cs @@ -4,7 +4,7 @@ using MQTTnet.Protocol; namespace MQTTnet.Packets { - public sealed class MqttSubAckPacket : MqttBasePacket, IMqttPacketWithIdentifier + public class MqttSubAckPacket : MqttBasePacket, IMqttPacketWithIdentifier { public ushort? PacketIdentifier { get; set; } diff --git a/Frameworks/MQTTnet.NetStandard/Packets/MqttSubscribePacket.cs b/Source/MQTTnet/Packets/MqttSubscribePacket.cs similarity index 86% rename from Frameworks/MQTTnet.NetStandard/Packets/MqttSubscribePacket.cs rename to Source/MQTTnet/Packets/MqttSubscribePacket.cs index 63aede16..60086c47 100644 --- a/Frameworks/MQTTnet.NetStandard/Packets/MqttSubscribePacket.cs +++ b/Source/MQTTnet/Packets/MqttSubscribePacket.cs @@ -3,7 +3,7 @@ using System.Linq; namespace MQTTnet.Packets { - public sealed class MqttSubscribePacket : MqttBasePacket, IMqttPacketWithIdentifier + public class MqttSubscribePacket : MqttBasePacket, IMqttPacketWithIdentifier { public ushort? PacketIdentifier { get; set; } diff --git a/Frameworks/MQTTnet.NetStandard/Packets/MqttUnsubAckPacket.cs b/Source/MQTTnet/Packets/MqttUnsubAckPacket.cs similarity index 73% rename from Frameworks/MQTTnet.NetStandard/Packets/MqttUnsubAckPacket.cs rename to Source/MQTTnet/Packets/MqttUnsubAckPacket.cs index 688195fa..ab5f9f62 100644 --- a/Frameworks/MQTTnet.NetStandard/Packets/MqttUnsubAckPacket.cs +++ b/Source/MQTTnet/Packets/MqttUnsubAckPacket.cs @@ -1,6 +1,6 @@ namespace MQTTnet.Packets { - public sealed class MqttUnsubAckPacket : MqttBasePacket, IMqttPacketWithIdentifier + public class MqttUnsubAckPacket : MqttBasePacket, IMqttPacketWithIdentifier { public ushort? PacketIdentifier { get; set; } diff --git a/Frameworks/MQTTnet.NetStandard/Packets/MqttUnsubscribe.cs b/Source/MQTTnet/Packets/MqttUnsubscribe.cs similarity index 62% rename from Frameworks/MQTTnet.NetStandard/Packets/MqttUnsubscribe.cs rename to Source/MQTTnet/Packets/MqttUnsubscribe.cs index 6c24a477..63895f12 100644 --- a/Frameworks/MQTTnet.NetStandard/Packets/MqttUnsubscribe.cs +++ b/Source/MQTTnet/Packets/MqttUnsubscribe.cs @@ -2,7 +2,7 @@ namespace MQTTnet.Packets { - public sealed class MqttUnsubscribePacket : MqttBasePacket, IMqttPacketWithIdentifier + public class MqttUnsubscribePacket : MqttBasePacket, IMqttPacketWithIdentifier { public ushort? PacketIdentifier { get; set; } @@ -11,7 +11,7 @@ namespace MQTTnet.Packets public override string ToString() { var topicFiltersText = string.Join(",", TopicFilters); - return "Subscribe: [PacketIdentifier=" + PacketIdentifier + "] [TopicFilters=" + topicFiltersText + "]"; + return "Unsubscribe: [PacketIdentifier=" + PacketIdentifier + "] [TopicFilters=" + topicFiltersText + "]"; } } } diff --git a/Frameworks/MQTTnet.NetStandard/Protocol/MqttConnectReturnCode.cs b/Source/MQTTnet/Protocol/MqttConnectReturnCode.cs similarity index 100% rename from Frameworks/MQTTnet.NetStandard/Protocol/MqttConnectReturnCode.cs rename to Source/MQTTnet/Protocol/MqttConnectReturnCode.cs diff --git a/Frameworks/MQTTnet.NetStandard/Protocol/MqttControlPacketType.cs b/Source/MQTTnet/Protocol/MqttControlPacketType.cs similarity index 100% rename from Frameworks/MQTTnet.NetStandard/Protocol/MqttControlPacketType.cs rename to Source/MQTTnet/Protocol/MqttControlPacketType.cs diff --git a/Frameworks/MQTTnet.NetStandard/Protocol/MqttQualityOfServiceLevel.cs b/Source/MQTTnet/Protocol/MqttQualityOfServiceLevel.cs similarity index 100% rename from Frameworks/MQTTnet.NetStandard/Protocol/MqttQualityOfServiceLevel.cs rename to Source/MQTTnet/Protocol/MqttQualityOfServiceLevel.cs diff --git a/Frameworks/MQTTnet.NetStandard/Protocol/MqttSubscribeReturnCode.cs b/Source/MQTTnet/Protocol/MqttSubscribeReturnCode.cs similarity index 100% rename from Frameworks/MQTTnet.NetStandard/Protocol/MqttSubscribeReturnCode.cs rename to Source/MQTTnet/Protocol/MqttSubscribeReturnCode.cs diff --git a/Source/MQTTnet/Serializer/Extensions.cs b/Source/MQTTnet/Serializer/Extensions.cs new file mode 100644 index 00000000..00ad7cfb --- /dev/null +++ b/Source/MQTTnet/Serializer/Extensions.cs @@ -0,0 +1,23 @@ +using System; + +namespace MQTTnet.Serializer +{ + public static class Extensions + { + public static byte[] ToArray(this ArraySegment source) + { + if (source.Array == null) + { + return null; + } + + var buffer = new byte[source.Count]; + if (buffer.Length > 0) + { + Array.Copy(source.Array, source.Offset, buffer, 0, buffer.Length); + } + + return buffer; + } + } +} diff --git a/Source/MQTTnet/Serializer/IMqttPacketSerializer.cs b/Source/MQTTnet/Serializer/IMqttPacketSerializer.cs new file mode 100644 index 00000000..e3066f9f --- /dev/null +++ b/Source/MQTTnet/Serializer/IMqttPacketSerializer.cs @@ -0,0 +1,17 @@ +using System; +using MQTTnet.Adapter; +using MQTTnet.Packets; + +namespace MQTTnet.Serializer +{ + public interface IMqttPacketSerializer + { + MqttProtocolVersion ProtocolVersion { get; set; } + + ArraySegment Serialize(MqttBasePacket mqttPacket); + + MqttBasePacket Deserialize(ReceivedMqttPacket receivedMqttPacket); + + void FreeBuffer(); + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Serializer/MqttFixedHeader.cs b/Source/MQTTnet/Serializer/MqttFixedHeader.cs new file mode 100644 index 00000000..a8c2015b --- /dev/null +++ b/Source/MQTTnet/Serializer/MqttFixedHeader.cs @@ -0,0 +1,15 @@ +namespace MQTTnet.Serializer +{ + public struct MqttFixedHeader + { + public MqttFixedHeader(byte flags, int remainingLength) + { + Flags = flags; + RemainingLength = remainingLength; + } + + public byte Flags { get; } + + public int RemainingLength { get; } + } +} diff --git a/Source/MQTTnet/Serializer/MqttPacketBodyReader.cs b/Source/MQTTnet/Serializer/MqttPacketBodyReader.cs new file mode 100644 index 00000000..2cf37dd4 --- /dev/null +++ b/Source/MQTTnet/Serializer/MqttPacketBodyReader.cs @@ -0,0 +1,68 @@ +using System; +using System.Text; + +namespace MQTTnet.Serializer +{ + public class MqttPacketBodyReader + { + private readonly byte[] _buffer; + private int _offset; + + public MqttPacketBodyReader(byte[] buffer, int offset) + { + _buffer = buffer; + _offset = offset; + } + + public int Length => _buffer.Length - _offset; + + public bool EndOfStream => _offset == _buffer.Length; + + public byte ReadByte() + { + ValidateReceiveBuffer(1); + return _buffer[_offset++]; + } + + public ArraySegment ReadRemainingData() + { + return new ArraySegment(_buffer, _offset, _buffer.Length - _offset); + } + + public ushort ReadUInt16() + { + ValidateReceiveBuffer(2); + + var msb = _buffer[_offset++]; + var lsb = _buffer[_offset++]; + + return (ushort)(msb << 8 | lsb); + } + + public ArraySegment ReadWithLengthPrefix() + { + var length = ReadUInt16(); + + ValidateReceiveBuffer(length); + + var result = new ArraySegment(_buffer, _offset, length); + _offset += length; + + return result; + } + + private void ValidateReceiveBuffer(ushort length) + { + if (_buffer.Length < _offset + length) + { + throw new ArgumentOutOfRangeException(nameof(_buffer), $"expected at least {_offset + length} bytes but there are only {_buffer.Length} bytes"); + } + } + + public string ReadStringWithLengthPrefix() + { + var buffer = ReadWithLengthPrefix(); + return Encoding.UTF8.GetString(buffer.Array, buffer.Offset, buffer.Count); + } + } +} diff --git a/Source/MQTTnet/Serializer/MqttPacketReader.cs b/Source/MQTTnet/Serializer/MqttPacketReader.cs new file mode 100644 index 00000000..32a92855 --- /dev/null +++ b/Source/MQTTnet/Serializer/MqttPacketReader.cs @@ -0,0 +1,84 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using MQTTnet.Channel; +using MQTTnet.Exceptions; +using MQTTnet.Internal; + +namespace MQTTnet.Serializer +{ + public static class MqttPacketReader + { + public static async Task ReadFixedHeaderAsync(IMqttChannel channel, byte[] fixedHeaderBuffer, byte[] singleByteBuffer, CancellationToken cancellationToken) + { + // The MQTT fixed header contains 1 byte of flags and at least 1 byte for the remaining data length. + // So in all cases at least 2 bytes must be read for a complete MQTT packet. + // async/await is used here because the next packet is received in a couple of minutes so the performance + // impact is acceptable according to a useless waiting thread. + var buffer = fixedHeaderBuffer; + var totalBytesRead = 0; + + while (totalBytesRead < buffer.Length) + { + var bytesRead = await channel.ReadAsync(buffer, totalBytesRead, buffer.Length - totalBytesRead, cancellationToken).ConfigureAwait(false); + if (bytesRead <= 0) + { + cancellationToken.ThrowIfCancellationRequested(); + ExceptionHelper.ThrowGracefulSocketClose(); + } + + totalBytesRead += bytesRead; + } + + var hasRemainingLength = buffer[1] != 0; + if (!hasRemainingLength) + { + return new MqttFixedHeader(buffer[0], 0); + } + + var bodyLength = ReadBodyLength(channel, buffer[1], singleByteBuffer, cancellationToken); + return new MqttFixedHeader(buffer[0], bodyLength); + } + + private static int ReadBodyLength(IMqttChannel channel, byte initialEncodedByte, byte[] singleByteBuffer, 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 = 128; + var value = initialEncodedByte & 127; + int encodedByte = initialEncodedByte; + + while ((encodedByte & 128) != 0) + { + cancellationToken.ThrowIfCancellationRequested(); + + // Here the async/await pattern is not used becuase the overhead of context switches + // is too big for reading 1 byte in a row. We expect that the remaining data was sent + // directly after the initial bytes. If the client disconnects just in this moment we + // will get an exception anyway. + encodedByte = ReadByte(channel, singleByteBuffer, cancellationToken); + + value += (byte)(encodedByte & 127) * multiplier; + if (multiplier > 128 * 128 * 128) + { + throw new MqttProtocolViolationException("Remaining length is invalid."); + } + + multiplier *= 128; + } + + return value; + } + + private static byte ReadByte(IMqttChannel channel, byte[] singleByteBuffer, CancellationToken cancellationToken) + { + var readCount = channel.ReadAsync(singleByteBuffer, 0, 1, cancellationToken).GetAwaiter().GetResult(); + if (readCount <= 0) + { + cancellationToken.ThrowIfCancellationRequested(); + ExceptionHelper.ThrowGracefulSocketClose(); + } + + return singleByteBuffer[0]; + } + } +} diff --git a/Frameworks/MQTTnet.NetStandard/Serializer/MqttPacketSerializer.cs b/Source/MQTTnet/Serializer/MqttPacketSerializer.cs similarity index 55% rename from Frameworks/MQTTnet.NetStandard/Serializer/MqttPacketSerializer.cs rename to Source/MQTTnet/Serializer/MqttPacketSerializer.cs index 0904c759..f1eb790a 100644 --- a/Frameworks/MQTTnet.NetStandard/Serializer/MqttPacketSerializer.cs +++ b/Source/MQTTnet/Serializer/MqttPacketSerializer.cs @@ -2,248 +2,276 @@ using MQTTnet.Packets; using MQTTnet.Protocol; using System; -using System.Collections.Generic; -using System.IO; using System.Linq; -using System.Text; +using MQTTnet.Adapter; namespace MQTTnet.Serializer { - public sealed class MqttPacketSerializer : IMqttPacketSerializer + public class MqttPacketSerializer : IMqttPacketSerializer { - private static byte[] ProtocolVersionV311Name { get; } = Encoding.UTF8.GetBytes("MQTT"); - private static byte[] ProtocolVersionV310Name { get; } = Encoding.UTF8.GetBytes("MQIs"); + private const int FixedHeaderSize = 1; + + private readonly MqttPacketWriter _packetWriter = new MqttPacketWriter(); public MqttProtocolVersion ProtocolVersion { get; set; } = MqttProtocolVersion.V311; - public ICollection> Serialize(MqttBasePacket packet) + public ArraySegment Serialize(MqttBasePacket packet) { if (packet == null) throw new ArgumentNullException(nameof(packet)); - using (var stream = new MemoryStream(128)) - using (var writer = new MqttPacketWriter(stream)) - { - var fixedHeader = SerializePacket(packet, writer); - var remainingLength = (int)stream.Length; - writer.Write(fixedHeader); - MqttPacketWriter.WriteRemainingLength(remainingLength, writer); - var headerLength = (int)stream.Length - remainingLength; + // Leave enough head space for max header size (fixed + 4 variable remaining length = 5 bytes) + _packetWriter.Reset(); + _packetWriter.Seek(5); -#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) - }; - } + var fixedHeader = SerializePacket(packet, _packetWriter); + var remainingLength = _packetWriter.Length - 5; + + var remainingLengthBuffer = MqttPacketWriter.EncodeRemainingLength(remainingLength); + + var headerSize = FixedHeaderSize + remainingLengthBuffer.Count; + var headerOffset = 5 - headerSize; + + // Position cursor on correct offset on beginining of array (has leading 0x0) + _packetWriter.Seek(headerOffset); + _packetWriter.Write(fixedHeader); + _packetWriter.Write(remainingLengthBuffer.Array, remainingLengthBuffer.Offset, remainingLengthBuffer.Count); + + var buffer = _packetWriter.GetBuffer(); + return new ArraySegment(buffer, headerOffset, _packetWriter.Length - headerOffset); } - public MqttBasePacket Deserialize(MqttPacketHeader header, MemoryStream body) + public MqttBasePacket Deserialize(ReceivedMqttPacket receivedMqttPacket) { - if (header == null) throw new ArgumentNullException(nameof(header)); - if (body == null) throw new ArgumentNullException(nameof(body)); + if (receivedMqttPacket == null) throw new ArgumentNullException(nameof(receivedMqttPacket)); - using (var reader = new MqttPacketReader(header, body)) + var controlPacketType = receivedMqttPacket.FixedHeader >> 4; + if (controlPacketType < 1 || controlPacketType > 14) { - return Deserialize(header, reader); + throw new MqttProtocolViolationException($"The packet type is invalid ({controlPacketType})."); + } + + switch ((MqttControlPacketType)controlPacketType) + { + case MqttControlPacketType.Connect: return DeserializeConnect(receivedMqttPacket.Body); + case MqttControlPacketType.ConnAck: return DeserializeConnAck(receivedMqttPacket.Body); + case MqttControlPacketType.Disconnect: return new MqttDisconnectPacket(); + case MqttControlPacketType.Publish: return DeserializePublish(receivedMqttPacket); + case MqttControlPacketType.PubAck: return DeserializePubAck(receivedMqttPacket.Body); + case MqttControlPacketType.PubRec: return DeserializePubRec(receivedMqttPacket.Body); + case MqttControlPacketType.PubRel: return DeserializePubRel(receivedMqttPacket.Body); + case MqttControlPacketType.PubComp: return DeserializePubComp(receivedMqttPacket.Body); + case MqttControlPacketType.PingReq: return new MqttPingReqPacket(); + case MqttControlPacketType.PingResp: return new MqttPingRespPacket(); + case MqttControlPacketType.Subscribe: return DeserializeSubscribe(receivedMqttPacket.Body); + case MqttControlPacketType.SubAck: return DeserializeSubAck(receivedMqttPacket.Body); + case MqttControlPacketType.Unsubscibe: return DeserializeUnsubscribe(receivedMqttPacket.Body); + case MqttControlPacketType.UnsubAck: return DeserializeUnsubAck(receivedMqttPacket.Body); + + default: throw new MqttProtocolViolationException($"Packet type ({controlPacketType}) not supported."); } } - private byte SerializePacket(MqttBasePacket packet, MqttPacketWriter writer) + public void FreeBuffer() + { + _packetWriter.FreeBuffer(); + } + + private byte SerializePacket(MqttBasePacket packet, MqttPacketWriter packetWriter) { switch (packet) { - case MqttConnectPacket connectPacket: return Serialize(connectPacket, writer); - case MqttConnAckPacket connAckPacket: return Serialize(connAckPacket, writer); + case MqttConnectPacket connectPacket: return Serialize(connectPacket, packetWriter); + case MqttConnAckPacket connAckPacket: return Serialize(connAckPacket, packetWriter); case MqttDisconnectPacket _: return SerializeEmptyPacket(MqttControlPacketType.Disconnect); case MqttPingReqPacket _: return SerializeEmptyPacket(MqttControlPacketType.PingReq); case MqttPingRespPacket _: return SerializeEmptyPacket(MqttControlPacketType.PingResp); - case MqttPublishPacket publishPacket: return Serialize(publishPacket, writer); - case MqttPubAckPacket pubAckPacket: return Serialize(pubAckPacket, writer); - case MqttPubRecPacket pubRecPacket: return Serialize(pubRecPacket, writer); - case MqttPubRelPacket pubRelPacket: return Serialize(pubRelPacket, writer); - case MqttPubCompPacket pubCompPacket: return Serialize(pubCompPacket, writer); - case MqttSubscribePacket subscribePacket: return Serialize(subscribePacket, writer); - case MqttSubAckPacket subAckPacket: return Serialize(subAckPacket, writer); - case MqttUnsubscribePacket unsubscribePacket: return Serialize(unsubscribePacket, writer); - case MqttUnsubAckPacket unsubAckPacket: return Serialize(unsubAckPacket, writer); + case MqttPublishPacket publishPacket: return Serialize(publishPacket, packetWriter); + case MqttPubAckPacket pubAckPacket: return Serialize(pubAckPacket, packetWriter); + case MqttPubRecPacket pubRecPacket: return Serialize(pubRecPacket, packetWriter); + case MqttPubRelPacket pubRelPacket: return Serialize(pubRelPacket, packetWriter); + case MqttPubCompPacket pubCompPacket: return Serialize(pubCompPacket, packetWriter); + case MqttSubscribePacket subscribePacket: return Serialize(subscribePacket, packetWriter); + case MqttSubAckPacket subAckPacket: return Serialize(subAckPacket, packetWriter); + case MqttUnsubscribePacket unsubscribePacket: return Serialize(unsubscribePacket, packetWriter); + case MqttUnsubAckPacket unsubAckPacket: return Serialize(unsubAckPacket, packetWriter); default: throw new MqttProtocolViolationException("Packet type invalid."); } } - private MqttBasePacket Deserialize(MqttPacketHeader header, MqttPacketReader reader) + private static MqttBasePacket DeserializeUnsubAck(MqttPacketBodyReader body) { - switch (header.ControlPacketType) - { - case MqttControlPacketType.Connect: return DeserializeConnect(reader); - case MqttControlPacketType.ConnAck: return DeserializeConnAck(reader); - case MqttControlPacketType.Disconnect: return new MqttDisconnectPacket(); - case MqttControlPacketType.Publish: return DeserializePublish(reader, header); - case MqttControlPacketType.PubAck: return DeserializePubAck(reader); - case MqttControlPacketType.PubRec: return DeserializePubRec(reader); - case MqttControlPacketType.PubRel: return DeserializePubRel(reader); - case MqttControlPacketType.PubComp: return DeserializePubComp(reader); - case MqttControlPacketType.PingReq: return new MqttPingReqPacket(); - case MqttControlPacketType.PingResp: return new MqttPingRespPacket(); - case MqttControlPacketType.Subscribe: return DeserializeSubscribe(reader); - case MqttControlPacketType.SubAck: return DeserializeSubAck(reader); - case MqttControlPacketType.Unsubscibe: return DeserializeUnsubscribe(reader); - case MqttControlPacketType.UnsubAck: return DeserializeUnsubAck(reader); - default: throw new MqttProtocolViolationException($"Packet type ({(int)header.ControlPacketType}) not supported."); - } - } + ThrowIfBodyIsEmpty(body); - private static MqttBasePacket DeserializeUnsubAck(MqttPacketReader reader) - { return new MqttUnsubAckPacket { - PacketIdentifier = reader.ReadUInt16() + PacketIdentifier = body.ReadUInt16() }; } - private static MqttBasePacket DeserializePubComp(MqttPacketReader reader) + private static MqttBasePacket DeserializePubComp(MqttPacketBodyReader body) { + ThrowIfBodyIsEmpty(body); + return new MqttPubCompPacket { - PacketIdentifier = reader.ReadUInt16() + PacketIdentifier = body.ReadUInt16() }; } - private static MqttBasePacket DeserializePubRel(MqttPacketReader reader) + private static MqttBasePacket DeserializePubRel(MqttPacketBodyReader body) { + ThrowIfBodyIsEmpty(body); + return new MqttPubRelPacket { - PacketIdentifier = reader.ReadUInt16() + PacketIdentifier = body.ReadUInt16() }; } - private static MqttBasePacket DeserializePubRec(MqttPacketReader reader) + private static MqttBasePacket DeserializePubRec(MqttPacketBodyReader body) { + ThrowIfBodyIsEmpty(body); + return new MqttPubRecPacket { - PacketIdentifier = reader.ReadUInt16() + PacketIdentifier = body.ReadUInt16() }; } - private static MqttBasePacket DeserializePubAck(MqttPacketReader reader) + private static MqttBasePacket DeserializePubAck(MqttPacketBodyReader body) { + ThrowIfBodyIsEmpty(body); + return new MqttPubAckPacket { - PacketIdentifier = reader.ReadUInt16() + PacketIdentifier = body.ReadUInt16() }; } - private static MqttBasePacket DeserializeUnsubscribe(MqttPacketReader reader) + private static MqttBasePacket DeserializeUnsubscribe(MqttPacketBodyReader body) { + ThrowIfBodyIsEmpty(body); + var packet = new MqttUnsubscribePacket { - PacketIdentifier = reader.ReadUInt16(), + PacketIdentifier = body.ReadUInt16(), }; - while (!reader.EndOfRemainingData) + while (!body.EndOfStream) { - packet.TopicFilters.Add(reader.ReadStringWithLengthPrefix()); + packet.TopicFilters.Add(body.ReadStringWithLengthPrefix()); } return packet; } - private static MqttBasePacket DeserializeSubscribe(MqttPacketReader reader) + private static MqttBasePacket DeserializeSubscribe(MqttPacketBodyReader body) { + ThrowIfBodyIsEmpty(body); + var packet = new MqttSubscribePacket { - PacketIdentifier = reader.ReadUInt16() + PacketIdentifier = body.ReadUInt16() }; - while (!reader.EndOfRemainingData) + while (!body.EndOfStream) { packet.TopicFilters.Add(new TopicFilter( - reader.ReadStringWithLengthPrefix(), - (MqttQualityOfServiceLevel)reader.ReadByte())); + body.ReadStringWithLengthPrefix(), + (MqttQualityOfServiceLevel)body.ReadByte())); } return packet; } - private static MqttBasePacket DeserializePublish(MqttPacketReader reader, MqttPacketHeader mqttPacketHeader) + private static MqttBasePacket DeserializePublish(ReceivedMqttPacket receivedMqttPacket) { - var fixedHeader = new ByteReader(mqttPacketHeader.FixedHeader); - var retain = fixedHeader.Read(); - var qualityOfServiceLevel = (MqttQualityOfServiceLevel)fixedHeader.Read(2); - var dup = fixedHeader.Read(); + ThrowIfBodyIsEmpty(receivedMqttPacket.Body); - var topic = reader.ReadStringWithLengthPrefix(); + var retain = (receivedMqttPacket.FixedHeader & 0x1) > 0; + var qualityOfServiceLevel = (MqttQualityOfServiceLevel)(receivedMqttPacket.FixedHeader >> 1 & 0x3); + var dup = (receivedMqttPacket.FixedHeader & 0x8) > 0; - ushort packetIdentifier = 0; + var topic = receivedMqttPacket.Body.ReadStringWithLengthPrefix(); + + ushort? packetIdentifier = null; if (qualityOfServiceLevel > MqttQualityOfServiceLevel.AtMostOnce) { - packetIdentifier = reader.ReadUInt16(); + packetIdentifier = receivedMqttPacket.Body.ReadUInt16(); } var packet = new MqttPublishPacket { + PacketIdentifier = packetIdentifier, Retain = retain, - QualityOfServiceLevel = qualityOfServiceLevel, - Dup = dup, Topic = topic, - Payload = reader.ReadRemainingData(), - PacketIdentifier = packetIdentifier + Payload = receivedMqttPacket.Body.ReadRemainingData().ToArray(), + QualityOfServiceLevel = qualityOfServiceLevel, + Dup = dup }; return packet; } - private static MqttBasePacket DeserializeConnect(MqttPacketReader reader) + private static MqttBasePacket DeserializeConnect(MqttPacketBodyReader body) { - reader.ReadBytes(2); // Skip 2 bytes + ThrowIfBodyIsEmpty(body); + + var protocolName = body.ReadStringWithLengthPrefix(); MqttProtocolVersion protocolVersion; - var protocolName = reader.ReadBytes(4); - if (protocolName.SequenceEqual(ProtocolVersionV310Name)) - { - reader.ReadBytes(2); - protocolVersion = MqttProtocolVersion.V310; - } - else if (protocolName.SequenceEqual(ProtocolVersionV311Name)) + if (protocolName == "MQTT") { + var protocolLevel = body.ReadByte(); + if (protocolLevel != 4) + { + throw new MqttProtocolViolationException($"Protocol level ({protocolLevel}) not supported for MQTT 3.1.1."); + } + protocolVersion = MqttProtocolVersion.V311; } + else if (protocolName == "MQIsdp") + { + var protocolLevel = body.ReadByte(); + if (protocolLevel != 3) + { + throw new MqttProtocolViolationException($"Protocol level ({protocolLevel}) not supported for MQTT 3.1."); + } + + protocolVersion = MqttProtocolVersion.V310; + } else { - throw new MqttProtocolViolationException("Protocol name is not supported."); + throw new MqttProtocolViolationException($"Protocol name ({protocolName}) is not supported."); } - reader.ReadByte(); // Skip protocol level - var connectFlags = reader.ReadByte(); - - var connectFlagsReader = new ByteReader(connectFlags); - connectFlagsReader.Read(); // Reserved. + var connectFlags = body.ReadByte(); + if ((connectFlags & 0x1) > 0) + { + throw new MqttProtocolViolationException("The first bit of the Connect Flags must be set to 0."); + } var packet = new MqttConnectPacket { ProtocolVersion = protocolVersion, - CleanSession = connectFlagsReader.Read() + CleanSession = (connectFlags & 0x2) > 0 }; - var willFlag = connectFlagsReader.Read(); - var willQoS = connectFlagsReader.Read(2); - var willRetain = connectFlagsReader.Read(); - var passwordFlag = connectFlagsReader.Read(); - var usernameFlag = connectFlagsReader.Read(); + var willFlag = (connectFlags & 0x4) > 0; + var willQoS = (connectFlags & 0x18) >> 3; + var willRetain = (connectFlags & 0x20) > 0; + var passwordFlag = (connectFlags & 0x40) > 0; + var usernameFlag = (connectFlags & 0x80) > 0; - packet.KeepAlivePeriod = reader.ReadUInt16(); - packet.ClientId = reader.ReadStringWithLengthPrefix(); + packet.KeepAlivePeriod = body.ReadUInt16(); + packet.ClientId = body.ReadStringWithLengthPrefix(); if (willFlag) { packet.WillMessage = new MqttApplicationMessage { - Topic = reader.ReadStringWithLengthPrefix(), - Payload = reader.ReadWithLengthPrefix(), + Topic = body.ReadStringWithLengthPrefix(), + Payload = body.ReadWithLengthPrefix().ToArray(), QualityOfServiceLevel = (MqttQualityOfServiceLevel)willQoS, Retain = willRetain }; @@ -251,49 +279,53 @@ namespace MQTTnet.Serializer if (usernameFlag) { - packet.Username = reader.ReadStringWithLengthPrefix(); + packet.Username = body.ReadStringWithLengthPrefix(); } if (passwordFlag) { - packet.Password = reader.ReadStringWithLengthPrefix(); + packet.Password = body.ReadStringWithLengthPrefix(); } ValidateConnectPacket(packet); return packet; } - private static MqttBasePacket DeserializeSubAck(MqttPacketReader reader) + private static MqttBasePacket DeserializeSubAck(MqttPacketBodyReader body) { + ThrowIfBodyIsEmpty(body); + var packet = new MqttSubAckPacket { - PacketIdentifier = reader.ReadUInt16() + PacketIdentifier = body.ReadUInt16() }; - while (!reader.EndOfRemainingData) + while (!body.EndOfStream) { - packet.SubscribeReturnCodes.Add((MqttSubscribeReturnCode)reader.ReadByte()); + packet.SubscribeReturnCodes.Add((MqttSubscribeReturnCode)body.ReadByte()); } return packet; } - private MqttBasePacket DeserializeConnAck(MqttPacketReader reader) + private MqttBasePacket DeserializeConnAck(MqttPacketBodyReader body) { + ThrowIfBodyIsEmpty(body); + var packet = new MqttConnAckPacket(); - var firstByteReader = new ByteReader(reader.ReadByte()); + var acknowledgeFlags = body.ReadByte(); if (ProtocolVersion == MqttProtocolVersion.V311) { - packet.IsSessionPresent = firstByteReader.Read(); + packet.IsSessionPresent = (acknowledgeFlags & 0x1) > 0; } - packet.ConnectReturnCode = (MqttConnectReturnCode)reader.ReadByte(); + packet.ConnectReturnCode = (MqttConnectReturnCode)body.ReadByte(); return packet; } - + private static void ValidateConnectPacket(MqttConnectPacket packet) { if (packet == null) throw new ArgumentNullException(nameof(packet)); @@ -304,114 +336,129 @@ namespace MQTTnet.Serializer } } + // ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local private static void ValidatePublishPacket(MqttPublishPacket packet) { - if (packet == null) throw new ArgumentNullException(nameof(packet)); - if (packet.QualityOfServiceLevel == 0 && packet.Dup) { throw new MqttProtocolViolationException("Dup flag must be false for QoS 0 packets [MQTT-3.3.1-2]."); } } - private byte Serialize(MqttConnectPacket packet, MqttPacketWriter writer) + private byte Serialize(MqttConnectPacket packet, MqttPacketWriter packetWriter) { ValidateConnectPacket(packet); // Write variable header - writer.Write(0x00, 0x04); // 3.1.2.1 Protocol Name if (ProtocolVersion == MqttProtocolVersion.V311) { - writer.Write(ProtocolVersionV311Name); - writer.Write(0x04); // 3.1.2.2 Protocol Level (4) + packetWriter.WriteWithLengthPrefix("MQTT"); + packetWriter.Write(4); // 3.1.2.2 Protocol Level 4 } else { - writer.Write(ProtocolVersionV310Name); - writer.Write(0x64, 0x70, 0x03); // Protocol Level (0x03) + packetWriter.WriteWithLengthPrefix("MQIsdp"); + packetWriter.Write(3); // Protocol Level 3 } - var connectFlags = new ByteWriter(); // 3.1.2.3 Connect Flags - connectFlags.Write(false); // Reserved - connectFlags.Write(packet.CleanSession); - connectFlags.Write(packet.WillMessage != null); + byte connectFlags = 0x0; + if (packet.CleanSession) + { + connectFlags |= 0x2; + } if (packet.WillMessage != null) { - connectFlags.Write((int)packet.WillMessage.QualityOfServiceLevel, 2); - connectFlags.Write(packet.WillMessage.Retain); - } - else - { - connectFlags.Write(0, 2); - connectFlags.Write(false); + connectFlags |= 0x4; + connectFlags |= (byte)((byte)packet.WillMessage.QualityOfServiceLevel << 3); + + if (packet.WillMessage.Retain) + { + connectFlags |= 0x20; + } } - connectFlags.Write(packet.Password != null); - connectFlags.Write(packet.Username != null); - - writer.Write(connectFlags); - writer.Write(packet.KeepAlivePeriod); - writer.WriteWithLengthPrefix(packet.ClientId); - - if (packet.WillMessage != null) + if (packet.Password != null && packet.Username == null) { - writer.WriteWithLengthPrefix(packet.WillMessage.Topic); - writer.WriteWithLengthPrefix(packet.WillMessage.Payload); - } - - if (packet.Username != null) - { - writer.WriteWithLengthPrefix(packet.Username); + throw new MqttProtocolViolationException("If the User Name Flag is set to 0, the Password Flag MUST be set to 0 [MQTT-3.1.2-22]."); } if (packet.Password != null) { - writer.WriteWithLengthPrefix(packet.Password); + connectFlags |= 0x40; + } + + if (packet.Username != null) + { + connectFlags |= 0x80; + } + + packetWriter.Write(connectFlags); + packetWriter.Write(packet.KeepAlivePeriod); + packetWriter.WriteWithLengthPrefix(packet.ClientId); + + if (packet.WillMessage != null) + { + packetWriter.WriteWithLengthPrefix(packet.WillMessage.Topic); + packetWriter.WriteWithLengthPrefix(packet.WillMessage.Payload); + } + + if (packet.Username != null) + { + packetWriter.WriteWithLengthPrefix(packet.Username); + } + + if (packet.Password != null) + { + packetWriter.WriteWithLengthPrefix(packet.Password); } return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.Connect); } - private byte Serialize(MqttConnAckPacket packet, MqttPacketWriter writer) + private byte Serialize(MqttConnAckPacket packet, MqttPacketWriter packetWriter) { if (ProtocolVersion == MqttProtocolVersion.V310) { - writer.Write(0); + packetWriter.Write(0); } else if (ProtocolVersion == MqttProtocolVersion.V311) { - var connectAcknowledgeFlags = new ByteWriter(); - connectAcknowledgeFlags.Write(packet.IsSessionPresent); - writer.Write(connectAcknowledgeFlags); + byte connectAcknowledgeFlags = 0x0; + if (packet.IsSessionPresent) + { + connectAcknowledgeFlags |= 0x1; + } + + packetWriter.Write(connectAcknowledgeFlags); } else { throw new MqttProtocolViolationException("Protocol version not supported."); } - writer.Write((byte)packet.ConnectReturnCode); + packetWriter.Write((byte)packet.ConnectReturnCode); return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.ConnAck); } - private static byte Serialize(MqttPubRelPacket packet, MqttPacketWriter writer) + private static byte Serialize(MqttPubRelPacket packet, MqttPacketWriter packetWriter) { if (!packet.PacketIdentifier.HasValue) { throw new MqttProtocolViolationException("PubRel packet has no packet identifier."); } - writer.Write(packet.PacketIdentifier.Value); + packetWriter.Write(packet.PacketIdentifier.Value); return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.PubRel, 0x02); } - private static byte Serialize(MqttPublishPacket packet, MqttPacketWriter writer) + private static byte Serialize(MqttPublishPacket packet, MqttPacketWriter packetWriter) { ValidatePublishPacket(packet); - writer.WriteWithLengthPrefix(packet.Topic); + packetWriter.WriteWithLengthPrefix(packet.Topic); if (packet.QualityOfServiceLevel > MqttQualityOfServiceLevel.AtMostOnce) { @@ -420,7 +467,7 @@ namespace MQTTnet.Serializer throw new MqttProtocolViolationException("Publish packet has no packet identifier."); } - writer.Write(packet.PacketIdentifier.Value); + packetWriter.Write(packet.PacketIdentifier.Value); } else { @@ -432,7 +479,7 @@ namespace MQTTnet.Serializer if (packet.Payload?.Length > 0) { - writer.Write(packet.Payload); + packetWriter.Write(packet.Payload, 0, packet.Payload.Length); } byte fixedHeader = 0; @@ -452,43 +499,43 @@ namespace MQTTnet.Serializer return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.Publish, fixedHeader); } - private static byte Serialize(MqttPubAckPacket packet, MqttPacketWriter writer) + private static byte Serialize(MqttPubAckPacket packet, MqttPacketWriter packetWriter) { if (!packet.PacketIdentifier.HasValue) { throw new MqttProtocolViolationException("PubAck packet has no packet identifier."); } - writer.Write(packet.PacketIdentifier.Value); + packetWriter.Write(packet.PacketIdentifier.Value); return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.PubAck); } - private static byte Serialize(MqttPubRecPacket packet, MqttPacketWriter writer) + private static byte Serialize(MqttPubRecPacket packet, MqttPacketWriter packetWriter) { if (!packet.PacketIdentifier.HasValue) { throw new MqttProtocolViolationException("PubRec packet has no packet identifier."); } - writer.Write(packet.PacketIdentifier.Value); + packetWriter.Write(packet.PacketIdentifier.Value); return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.PubRec); } - private static byte Serialize(MqttPubCompPacket packet, MqttPacketWriter writer) + private static byte Serialize(MqttPubCompPacket packet, MqttPacketWriter packetWriter) { if (!packet.PacketIdentifier.HasValue) { throw new MqttProtocolViolationException("PubComp packet has no packet identifier."); } - writer.Write(packet.PacketIdentifier.Value); + packetWriter.Write(packet.PacketIdentifier.Value); return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.PubComp); } - private static byte Serialize(MqttSubscribePacket packet, MqttPacketWriter writer) + private static byte Serialize(MqttSubscribePacket packet, MqttPacketWriter packetWriter) { if (!packet.TopicFilters.Any()) throw new MqttProtocolViolationException("At least one topic filter must be set [MQTT-3.8.3-3]."); @@ -497,41 +544,41 @@ namespace MQTTnet.Serializer throw new MqttProtocolViolationException("Subscribe packet has no packet identifier."); } - writer.Write(packet.PacketIdentifier.Value); + packetWriter.Write(packet.PacketIdentifier.Value); if (packet.TopicFilters?.Count > 0) { foreach (var topicFilter in packet.TopicFilters) { - writer.WriteWithLengthPrefix(topicFilter.Topic); - writer.Write((byte)topicFilter.QualityOfServiceLevel); + packetWriter.WriteWithLengthPrefix(topicFilter.Topic); + packetWriter.Write((byte)topicFilter.QualityOfServiceLevel); } } return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.Subscribe, 0x02); } - private static byte Serialize(MqttSubAckPacket packet, MqttPacketWriter writer) + private static byte Serialize(MqttSubAckPacket packet, MqttPacketWriter packetWriter) { if (!packet.PacketIdentifier.HasValue) { throw new MqttProtocolViolationException("SubAck packet has no packet identifier."); } - writer.Write(packet.PacketIdentifier.Value); + packetWriter.Write(packet.PacketIdentifier.Value); if (packet.SubscribeReturnCodes?.Any() == true) { foreach (var packetSubscribeReturnCode in packet.SubscribeReturnCodes) { - writer.Write((byte)packetSubscribeReturnCode); + packetWriter.Write((byte)packetSubscribeReturnCode); } } return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.SubAck); } - private static byte Serialize(MqttUnsubscribePacket packet, MqttPacketWriter writer) + private static byte Serialize(MqttUnsubscribePacket packet, MqttPacketWriter packetWriter) { if (!packet.TopicFilters.Any()) throw new MqttProtocolViolationException("At least one topic filter must be set [MQTT-3.10.3-2]."); @@ -540,27 +587,27 @@ namespace MQTTnet.Serializer throw new MqttProtocolViolationException("Unsubscribe packet has no packet identifier."); } - writer.Write(packet.PacketIdentifier.Value); + packetWriter.Write(packet.PacketIdentifier.Value); if (packet.TopicFilters?.Any() == true) { foreach (var topicFilter in packet.TopicFilters) { - writer.WriteWithLengthPrefix(topicFilter); + packetWriter.WriteWithLengthPrefix(topicFilter); } } return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.Unsubscibe, 0x02); } - private static byte Serialize(MqttUnsubAckPacket packet, BinaryWriter writer) + private static byte Serialize(MqttUnsubAckPacket packet, MqttPacketWriter packetWriter) { if (!packet.PacketIdentifier.HasValue) { throw new MqttProtocolViolationException("UnsubAck packet has no packet identifier."); } - writer.Write(packet.PacketIdentifier.Value); + packetWriter.Write(packet.PacketIdentifier.Value); return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.UnsubAck); } @@ -568,5 +615,14 @@ namespace MQTTnet.Serializer { return MqttPacketWriter.BuildFixedHeader(type); } + + // ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local + private static void ThrowIfBodyIsEmpty(MqttPacketBodyReader body) + { + if (body == null || body.Length == 0) + { + throw new MqttProtocolViolationException("Data from the body is required but not present."); + } + } } } diff --git a/Source/MQTTnet/Serializer/MqttPacketWriter.cs b/Source/MQTTnet/Serializer/MqttPacketWriter.cs new file mode 100644 index 00000000..c7f1bc2c --- /dev/null +++ b/Source/MQTTnet/Serializer/MqttPacketWriter.cs @@ -0,0 +1,170 @@ +using System; +using System.Text; +using MQTTnet.Protocol; + +namespace MQTTnet.Serializer +{ + /// + /// This is a custom implementation of a memory stream which provides only MQTTnet relevant features. + /// The goal is to avoid lots of argument checks like in the original stream. The growth rule is the + /// same as for the original MemoryStream in .net. Also this implementation allows accessing the internal + /// buffer for all platforms and .net framework versions (which is not available at the regular MemoryStream). + /// + public class MqttPacketWriter + { + public static int MaxBufferSize = 4096; + + private byte[] _buffer = new byte[128]; + + private int _position; + + public int Length { get; private set; } + + public static byte BuildFixedHeader(MqttControlPacketType packetType, byte flags = 0) + { + var fixedHeader = (int)packetType << 4; + fixedHeader |= flags; + return (byte)fixedHeader; + } + + public static ArraySegment EncodeRemainingLength(int length) + { + // write the encoded remaining length right aligned on the 4 byte buffer + if (length <= 0) + { + return new ArraySegment(new byte[1], 0, 1); + } + + var buffer = new byte[4]; + var bufferOffset = 0; + + // Algorithm taken from http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html. + var x = length; + do + { + var encodedByte = x % 128; + x = x / 128; + if (x > 0) + { + encodedByte = encodedByte | 128; + } + + buffer[bufferOffset] = (byte)encodedByte; + bufferOffset++; + } while (x > 0); + + return new ArraySegment(buffer, 0, bufferOffset); + } + + public void WriteWithLengthPrefix(string value) + { + WriteWithLengthPrefix(Encoding.UTF8.GetBytes(value ?? string.Empty)); + } + + public void WriteWithLengthPrefix(byte[] value) + { + EnsureAdditionalCapacity(value.Length + 2); + + Write((ushort)value.Length); + Write(value, 0, value.Length); + } + + public void Write(byte @byte) + { + EnsureAdditionalCapacity(1); + + _buffer[_position] = @byte; + IncreasePostition(1); + } + + public void Write(ushort value) + { + EnsureAdditionalCapacity(2); + + _buffer[_position] = (byte)(value >> 8); + IncreasePostition(1); + _buffer[_position] = (byte)value; + IncreasePostition(1); + } + + public void Write(byte[] array, int offset, int count) + { + if (array == null) throw new ArgumentNullException(nameof(array)); + + EnsureAdditionalCapacity(count); + + Array.Copy(array, offset, _buffer, _position, count); + IncreasePostition(count); + } + + public void Reset() + { + Length = 5; + } + + public void Seek(int offset) + { + EnsureCapacity(offset); + _position = offset; + } + + public byte[] GetBuffer() + { + return _buffer; + } + + public void FreeBuffer() + { + // This method frees the used memory by shrinking the buffer. This is required because the buffer + // is used across several messages. In general this is not a big issue because subsequent Ping packages + // have the same size but a very big publish package with 100 MB of payload will increase the buffer + // a lot and the size will never reduced. So this method tries to find a size which can be held in + // memory for a long time without causing troubles. + + if (_buffer.Length < MaxBufferSize) + { + return; + } + + Array.Resize(ref _buffer, MaxBufferSize); + } + + private void EnsureAdditionalCapacity(int additionalCapacity) + { + var freeSpace = _buffer.Length - _position; + if (freeSpace >= additionalCapacity) + { + return; + } + + EnsureCapacity(_buffer.Length + additionalCapacity - freeSpace); + } + + private void EnsureCapacity(int capacity) + { + var newBufferLength = _buffer.Length; + + if (newBufferLength >= capacity) + { + return; + } + + while (newBufferLength < capacity) + { + newBufferLength *= 2; + } + + Array.Resize(ref _buffer, newBufferLength); + } + + private void IncreasePostition(int length) + { + _position += length; + + if (_position > Length) + { + Length = _position; + } + } + } +} diff --git a/Frameworks/MQTTnet.NetStandard/Serializer/MqttProtocolVersion.cs b/Source/MQTTnet/Serializer/MqttProtocolVersion.cs similarity index 100% rename from Frameworks/MQTTnet.NetStandard/Serializer/MqttProtocolVersion.cs rename to Source/MQTTnet/Serializer/MqttProtocolVersion.cs diff --git a/Frameworks/MQTTnet.NetStandard/Server/CheckSubscriptionsResult.cs b/Source/MQTTnet/Server/CheckSubscriptionsResult.cs similarity index 100% rename from Frameworks/MQTTnet.NetStandard/Server/CheckSubscriptionsResult.cs rename to Source/MQTTnet/Server/CheckSubscriptionsResult.cs diff --git a/Source/MQTTnet/Server/IMqttClientSession.cs b/Source/MQTTnet/Server/IMqttClientSession.cs new file mode 100644 index 00000000..a94ad185 --- /dev/null +++ b/Source/MQTTnet/Server/IMqttClientSession.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MQTTnet.Adapter; +using MQTTnet.Packets; + +namespace MQTTnet.Server +{ + public interface IMqttClientSession : IDisposable + { + string ClientId { get; } + void FillStatus(MqttClientSessionStatus status); + + void EnqueueApplicationMessage(MqttClientSession senderClientSession, MqttPublishPacket publishPacket); + void ClearPendingApplicationMessages(); + + Task RunAsync(MqttConnectPacket connectPacket, IMqttChannelAdapter adapter); + void Stop(MqttClientDisconnectType disconnectType); + + Task SubscribeAsync(IList topicFilters); + Task UnsubscribeAsync(IList topicFilters); + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Server/IMqttClientSessionStatus.cs b/Source/MQTTnet/Server/IMqttClientSessionStatus.cs new file mode 100644 index 00000000..d9c55938 --- /dev/null +++ b/Source/MQTTnet/Server/IMqttClientSessionStatus.cs @@ -0,0 +1,29 @@ +using System; +using System.Threading.Tasks; +using MQTTnet.Serializer; + +namespace MQTTnet.Server +{ + public interface IMqttClientSessionStatus + { + string ClientId { get; } + + string Endpoint { get; } + + bool IsConnected { get; } + + MqttProtocolVersion? ProtocolVersion { get; } + + TimeSpan LastPacketReceived { get; } + + TimeSpan LastNonKeepAlivePacketReceived { get; } + + int PendingApplicationMessagesCount { get; } + + Task DisconnectAsync(); + + Task DeleteSessionAsync(); + + Task ClearPendingApplicationMessagesAsync(); + } +} diff --git a/Frameworks/MQTTnet.NetStandard/Server/IMqttServer.cs b/Source/MQTTnet/Server/IMqttServer.cs similarity index 84% rename from Frameworks/MQTTnet.NetStandard/Server/IMqttServer.cs rename to Source/MQTTnet/Server/IMqttServer.cs index 229e79c5..e724a65f 100644 --- a/Frameworks/MQTTnet.NetStandard/Server/IMqttServer.cs +++ b/Source/MQTTnet/Server/IMqttServer.cs @@ -6,7 +6,8 @@ namespace MQTTnet.Server { public interface IMqttServer : IApplicationMessageReceiver, IApplicationMessagePublisher { - event EventHandler Started; + event EventHandler Started; + event EventHandler Stopped; event EventHandler ClientConnected; event EventHandler ClientDisconnected; @@ -15,7 +16,7 @@ namespace MQTTnet.Server IMqttServerOptions Options { get; } - Task> GetConnectedClientsAsync(); + Task> GetClientSessionsStatusAsync(); Task SubscribeAsync(string clientId, IList topicFilters); Task UnsubscribeAsync(string clientId, IList topicFilters); diff --git a/Frameworks/MQTTnet.NetStandard/Server/IMqttServerFactory.cs b/Source/MQTTnet/Server/IMqttServerFactory.cs similarity index 100% rename from Frameworks/MQTTnet.NetStandard/Server/IMqttServerFactory.cs rename to Source/MQTTnet/Server/IMqttServerFactory.cs diff --git a/Frameworks/MQTTnet.NetStandard/Server/IMqttServerOptions.cs b/Source/MQTTnet/Server/IMqttServerOptions.cs similarity index 51% rename from Frameworks/MQTTnet.NetStandard/Server/IMqttServerOptions.cs rename to Source/MQTTnet/Server/IMqttServerOptions.cs index cc58eeb8..2ae4c9fa 100644 --- a/Frameworks/MQTTnet.NetStandard/Server/IMqttServerOptions.cs +++ b/Source/MQTTnet/Server/IMqttServerOptions.cs @@ -4,15 +4,20 @@ namespace MQTTnet.Server { public interface IMqttServerOptions { - int ConnectionBacklog { get; } + bool EnablePersistentSessions { get; } + + int MaxPendingMessagesPerClient { get; } + MqttPendingMessagesOverflowStrategy PendingMessagesOverflowStrategy { get; } + TimeSpan DefaultCommunicationTimeout { get; } Action ConnectionValidator { get; } Action SubscriptionInterceptor { get; } Action ApplicationMessageInterceptor { get; } + Action ClientMessageQueueInterceptor { get; set; } - MqttServerDefaultEndpointOptions DefaultEndpointOptions { get; } - MqttServerTlsEndpointOptions TlsEndpointOptions { get; } + MqttServerTcpEndpointOptions DefaultEndpointOptions { get; } + MqttServerTlsTcpEndpointOptions TlsEndpointOptions { get; } IMqttServerStorage Storage { get; } } diff --git a/Frameworks/MQTTnet.NetStandard/Server/IMqttServerStorage.cs b/Source/MQTTnet/Server/IMqttServerStorage.cs similarity index 100% rename from Frameworks/MQTTnet.NetStandard/Server/IMqttServerStorage.cs rename to Source/MQTTnet/Server/IMqttServerStorage.cs diff --git a/Frameworks/MQTTnet.NetStandard/Server/MqttApplicationMessageInterceptorContext.cs b/Source/MQTTnet/Server/MqttApplicationMessageInterceptorContext.cs similarity index 100% rename from Frameworks/MQTTnet.NetStandard/Server/MqttApplicationMessageInterceptorContext.cs rename to Source/MQTTnet/Server/MqttApplicationMessageInterceptorContext.cs diff --git a/Source/MQTTnet/Server/MqttClientConnectedEventArgs.cs b/Source/MQTTnet/Server/MqttClientConnectedEventArgs.cs new file mode 100644 index 00000000..6f043b44 --- /dev/null +++ b/Source/MQTTnet/Server/MqttClientConnectedEventArgs.cs @@ -0,0 +1,14 @@ +using System; + +namespace MQTTnet.Server +{ + public class MqttClientConnectedEventArgs : EventArgs + { + public MqttClientConnectedEventArgs(string clientId) + { + ClientId = clientId ?? throw new ArgumentNullException(nameof(clientId)); + } + + public string ClientId { get; } + } +} diff --git a/Frameworks/MQTTnet.NetStandard/Server/MqttClientDisconnectType.cs b/Source/MQTTnet/Server/MqttClientDisconnectType.cs similarity index 100% rename from Frameworks/MQTTnet.NetStandard/Server/MqttClientDisconnectType.cs rename to Source/MQTTnet/Server/MqttClientDisconnectType.cs diff --git a/Frameworks/MQTTnet.NetStandard/Server/MqttClientDisconnectedEventArgs.cs b/Source/MQTTnet/Server/MqttClientDisconnectedEventArgs.cs similarity index 52% rename from Frameworks/MQTTnet.NetStandard/Server/MqttClientDisconnectedEventArgs.cs rename to Source/MQTTnet/Server/MqttClientDisconnectedEventArgs.cs index 6dd505e5..7e88548e 100644 --- a/Frameworks/MQTTnet.NetStandard/Server/MqttClientDisconnectedEventArgs.cs +++ b/Source/MQTTnet/Server/MqttClientDisconnectedEventArgs.cs @@ -4,13 +4,13 @@ namespace MQTTnet.Server { public class MqttClientDisconnectedEventArgs : EventArgs { - public MqttClientDisconnectedEventArgs(ConnectedMqttClient client, bool wasCleanDisconnect) + public MqttClientDisconnectedEventArgs(string clientId, bool wasCleanDisconnect) { - Client = client ?? throw new ArgumentNullException(nameof(client)); + ClientId = clientId ?? throw new ArgumentNullException(nameof(clientId)); WasCleanDisconnect = wasCleanDisconnect; } - public ConnectedMqttClient Client { get; } + public string ClientId { get; } public bool WasCleanDisconnect { get; } } diff --git a/Frameworks/MQTTnet.NetStandard/Server/MqttClientKeepAliveMonitor.cs b/Source/MQTTnet/Server/MqttClientKeepAliveMonitor.cs similarity index 57% rename from Frameworks/MQTTnet.NetStandard/Server/MqttClientKeepAliveMonitor.cs rename to Source/MQTTnet/Server/MqttClientKeepAliveMonitor.cs index 3f215369..b3628612 100644 --- a/Frameworks/MQTTnet.NetStandard/Server/MqttClientKeepAliveMonitor.cs +++ b/Source/MQTTnet/Server/MqttClientKeepAliveMonitor.cs @@ -7,22 +7,23 @@ using MQTTnet.Packets; namespace MQTTnet.Server { - public sealed class MqttClientKeepAliveMonitor + public 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) + private readonly IMqttClientSession _clientSession; + private readonly IMqttNetChildLogger _logger; + + private bool _isPaused; + + public MqttClientKeepAliveMonitor(IMqttClientSession clientSession, IMqttNetChildLogger logger) { - _clientId = clientId; - _timeoutCallback = timeoutCallback; - _logger = logger; + if (logger == null) throw new ArgumentNullException(nameof(logger)); + + _clientSession = clientSession ?? throw new ArgumentNullException(nameof(clientSession)); + + _logger = logger.CreateChildLogger(nameof(MqttClientKeepAliveMonitor)); } public TimeSpan LastPacketReceived => _lastPacketReceivedTracker.Elapsed; @@ -36,14 +37,26 @@ namespace MQTTnet.Server return; } - _workerTask = Task.Run(() => RunAsync(keepAlivePeriod, cancellationToken).ConfigureAwait(false), cancellationToken); + Task.Run(() => RunAsync(keepAlivePeriod, cancellationToken), cancellationToken); } - public void WaitForCompletion() + public void Pause() { - if (_workerTask != null) + _isPaused = true; + } + + public void Resume() + { + _isPaused = false; + } + + public void PacketReceived(MqttBasePacket packet) + { + _lastPacketReceivedTracker.Restart(); + + if (!(packet is MqttPingReqPacket)) { - Task.WaitAll(_workerTask); + _lastNonKeepAlivePacketReceivedTracker.Restart(); } } @@ -57,15 +70,11 @@ namespace MQTTnet.Server while (!cancellationToken.IsCancellationRequested) { // Values described here: [MQTT-3.1.2-24]. - if (_lastPacketReceivedTracker.Elapsed.TotalSeconds > keepAlivePeriod * 1.5D) + if (!_isPaused && _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); - } - + _logger.Warning(null, "Client '{0}': Did not receive any packet or keep alive signal.", _clientSession.ClientId); + _clientSession.Stop(MqttClientDisconnectType.NotClean); + return; } @@ -77,21 +86,11 @@ namespace MQTTnet.Server } catch (Exception exception) { - _logger.Error(exception, "Client '{0}': Unhandled exception while checking keep alive timeouts.", _clientId); + _logger.Error(exception, "Client '{0}': Unhandled exception while checking keep alive timeouts.", _clientSession.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(); + _logger.Verbose("Client {0}: Stopped checking keep alive timeout.", _clientSession.ClientId); } } } diff --git a/Source/MQTTnet/Server/MqttClientMessageQueueInterceptorContext.cs b/Source/MQTTnet/Server/MqttClientMessageQueueInterceptorContext.cs new file mode 100644 index 00000000..ca0e2a0e --- /dev/null +++ b/Source/MQTTnet/Server/MqttClientMessageQueueInterceptorContext.cs @@ -0,0 +1,20 @@ +namespace MQTTnet.Server +{ + public class MqttClientMessageQueueInterceptorContext + { + public MqttClientMessageQueueInterceptorContext(string senderClientId, string receiverClientId, MqttApplicationMessage applicationMessage) + { + SenderClientId = senderClientId; + ReceiverClientId = receiverClientId; + ApplicationMessage = applicationMessage; + } + + public string SenderClientId { get; } + + public string ReceiverClientId { get; } + + public MqttApplicationMessage ApplicationMessage { get; set; } + + public bool AcceptEnqueue { get; set; } = true; + } +} diff --git a/Source/MQTTnet/Server/MqttClientPendingPacketsQueue.cs b/Source/MQTTnet/Server/MqttClientPendingPacketsQueue.cs new file mode 100644 index 00000000..503d9926 --- /dev/null +++ b/Source/MQTTnet/Server/MqttClientPendingPacketsQueue.cs @@ -0,0 +1,175 @@ +using System; +using System.Collections.Generic; +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.Server +{ + public class MqttClientPendingPacketsQueue : IDisposable + { + private readonly Queue _queue = new Queue(); + private readonly AsyncAutoResetEvent _queueAutoResetEvent = new AsyncAutoResetEvent(); + + private readonly IMqttServerOptions _options; + private readonly MqttClientSession _clientSession; + private readonly IMqttNetChildLogger _logger; + + public MqttClientPendingPacketsQueue(IMqttServerOptions options, MqttClientSession clientSession, IMqttNetChildLogger logger) + { + if (logger == null) throw new ArgumentNullException(nameof(logger)); + _options = options ?? throw new ArgumentNullException(nameof(options)); + _clientSession = clientSession ?? throw new ArgumentNullException(nameof(clientSession)); + + _logger = logger.CreateChildLogger(nameof(MqttClientPendingPacketsQueue)); + } + + public int Count + { + get + { + lock (_queue) + { + return _queue.Count; + } + } + } + + public void Start(IMqttChannelAdapter adapter, CancellationToken cancellationToken) + { + if (adapter == null) throw new ArgumentNullException(nameof(adapter)); + + if (cancellationToken.IsCancellationRequested) + { + return; + } + + Task.Run(() => SendQueuedPacketsAsync(adapter, cancellationToken), cancellationToken); + } + + public void Enqueue(MqttBasePacket packet) + { + if (packet == null) throw new ArgumentNullException(nameof(packet)); + + lock (_queue) + { + if (_queue.Count >= _options.MaxPendingMessagesPerClient) + { + if (_options.PendingMessagesOverflowStrategy == MqttPendingMessagesOverflowStrategy.DropNewMessage) + { + return; + } + + if (_options.PendingMessagesOverflowStrategy == MqttPendingMessagesOverflowStrategy.DropOldestQueuedMessage) + { + _queue.Dequeue(); + } + } + + _queue.Enqueue(packet); + } + + _queueAutoResetEvent.Set(); + + _logger.Verbose("Enqueued packet (ClientId: {0}).", _clientSession.ClientId); + } + + public void Clear() + { + lock (_queue) + { + _queue.Clear(); + } + } + + public void Dispose() + { + } + + private async Task SendQueuedPacketsAsync(IMqttChannelAdapter adapter, CancellationToken cancellationToken) + { + try + { + while (!cancellationToken.IsCancellationRequested) + { + await TrySendNextQueuedPacketAsync(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 TrySendNextQueuedPacketAsync(IMqttChannelAdapter adapter, CancellationToken cancellationToken) + { + MqttBasePacket packet = null; + try + { + lock (_queue) + { + if (_queue.Count > 0) + { + packet = _queue.Dequeue(); + } + } + + if (packet == null) + { + await _queueAutoResetEvent.WaitOneAsync(cancellationToken).ConfigureAwait(false); + return; + } + + if (cancellationToken.IsCancellationRequested) + { + return; + } + + adapter.SendPacketAsync(packet, cancellationToken).GetAwaiter().GetResult(); + + _logger.Verbose("Enqueued packet sent (ClientId: {0}).", _clientSession.ClientId); + } + catch (Exception exception) + { + if (exception is MqttCommunicationTimedOutException) + { + _logger.Warning(exception, "Sending publish packet failed: Timeout (ClientId: {0}).", _clientSession.ClientId); + } + else if (exception is MqttCommunicationException) + { + _logger.Warning(exception, "Sending publish packet failed: 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; + + Enqueue(publishPacket); + } + } + + if (!cancellationToken.IsCancellationRequested) + { + _clientSession.Stop(MqttClientDisconnectType.NotClean); + } + } + } + } +} diff --git a/Source/MQTTnet/Server/MqttClientSession.cs b/Source/MQTTnet/Server/MqttClientSession.cs new file mode 100644 index 00000000..429fdb45 --- /dev/null +++ b/Source/MQTTnet/Server/MqttClientSession.cs @@ -0,0 +1,416 @@ +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; + +namespace MQTTnet.Server +{ + public class MqttClientSession : IMqttClientSession + { + private readonly MqttPacketIdentifierProvider _packetIdentifierProvider = new MqttPacketIdentifierProvider(); + + private readonly MqttRetainedMessagesManager _retainedMessagesManager; + private readonly MqttClientKeepAliveMonitor _keepAliveMonitor; + private readonly MqttClientPendingPacketsQueue _pendingPacketsQueue; + private readonly MqttClientSubscriptionsManager _subscriptionsManager; + private readonly MqttClientSessionsManager _sessionsManager; + + private readonly IMqttNetChildLogger _logger; + private readonly IMqttServerOptions _options; + + private CancellationTokenSource _cancellationTokenSource; + private MqttApplicationMessage _willMessage; + private bool _wasCleanDisconnect; + private IMqttChannelAdapter _adapter; + + public MqttClientSession( + string clientId, + IMqttServerOptions options, + MqttClientSessionsManager sessionsManager, + MqttRetainedMessagesManager retainedMessagesManager, + IMqttNetChildLogger logger) + { + if (logger == null) throw new ArgumentNullException(nameof(logger)); + + _options = options ?? throw new ArgumentNullException(nameof(options)); + _sessionsManager = sessionsManager; + _retainedMessagesManager = retainedMessagesManager ?? throw new ArgumentNullException(nameof(retainedMessagesManager)); + + ClientId = clientId; + + _logger = logger.CreateChildLogger(nameof(MqttClientSession)); + + _keepAliveMonitor = new MqttClientKeepAliveMonitor(this, _logger); + _subscriptionsManager = new MqttClientSubscriptionsManager(clientId, _options, sessionsManager.Server); + _pendingPacketsQueue = new MqttClientPendingPacketsQueue(_options, this, _logger); + } + + public string ClientId { get; } + + public void FillStatus(MqttClientSessionStatus status) + { + status.ClientId = ClientId; + status.IsConnected = _adapter != null; + status.Endpoint = _adapter?.Endpoint; + status.ProtocolVersion = _adapter?.PacketSerializer?.ProtocolVersion; + status.PendingApplicationMessagesCount = _pendingPacketsQueue.Count; + status.LastPacketReceived = _keepAliveMonitor.LastPacketReceived; + status.LastNonKeepAlivePacketReceived = _keepAliveMonitor.LastNonKeepAlivePacketReceived; + } + + 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 + { + _adapter = adapter; + adapter.ReadingPacketStarted += OnAdapterReadingPacketStarted; + adapter.ReadingPacketCompleted += OnAdapterReadingPacketCompleted; + + _cancellationTokenSource = new CancellationTokenSource(); + _wasCleanDisconnect = false; + _willMessage = connectPacket.WillMessage; + + _pendingPacketsQueue.Start(adapter, _cancellationTokenSource.Token); + _keepAliveMonitor.Start(connectPacket.KeepAlivePeriod, _cancellationTokenSource.Token); + + while (!_cancellationTokenSource.IsCancellationRequested) + { + var packet = await adapter.ReceivePacketAsync(TimeSpan.Zero, _cancellationTokenSource.Token).ConfigureAwait(false); + if (packet != null) + { + _keepAliveMonitor.PacketReceived(packet); + ProcessReceivedPacket(adapter, packet, _cancellationTokenSource.Token); + } + } + } + catch (OperationCanceledException) + { + } + catch (Exception exception) + { + if (exception is MqttCommunicationException) + { + if (exception is MqttCommunicationClosedGracefullyException) + { + _logger.Verbose("Client '{0}': Connection closed gracefully.", ClientId); + } + else + { + _logger.Warning(exception, "Client '{0}': Communication exception while receiving client packets.", ClientId); + } + } + else + { + _logger.Error(exception, "Client '{0}': Unhandled exception while receiving client packets.", ClientId); + } + + Stop(MqttClientDisconnectType.NotClean); + } + finally + { + if (_adapter != null) + { + _adapter.ReadingPacketStarted -= OnAdapterReadingPacketStarted; + _adapter.ReadingPacketCompleted -= OnAdapterReadingPacketCompleted; + } + + _adapter = null; + + _cancellationTokenSource?.Dispose(); + _cancellationTokenSource = null; + } + + return _wasCleanDisconnect; + } + + public void Stop(MqttClientDisconnectType type) + { + try + { + var cts = _cancellationTokenSource; + if (cts == null || cts.IsCancellationRequested) + { + return; + } + + _wasCleanDisconnect = type == MqttClientDisconnectType.Clean; + + _cancellationTokenSource?.Cancel(false); + + if (_willMessage != null && !_wasCleanDisconnect) + { + _sessionsManager.EnqueueApplicationMessage(this, _willMessage.ToPublishPacket()); + } + + _willMessage = null; + } + finally + { + _logger.Info("Client '{0}': Session stopped.", ClientId); + } + } + + public void EnqueueApplicationMessage(MqttClientSession senderClientSession, MqttPublishPacket publishPacket) + { + if (publishPacket == null) throw new ArgumentNullException(nameof(publishPacket)); + + var checkSubscriptionsResult = _subscriptionsManager.CheckSubscriptions(publishPacket.Topic, publishPacket.QualityOfServiceLevel); + if (!checkSubscriptionsResult.IsSubscribed) + { + return; + } + + publishPacket = new MqttPublishPacket + { + Topic = publishPacket.Topic, + Payload = publishPacket.Payload, + QualityOfServiceLevel = checkSubscriptionsResult.QualityOfServiceLevel, + Retain = false, + Dup = false + }; + + if (publishPacket.QualityOfServiceLevel > 0) + { + publishPacket.PacketIdentifier = _packetIdentifierProvider.GetNewPacketIdentifier(); + } + + if (_options.ClientMessageQueueInterceptor != null) + { + var context = new MqttClientMessageQueueInterceptorContext( + senderClientSession?.ClientId, + ClientId, + publishPacket.ToApplicationMessage()); + + _options.ClientMessageQueueInterceptor?.Invoke(context); + + if (!context.AcceptEnqueue || context.ApplicationMessage == null) + { + return; + } + + publishPacket.Topic = context.ApplicationMessage.Topic; + publishPacket.Payload = context.ApplicationMessage.Payload; + publishPacket.QualityOfServiceLevel = context.ApplicationMessage.QualityOfServiceLevel; + } + + _pendingPacketsQueue.Enqueue(publishPacket); + } + + public Task SubscribeAsync(IList topicFilters) + { + if (topicFilters == null) throw new ArgumentNullException(nameof(topicFilters)); + + _subscriptionsManager.Subscribe(new MqttSubscribePacket + { + TopicFilters = topicFilters + }); + + EnqueueSubscribedRetainedMessages(topicFilters); + return Task.FromResult(0); + } + + public Task UnsubscribeAsync(IList topicFilters) + { + if (topicFilters == null) throw new ArgumentNullException(nameof(topicFilters)); + + _subscriptionsManager.Unsubscribe(new MqttUnsubscribePacket + { + TopicFilters = topicFilters + }); + + return Task.FromResult(0); + } + + public void ClearPendingApplicationMessages() + { + _pendingPacketsQueue.Clear(); + } + + public void Dispose() + { + _pendingPacketsQueue?.Dispose(); + + _cancellationTokenSource?.Dispose(); + } + + private void ProcessReceivedPacket(IMqttChannelAdapter adapter, MqttBasePacket packet, CancellationToken cancellationToken) + { + if (packet is MqttPublishPacket publishPacket) + { + HandleIncomingPublishPacket(adapter, publishPacket, cancellationToken); + return; + } + + if (packet is MqttPingReqPacket) + { + adapter.SendPacketAsync(new MqttPingRespPacket(), cancellationToken).GetAwaiter().GetResult(); + return; + } + + if (packet is MqttPubRelPacket pubRelPacket) + { + var responsePacket = new MqttPubCompPacket + { + PacketIdentifier = pubRelPacket.PacketIdentifier + }; + + adapter.SendPacketAsync(responsePacket, cancellationToken).GetAwaiter().GetResult(); + return; + } + + if (packet is MqttPubRecPacket pubRecPacket) + { + var responsePacket = new MqttPubRelPacket + { + PacketIdentifier = pubRecPacket.PacketIdentifier + }; + + adapter.SendPacketAsync(responsePacket, cancellationToken).GetAwaiter().GetResult(); + return; + } + + if (packet is MqttPubAckPacket || packet is MqttPubCompPacket) + { + return; + } + + if (packet is MqttSubscribePacket subscribePacket) + { + HandleIncomingSubscribePacket(adapter, subscribePacket, cancellationToken); + return; + } + + if (packet is MqttUnsubscribePacket unsubscribePacket) + { + HandleIncomingUnsubscribePacket(adapter, unsubscribePacket, cancellationToken); + return; + } + + if (packet is MqttDisconnectPacket) + { + Stop(MqttClientDisconnectType.Clean); + return; + } + + if (packet is MqttConnectPacket) + { + Stop(MqttClientDisconnectType.NotClean); + return; + } + + _logger.Warning(null, "Client '{0}': Received not supported packet ({1}). Closing connection.", ClientId, packet); + Stop(MqttClientDisconnectType.NotClean); + } + + private void EnqueueSubscribedRetainedMessages(ICollection topicFilters) + { + var retainedMessages = _retainedMessagesManager.GetSubscribedMessages(topicFilters); + foreach (var applicationMessage in retainedMessages) + { + EnqueueApplicationMessage(null, applicationMessage.ToPublishPacket()); + } + } + + private void HandleIncomingSubscribePacket(IMqttChannelAdapter adapter, MqttSubscribePacket subscribePacket, CancellationToken cancellationToken) + { + var subscribeResult = _subscriptionsManager.Subscribe(subscribePacket); + adapter.SendPacketAsync(subscribeResult.ResponsePacket, cancellationToken).GetAwaiter().GetResult(); + + if (subscribeResult.CloseConnection) + { + Stop(MqttClientDisconnectType.NotClean); + return; + } + + EnqueueSubscribedRetainedMessages(subscribePacket.TopicFilters); + } + + private void HandleIncomingUnsubscribePacket(IMqttChannelAdapter adapter, MqttUnsubscribePacket unsubscribePacket, CancellationToken cancellationToken) + { + var unsubscribeResult = _subscriptionsManager.Unsubscribe(unsubscribePacket); + adapter.SendPacketAsync(unsubscribeResult, cancellationToken).GetAwaiter().GetResult(); + } + + private void HandleIncomingPublishPacket(IMqttChannelAdapter adapter, MqttPublishPacket publishPacket, CancellationToken cancellationToken) + { + switch (publishPacket.QualityOfServiceLevel) + { + case MqttQualityOfServiceLevel.AtMostOnce: + { + HandleIncomingPublishPacketWithQoS0(publishPacket); + break; + } + case MqttQualityOfServiceLevel.AtLeastOnce: + { + HandleIncomingPublishPacketWithQoS1(adapter, publishPacket, cancellationToken); + break; + } + case MqttQualityOfServiceLevel.ExactlyOnce: + { + HandleIncomingPublishPacketWithQoS2(adapter, publishPacket, cancellationToken); + break; + } + default: + { + throw new MqttCommunicationException("Received a not supported QoS level."); + } + } + } + + private void HandleIncomingPublishPacketWithQoS0(MqttPublishPacket publishPacket) + { + _sessionsManager.EnqueueApplicationMessage(this, publishPacket); + } + + private void HandleIncomingPublishPacketWithQoS1( + IMqttChannelAdapter adapter, + MqttPublishPacket publishPacket, + CancellationToken cancellationToken) + { + _sessionsManager.EnqueueApplicationMessage(this, publishPacket); + + var response = new MqttPubAckPacket + { + PacketIdentifier = publishPacket.PacketIdentifier + }; + + adapter.SendPacketAsync(response, cancellationToken).GetAwaiter().GetResult(); + } + + private void HandleIncomingPublishPacketWithQoS2( + IMqttChannelAdapter adapter, + MqttPublishPacket publishPacket, + CancellationToken cancellationToken) + { + // QoS 2 is implement as method "B" (4.3.3 QoS 2: Exactly once delivery) + _sessionsManager.EnqueueApplicationMessage(this, publishPacket); + + var response = new MqttPubRecPacket + { + PacketIdentifier = publishPacket.PacketIdentifier + }; + + adapter.SendPacketAsync(response, cancellationToken).GetAwaiter().GetResult(); + } + + private void OnAdapterReadingPacketCompleted(object sender, EventArgs e) + { + _keepAliveMonitor?.Pause(); + } + + private void OnAdapterReadingPacketStarted(object sender, EventArgs e) + { + _keepAliveMonitor?.Resume(); + } + } +} diff --git a/Source/MQTTnet/Server/MqttClientSessionStatus.cs b/Source/MQTTnet/Server/MqttClientSessionStatus.cs new file mode 100644 index 00000000..4d83357e --- /dev/null +++ b/Source/MQTTnet/Server/MqttClientSessionStatus.cs @@ -0,0 +1,53 @@ +using System; +using System.Threading.Tasks; +using MQTTnet.Serializer; + +namespace MQTTnet.Server +{ + public class MqttClientSessionStatus : IMqttClientSessionStatus + { + private readonly MqttClientSessionsManager _sessionsManager; + private readonly MqttClientSession _session; + + public MqttClientSessionStatus(MqttClientSessionsManager sessionsManager, MqttClientSession session) + { + _sessionsManager = sessionsManager; + _session = session; + } + + public string ClientId { get; set; } + public string Endpoint { get; set; } + public bool IsConnected { get; set; } + public MqttProtocolVersion? ProtocolVersion { get; set; } + public TimeSpan LastPacketReceived { get; set; } + public TimeSpan LastNonKeepAlivePacketReceived { get; set; } + public int PendingApplicationMessagesCount { get; set; } + + public Task DisconnectAsync() + { + _session.Stop(MqttClientDisconnectType.NotClean); + return Task.FromResult(0); + } + + public Task DeleteSessionAsync() + { + try + { + _session.Stop(MqttClientDisconnectType.NotClean); + } + finally + { + _sessionsManager.DeleteSession(ClientId); + } + + return Task.FromResult(0); + } + + public Task ClearPendingApplicationMessagesAsync() + { + _session.ClearPendingApplicationMessages(); + + return Task.FromResult(0); + } + } +} diff --git a/Source/MQTTnet/Server/MqttClientSessionsManager.cs b/Source/MQTTnet/Server/MqttClientSessionsManager.cs new file mode 100644 index 00000000..a41be4f1 --- /dev/null +++ b/Source/MQTTnet/Server/MqttClientSessionsManager.cs @@ -0,0 +1,339 @@ +using System; +using System.Collections.Concurrent; +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.Internal; +using MQTTnet.Packets; +using MQTTnet.Protocol; + +namespace MQTTnet.Server +{ + public class MqttClientSessionsManager : IDisposable + { + private readonly BlockingCollection _messageQueue = new BlockingCollection(); + + /// + /// manual locking dictionaries is faster than using concurrent dictionary + /// + private readonly Dictionary _sessions = new Dictionary(); + + private readonly CancellationToken _cancellationToken; + + private readonly MqttRetainedMessagesManager _retainedMessagesManager; + private readonly IMqttServerOptions _options; + private readonly IMqttNetChildLogger _logger; + + public MqttClientSessionsManager(IMqttServerOptions options, MqttServer server, MqttRetainedMessagesManager retainedMessagesManager, CancellationToken cancellationToken, IMqttNetChildLogger logger) + { + if (logger == null) throw new ArgumentNullException(nameof(logger)); + + _logger = logger.CreateChildLogger(nameof(MqttClientSessionsManager)); + + _cancellationToken = cancellationToken; + _options = options ?? throw new ArgumentNullException(nameof(options)); + Server = server ?? throw new ArgumentNullException(nameof(server)); + _retainedMessagesManager = retainedMessagesManager ?? throw new ArgumentNullException(nameof(retainedMessagesManager)); + } + + public MqttServer Server { get; } + + public void Start() + { + Task.Factory.StartNew(() => ProcessQueuedApplicationMessages(_cancellationToken), _cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Default); + } + + public void Stop() + { + lock (_sessions) + { + foreach (var session in _sessions) + { + session.Value.Stop(MqttClientDisconnectType.NotClean); + } + + _sessions.Clear(); + } + } + + public Task StartSession(IMqttChannelAdapter clientAdapter) + { + return Task.Run(() => RunSession(clientAdapter, _cancellationToken), _cancellationToken); + } + + public Task> GetClientStatusAsync() + { + var result = new List(); + + foreach (var session in GetSessions()) + { + var status = new MqttClientSessionStatus(this, session); + session.FillStatus(status); + + result.Add(status); + } + + return Task.FromResult((IList)result); + } + + public void EnqueueApplicationMessage(MqttClientSession senderClientSession, MqttPublishPacket publishPacket) + { + if (publishPacket == null) throw new ArgumentNullException(nameof(publishPacket)); + + _messageQueue.Add(new MqttEnqueuedApplicationMessage(senderClientSession, publishPacket), _cancellationToken); + } + + public Task SubscribeAsync(string clientId, IList topicFilters) + { + if (clientId == null) throw new ArgumentNullException(nameof(clientId)); + if (topicFilters == null) throw new ArgumentNullException(nameof(topicFilters)); + + lock (_sessions) + { + if (!_sessions.TryGetValue(clientId, out var session)) + { + throw new InvalidOperationException($"Client session '{clientId}' is unknown."); + } + + return session.SubscribeAsync(topicFilters); + } + } + + public Task UnsubscribeAsync(string clientId, IList topicFilters) + { + if (clientId == null) throw new ArgumentNullException(nameof(clientId)); + if (topicFilters == null) throw new ArgumentNullException(nameof(topicFilters)); + + lock (_sessions) + { + if (!_sessions.TryGetValue(clientId, out var session)) + { + throw new InvalidOperationException($"Client session '{clientId}' is unknown."); + } + + return session.UnsubscribeAsync(topicFilters); + } + } + + public void DeleteSession(string clientId) + { + lock (_sessions) + { + _sessions.Remove(clientId); + } + _logger.Verbose("Session for client '{0}' deleted.", clientId); + } + + public void Dispose() + { + _messageQueue?.Dispose(); + } + + private void ProcessQueuedApplicationMessages(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + try + { + var enqueuedApplicationMessage = _messageQueue.Take(cancellationToken); + var sender = enqueuedApplicationMessage.Sender; + var applicationMessage = enqueuedApplicationMessage.PublishPacket.ToApplicationMessage(); + + var interceptorContext = InterceptApplicationMessage(sender, applicationMessage); + if (interceptorContext != null) + { + if (interceptorContext.CloseConnection) + { + enqueuedApplicationMessage.Sender.Stop(MqttClientDisconnectType.NotClean); + } + + if (interceptorContext.ApplicationMessage == null || !interceptorContext.AcceptPublish) + { + return; + } + + applicationMessage = interceptorContext.ApplicationMessage; + } + + Server.OnApplicationMessageReceived(sender?.ClientId, applicationMessage); + + if (applicationMessage.Retain) + { + _retainedMessagesManager.HandleMessageAsync(sender?.ClientId, applicationMessage).GetAwaiter().GetResult(); + } + + foreach (var clientSession in GetSessions()) + { + clientSession.EnqueueApplicationMessage(enqueuedApplicationMessage.Sender, applicationMessage.ToPublishPacket()); + } + } + catch (OperationCanceledException) + { + } + catch (Exception exception) + { + _logger.Error(exception, "Unhandled exception while processing queued application message."); + } + } + } + + private List GetSessions() + { + lock (_sessions) + { + return _sessions.Values.ToList(); + } + } + + private async Task RunSession(IMqttChannelAdapter clientAdapter, CancellationToken cancellationToken) + { + var clientId = string.Empty; + var wasCleanDisconnect = false; + + try + { + var firstPacket = await clientAdapter.ReceivePacketAsync(_options.DefaultCommunicationTimeout, cancellationToken).ConfigureAwait(false); + if (firstPacket == null) + { + return; + } + + if (!(firstPacket 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.SendPacketAsync( + new MqttConnAckPacket + { + ConnectReturnCode = connectReturnCode + }, + cancellationToken).ConfigureAwait(false); + + return; + } + + var result = PrepareClientSession(connectPacket); + var clientSession = result.Session; + + await clientAdapter.SendPacketAsync( + new MqttConnAckPacket + { + ConnectReturnCode = connectReturnCode, + IsSessionPresent = result.IsExistingSession + }, + cancellationToken).ConfigureAwait(false); + + Server.OnClientConnected(clientId); + + 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, CancellationToken.None).ConfigureAwait(false); + clientAdapter.Dispose(); + } + catch (Exception exception) + { + _logger.Error(exception, exception.Message); + } + + if (!_options.EnablePersistentSessions) + { + DeleteSession(clientId); + } + + Server.OnClientDisconnected(clientId, wasCleanDisconnect); + } + } + + 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 PrepareClientSessionResult PrepareClientSession(MqttConnectPacket connectPacket) + { + lock (_sessions) + { + var isSessionPresent = _sessions.TryGetValue(connectPacket.ClientId, out var clientSession); + if (isSessionPresent) + { + if (connectPacket.CleanSession) + { + _sessions.Remove(connectPacket.ClientId); + + clientSession.Stop(MqttClientDisconnectType.Clean); + 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, this, _retainedMessagesManager, _logger); + _sessions[connectPacket.ClientId] = clientSession; + + _logger.Verbose("Created a new session for client '{0}'.", connectPacket.ClientId); + } + + return new PrepareClientSessionResult { IsExistingSession = isExistingSession, Session = clientSession }; + } + } + + private MqttApplicationMessageInterceptorContext InterceptApplicationMessage(MqttClientSession sender, MqttApplicationMessage applicationMessage) + { + var interceptor = _options.ApplicationMessageInterceptor; + if (interceptor == null) + { + return null; + } + + var interceptorContext = new MqttApplicationMessageInterceptorContext(sender?.ClientId, applicationMessage); + interceptor(interceptorContext); + return interceptorContext; + } + } +} \ No newline at end of file diff --git a/Frameworks/MQTTnet.NetStandard/Server/MqttClientSubscribeResult.cs b/Source/MQTTnet/Server/MqttClientSubscribeResult.cs similarity index 100% rename from Frameworks/MQTTnet.NetStandard/Server/MqttClientSubscribeResult.cs rename to Source/MQTTnet/Server/MqttClientSubscribeResult.cs diff --git a/Frameworks/MQTTnet.NetStandard/Server/MqttClientSubscribedTopicEventArgs.cs b/Source/MQTTnet/Server/MqttClientSubscribedTopicEventArgs.cs similarity index 100% rename from Frameworks/MQTTnet.NetStandard/Server/MqttClientSubscribedTopicEventArgs.cs rename to Source/MQTTnet/Server/MqttClientSubscribedTopicEventArgs.cs diff --git a/Frameworks/MQTTnet.NetStandard/Server/MqttClientSubscriptionsManager.cs b/Source/MQTTnet/Server/MqttClientSubscriptionsManager.cs similarity index 53% rename from Frameworks/MQTTnet.NetStandard/Server/MqttClientSubscriptionsManager.cs rename to Source/MQTTnet/Server/MqttClientSubscriptionsManager.cs index e4ee9214..83ac0339 100644 --- a/Frameworks/MQTTnet.NetStandard/Server/MqttClientSubscriptionsManager.cs +++ b/Source/MQTTnet/Server/MqttClientSubscriptionsManager.cs @@ -1,30 +1,26 @@ 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 + public class MqttClientSubscriptionsManager { - private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); private readonly Dictionary _subscriptions = new Dictionary(); private readonly IMqttServerOptions _options; + private readonly MqttServer _server; private readonly string _clientId; - public MqttClientSubscriptionsManager(IMqttServerOptions options, string clientId) + public MqttClientSubscriptionsManager(string clientId, IMqttServerOptions options, MqttServer server) { - _options = options ?? throw new ArgumentNullException(nameof(options)); _clientId = clientId ?? throw new ArgumentNullException(nameof(clientId)); + _options = options ?? throw new ArgumentNullException(nameof(options)); + _server = server; } - public Action TopicSubscribedCallback { get; set; } - public Action TopicUnsubscribedCallback { get; set; } - - public async Task SubscribeAsync(MqttSubscribePacket subscribePacket) + public MqttClientSubscribeResult Subscribe(MqttSubscribePacket subscribePacket) { if (subscribePacket == null) throw new ArgumentNullException(nameof(subscribePacket)); @@ -38,58 +34,50 @@ namespace MQTTnet.Server CloseConnection = false }; - await _semaphore.WaitAsync().ConfigureAwait(false); - try + foreach (var topicFilter in subscribePacket.TopicFilters) { - foreach (var topicFilter in subscribePacket.TopicFilters) + var interceptorContext = InterceptSubscribe(topicFilter); + if (!interceptorContext.AcceptSubscription) { - var interceptorContext = InterceptSubscribe(topicFilter); - if (!interceptorContext.AcceptSubscription) - { - result.ResponsePacket.SubscribeReturnCodes.Add(MqttSubscribeReturnCode.Failure); - } - else - { - result.ResponsePacket.SubscribeReturnCodes.Add(ConvertToMaximumQoS(topicFilter.QualityOfServiceLevel)); - } + result.ResponsePacket.SubscribeReturnCodes.Add(MqttSubscribeReturnCode.Failure); + } + else + { + result.ResponsePacket.SubscribeReturnCodes.Add(ConvertToMaximumQoS(topicFilter.QualityOfServiceLevel)); + } - if (interceptorContext.CloseConnection) - { - result.CloseConnection = true; - } + if (interceptorContext.CloseConnection) + { + result.CloseConnection = true; + } - if (interceptorContext.AcceptSubscription) + if (interceptorContext.AcceptSubscription) + { + lock (_subscriptions) { _subscriptions[topicFilter.Topic] = topicFilter.QualityOfServiceLevel; - TopicSubscribedCallback?.Invoke(_clientId, topicFilter); } + + _server.OnClientSubscribedTopic(_clientId, topicFilter); } } - finally - { - _semaphore.Release(); - } return result; } - public async Task UnsubscribeAsync(MqttUnsubscribePacket unsubscribePacket) + public MqttUnsubAckPacket Unsubscribe(MqttUnsubscribePacket unsubscribePacket) { if (unsubscribePacket == null) throw new ArgumentNullException(nameof(unsubscribePacket)); - await _semaphore.WaitAsync().ConfigureAwait(false); - try + lock (_subscriptions) { foreach (var topicFilter in unsubscribePacket.TopicFilters) { _subscriptions.Remove(topicFilter); - TopicUnsubscribedCallback?.Invoke(_clientId, topicFilter); + + _server.OnClientUnsubscribedTopic(_clientId, topicFilter); } } - finally - { - _semaphore.Release(); - } return new MqttUnsubAckPacket { @@ -97,43 +85,32 @@ namespace MQTTnet.Server }; } - public async Task CheckSubscriptionsAsync(MqttApplicationMessage applicationMessage) + public CheckSubscriptionsResult CheckSubscriptions(string topic, MqttQualityOfServiceLevel qosLevel) { - if (applicationMessage == null) throw new ArgumentNullException(nameof(applicationMessage)); + var qosLevels = new HashSet(); - await _semaphore.WaitAsync().ConfigureAwait(false); - try + lock (_subscriptions) { - var qosLevels = new HashSet(); foreach (var subscription in _subscriptions) { - if (!MqttTopicFilterComparer.IsMatch(applicationMessage.Topic, subscription.Key)) + if (!MqttTopicFilterComparer.IsMatch(topic, subscription.Key)) { continue; } qosLevels.Add(subscription.Value); } - - if (qosLevels.Count == 0) - { - return new CheckSubscriptionsResult - { - IsSubscribed = false - }; - } - - return CreateSubscriptionResult(applicationMessage, qosLevels); } - finally + + if (qosLevels.Count == 0) { - _semaphore.Release(); + return new CheckSubscriptionsResult + { + IsSubscribed = false + }; } - } - public void Dispose() - { - _semaphore?.Dispose(); + return CreateSubscriptionResult(qosLevel, qosLevels); } private static MqttSubscribeReturnCode ConvertToMaximumQoS(MqttQualityOfServiceLevel qualityOfServiceLevel) @@ -154,12 +131,12 @@ namespace MQTTnet.Server return interceptorContext; } - private static CheckSubscriptionsResult CreateSubscriptionResult(MqttApplicationMessage applicationMessage, HashSet subscribedQoSLevels) + private static CheckSubscriptionsResult CreateSubscriptionResult(MqttQualityOfServiceLevel qosLevel, HashSet subscribedQoSLevels) { MqttQualityOfServiceLevel effectiveQoS; - if (subscribedQoSLevels.Contains(applicationMessage.QualityOfServiceLevel)) + if (subscribedQoSLevels.Contains(qosLevel)) { - effectiveQoS = applicationMessage.QualityOfServiceLevel; + effectiveQoS = qosLevel; } else if (subscribedQoSLevels.Count == 1) { diff --git a/Frameworks/MQTTnet.NetStandard/Server/MqttClientUnSubscribedTopicEventArgs.cs b/Source/MQTTnet/Server/MqttClientUnSubscribedTopicEventArgs.cs similarity index 100% rename from Frameworks/MQTTnet.NetStandard/Server/MqttClientUnSubscribedTopicEventArgs.cs rename to Source/MQTTnet/Server/MqttClientUnSubscribedTopicEventArgs.cs diff --git a/Frameworks/MQTTnet.NetStandard/Server/MqttConnectionValidatorContext.cs b/Source/MQTTnet/Server/MqttConnectionValidatorContext.cs similarity index 100% rename from Frameworks/MQTTnet.NetStandard/Server/MqttConnectionValidatorContext.cs rename to Source/MQTTnet/Server/MqttConnectionValidatorContext.cs diff --git a/Source/MQTTnet/Server/MqttEnqueuedApplicationMessage.cs b/Source/MQTTnet/Server/MqttEnqueuedApplicationMessage.cs new file mode 100644 index 00000000..37591d89 --- /dev/null +++ b/Source/MQTTnet/Server/MqttEnqueuedApplicationMessage.cs @@ -0,0 +1,17 @@ +using MQTTnet.Packets; + +namespace MQTTnet.Server +{ + public class MqttEnqueuedApplicationMessage + { + public MqttEnqueuedApplicationMessage(MqttClientSession sender, MqttPublishPacket publishPacket) + { + Sender = sender; + PublishPacket = publishPacket; + } + + public MqttClientSession Sender { get; } + + public MqttPublishPacket PublishPacket { get; } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Server/MqttPendingMessagesOverflowStrategy.cs b/Source/MQTTnet/Server/MqttPendingMessagesOverflowStrategy.cs new file mode 100644 index 00000000..1601487e --- /dev/null +++ b/Source/MQTTnet/Server/MqttPendingMessagesOverflowStrategy.cs @@ -0,0 +1,8 @@ +namespace MQTTnet.Server +{ + public enum MqttPendingMessagesOverflowStrategy + { + DropOldestQueuedMessage, + DropNewMessage + } +} diff --git a/Source/MQTTnet/Server/MqttRetainedMessagesManager.cs b/Source/MQTTnet/Server/MqttRetainedMessagesManager.cs new file mode 100644 index 00000000..86c321fc --- /dev/null +++ b/Source/MQTTnet/Server/MqttRetainedMessagesManager.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using MQTTnet.Diagnostics; + +namespace MQTTnet.Server +{ + public class MqttRetainedMessagesManager + { + private readonly Dictionary _messages = new Dictionary(); + + private readonly IMqttNetChildLogger _logger; + private readonly IMqttServerOptions _options; + + public MqttRetainedMessagesManager(IMqttServerOptions options, IMqttNetChildLogger logger) + { + if (logger == null) throw new ArgumentNullException(nameof(logger)); + _logger = logger.CreateChildLogger(nameof(MqttRetainedMessagesManager)); + _options = options ?? throw new ArgumentNullException(nameof(options)); + } + + public async Task LoadMessagesAsync() + { + if (_options.Storage == null) + { + return; + } + + try + { + var retainedMessages = await _options.Storage.LoadRetainedMessagesAsync().ConfigureAwait(false); + + lock (_messages) + { + _messages.Clear(); + foreach (var retainedMessage in retainedMessages) + { + _messages[retainedMessage.Topic] = retainedMessage; + } + } + } + catch (Exception exception) + { + _logger.Error(exception, "Unhandled exception while loading retained messages."); + } + } + + public async Task HandleMessageAsync(string clientId, MqttApplicationMessage applicationMessage) + { + if (applicationMessage == null) throw new ArgumentNullException(nameof(applicationMessage)); + + try + { + await HandleMessageInternalAsync(clientId, applicationMessage).ConfigureAwait(false); + } + catch (Exception exception) + { + _logger.Error(exception, "Unhandled exception while handling retained messages."); + } + } + + public IList GetSubscribedMessages(ICollection topicFilters) + { + var retainedMessages = new List(); + + lock (_messages) + { + foreach (var retainedMessage in _messages.Values) + { + foreach (var topicFilter in topicFilters) + { + if (!MqttTopicFilterComparer.IsMatch(retainedMessage.Topic, topicFilter.Topic)) + { + continue; + } + + retainedMessages.Add(retainedMessage); + break; + } + } + } + + return retainedMessages; + } + + private async Task HandleMessageInternalAsync(string clientId, MqttApplicationMessage applicationMessage) + { + var saveIsRequired = false; + + lock (_messages) + { + if (applicationMessage.Payload?.Length == 0) + { + saveIsRequired = _messages.Remove(applicationMessage.Topic); + _logger.Info("Client '{0}' cleared retained message for topic '{1}'.", clientId, applicationMessage.Topic); + } + else + { + if (!_messages.TryGetValue(applicationMessage.Topic, out var existingMessage)) + { + _messages[applicationMessage.Topic] = applicationMessage; + saveIsRequired = true; + } + else + { + if (existingMessage.QualityOfServiceLevel != applicationMessage.QualityOfServiceLevel || !existingMessage.Payload.SequenceEqual(applicationMessage.Payload ?? new byte[0])) + { + _messages[applicationMessage.Topic] = applicationMessage; + saveIsRequired = true; + } + } + + _logger.Info("Client '{0}' set retained message for topic '{1}'.", clientId, applicationMessage.Topic); + } + } + + if (!saveIsRequired) + { + _logger.Verbose("Skipped saving retained messages because no changes were detected."); + } + + if (saveIsRequired && _options.Storage != null) + { + List messages; + lock (_messages) + { + messages = _messages.Values.ToList(); + } + + await _options.Storage.SaveRetainedMessagesAsync(messages).ConfigureAwait(false); + } + } + } +} diff --git a/Frameworks/MQTTnet.NetStandard/Server/MqttServer.cs b/Source/MQTTnet/Server/MqttServer.cs similarity index 61% rename from Frameworks/MQTTnet.NetStandard/Server/MqttServer.cs rename to Source/MQTTnet/Server/MqttServer.cs index 63294420..dd732f1b 100644 --- a/Frameworks/MQTTnet.NetStandard/Server/MqttServer.cs +++ b/Source/MQTTnet/Server/MqttServer.cs @@ -5,27 +5,30 @@ using System.Threading; using System.Threading.Tasks; using MQTTnet.Adapter; using MQTTnet.Diagnostics; +using MQTTnet.Internal; namespace MQTTnet.Server { public class MqttServer : IMqttServer { private readonly ICollection _adapters; - private readonly IMqttNetLogger _logger; + private readonly IMqttNetChildLogger _logger; private MqttClientSessionsManager _clientSessionsManager; private MqttRetainedMessagesManager _retainedMessagesManager; private CancellationTokenSource _cancellationTokenSource; - public MqttServer(IEnumerable adapters, IMqttNetLogger logger) + public MqttServer(IEnumerable adapters, IMqttNetChildLogger logger) { if (adapters == null) throw new ArgumentNullException(nameof(adapters)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + if (logger == null) throw new ArgumentNullException(nameof(logger)); + _logger = logger.CreateChildLogger(nameof(MqttServer)); _adapters = adapters.ToList(); } - public event EventHandler Started; + public event EventHandler Started; + public event EventHandler Stopped; public event EventHandler ClientConnected; public event EventHandler ClientDisconnected; @@ -36,9 +39,9 @@ namespace MQTTnet.Server public IMqttServerOptions Options { get; private set; } - public Task> GetConnectedClientsAsync() + public Task> GetClientSessionsStatusAsync() { - return _clientSessionsManager.GetConnectedClientsAsync(); + return _clientSessionsManager.GetClientStatusAsync(); } public Task SubscribeAsync(string clientId, IList topicFilters) @@ -57,16 +60,15 @@ namespace MQTTnet.Server return _clientSessionsManager.UnsubscribeAsync(clientId, topicFilters); } - public async Task PublishAsync(IEnumerable applicationMessages) + public Task PublishAsync(MqttApplicationMessage applicationMessage) { - if (applicationMessages == null) throw new ArgumentNullException(nameof(applicationMessages)); + if (applicationMessage == null) throw new ArgumentNullException(nameof(applicationMessage)); if (_cancellationTokenSource == null) throw new InvalidOperationException("The server is not started."); - foreach (var applicationMessage in applicationMessages) - { - await _clientSessionsManager.DispatchApplicationMessageAsync(null, applicationMessage); - } + _clientSessionsManager.EnqueueApplicationMessage(null, applicationMessage.ToPublishPacket()); + + return Task.FromResult(0); } public async Task StartAsync(IMqttServerOptions options) @@ -78,25 +80,19 @@ namespace MQTTnet.Server _cancellationTokenSource = new CancellationTokenSource(); _retainedMessagesManager = new MqttRetainedMessagesManager(Options, _logger); - await _retainedMessagesManager.LoadMessagesAsync(); + await _retainedMessagesManager.LoadMessagesAsync().ConfigureAwait(false); - _clientSessionsManager = new MqttClientSessionsManager(Options, _retainedMessagesManager, _logger) - { - ClientConnectedCallback = OnClientConnected, - ClientDisconnectedCallback = OnClientDisconnected, - ClientSubscribedTopicCallback = OnClientSubscribedTopic, - ClientUnsubscribedTopicCallback = OnClientUnsubscribedTopic, - ApplicationMessageReceivedCallback = OnApplicationMessageReceived - }; + _clientSessionsManager = new MqttClientSessionsManager(Options, this, _retainedMessagesManager, _cancellationTokenSource.Token, _logger); + _clientSessionsManager.Start(); foreach (var adapter in _adapters) { adapter.ClientAccepted += OnClientAccepted; - await adapter.StartAsync(Options); + await adapter.StartAsync(Options).ConfigureAwait(false); } - _logger.Info("Started."); - Started?.Invoke(this, new MqttServerStartedEventArgs()); + _logger.Info("Started."); + Started?.Invoke(this, EventArgs.Empty); } public async Task StopAsync() @@ -109,61 +105,60 @@ namespace MQTTnet.Server } _cancellationTokenSource.Cancel(false); - _cancellationTokenSource.Dispose(); - + foreach (var adapter in _adapters) { adapter.ClientAccepted -= OnClientAccepted; - await adapter.StopAsync(); + await adapter.StopAsync().ConfigureAwait(false); } - await _clientSessionsManager.StopAsync(); + _clientSessionsManager.Stop(); - _logger.Info("Stopped."); + _logger.Info("Stopped."); + Stopped?.Invoke(this, EventArgs.Empty); } finally { - _clientSessionsManager?.Dispose(); - _retainedMessagesManager?.Dispose(); - + _cancellationTokenSource?.Dispose(); _cancellationTokenSource = null; + _retainedMessagesManager = null; + + _clientSessionsManager?.Dispose(); _clientSessionsManager = null; } } - private void OnClientConnected(ConnectedMqttClient client) + internal void OnClientConnected(string clientId) { - _logger.Info("Client '{0}': Connected.", client.ClientId); - ClientConnected?.Invoke(this, new MqttClientConnectedEventArgs(client)); + _logger.Info("Client '{0}': Connected.", clientId); + ClientConnected?.Invoke(this, new MqttClientConnectedEventArgs(clientId)); } - private void OnClientDisconnected(ConnectedMqttClient client, bool wasCleanDisconnect) + internal void OnClientDisconnected(string clientId, bool wasCleanDisconnect) { - _logger.Info("Client '{0}': Disconnected (clean={1}).", client.ClientId, wasCleanDisconnect); - ClientDisconnected?.Invoke(this, new MqttClientDisconnectedEventArgs(client, wasCleanDisconnect)); + _logger.Info("Client '{0}': Disconnected (clean={1}).", clientId, wasCleanDisconnect); + ClientDisconnected?.Invoke(this, new MqttClientDisconnectedEventArgs(clientId, wasCleanDisconnect)); } - private void OnClientSubscribedTopic(string clientId, TopicFilter topicFilter) + internal void OnClientSubscribedTopic(string clientId, TopicFilter topicFilter) { ClientSubscribedTopic?.Invoke(this, new MqttClientSubscribedTopicEventArgs(clientId, topicFilter)); } - private void OnClientUnsubscribedTopic(string clientId, string topicFilter) + internal void OnClientUnsubscribedTopic(string clientId, string topicFilter) { ClientUnsubscribedTopic?.Invoke(this, new MqttClientUnsubscribedTopicEventArgs(clientId, topicFilter)); } - private void OnApplicationMessageReceived(string clientId, MqttApplicationMessage applicationMessage) + internal 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); + eventArgs.SessionTask = _clientSessionsManager.StartSession(eventArgs.Client); } } } diff --git a/Source/MQTTnet/Server/MqttServerOptions.cs b/Source/MQTTnet/Server/MqttServerOptions.cs new file mode 100644 index 00000000..6c7c4f94 --- /dev/null +++ b/Source/MQTTnet/Server/MqttServerOptions.cs @@ -0,0 +1,28 @@ +using System; + +namespace MQTTnet.Server +{ + public class MqttServerOptions : IMqttServerOptions + { + public MqttServerTcpEndpointOptions DefaultEndpointOptions { get; } = new MqttServerTcpEndpointOptions(); + + public MqttServerTlsTcpEndpointOptions TlsEndpointOptions { get; } = new MqttServerTlsTcpEndpointOptions(); + + public bool EnablePersistentSessions { get; set; } + + public int MaxPendingMessagesPerClient { get; set; } = 250; + public MqttPendingMessagesOverflowStrategy PendingMessagesOverflowStrategy { get; set; } = MqttPendingMessagesOverflowStrategy.DropOldestQueuedMessage; + + public TimeSpan DefaultCommunicationTimeout { get; set; } = TimeSpan.FromSeconds(15); + + public Action ConnectionValidator { get; set; } + + public Action ApplicationMessageInterceptor { get; set; } + + public Action ClientMessageQueueInterceptor { get; set; } + + public Action SubscriptionInterceptor { get; set; } + + public IMqttServerStorage Storage { get; set; } + } +} diff --git a/Frameworks/MQTTnet.NetStandard/Server/MqttServerOptionsBuilder.cs b/Source/MQTTnet/Server/MqttServerOptionsBuilder.cs similarity index 76% rename from Frameworks/MQTTnet.NetStandard/Server/MqttServerOptionsBuilder.cs rename to Source/MQTTnet/Server/MqttServerOptionsBuilder.cs index 18b2de29..f54f379f 100644 --- a/Frameworks/MQTTnet.NetStandard/Server/MqttServerOptionsBuilder.cs +++ b/Source/MQTTnet/Server/MqttServerOptionsBuilder.cs @@ -9,7 +9,14 @@ namespace MQTTnet.Server public MqttServerOptionsBuilder WithConnectionBacklog(int value) { - _options.ConnectionBacklog = value; + _options.DefaultEndpointOptions.ConnectionBacklog = value; + _options.TlsEndpointOptions.ConnectionBacklog = value; + return this; + } + + public MqttServerOptionsBuilder WithMaxPendingMessagesPerClient(int value) + { + _options.MaxPendingMessagesPerClient = value; return this; } @@ -25,7 +32,7 @@ namespace MQTTnet.Server return this; } - public MqttServerOptionsBuilder WithDefaultEndpointPort(int? value) + public MqttServerOptionsBuilder WithDefaultEndpointPort(int value) { _options.DefaultEndpointOptions.Port = value; return this; @@ -33,10 +40,16 @@ namespace MQTTnet.Server public MqttServerOptionsBuilder WithDefaultEndpointBoundIPAddress(IPAddress value) { - _options.DefaultEndpointOptions.BoundIPAddress = value ?? IPAddress.Any; + _options.DefaultEndpointOptions.BoundInterNetworkAddress = value ?? IPAddress.Any; return this; } - + + public MqttServerOptionsBuilder WithDefaultEndpointBoundIPV6Address(IPAddress value) + { + _options.DefaultEndpointOptions.BoundInterNetworkV6Address = value ?? IPAddress.Any; + return this; + } + public MqttServerOptionsBuilder WithoutDefaultEndpoint() { _options.DefaultEndpointOptions.IsEnabled = false; @@ -49,7 +62,7 @@ namespace MQTTnet.Server return this; } - public MqttServerOptionsBuilder WithEncryptedEndpointPort(int? value) + public MqttServerOptionsBuilder WithEncryptedEndpointPort(int value) { _options.TlsEndpointOptions.Port = value; return this; @@ -57,7 +70,7 @@ namespace MQTTnet.Server public MqttServerOptionsBuilder WithEncryptedEndpointBoundIPAddress(IPAddress value) { - _options.TlsEndpointOptions.BoundIPAddress = value; + _options.TlsEndpointOptions.BoundInterNetworkAddress = value; return this; } @@ -97,6 +110,12 @@ namespace MQTTnet.Server return this; } + public MqttServerOptionsBuilder WithPersistentSessions() + { + _options.EnablePersistentSessions = true; + return this; + } + public IMqttServerOptions Build() { return _options; diff --git a/Source/MQTTnet/Server/MqttServerTcpEndpointBaseOptions.cs b/Source/MQTTnet/Server/MqttServerTcpEndpointBaseOptions.cs new file mode 100644 index 00000000..45df2780 --- /dev/null +++ b/Source/MQTTnet/Server/MqttServerTcpEndpointBaseOptions.cs @@ -0,0 +1,21 @@ +using System.Net; + +namespace MQTTnet.Server +{ + public abstract class MqttServerTcpEndpointBaseOptions + { + public bool IsEnabled { get; set; } + + public int Port { get; set; } + + public int ConnectionBacklog { get; set; } = 10; + +#if WINDOWS_UWP + public int BufferSize { get; set; } = 4096; +#endif + + public IPAddress BoundInterNetworkAddress { get; set; } = IPAddress.Any; + + public IPAddress BoundInterNetworkV6Address { get; set; } = IPAddress.IPv6Any; + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Server/MqttServerTcpEndpointOptions.cs b/Source/MQTTnet/Server/MqttServerTcpEndpointOptions.cs new file mode 100644 index 00000000..0eb6dc2b --- /dev/null +++ b/Source/MQTTnet/Server/MqttServerTcpEndpointOptions.cs @@ -0,0 +1,11 @@ +namespace MQTTnet.Server +{ + public class MqttServerTcpEndpointOptions : MqttServerTcpEndpointBaseOptions + { + public MqttServerTcpEndpointOptions() + { + IsEnabled = true; + Port = 1883; + } + } +} diff --git a/Source/MQTTnet/Server/MqttServerTlsTcpEndpointOptions.cs b/Source/MQTTnet/Server/MqttServerTlsTcpEndpointOptions.cs new file mode 100644 index 00000000..76d6c862 --- /dev/null +++ b/Source/MQTTnet/Server/MqttServerTlsTcpEndpointOptions.cs @@ -0,0 +1,12 @@ +namespace MQTTnet.Server +{ + public class MqttServerTlsTcpEndpointOptions : MqttServerTcpEndpointBaseOptions + { + public MqttServerTlsTcpEndpointOptions() + { + Port = 8883; + } + + public byte[] Certificate { get; set; } + } +} diff --git a/Frameworks/MQTTnet.NetStandard/Server/MqttSubscribeResult.cs b/Source/MQTTnet/Server/MqttSubscribeResult.cs similarity index 100% rename from Frameworks/MQTTnet.NetStandard/Server/MqttSubscribeResult.cs rename to Source/MQTTnet/Server/MqttSubscribeResult.cs diff --git a/Frameworks/MQTTnet.NetStandard/Server/MqttSubscriptionInterceptorContext.cs b/Source/MQTTnet/Server/MqttSubscriptionInterceptorContext.cs similarity index 100% rename from Frameworks/MQTTnet.NetStandard/Server/MqttSubscriptionInterceptorContext.cs rename to Source/MQTTnet/Server/MqttSubscriptionInterceptorContext.cs diff --git a/Source/MQTTnet/Server/MqttTopicFilterComparer.cs b/Source/MQTTnet/Server/MqttTopicFilterComparer.cs new file mode 100644 index 00000000..0b0fe6b2 --- /dev/null +++ b/Source/MQTTnet/Server/MqttTopicFilterComparer.cs @@ -0,0 +1,121 @@ +using System; + +namespace MQTTnet.Server +{ + public static class MqttTopicFilterComparer + { + private const char LevelSeparator = '/'; + private const char MultiLevelWildcard = '#'; + private const char SingleLevelWildcard = '+'; + + public static bool IsMatch(string topic, string filter) + { + if (string.IsNullOrEmpty(topic)) throw new ArgumentNullException(nameof(topic)); + if (string.IsNullOrEmpty(filter)) throw new ArgumentNullException(nameof(filter)); + + var sPos = 0; + var sLen = filter.Length; + var tPos = 0; + var tLen = topic.Length; + + while (sPos < sLen && tPos < tLen) + { + if (filter[sPos] == topic[tPos]) + { + if (tPos == tLen - 1) + { + // Check for e.g. foo matching foo/# + if (sPos == sLen - 3 + && filter[sPos + 1] == LevelSeparator + && filter[sPos + 2] == MultiLevelWildcard) + { + return true; + } + } + + sPos++; + tPos++; + + if (sPos == sLen && tPos == tLen) + { + return true; + } + + if (tPos == tLen && sPos == sLen - 1 && filter[sPos] == SingleLevelWildcard) + { + if (sPos > 0 && filter[sPos - 1] != LevelSeparator) + { + // Invalid filter string + return false; + } + + return true; + } + } + else + { + if (filter[sPos] == SingleLevelWildcard) + { + // Check for bad "+foo" or "a/+foo" subscription + if (sPos > 0 && filter[sPos - 1] != LevelSeparator) + { + // Invalid filter string + return false; + } + + // Check for bad "foo+" or "foo+/a" subscription + if (sPos < sLen - 1 && filter[sPos + 1] != LevelSeparator) + { + // Invalid filter string + return false; + } + + sPos++; + while (tPos < tLen && topic[tPos] != LevelSeparator) + { + tPos++; + } + + if (tPos == tLen && sPos == sLen) + { + return true; + } + } + else if (filter[sPos] == MultiLevelWildcard) + { + if (sPos > 0 && filter[sPos - 1] != LevelSeparator) + { + // Invalid filter string + return false; + } + + if (sPos + 1 != sLen) + { + // Invalid filter string + return false; + } + + return true; + } + else + { + // Check for e.g. foo/bar matching foo/+/# + if (sPos > 0 + && sPos + 2 == sLen + && tPos == tLen + && filter[sPos - 1] == SingleLevelWildcard + && filter[sPos] == LevelSeparator + && filter[sPos + 1] == MultiLevelWildcard) + { + return true; + } + + return false; + } + } + } + + return false; + } + } +} diff --git a/Frameworks/MQTTnet.NetStandard/Server/GetOrCreateClientSessionResult.cs b/Source/MQTTnet/Server/PrepareClientSessionResult.cs similarity index 73% rename from Frameworks/MQTTnet.NetStandard/Server/GetOrCreateClientSessionResult.cs rename to Source/MQTTnet/Server/PrepareClientSessionResult.cs index 8f5f085e..9a655bec 100644 --- a/Frameworks/MQTTnet.NetStandard/Server/GetOrCreateClientSessionResult.cs +++ b/Source/MQTTnet/Server/PrepareClientSessionResult.cs @@ -1,6 +1,6 @@ namespace MQTTnet.Server { - public sealed class GetOrCreateClientSessionResult + public class PrepareClientSessionResult { public bool IsExistingSession { get; set; } diff --git a/Frameworks/MQTTnet.NetStandard/TopicFilter.cs b/Source/MQTTnet/TopicFilter.cs similarity index 93% rename from Frameworks/MQTTnet.NetStandard/TopicFilter.cs rename to Source/MQTTnet/TopicFilter.cs index 1dad73d0..7eebbc2c 100644 --- a/Frameworks/MQTTnet.NetStandard/TopicFilter.cs +++ b/Source/MQTTnet/TopicFilter.cs @@ -2,7 +2,7 @@ namespace MQTTnet { - public sealed class TopicFilter + public class TopicFilter { public TopicFilter(string topic, MqttQualityOfServiceLevel qualityOfServiceLevel) { diff --git a/Frameworks/MQTTnet.NetStandard/TopicFilterBuilder.cs b/Source/MQTTnet/TopicFilterBuilder.cs similarity index 100% rename from Frameworks/MQTTnet.NetStandard/TopicFilterBuilder.cs rename to Source/MQTTnet/TopicFilterBuilder.cs diff --git a/Frameworks/MQTTnet.NetStandard/codeSigningKey.pfx b/Source/MQTTnet/codeSigningKey.pfx similarity index 100% rename from Frameworks/MQTTnet.NetStandard/codeSigningKey.pfx rename to Source/MQTTnet/codeSigningKey.pfx diff --git a/Tests/MQTTnet.AspNetCore.Tests/MQTTnet.AspNetCore.Tests.csproj b/Tests/MQTTnet.AspNetCore.Tests/MQTTnet.AspNetCore.Tests.csproj new file mode 100644 index 00000000..77dfff97 --- /dev/null +++ b/Tests/MQTTnet.AspNetCore.Tests/MQTTnet.AspNetCore.Tests.csproj @@ -0,0 +1,19 @@ + + + + netcoreapp2.1 + + false + + + + + + + + + + + + + diff --git a/Tests/MQTTnet.AspNetCore.Tests/ReaderExtensionsTest.cs b/Tests/MQTTnet.AspNetCore.Tests/ReaderExtensionsTest.cs new file mode 100644 index 00000000..e6cdef23 --- /dev/null +++ b/Tests/MQTTnet.AspNetCore.Tests/ReaderExtensionsTest.cs @@ -0,0 +1,45 @@ +using System.Buffers; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.AspNetCore; +using MQTTnet.Packets; +using MQTTnet.Serializer; + +namespace MQTTnet.AspNetCore.Tests +{ + [TestClass] + public class ReaderExtensionsTest + { + [TestMethod] + public void TestTryDeserialize() + { + var serializer = new MqttPacketSerializer(); + + var buffer = serializer.Serialize(new MqttPublishPacket() {Topic = "a", Payload = new byte[5]}); + + var sequence = new ReadOnlySequence(buffer.Array, buffer.Offset, buffer.Count); + + var part = sequence; + MqttBasePacket packet; + var consumed = part.Start; + var observed = part.Start; + var result = false; + + part = sequence.Slice(sequence.Start, 0); // empty message should fail + result = serializer.TryDeserialize(part, out packet, out consumed, out observed); + Assert.IsFalse(result); + + + part = sequence.Slice(sequence.Start, 1); // partial fixed header should fail + result = serializer.TryDeserialize(part, out packet, out consumed, out observed); + Assert.IsFalse(result); + + part = sequence.Slice(sequence.Start, 4); // partial body should fail + result = serializer.TryDeserialize(part, out packet, out consumed, out observed); + Assert.IsFalse(result); + + part = sequence; // complete msg should work + result = serializer.TryDeserialize(part, out packet, out consumed, out observed); + Assert.IsTrue(result); + } + } +} diff --git a/Tests/MQTTnet.Benchmarks/AllowNonOptimized.cs b/Tests/MQTTnet.Benchmarks/AllowNonOptimized.cs new file mode 100644 index 00000000..ecb31a20 --- /dev/null +++ b/Tests/MQTTnet.Benchmarks/AllowNonOptimized.cs @@ -0,0 +1,23 @@ +using System.Linq; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Validators; + +namespace MQTTnet.Benchmarks +{ + /// + /// this options may be used to run benchmarks in debugmode and attach a performance profiler + /// https://benchmarkdotnet.org/Configs/Configs.htm + /// + public class AllowNonOptimized : ManualConfig + { + public AllowNonOptimized() + { + Add(JitOptimizationsValidator.DontFailOnError); // ALLOW NON-OPTIMIZED DLLS + Add(DefaultConfig.Instance.GetLoggers().ToArray()); // manual config has no loggers by default + Add(DefaultConfig.Instance.GetExporters().ToArray()); // manual config has no exporters by default + Add(DefaultConfig.Instance.GetColumnProviders().ToArray()); // manual config has no columns by default + Add(Job.InProcess); + } + } +} \ No newline at end of file diff --git a/Tests/MQTTnet.Benchmarks/App.config b/Tests/MQTTnet.Benchmarks/App.config index 5a41bc2c..662f695c 100644 --- a/Tests/MQTTnet.Benchmarks/App.config +++ b/Tests/MQTTnet.Benchmarks/App.config @@ -1,54 +1,6 @@  - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Tests/MQTTnet.Benchmarks/ChannelAdapterBenchmark.cs b/Tests/MQTTnet.Benchmarks/ChannelAdapterBenchmark.cs new file mode 100644 index 00000000..7f068d60 --- /dev/null +++ b/Tests/MQTTnet.Benchmarks/ChannelAdapterBenchmark.cs @@ -0,0 +1,85 @@ +using BenchmarkDotNet.Attributes; +using MQTTnet.Adapter; +using MQTTnet.Diagnostics; +using MQTTnet.Internal; +using MQTTnet.Packets; +using MQTTnet.Serializer; +using System; +using System.IO; +using System.Threading; + +namespace MQTTnet.Benchmarks +{ + [MemoryDiagnoser] + public class ChannelAdapterBenchmark + { + private MqttChannelAdapter _channelAdapter; + private int _iterations; + private MemoryStream _stream; + private MqttPublishPacket _packet; + + [GlobalSetup] + public void Setup() + { + var message = new MqttApplicationMessageBuilder() + .WithTopic("A") + .Build(); + + _packet = message.ToPublishPacket(); + var serializer = new MqttPacketSerializer(); + var serializedPacket = Join(serializer.Serialize(_packet)); + + _iterations = 10000; + + _stream = new MemoryStream(_iterations * serializedPacket.Length); + + for (var i = 0; i < _iterations; i++) + { + _stream.Write(serializedPacket, 0, serializedPacket.Length); + } + + _stream.Position = 0; + + var channel = new TestMqttChannel(_stream); + + _channelAdapter = new MqttChannelAdapter(channel, serializer, new MqttNetLogger().CreateChildLogger(nameof(MqttChannelAdapter))); + } + + [Benchmark] + public void Receive_10000_Messages() + { + _stream.Position = 0; + + for (var i = 0; i < 10000; i++) + { + _channelAdapter.ReceivePacketAsync(TimeSpan.Zero, CancellationToken.None).GetAwaiter().GetResult(); + } + + _stream.Position = 0; + } + + [Benchmark] + public void Send_10000_Messages() + { + _stream.Position = 0; + + for (var i = 0; i < 10000; i++) + { + _channelAdapter.SendPacketAsync(_packet, CancellationToken.None).GetAwaiter().GetResult(); + } + + _stream.Position = 0; + } + + private static byte[] Join(params ArraySegment[] 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.Benchmarks/LoggerBenchmark.cs b/Tests/MQTTnet.Benchmarks/LoggerBenchmark.cs new file mode 100644 index 00000000..2dce560a --- /dev/null +++ b/Tests/MQTTnet.Benchmarks/LoggerBenchmark.cs @@ -0,0 +1,54 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Attributes.Exporters; +using BenchmarkDotNet.Attributes.Jobs; +using MQTTnet.Diagnostics; + +namespace MQTTnet.Benchmarks +{ + [ClrJob] + [RPlotExporter] + [MemoryDiagnoser] + public class LoggerBenchmark + { + private IMqttNetLogger _logger; + private IMqttNetChildLogger _childLogger; + private bool _useHandler; + + [GlobalSetup] + public void Setup() + { + _logger = new MqttNetLogger("1"); + _childLogger = _logger.CreateChildLogger("child"); + + MqttNetGlobalLogger.LogMessagePublished += OnLogMessagePublished; + } + + private void OnLogMessagePublished(object sender, MqttNetLogMessagePublishedEventArgs eventArgs) + { + if (_useHandler) + { + eventArgs.TraceMessage.ToString(); + } + } + + [Benchmark] + public void Log_10000_Messages_NoHandler() + { + _useHandler = false; + for (var i = 0; i < 10000; i++) + { + _childLogger.Verbose("test log message {0}", "parameter"); + } + } + + [Benchmark] + public void Log_10000_Messages_WithHandler() + { + _useHandler = true; + for (var i = 0; i < 10000; i++) + { + _childLogger.Verbose("test log message {0}", "parameter"); + } + } + } +} diff --git a/Tests/MQTTnet.Benchmarks/MQTTnet.Benchmarks.csproj b/Tests/MQTTnet.Benchmarks/MQTTnet.Benchmarks.csproj index 49773119..2a6b91a5 100644 --- a/Tests/MQTTnet.Benchmarks/MQTTnet.Benchmarks.csproj +++ b/Tests/MQTTnet.Benchmarks/MQTTnet.Benchmarks.csproj @@ -1,169 +1,21 @@ - - - + + - Debug - AnyCPU - {998D04DD-7CB0-45F5-A393-E2495C16399E} Exe - MQTTnet.Benchmarks - MQTTnet.Benchmarks - v4.6.2 - 512 - true - - - AnyCPU - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - AnyCPU - pdbonly - true - bin\Release\ - TRACE - prompt - 4 + Full + net461 + 7.2 + - - ..\..\packages\BenchmarkDotNet.0.10.14\lib\net46\BenchmarkDotNet.dll - - - ..\..\packages\BenchmarkDotNet.Core.0.10.14\lib\net46\BenchmarkDotNet.Core.dll - - - ..\..\packages\BenchmarkDotNet.Toolchains.Roslyn.0.10.14\lib\net46\BenchmarkDotNet.Toolchains.Roslyn.dll - - - ..\..\packages\Microsoft.CodeAnalysis.Common.2.6.1\lib\netstandard1.3\Microsoft.CodeAnalysis.dll - - - ..\..\packages\Microsoft.CodeAnalysis.CSharp.2.6.1\lib\netstandard1.3\Microsoft.CodeAnalysis.CSharp.dll - - - ..\..\packages\Microsoft.DotNet.InternalAbstractions.1.0.0\lib\net451\Microsoft.DotNet.InternalAbstractions.dll - - - ..\..\packages\Microsoft.DotNet.PlatformAbstractions.1.1.1\lib\net451\Microsoft.DotNet.PlatformAbstractions.dll - - - ..\..\packages\Microsoft.Win32.Registry.4.3.0\lib\net46\Microsoft.Win32.Registry.dll - - - - ..\..\packages\System.AppContext.4.3.0\lib\net46\System.AppContext.dll - True - - - ..\..\packages\System.Collections.Immutable.1.3.1\lib\portable-net45+win8+wp8+wpa81\System.Collections.Immutable.dll - True - - - - ..\..\packages\System.Console.4.3.0\lib\net46\System.Console.dll - - - - ..\..\packages\System.Diagnostics.FileVersionInfo.4.3.0\lib\net46\System.Diagnostics.FileVersionInfo.dll - - - ..\..\packages\System.Diagnostics.StackTrace.4.3.0\lib\net46\System.Diagnostics.StackTrace.dll - - - ..\..\packages\System.IO.Compression.4.3.0\lib\net46\System.IO.Compression.dll - True - - - ..\..\packages\System.IO.FileSystem.4.3.0\lib\net46\System.IO.FileSystem.dll - - - ..\..\packages\System.IO.FileSystem.Primitives.4.3.0\lib\net46\System.IO.FileSystem.Primitives.dll - - - - - ..\..\packages\System.Reflection.4.3.0\lib\net462\System.Reflection.dll - - - ..\..\packages\System.Reflection.Metadata.1.4.2\lib\portable-net45+win8\System.Reflection.Metadata.dll - - - ..\..\packages\System.Runtime.4.3.0\lib\net462\System.Runtime.dll - - - ..\..\packages\System.Runtime.Extensions.4.3.0\lib\net462\System.Runtime.Extensions.dll - - - ..\..\packages\System.Runtime.InteropServices.4.3.0\lib\net462\System.Runtime.InteropServices.dll - - - ..\..\packages\System.Security.Cryptography.Algorithms.4.3.0\lib\net461\System.Security.Cryptography.Algorithms.dll - - - ..\..\packages\System.Security.Cryptography.Encoding.4.3.0\lib\net46\System.Security.Cryptography.Encoding.dll - - - ..\..\packages\System.Security.Cryptography.Primitives.4.3.0\lib\net46\System.Security.Cryptography.Primitives.dll - - - ..\..\packages\System.Security.Cryptography.X509Certificates.4.3.0\lib\net461\System.Security.Cryptography.X509Certificates.dll - - - ..\..\packages\System.Text.Encoding.CodePages.4.3.0\lib\net46\System.Text.Encoding.CodePages.dll - - - ..\..\packages\System.Threading.Tasks.Extensions.4.3.0\lib\portable-net45+win8+wp8+wpa81\System.Threading.Tasks.Extensions.dll - - - ..\..\packages\System.Threading.Thread.4.3.0\lib\net46\System.Threading.Thread.dll - - - ..\..\packages\System.ValueTuple.4.3.0\lib\netstandard1.0\System.ValueTuple.dll - - - - - - - - - ..\..\packages\System.Xml.ReaderWriter.4.3.0\lib\net46\System.Xml.ReaderWriter.dll - - - ..\..\packages\System.Xml.XmlDocument.4.3.0\lib\net46\System.Xml.XmlDocument.dll - - - ..\..\packages\System.Xml.XPath.4.3.0\lib\net46\System.Xml.XPath.dll - - - ..\..\packages\System.Xml.XPath.XDocument.4.3.0\lib\net46\System.Xml.XPath.XDocument.dll - + + + + - - - + + - - - - - - - - - - - {3587E506-55A2-4EB3-99C7-DC01E42D25D2} - MQTTnet.NetStandard - - - + \ No newline at end of file diff --git a/Tests/MQTTnet.Benchmarks/MessageProcessingBenchmark.cs b/Tests/MQTTnet.Benchmarks/MessageProcessingBenchmark.cs index 53b71fd6..f6ff61a3 100644 --- a/Tests/MQTTnet.Benchmarks/MessageProcessingBenchmark.cs +++ b/Tests/MQTTnet.Benchmarks/MessageProcessingBenchmark.cs @@ -9,6 +9,7 @@ namespace MQTTnet.Benchmarks { [ClrJob] [RPlotExporter, RankColumn] + [MemoryDiagnoser] public class MessageProcessingBenchmark { private IMqttServer _mqttServer; diff --git a/Tests/MQTTnet.Benchmarks/MessageProcessingMqttConnectionContextBenchmark.cs b/Tests/MQTTnet.Benchmarks/MessageProcessingMqttConnectionContextBenchmark.cs new file mode 100644 index 00000000..1e2fbb2b --- /dev/null +++ b/Tests/MQTTnet.Benchmarks/MessageProcessingMqttConnectionContextBenchmark.cs @@ -0,0 +1,76 @@ +using BenchmarkDotNet.Attributes; +using MQTTnet.Client; + + +using MQTTnet.AspNetCore; + +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using MQTTnet.Server; +using MQTTnet.Diagnostics; +using MQTTnet.AspNetCore.Client; + +namespace MQTTnet.Benchmarks +{ + [MemoryDiagnoser] + public class MessageProcessingMqttConnectionContextBenchmark + { + private IWebHost _host; + private IMqttClient _mqttClient; + private MqttApplicationMessage _message; + + [GlobalSetup] + public void Setup() + { + _host = WebHost.CreateDefaultBuilder() + .UseKestrel(o => o.ListenAnyIP(1883, l => l.UseMqtt())) + .ConfigureServices(services => { + var mqttServerOptions = new MqttServerOptionsBuilder() + .WithoutDefaultEndpoint() + .Build(); + services + .AddHostedMqttServer(mqttServerOptions) + .AddMqttConnectionHandler(); + }) + .Configure(app => { + app.UseMqttServer(s => { + + }); + }) + .Build(); + + var factory = new MqttFactory(); + _mqttClient = factory.CreateMqttClient(new MqttNetLogger(), new MqttClientConnectionContextFactory()); + + _host.StartAsync().GetAwaiter().GetResult(); + + var clientOptions = new MqttClientOptionsBuilder() + .WithTcpServer("localhost").Build(); + + _mqttClient.ConnectAsync(clientOptions).GetAwaiter().GetResult(); + + _message = new MqttApplicationMessageBuilder() + .WithTopic("A") + .Build(); + } + + [GlobalCleanup] + public void Cleanup() + { + _mqttClient.DisconnectAsync().GetAwaiter().GetResult(); + _mqttClient.Dispose(); + + _host.StopAsync().GetAwaiter().GetResult(); + _host.Dispose(); + } + + [Benchmark] + public void Send_10000_Messages() + { + for (var i = 0; i < 10000; i++) + { + _mqttClient.PublishAsync(_message).GetAwaiter().GetResult(); + } + } + } +} diff --git a/Tests/MQTTnet.Benchmarks/MqttTcpChannelBenchmark.cs b/Tests/MQTTnet.Benchmarks/MqttTcpChannelBenchmark.cs new file mode 100644 index 00000000..1f29c97a --- /dev/null +++ b/Tests/MQTTnet.Benchmarks/MqttTcpChannelBenchmark.cs @@ -0,0 +1,73 @@ +using BenchmarkDotNet.Attributes; +using MQTTnet.Channel; +using MQTTnet.Client; +using MQTTnet.Diagnostics; +using MQTTnet.Implementations; +using MQTTnet.Server; +using System.Threading; +using System.Threading.Tasks; + +namespace MQTTnet.Benchmarks +{ + [MemoryDiagnoser] + public class MqttTcpChannelBenchmark + { + private IMqttServer _mqttServer; + private IMqttChannel _serverChannel; + private IMqttChannel _clientChannel; + + [GlobalSetup] + public void Setup() + { + var factory = new MqttFactory(); + var tcpServer = new MqttTcpServerAdapter(new MqttNetLogger().CreateChildLogger()); + tcpServer.ClientAccepted += (sender, args) => _serverChannel = (IMqttChannel)args.Client.GetType().GetField("_channel", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).GetValue(args.Client); + + _mqttServer = factory.CreateMqttServer(new[] { tcpServer }, new MqttNetLogger()); + + var serverOptions = new MqttServerOptionsBuilder().Build(); + _mqttServer.StartAsync(serverOptions).GetAwaiter().GetResult(); + + var clientOptions = new MqttClientOptionsBuilder() + .WithTcpServer("localhost").Build(); + + var tcpOptions = (MqttClientTcpOptions)clientOptions.ChannelOptions; + _clientChannel = new MqttTcpChannel(new MqttClientOptions { ChannelOptions = tcpOptions }); + + _clientChannel.ConnectAsync(CancellationToken.None).GetAwaiter().GetResult(); + } + + [Benchmark] + public async Task Send_10000_Chunks() + { + var size = 5; + var iterations = 10000; + + await Task.WhenAll(WriteAsync(iterations, size), ReadAsync(iterations, size)); + } + + private async Task ReadAsync(int iterations, int size) + { + await Task.Yield(); + + var expected = iterations * size; + long read = 0; + + while (read < expected) + { + var readresult = await _clientChannel.ReadAsync(new byte[size], 0, size, CancellationToken.None).ConfigureAwait(false); + read += readresult; + } + } + + private async Task WriteAsync(int iterations, int size) + { + await Task.Yield(); + + for (var i = 0; i < iterations; i++) + { + await _serverChannel.WriteAsync(new byte[size], 0, size, CancellationToken.None).ConfigureAwait(false); + } + } + } +} diff --git a/Tests/MQTTnet.Benchmarks/Program.cs b/Tests/MQTTnet.Benchmarks/Program.cs index 965ddb55..9407ccfe 100644 --- a/Tests/MQTTnet.Benchmarks/Program.cs +++ b/Tests/MQTTnet.Benchmarks/Program.cs @@ -1,5 +1,6 @@ using System; using BenchmarkDotNet.Running; +using MQTTnet.Diagnostics; namespace MQTTnet.Benchmarks { @@ -7,7 +8,45 @@ namespace MQTTnet.Benchmarks { public static void Main(string[] args) { - var summary = BenchmarkRunner.Run(); + Console.WriteLine($"MQTTnet - BenchmarkApp.{TargetFrameworkInfoProvider.TargetFramework}"); + Console.WriteLine("1 = MessageProcessingBenchmark"); + Console.WriteLine("2 = SerializerBenchmark"); + Console.WriteLine("3 = LoggerBenchmark"); + Console.WriteLine("4 = TopicFilterComparerBenchmark"); + Console.WriteLine("5 = ChannelAdapterBenchmark"); + Console.WriteLine("6 = MqttTcpChannelBenchmark"); + Console.WriteLine("7 = TcpPipesBenchmark"); + Console.WriteLine("8 = MessageProcessingMqttConnectionContextBenchmark"); + + var pressedKey = Console.ReadKey(true); + switch (pressedKey.KeyChar) + { + case '1': + BenchmarkRunner.Run(); + break; + case '2': + BenchmarkRunner.Run(); + break; + case '3': + BenchmarkRunner.Run(); + break; + case '4': + BenchmarkRunner.Run(); + break; + case '5': + BenchmarkRunner.Run(); + break; + case '6': + BenchmarkRunner.Run(); + break; + case '7': + BenchmarkRunner.Run(); + break; + case '8': + BenchmarkRunner.Run(new AllowNonOptimized()); + break; + } + Console.ReadLine(); } } diff --git a/Tests/MQTTnet.Benchmarks/Properties/AssemblyInfo.cs b/Tests/MQTTnet.Benchmarks/Properties/AssemblyInfo.cs deleted file mode 100644 index 4df83f6e..00000000 --- a/Tests/MQTTnet.Benchmarks/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("MQTTnet.Benchmarks")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("MQTTnet.Benchmarks")] -[assembly: AssemblyCopyright("Copyright © 2018")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("998d04dd-7cb0-45f5-a393-e2495c16399e")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Tests/MQTTnet.Benchmarks/SerializerBenchmark.cs b/Tests/MQTTnet.Benchmarks/SerializerBenchmark.cs new file mode 100644 index 00000000..8beb7b49 --- /dev/null +++ b/Tests/MQTTnet.Benchmarks/SerializerBenchmark.cs @@ -0,0 +1,113 @@ +using BenchmarkDotNet.Attributes; +using MQTTnet.Packets; +using MQTTnet.Serializer; +using MQTTnet.Internal; +using BenchmarkDotNet.Attributes.Jobs; +using BenchmarkDotNet.Attributes.Exporters; +using System; +using System.Threading; +using System.Threading.Tasks; +using MQTTnet.Adapter; +using MQTTnet.Channel; + +namespace MQTTnet.Benchmarks +{ + [ClrJob] + [RPlotExporter] + [MemoryDiagnoser] + public class SerializerBenchmark + { + private MqttBasePacket _packet; + private ArraySegment _serializedPacket; + private MqttPacketSerializer _serializer; + + [GlobalSetup] + public void Setup() + { + var message = new MqttApplicationMessageBuilder() + .WithTopic("A") + .Build(); + + _packet = message.ToPublishPacket(); + _serializer = new MqttPacketSerializer(); + _serializedPacket = _serializer.Serialize(_packet); + } + + [Benchmark] + public void Serialize_10000_Messages() + { + for (var i = 0; i < 10000; i++) + { + _serializer.Serialize(_packet); + _serializer.FreeBuffer(); + } + } + + [Benchmark] + public void Deserialize_10000_Messages() + { + var channel = new BenchmarkMqttChannel(_serializedPacket); + var fixedHeader = new byte[2]; + var singleByteBuffer = new byte[1]; + + for (var i = 0; i < 10000; i++) + { + channel.Reset(); + + var header = MqttPacketReader.ReadFixedHeaderAsync(channel, fixedHeader, singleByteBuffer, CancellationToken.None).GetAwaiter().GetResult(); + + var receivedPacket = new ReceivedMqttPacket( + header.Flags, + new MqttPacketBodyReader(_serializedPacket.Array, _serializedPacket.Count - header.RemainingLength)); + + _serializer.Deserialize(receivedPacket); + } + } + + private class BenchmarkMqttChannel : IMqttChannel + { + private readonly ArraySegment _buffer; + private int _position; + + public BenchmarkMqttChannel(ArraySegment buffer) + { + _buffer = buffer; + _position = _buffer.Offset; + } + + public string Endpoint { get; } + + public void Reset() + { + _position = _buffer.Offset; + } + + public Task ConnectAsync(CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task DisconnectAsync() + { + throw new NotImplementedException(); + } + + public Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + Array.Copy(_buffer.Array, _position, buffer, offset, count); + _position += count; + + return Task.FromResult(count); + } + + public Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public void Dispose() + { + } + } + } +} diff --git a/Tests/MQTTnet.Benchmarks/TcpPipesBenchmark.cs b/Tests/MQTTnet.Benchmarks/TcpPipesBenchmark.cs new file mode 100644 index 00000000..4252ab36 --- /dev/null +++ b/Tests/MQTTnet.Benchmarks/TcpPipesBenchmark.cs @@ -0,0 +1,74 @@ +using System.IO.Pipelines; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using MQTTnet.AspNetCore.Client.Tcp; + +namespace MQTTnet.Benchmarks +{ + [MemoryDiagnoser] + public class TcpPipesBenchmark + { + private IDuplexPipe _client; + private IDuplexPipe _server; + + [GlobalSetup] + public void Setup() + { + var server = new TcpListener(IPAddress.Any, 1883); + + server.Start(1); + + var task = Task.Run(() => server.AcceptSocket()); + + var clientConnection = new TcpConnection(new IPEndPoint(IPAddress.Loopback, 1883)); + + clientConnection.StartAsync().GetAwaiter().GetResult(); + _client = clientConnection.Transport; + + var serverConnection = new TcpConnection(task.GetAwaiter().GetResult()); + serverConnection.StartAsync().GetAwaiter().GetResult(); + _server = serverConnection.Transport; + } + + + [Benchmark] + public async Task Send_10000_Chunks_Pipe() + { + var size = 5; + var iterations = 10000; + + await Task.WhenAll(WriteAsync(iterations, size), ReadAsync(iterations, size)); + } + + private async Task ReadAsync(int iterations, int size) + { + await Task.Yield(); + + var expected = iterations * size; + long read = 0; + var input = _client.Input; + + while (read < expected) + { + var readresult = await input.ReadAsync(CancellationToken.None).ConfigureAwait(false); + input.AdvanceTo(readresult.Buffer.End); + read += readresult.Buffer.Length; + } + } + + private async Task WriteAsync(int iterations, int size) + { + await Task.Yield(); + + var output = _server.Output; + + for (var i = 0; i < iterations; i++) + { + await output.WriteAsync(new byte[size], CancellationToken.None).ConfigureAwait(false); + } + } + } +} diff --git a/Tests/MQTTnet.Benchmarks/TopicFilterComparerBenchmark.cs b/Tests/MQTTnet.Benchmarks/TopicFilterComparerBenchmark.cs new file mode 100644 index 00000000..6d9a0321 --- /dev/null +++ b/Tests/MQTTnet.Benchmarks/TopicFilterComparerBenchmark.cs @@ -0,0 +1,91 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Attributes.Exporters; +using BenchmarkDotNet.Attributes.Jobs; +using MQTTnet.Server; +using System; + +namespace MQTTnet.Benchmarks +{ + [ClrJob] + [RPlotExporter] + [MemoryDiagnoser] + public class TopicFilterComparerBenchmark + { + private static readonly char[] TopicLevelSeparator = { '/' }; + + [GlobalSetup] + public void Setup() + { + } + + [Benchmark] + public void MqttTopicFilterComparer_10000_StringSplitMethod() + { + for (var i = 0; i < 10000; i++) + { + LegacyMethodByStringSplit("sport/tennis/player1", "sport/#"); + LegacyMethodByStringSplit("sport/tennis/player1/ranking", "sport/#/ranking"); + LegacyMethodByStringSplit("sport/tennis/player1/score/wimbledon", "sport/+/player1/#"); + LegacyMethodByStringSplit("sport/tennis/player1", "sport/tennis/+"); + LegacyMethodByStringSplit("/finance", "+/+"); + LegacyMethodByStringSplit("/finance", "/+"); + LegacyMethodByStringSplit("/finance", "+"); + } + } + + [Benchmark] + public void MqttTopicFilterComparer_10000_LoopMethod() + { + for (var i = 0; i < 10000; i++) + { + MqttTopicFilterComparer.IsMatch("sport/tennis/player1", "sport/#"); + MqttTopicFilterComparer.IsMatch("sport/tennis/player1/ranking", "sport/#/ranking"); + MqttTopicFilterComparer.IsMatch("sport/tennis/player1/score/wimbledon", "sport/+/player1/#"); + MqttTopicFilterComparer.IsMatch("sport/tennis/player1", "sport/tennis/+"); + MqttTopicFilterComparer.IsMatch("/finance", "+/+"); + MqttTopicFilterComparer.IsMatch("/finance", "/+"); + MqttTopicFilterComparer.IsMatch("/finance", "+"); + } + } + + private static bool LegacyMethodByStringSplit(string topic, string filter) + { + if (topic == null) throw new ArgumentNullException(nameof(topic)); + if (filter == null) throw new ArgumentNullException(nameof(filter)); + + if (string.Equals(topic, filter, StringComparison.Ordinal)) + { + return true; + } + + var fragmentsTopic = topic.Split(TopicLevelSeparator, StringSplitOptions.None); + var fragmentsFilter = filter.Split(TopicLevelSeparator, StringSplitOptions.None); + + // # > In either case it MUST be the last character specified in the Topic Filter [MQTT-4.7.1-2]. + for (var i = 0; i < fragmentsFilter.Length; i++) + { + if (fragmentsFilter[i] == "+") + { + continue; + } + + if (fragmentsFilter[i] == "#") + { + return true; + } + + if (i >= fragmentsTopic.Length) + { + return false; + } + + if (!string.Equals(fragmentsFilter[i], fragmentsTopic[i], StringComparison.Ordinal)) + { + return false; + } + } + + return fragmentsTopic.Length == fragmentsFilter.Length; + } + } +} diff --git a/Tests/MQTTnet.Core.Tests/AsyncAutoResentEventTests.cs b/Tests/MQTTnet.Core.Tests/AsyncAutoResentEventTests.cs new file mode 100644 index 00000000..7087ff04 --- /dev/null +++ b/Tests/MQTTnet.Core.Tests/AsyncAutoResentEventTests.cs @@ -0,0 +1,210 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Internal; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace MQTTnet.Core.Tests +{ + [TestClass] + // Inspired from the vs-threading tests (https://github.com/Microsoft/vs-threading/blob/master/src/Microsoft.VisualStudio.Threading.Tests/AsyncAutoResetEventTests.cs) + public class AsyncAutoResetEventTests + { + private readonly AsyncAutoResetEvent _aare; + + public AsyncAutoResetEventTests() + { + _aare = new AsyncAutoResetEvent(); + } + + [TestMethod] + public async Task SingleThreadedPulse() + { + for (int i = 0; i < 5; i++) + { + var t = _aare.WaitOneAsync(); + Assert.IsFalse(t.IsCompleted); + _aare.Set(); + await t; + Assert.IsTrue(t.IsCompleted); + } + } + + [TestMethod] + public async Task MultipleSetOnlySignalsOnce() + { + _aare.Set(); + _aare.Set(); + await _aare.WaitOneAsync(); + var t = _aare.WaitOneAsync(); + Assert.IsFalse(t.IsCompleted); + await Task.Delay(500); + Assert.IsFalse(t.IsCompleted); + _aare.Set(); + await t; + Assert.IsTrue(t.IsCompleted); + } + + [TestMethod] + public async Task OrderPreservingQueue() + { + var waiters = new Task[5]; + for (int i = 0; i < waiters.Length; i++) + { + waiters[i] = _aare.WaitOneAsync(); + } + + for (int i = 0; i < waiters.Length; i++) + { + _aare.Set(); + await waiters[i].ConfigureAwait(false); + } + } + + /// + /// Verifies that inlining continuations do not have to complete execution before Set() returns. + /// + [TestMethod] + public async Task SetReturnsBeforeInlinedContinuations() + { + var setReturned = new ManualResetEventSlim(); + var inlinedContinuation = _aare.WaitOneAsync() + .ContinueWith(delegate + { + // Arrange to synchronously block the continuation until Set() has returned, + // which would deadlock if Set does not return until inlined continuations complete. + Assert.IsTrue(setReturned.Wait(500)); + }); + await Task.Delay(100); + _aare.Set(); + setReturned.Set(); + Assert.IsTrue(inlinedContinuation.Wait(500)); + } + + [TestMethod] + public void WaitAsync_WithCancellationToken() + { + var cts = new CancellationTokenSource(); + Task waitTask = _aare.WaitOneAsync(cts.Token); + Assert.IsFalse(waitTask.IsCompleted); + + // Cancel the request and ensure that it propagates to the task. + cts.Cancel(); + try + { + waitTask.GetAwaiter().GetResult(); + Assert.IsTrue(false, "Task was expected to transition to a canceled state."); + } + catch (OperationCanceledException ex) + { + Assert.AreEqual(cts.Token, ex.CancellationToken); + } + + // Now set the event and verify that a future waiter gets the signal immediately. + _aare.Set(); + waitTask = _aare.WaitOneAsync(); + Assert.AreEqual(TaskStatus.RanToCompletion, waitTask.Status); + } + + [TestMethod] + public void WaitAsync_WithCancellationToken_Precanceled() + { + // We construct our own pre-canceled token so that we can do + // a meaningful identity check later. + var tokenSource = new CancellationTokenSource(); + tokenSource.Cancel(); + var token = tokenSource.Token; + + // Verify that a pre-set signal is not reset by a canceled wait request. + _aare.Set(); + try + { + _aare.WaitOneAsync(token).GetAwaiter().GetResult(); + Assert.IsTrue(false, "Task was expected to transition to a canceled state."); + } + catch (OperationCanceledException ex) + { + Assert.AreEqual(token, ex.CancellationToken); + } + + // Verify that the signal was not acquired. + Task waitTask = _aare.WaitOneAsync(); + Assert.AreEqual(TaskStatus.RanToCompletion, waitTask.Status); + } + + [TestMethod] + public async Task WaitAsync_WithTimeout() + { + Task waitTask = _aare.WaitOneAsync(TimeSpan.FromMilliseconds(500)); + Assert.IsFalse(waitTask.IsCompleted); + + // Cancel the request and ensure that it propagates to the task. + await Task.Delay(1000).ConfigureAwait(false); + try + { + waitTask.GetAwaiter().GetResult(); + Assert.IsTrue(false, "Task was expected to transition to a timeout state."); + } + catch (TimeoutException) + { + Assert.IsTrue(true); + } + + // Now set the event and verify that a future waiter gets the signal immediately. + _aare.Set(); + waitTask = _aare.WaitOneAsync(TimeSpan.FromMilliseconds(500)); + Assert.AreEqual(TaskStatus.RanToCompletion, waitTask.Status); + } + + [TestMethod] + public void WaitAsync_Canceled_DoesNotInlineContinuations() + { + var cts = new CancellationTokenSource(); + var task = _aare.WaitOneAsync(cts.Token); + + var completingActionFinished = new ManualResetEventSlim(); + var continuation = task.ContinueWith( + _ => Assert.IsTrue(completingActionFinished.Wait(500)), + CancellationToken.None, + TaskContinuationOptions.None, + TaskScheduler.Default); + + cts.Cancel(); + completingActionFinished.Set(); + + // Rethrow the exception if it turned out it deadlocked. + continuation.GetAwaiter().GetResult(); + } + + [TestMethod] + public async Task AsyncAutoResetEvent() + { + var aare = new AsyncAutoResetEvent(); + + var globalI = 0; +#pragma warning disable 4014 + Task.Run(async () => +#pragma warning restore 4014 + { + await aare.WaitOneAsync(CancellationToken.None); + globalI += 1; + }); + +#pragma warning disable 4014 + Task.Run(async () => +#pragma warning restore 4014 + { + await aare.WaitOneAsync(CancellationToken.None); + globalI += 2; + }); + + await Task.Delay(500); + aare.Set(); + await Task.Delay(500); + aare.Set(); + await Task.Delay(100); + + Assert.AreEqual(3, globalI); + } + } +} \ No newline at end of file diff --git a/Tests/MQTTnet.Core.Tests/AsyncLockTests.cs b/Tests/MQTTnet.Core.Tests/AsyncLockTests.cs new file mode 100644 index 00000000..43f2486c --- /dev/null +++ b/Tests/MQTTnet.Core.Tests/AsyncLockTests.cs @@ -0,0 +1,39 @@ +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Internal; + +namespace MQTTnet.Core.Tests +{ + [TestClass] + public class AsyncLockTests + { + [TestMethod] + public void AsyncLock() + { + const int ThreadsCount = 10; + + var threads = new Task[ThreadsCount]; + var @lock = new AsyncLock(); + var globalI = 0; + for (var i = 0; i < ThreadsCount; i++) + { +#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + threads[i] = Task.Run(async () => + { + using (var releaser = await @lock.LockAsync(CancellationToken.None)) + { + var localI = globalI; + await Task.Delay(10); // Increase the chance for wrong data. + localI++; + globalI = localI; + } + }); +#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + } + + Task.WaitAll(threads); + Assert.AreEqual(ThreadsCount, globalI); + } + } +} diff --git a/Tests/MQTTnet.Core.Tests/ByteReaderTests.cs b/Tests/MQTTnet.Core.Tests/ByteReaderTests.cs deleted file mode 100644 index e2173cbc..00000000 --- a/Tests/MQTTnet.Core.Tests/ByteReaderTests.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using MQTTnet.Serializer; - -namespace MQTTnet.Core.Tests -{ - [TestClass] - public class ByteReaderTests - { - [TestMethod] - public void ByteReader_ReadToEnd() - { - var reader = new ByteReader(85); - Assert.IsTrue(reader.Read()); - Assert.IsFalse(reader.Read()); - Assert.IsTrue(reader.Read()); - Assert.IsFalse(reader.Read()); - Assert.IsTrue(reader.Read()); - Assert.IsFalse(reader.Read()); - Assert.IsTrue(reader.Read()); - Assert.IsFalse(reader.Read()); - } - - [TestMethod] - public void ByteReader_ReadPartial() - { - var reader = new ByteReader(15); - Assert.AreEqual(3, reader.Read(2)); - } - } -} diff --git a/Tests/MQTTnet.Core.Tests/ByteWriterTests.cs b/Tests/MQTTnet.Core.Tests/ByteWriterTests.cs deleted file mode 100644 index 881df5c5..00000000 --- a/Tests/MQTTnet.Core.Tests/ByteWriterTests.cs +++ /dev/null @@ -1,51 +0,0 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using MQTTnet.Serializer; - -namespace MQTTnet.Core.Tests -{ - [TestClass] - public class ByteWriterTests - { - [TestMethod] - public void ByteWriter_WriteMultipleAll() - { - var b = new ByteWriter(); - Assert.AreEqual(0, b.Value); - b.Write(3, 2); - Assert.AreEqual(3, b.Value); - } - - [TestMethod] - public void ByteWriter_WriteMultiplePartial() - { - var b = new ByteWriter(); - Assert.AreEqual(0, b.Value); - b.Write(255, 2); - Assert.AreEqual(3, b.Value); - } - - [TestMethod] - public void ByteWriter_WriteTo0xFF() - { - var b = new ByteWriter(); - - Assert.AreEqual(0, b.Value); - b.Write(true); - Assert.AreEqual(1, b.Value); - b.Write(true); - Assert.AreEqual(3, b.Value); - b.Write(true); - Assert.AreEqual(7, b.Value); - b.Write(true); - Assert.AreEqual(15, b.Value); - b.Write(true); - Assert.AreEqual(31, b.Value); - b.Write(true); - Assert.AreEqual(63, b.Value); - b.Write(true); - Assert.AreEqual(127, b.Value); - b.Write(true); - Assert.AreEqual(255, b.Value); - } - } -} diff --git a/Tests/MQTTnet.Core.Tests/ExtensionTests.cs b/Tests/MQTTnet.Core.Tests/ExtensionTests.cs index 053b0ca3..fdb5888e 100644 --- a/Tests/MQTTnet.Core.Tests/ExtensionTests.cs +++ b/Tests/MQTTnet.Core.Tests/ExtensionTests.cs @@ -1,9 +1,9 @@ using System; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Exceptions; -using MQTTnet.Internal; namespace MQTTnet.Core.Tests { @@ -14,20 +14,20 @@ namespace MQTTnet.Core.Tests [TestMethod] public async Task TimeoutAfter() { - await Task.Delay(TimeSpan.FromMilliseconds(500)).TimeoutAfter(TimeSpan.FromMilliseconds(100)); + await MQTTnet.Internal.TaskExtensions.TimeoutAfterAsync(ct => Task.Delay(TimeSpan.FromMilliseconds(500), ct), TimeSpan.FromMilliseconds(100), CancellationToken.None); } [ExpectedException(typeof(MqttCommunicationTimedOutException))] [TestMethod] public async Task TimeoutAfterWithResult() { - await Task.Delay(TimeSpan.FromMilliseconds(500)).ContinueWith(t => 5).TimeoutAfter(TimeSpan.FromMilliseconds(100)); + await MQTTnet.Internal.TaskExtensions.TimeoutAfterAsync(ct => Task.Delay(TimeSpan.FromMilliseconds(500), ct).ContinueWith(t => 5, ct), TimeSpan.FromMilliseconds(100), CancellationToken.None); } [TestMethod] public async Task TimeoutAfterCompleteInTime() { - var result = await Task.Delay(TimeSpan.FromMilliseconds(100)).ContinueWith(t => 5).TimeoutAfter(TimeSpan.FromMilliseconds(500)); + var result = await MQTTnet.Internal.TaskExtensions.TimeoutAfterAsync(ct => Task.Delay(TimeSpan.FromMilliseconds(100), ct).ContinueWith(t => 5, ct), TimeSpan.FromMilliseconds(500), CancellationToken.None); Assert.AreEqual(5, result); } @@ -36,17 +36,17 @@ namespace MQTTnet.Core.Tests { try { - await Task.Run(() => + await MQTTnet.Internal.TaskExtensions.TimeoutAfterAsync(ct => Task.Run(() => { var iis = new int[0]; iis[1] = 0; - }).TimeoutAfter(TimeSpan.FromSeconds(1)); + }, ct), TimeSpan.FromSeconds(1), CancellationToken.None); Assert.Fail(); } - catch (MqttCommunicationException e) + catch (Exception e) { - Assert.IsTrue(e.InnerException is IndexOutOfRangeException); + Assert.IsTrue(e is IndexOutOfRangeException); } } @@ -55,17 +55,18 @@ namespace MQTTnet.Core.Tests { try { - await Task.Run(() => + await MQTTnet.Internal.TaskExtensions.TimeoutAfterAsync(ct => Task.Run(() => { var iis = new int[0]; - return iis[1]; - }).TimeoutAfter(TimeSpan.FromSeconds(1)); + iis[1] = 0; + return iis[0]; + }, ct), TimeSpan.FromSeconds(1), CancellationToken.None); Assert.Fail(); } - catch (MqttCommunicationException e) + catch (Exception e) { - Assert.IsTrue(e.InnerException is IndexOutOfRangeException); + Assert.IsTrue(e is IndexOutOfRangeException); } } @@ -73,7 +74,10 @@ namespace MQTTnet.Core.Tests public async Task TimeoutAfterMemoryUsage() { var tasks = Enumerable.Range(0, 100000) - .Select(i => Task.Delay(TimeSpan.FromMilliseconds(1)).TimeoutAfter(TimeSpan.FromMinutes(1))); + .Select(i => + { + return MQTTnet.Internal.TaskExtensions.TimeoutAfterAsync(ct => Task.Delay(TimeSpan.FromMilliseconds(1), ct), TimeSpan.FromMinutes(1), CancellationToken.None); + }); await Task.WhenAll(tasks); AssertIsLess(3_000_000, GC.GetTotalMemory(true)); diff --git a/Tests/MQTTnet.Core.Tests/MQTTnet.Core.Tests.csproj b/Tests/MQTTnet.Core.Tests/MQTTnet.Core.Tests.csproj index b03d0d11..40697e94 100644 --- a/Tests/MQTTnet.Core.Tests/MQTTnet.Core.Tests.csproj +++ b/Tests/MQTTnet.Core.Tests/MQTTnet.Core.Tests.csproj @@ -7,13 +7,13 @@ - - - + + + - + \ No newline at end of file diff --git a/Tests/MQTTnet.Core.Tests/MqttKeepAliveMonitorTests.cs b/Tests/MQTTnet.Core.Tests/MqttKeepAliveMonitorTests.cs index 206942f4..267b7bca 100644 --- a/Tests/MQTTnet.Core.Tests/MqttKeepAliveMonitorTests.cs +++ b/Tests/MQTTnet.Core.Tests/MqttKeepAliveMonitorTests.cs @@ -1,6 +1,9 @@ -using System.Threading; +using System; +using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Adapter; using MQTTnet.Diagnostics; using MQTTnet.Packets; using MQTTnet.Server; @@ -13,41 +16,31 @@ namespace MQTTnet.Core.Tests [TestMethod] public void KeepAlive_Timeout() { - var timeoutCalledCount = 0; + var clientSession = new TestClientSession(); + var monitor = new MqttClientKeepAliveMonitor(clientSession, new MqttNetLogger().CreateChildLogger()); - var monitor = new MqttClientKeepAliveMonitor(string.Empty, delegate - { - timeoutCalledCount++; - return Task.FromResult(0); - }, new MqttNetLogger()); - - Assert.AreEqual(0, timeoutCalledCount); + Assert.AreEqual(0, clientSession.StopCalledCount); monitor.Start(1, CancellationToken.None); - Assert.AreEqual(0, timeoutCalledCount); + Assert.AreEqual(0, clientSession.StopCalledCount); Thread.Sleep(2000); // Internally the keep alive timeout is multiplied with 1.5 as per protocol specification. - Assert.AreEqual(1, timeoutCalledCount); + Assert.AreEqual(1, clientSession.StopCalledCount); } [TestMethod] public void KeepAlive_NoTimeout() { - var timeoutCalledCount = 0; + var clientSession = new TestClientSession(); + var monitor = new MqttClientKeepAliveMonitor(clientSession, new MqttNetLogger().CreateChildLogger()); - var monitor = new MqttClientKeepAliveMonitor(string.Empty, delegate - { - timeoutCalledCount++; - return Task.FromResult(0); - }, new MqttNetLogger()); - - Assert.AreEqual(0, timeoutCalledCount); + Assert.AreEqual(0, clientSession.StopCalledCount); monitor.Start(1, CancellationToken.None); - Assert.AreEqual(0, timeoutCalledCount); + Assert.AreEqual(0, clientSession.StopCalledCount); // Simulate traffic. Thread.Sleep(1000); // Internally the keep alive timeout is multiplied with 1.5 as per protocol specification. @@ -56,11 +49,57 @@ namespace MQTTnet.Core.Tests monitor.PacketReceived(new MqttPublishPacket()); Thread.Sleep(1000); - Assert.AreEqual(0, timeoutCalledCount); + Assert.AreEqual(0, clientSession.StopCalledCount); Thread.Sleep(2000); - Assert.AreEqual(1, timeoutCalledCount); + Assert.AreEqual(1, clientSession.StopCalledCount); + } + + private class TestClientSession : IMqttClientSession + { + public string ClientId { get; } + + public int StopCalledCount { get; set; } + + public void FillStatus(MqttClientSessionStatus status) + { + throw new NotSupportedException(); + } + + public void EnqueueApplicationMessage(MqttClientSession senderClientSession, MqttPublishPacket publishPacket) + { + throw new NotSupportedException(); + } + + public void ClearPendingApplicationMessages() + { + throw new NotSupportedException(); + } + + public Task RunAsync(MqttConnectPacket connectPacket, IMqttChannelAdapter adapter) + { + throw new NotSupportedException(); + } + + public void Stop(MqttClientDisconnectType disconnectType) + { + StopCalledCount++; + } + + public Task SubscribeAsync(IList topicFilters) + { + throw new NotSupportedException(); + } + + public Task UnsubscribeAsync(IList topicFilters) + { + throw new NotSupportedException(); + } + + public void Dispose() + { + } } } } diff --git a/Tests/MQTTnet.Core.Tests/MqttPacketReaderTests.cs b/Tests/MQTTnet.Core.Tests/MqttPacketReaderTests.cs index 4d28035e..39aeeff2 100644 --- a/Tests/MQTTnet.Core.Tests/MqttPacketReaderTests.cs +++ b/Tests/MQTTnet.Core.Tests/MqttPacketReaderTests.cs @@ -1,6 +1,8 @@ using System.IO; using System.Threading; using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Exceptions; +using MQTTnet.Internal; using MQTTnet.Serializer; namespace MQTTnet.Core.Tests @@ -9,12 +11,12 @@ namespace MQTTnet.Core.Tests public class MqttPacketReaderTests { [TestMethod] + [ExpectedException(typeof(MqttCommunicationClosedGracefullyException))] public void MqttPacketReader_EmptyStream() { - var memStream = new MemoryStream(); - var header = MqttPacketReader.ReadHeaderAsync(memStream, CancellationToken.None).GetAwaiter().GetResult(); - - Assert.IsNull(header); + var fixedHeader = new byte[2]; + var singleByteBuffer = new byte[1]; + MqttPacketReader.ReadFixedHeaderAsync(new TestMqttChannel(new MemoryStream()), fixedHeader, singleByteBuffer, CancellationToken.None).GetAwaiter().GetResult(); } } } diff --git a/Tests/MQTTnet.Core.Tests/MqttPacketSerializerTests.cs b/Tests/MQTTnet.Core.Tests/MqttPacketSerializerTests.cs index 1111d907..d8d23e29 100644 --- a/Tests/MQTTnet.Core.Tests/MqttPacketSerializerTests.cs +++ b/Tests/MQTTnet.Core.Tests/MqttPacketSerializerTests.cs @@ -1,9 +1,10 @@ using System; -using System.Collections.Generic; using System.IO; using System.Text; using System.Threading; using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Adapter; +using MQTTnet.Internal; using MQTTnet.Packets; using MQTTnet.Protocol; using MQTTnet.Serializer; @@ -25,7 +26,7 @@ namespace MQTTnet.Core.Tests CleanSession = true }; - SerializeAndCompare(p, "EB0ABE1RSXNkcAPCAHsAA1hZWgAEVVNFUgAEUEFTUw==", MqttProtocolVersion.V310); + SerializeAndCompare(p, "EB0ABk1RSXNkcAPCAHsAA1hZWgAEVVNFUgAEUEFTUw==", MqttProtocolVersion.V310); } [TestMethod] @@ -198,6 +199,64 @@ namespace MQTTnet.Core.Tests DeserializeAndCompare(p, "Ow4ABUEvQi9DAHtIRUxMTw=="); } + [TestMethod] + public void DeserializeV311_MqttPublishPacket_Qos1() + { + var p = new MqttPublishPacket + { + QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce, + }; + + var p2 = Roundtrip(p); + + Assert.AreEqual(p.QualityOfServiceLevel, p2.QualityOfServiceLevel); + Assert.AreEqual(p.Dup, p2.Dup); + } + + [TestMethod] + public void DeserializeV311_MqttPublishPacket_Qos2() + { + var p = new MqttPublishPacket + { + QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce, + PacketIdentifier = 1 + }; + + var p2 = Roundtrip(p); + + Assert.AreEqual(p.QualityOfServiceLevel, p2.QualityOfServiceLevel); + Assert.AreEqual(p.Dup, p2.Dup); + } + + [TestMethod] + public void DeserializeV311_MqttPublishPacket_Qos3() + { + var p = new MqttPublishPacket + { + QualityOfServiceLevel = MqttQualityOfServiceLevel.ExactlyOnce, + PacketIdentifier = 1 + }; + + var p2 = Roundtrip(p); + + Assert.AreEqual(p.QualityOfServiceLevel, p2.QualityOfServiceLevel); + Assert.AreEqual(p.Dup, p2.Dup); + } + + + [TestMethod] + public void DeserializeV311_MqttPublishPacket_DupFalse() + { + var p = new MqttPublishPacket + { + Dup = false, + }; + + var p2 = Roundtrip(p); + + Assert.AreEqual(p.Dup, p2.Dup); + } + [TestMethod] public void SerializeV311_MqttPubAckPacket() { @@ -403,9 +462,9 @@ namespace MQTTnet.Core.Tests private static void SerializeAndCompare(MqttBasePacket packet, string expectedBase64Value, MqttProtocolVersion protocolVersion = MqttProtocolVersion.V311) { var serializer = new MqttPacketSerializer { ProtocolVersion = protocolVersion }; - var chunks = serializer.Serialize(packet); + var data = serializer.Serialize(packet); - Assert.AreEqual(expectedBase64Value, Convert.ToBase64String(Join(chunks))); + Assert.AreEqual(expectedBase64Value, Convert.ToBase64String(Join(data))); } private static void DeserializeAndCompare(MqttBasePacket packet, string expectedBase64Value, MqttProtocolVersion protocolVersion = MqttProtocolVersion.V311) @@ -416,11 +475,14 @@ namespace MQTTnet.Core.Tests using (var headerStream = new MemoryStream(Join(buffer1))) { - var header = MqttPacketReader.ReadHeaderAsync(headerStream, CancellationToken.None).GetAwaiter().GetResult(); - - using (var bodyStream = new MemoryStream(Join(buffer1), (int)headerStream.Position, header.BodyLength)) + var channel = new TestMqttChannel(headerStream); + var fixedHeader = new byte[2]; + var singleByteBuffer = new byte[1]; + var header = MqttPacketReader.ReadFixedHeaderAsync(channel, fixedHeader, singleByteBuffer, CancellationToken.None).GetAwaiter().GetResult(); + + using (var bodyStream = new MemoryStream(Join(buffer1), (int)headerStream.Position, header.RemainingLength)) { - var deserializedPacket = serializer.Deserialize(header, bodyStream); + var deserializedPacket = serializer.Deserialize(new ReceivedMqttPacket(header.Flags, new MqttPacketBodyReader(bodyStream.ToArray(), 0))); var buffer2 = serializer.Serialize(deserializedPacket); Assert.AreEqual(expectedBase64Value, Convert.ToBase64String(Join(buffer2))); @@ -428,7 +490,29 @@ namespace MQTTnet.Core.Tests } } - private static byte[] Join(IEnumerable> chunks) + private static T Roundtrip(T packet, MqttProtocolVersion protocolVersion = MqttProtocolVersion.V311) + where T : MqttBasePacket + { + var serializer = new MqttPacketSerializer { ProtocolVersion = protocolVersion }; + + var buffer1 = serializer.Serialize(packet); + + using (var headerStream = new MemoryStream(Join(buffer1))) + { + var channel = new TestMqttChannel(headerStream); + var fixedHeader = new byte[2]; + var singleByteBuffer = new byte[1]; + + var header = MqttPacketReader.ReadFixedHeaderAsync(channel, fixedHeader, singleByteBuffer, CancellationToken.None).GetAwaiter().GetResult(); + + using (var bodyStream = new MemoryStream(Join(buffer1), (int)headerStream.Position, header.RemainingLength)) + { + return (T)serializer.Deserialize(new ReceivedMqttPacket(header.Flags, new MqttPacketBodyReader(bodyStream.ToArray(), 0))); + } + } + } + + private static byte[] Join(params ArraySegment[] chunks) { var buffer = new MemoryStream(); foreach (var chunk in chunks) diff --git a/Tests/MQTTnet.Core.Tests/MqttPacketWriterTests.cs b/Tests/MQTTnet.Core.Tests/MqttPacketWriterTests.cs new file mode 100644 index 00000000..8b73b739 --- /dev/null +++ b/Tests/MQTTnet.Core.Tests/MqttPacketWriterTests.cs @@ -0,0 +1,25 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Serializer; + +namespace MQTTnet.Core.Tests +{ + [TestClass] + public class MqttPacketWriterTests + { + [TestMethod] + public void WritePacket() + { + var writer = new MqttPacketWriter(); + Assert.AreEqual(0, writer.Length); + + writer.WriteWithLengthPrefix("1234567890"); + Assert.AreEqual(10 + 2, writer.Length); + + writer.WriteWithLengthPrefix(new byte[300]); + Assert.AreEqual(300 + 2 + 12, writer.Length); + + writer.WriteWithLengthPrefix(new byte[5000]); + Assert.AreEqual(5000 + 2 + 300 + 2 + 12, writer.Length); + } + } +} diff --git a/Tests/MQTTnet.Core.Tests/MqttServerTests.cs b/Tests/MQTTnet.Core.Tests/MqttServerTests.cs index dc2f5251..ea258f56 100644 --- a/Tests/MQTTnet.Core.Tests/MqttServerTests.cs +++ b/Tests/MQTTnet.Core.Tests/MqttServerTests.cs @@ -1,12 +1,14 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Threading.Tasks; -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Client; using MQTTnet.Diagnostics; using MQTTnet.Protocol; using MQTTnet.Server; -using MQTTnet.Client; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using MQTTnet.Implementations; namespace MQTTnet.Core.Tests { @@ -58,8 +60,8 @@ namespace MQTTnet.Core.Tests 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); + var c1 = await serverAdapter.ConnectTestClient("c1"); + var c2 = await serverAdapter.ConnectTestClient("c2", willMessage); c1.ApplicationMessageReceived += (_, __) => receivedMessagesCount++; await c1.SubscribeAsync(new TopicFilterBuilder().WithTopic("#").Build()); @@ -90,15 +92,16 @@ namespace MQTTnet.Core.Tests { await s.StartAsync(new MqttServerOptions()); - var c1 = await serverAdapter.ConnectTestClient(s, "c1"); - var c2 = await serverAdapter.ConnectTestClient(s, "c2"); + var c1 = await serverAdapter.ConnectTestClient("c1"); + var c2 = await serverAdapter.ConnectTestClient("c2"); c1.ApplicationMessageReceived += (_, __) => receivedMessagesCount++; var message = new MqttApplicationMessageBuilder().WithTopic("a").WithAtLeastOnceQoS().Build(); - + await c2.PublishAsync(message); + await Task.Delay(1000); Assert.AreEqual(0, receivedMessagesCount); - + var subscribeEventCalled = false; s.ClientSubscribedTopic += (_, e) => { @@ -106,14 +109,13 @@ namespace MQTTnet.Core.Tests }; await c1.SubscribeAsync(new TopicFilter("a", MqttQualityOfServiceLevel.AtLeastOnce)); - await Task.Delay(100); + await Task.Delay(500); 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) => { @@ -121,12 +123,11 @@ namespace MQTTnet.Core.Tests }; await c1.UnsubscribeAsync("a"); - await Task.Delay(100); + await Task.Delay(500); Assert.IsTrue(unsubscribeEventCalled, "Unsubscribe event not called."); await c2.PublishAsync(message); - - await Task.Delay(500); + await Task.Delay(1000); Assert.AreEqual(1, receivedMessagesCount); } finally @@ -149,7 +150,7 @@ namespace MQTTnet.Core.Tests { await s.StartAsync(new MqttServerOptions()); - var c1 = await serverAdapter.ConnectTestClient(s, "c1"); + var c1 = await serverAdapter.ConnectTestClient("c1"); c1.ApplicationMessageReceived += (_, __) => receivedMessagesCount++; @@ -167,6 +168,106 @@ namespace MQTTnet.Core.Tests Assert.AreEqual(1, receivedMessagesCount); } + [TestMethod] + public async Task MqttServer_Publish_MultipleClients() + { + var serverAdapter = new MqttTcpServerAdapter(new MqttNetLogger().CreateChildLogger()); + var s = new MqttFactory().CreateMqttServer(new[] { serverAdapter }, new MqttNetLogger()); + var receivedMessagesCount = 0; + var locked = new object(); + + var clientOptions = new MqttClientOptionsBuilder() + .WithTcpServer("localhost") + .Build(); + var clientOptions2 = new MqttClientOptionsBuilder() + .WithTcpServer("localhost") + .Build(); + + try + { + await s.StartAsync(new MqttServerOptions()); + + var c1 = new MqttFactory().CreateMqttClient(); + var c2 = new MqttFactory().CreateMqttClient(); + + await c1.ConnectAsync(clientOptions); + await c2.ConnectAsync(clientOptions2); + + c1.ApplicationMessageReceived += (_, __) => + { + lock (locked) + { + receivedMessagesCount++; + } + }; + + c2.ApplicationMessageReceived += (_, __) => + { + lock (locked) + { + receivedMessagesCount++; + } + }; + + var message = new MqttApplicationMessageBuilder().WithTopic("a").WithAtLeastOnceQoS().Build(); + await c1.SubscribeAsync(new TopicFilter("a", MqttQualityOfServiceLevel.AtLeastOnce)); + await c2.SubscribeAsync(new TopicFilter("a", MqttQualityOfServiceLevel.AtLeastOnce)); + + //await Task.WhenAll(Publish(c1, message), Publish(c2, message)); + await Publish(c1, message); + + await Task.Delay(500); + } + finally + { + await s.StopAsync(); + } + + Assert.AreEqual(2000, receivedMessagesCount); + } + + private static async Task Publish(IMqttClient c1, MqttApplicationMessage message) + { + for (int i = 0; i < 1000; i++) + { + await c1.PublishAsync(message); + } + } + + [TestMethod] + public async Task MqttServer_RetainedMessagesFlow() + { + var retainedMessage = new MqttApplicationMessageBuilder().WithTopic("r").WithPayload("r").WithRetainFlag().Build(); + var serverAdapter = new TestMqttServerAdapter(); + var s = new MqttFactory().CreateMqttServer(new[] { serverAdapter }, new MqttNetLogger()); + await s.StartAsync(new MqttServerOptions()); + var c1 = await serverAdapter.ConnectTestClient("c1"); + await c1.PublishAsync(retainedMessage); + Thread.Sleep(500); + await c1.DisconnectAsync(); + Thread.Sleep(500); + + var receivedMessages = 0; + var c2 = await serverAdapter.ConnectTestClient("c2"); + c2.ApplicationMessageReceived += (_, e) => + { + receivedMessages++; + }; + + for (var i = 0; i < 5; i++) + { + await c2.UnsubscribeAsync("r"); + await Task.Delay(500); + Assert.AreEqual(i, receivedMessages); + + await c2.SubscribeAsync("r"); + await Task.Delay(500); + Assert.AreEqual(i + 1, receivedMessages); + } + + await c2.DisconnectAsync(); + } + [TestMethod] public async Task MqttServer_NoRetainedMessage() { @@ -179,11 +280,11 @@ namespace MQTTnet.Core.Tests { await s.StartAsync(new MqttServerOptions()); - var c1 = await serverAdapter.ConnectTestClient(s, "c1"); - await c1.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("retained").WithPayload(new byte[3]).Build()); + var c1 = await serverAdapter.ConnectTestClient("c1"); + await c1.PublishAsync(builder => builder.WithTopic("retained").WithPayload(new byte[3])); await c1.DisconnectAsync(); - var c2 = await serverAdapter.ConnectTestClient(s, "c2"); + var c2 = await serverAdapter.ConnectTestClient("c2"); c2.ApplicationMessageReceived += (_, __) => receivedMessagesCount++; await c2.SubscribeAsync(new TopicFilterBuilder().WithTopic("retained").Build()); @@ -208,11 +309,11 @@ namespace MQTTnet.Core.Tests { await s.StartAsync(new MqttServerOptions()); - var c1 = await serverAdapter.ConnectTestClient(s, "c1"); + var c1 = await serverAdapter.ConnectTestClient("c1"); await c1.PublishAndWaitForAsync(s, new MqttApplicationMessageBuilder().WithTopic("retained").WithPayload(new byte[3]).WithRetainFlag().Build()); await c1.DisconnectAsync(); - var c2 = await serverAdapter.ConnectTestClient(s, "c2"); + var c2 = await serverAdapter.ConnectTestClient("c2"); c2.ApplicationMessageReceived += (_, __) => receivedMessagesCount++; await c2.SubscribeAsync(new TopicFilterBuilder().WithTopic("retained").Build()); @@ -237,15 +338,16 @@ namespace MQTTnet.Core.Tests { 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()); + var c1 = await serverAdapter.ConnectTestClient("c1"); + await c1.PublishAsync(builder => builder.WithTopic("retained").WithPayload(new byte[3]).WithRetainFlag()); + await c1.PublishAsync(builder => builder.WithTopic("retained").WithPayload(new byte[0]).WithRetainFlag()); await c1.DisconnectAsync(); - - var c2 = await serverAdapter.ConnectTestClient(s, "c2"); - c2.ApplicationMessageReceived += (_, __) => receivedMessagesCount++; - await c2.SubscribeAsync(new TopicFilter("retained", MqttQualityOfServiceLevel.AtMostOnce)); + var c2 = await serverAdapter.ConnectTestClient("c2"); + c2.ApplicationMessageReceived += (_, __) => receivedMessagesCount++; + + await Task.Delay(200); + await c2.SubscribeAsync(new TopicFilter("retained", MqttQualityOfServiceLevel.AtMostOnce)); await Task.Delay(500); } finally @@ -270,7 +372,7 @@ namespace MQTTnet.Core.Tests await s.StartAsync(options); - var c1 = await serverAdapter.ConnectTestClient(s, "c1"); + var c1 = await serverAdapter.ConnectTestClient("c1"); await c1.PublishAndWaitForAsync(s, new MqttApplicationMessageBuilder().WithTopic("retained").WithPayload(new byte[3]).WithRetainFlag().Build()); await c1.DisconnectAsync(); @@ -290,7 +392,7 @@ namespace MQTTnet.Core.Tests var options = new MqttServerOptions { Storage = storage }; await s.StartAsync(options); - var c2 = await serverAdapter.ConnectTestClient(s, "c2"); + var c2 = await serverAdapter.ConnectTestClient("c2"); c2.ApplicationMessageReceived += (_, __) => receivedMessagesCount++; await c2.SubscribeAsync(new TopicFilterBuilder().WithTopic("retained").Build()); @@ -321,8 +423,8 @@ namespace MQTTnet.Core.Tests await s.StartAsync(options); - var c1 = await serverAdapter.ConnectTestClient(s, "c1"); - var c2 = await serverAdapter.ConnectTestClient(s, "c2"); + var c1 = await serverAdapter.ConnectTestClient("c1"); + var c2 = await serverAdapter.ConnectTestClient("c2"); await c2.SubscribeAsync(new TopicFilterBuilder().WithTopic("test").Build()); var isIntercepted = false; @@ -331,8 +433,7 @@ namespace MQTTnet.Core.Tests isIntercepted = string.Compare("extended", Encoding.UTF8.GetString(args.ApplicationMessage.Payload), StringComparison.Ordinal) == 0; }; - var m = new MqttApplicationMessageBuilder().WithTopic("test").Build(); - await c1.PublishAsync(m); + await c1.PublishAsync(builder => builder.WithTopic("test")); await c1.DisconnectAsync(); await Task.Delay(500); @@ -356,8 +457,8 @@ namespace MQTTnet.Core.Tests { await s.StartAsync(new MqttServerOptions()); - var c1 = await serverAdapter.ConnectTestClient(s, "c1"); - var c2 = await serverAdapter.ConnectTestClient(s, "c2"); + var c1 = await serverAdapter.ConnectTestClient("c1"); + var c2 = await serverAdapter.ConnectTestClient("c2"); c1.ApplicationMessageReceived += (_, e) => { @@ -368,9 +469,9 @@ namespace MQTTnet.Core.Tests }; await c1.SubscribeAsync("A", MqttQualityOfServiceLevel.AtMostOnce); - await c2.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("A").WithPayload(Encoding.UTF8.GetBytes("The body")).Build()); + await c2.PublishAsync(builder => builder.WithTopic("A").WithPayload(Encoding.UTF8.GetBytes("The body"))); - await Task.Delay(500); + await Task.Delay(1000); } finally { @@ -411,13 +512,13 @@ namespace MQTTnet.Core.Tests { await s.StartAsync(new MqttServerOptions()); - var c1 = await serverAdapter.ConnectTestClient(s, "c1"); - var c2 = await serverAdapter.ConnectTestClient(s, "c2"); + var c1 = await serverAdapter.ConnectTestClient("c1"); + var c2 = await serverAdapter.ConnectTestClient("c2"); c1.ApplicationMessageReceived += (_, __) => receivedMessagesCount++; await c1.SubscribeAsync(new TopicFilterBuilder().WithTopic(topicFilter).WithQualityOfServiceLevel(filterQualityOfServiceLevel).Build()); - await c2.PublishAsync(new MqttApplicationMessageBuilder().WithTopic(topic).WithPayload(new byte[0]).WithQualityOfServiceLevel(qualityOfServiceLevel).Build()); + await c2.PublishAsync(builder => builder.WithTopic(topic).WithPayload(new byte[0]).WithQualityOfServiceLevel(qualityOfServiceLevel)); await Task.Delay(500); await c1.UnsubscribeAsync(topicFilter); diff --git a/Tests/MQTTnet.Core.Tests/MqttSubscriptionsManagerTests.cs b/Tests/MQTTnet.Core.Tests/MqttSubscriptionsManagerTests.cs index 8b2863af..268e5fee 100644 --- a/Tests/MQTTnet.Core.Tests/MqttSubscriptionsManagerTests.cs +++ b/Tests/MQTTnet.Core.Tests/MqttSubscriptionsManagerTests.cs @@ -1,4 +1,6 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Adapter; +using MQTTnet.Diagnostics; using MQTTnet.Packets; using MQTTnet.Protocol; using MQTTnet.Server; @@ -11,20 +13,14 @@ namespace MQTTnet.Core.Tests [TestMethod] public void MqttSubscriptionsManager_SubscribeSingleSuccess() { - var sm = new MqttClientSubscriptionsManager(new MqttServerOptions(), ""); + var sm = new MqttClientSubscriptionsManager("", new MqttServerOptions(), new MqttServer(new IMqttServerAdapter[0], new MqttNetLogger().CreateChildLogger(""))); var sp = new MqttSubscribePacket(); sp.TopicFilters.Add(new TopicFilterBuilder().WithTopic("A/B/C").Build()); - sm.SubscribeAsync(sp).Wait(); + sm.Subscribe(sp); - var pp = new MqttApplicationMessage - { - Topic = "A/B/C", - QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce - }; - - var result = sm.CheckSubscriptionsAsync(pp).Result; + var result = sm.CheckSubscriptions("A/B/C", MqttQualityOfServiceLevel.AtMostOnce); Assert.IsTrue(result.IsSubscribed); Assert.AreEqual(result.QualityOfServiceLevel, MqttQualityOfServiceLevel.AtMostOnce); } @@ -32,20 +28,14 @@ namespace MQTTnet.Core.Tests [TestMethod] public void MqttSubscriptionsManager_SubscribeDifferentQoSSuccess() { - var sm = new MqttClientSubscriptionsManager(new MqttServerOptions(), ""); + var sm = new MqttClientSubscriptionsManager("", new MqttServerOptions(), new MqttServer(new IMqttServerAdapter[0], new MqttNetLogger().CreateChildLogger(""))); var sp = new MqttSubscribePacket(); sp.TopicFilters.Add(new TopicFilter("A/B/C", MqttQualityOfServiceLevel.AtMostOnce)); - sm.SubscribeAsync(sp).Wait(); + sm.Subscribe(sp); - var pp = new MqttApplicationMessage - { - Topic = "A/B/C", - QualityOfServiceLevel = MqttQualityOfServiceLevel.ExactlyOnce - }; - - var result = sm.CheckSubscriptionsAsync(pp).Result; + var result = sm.CheckSubscriptions("A/B/C", MqttQualityOfServiceLevel.ExactlyOnce); Assert.IsTrue(result.IsSubscribed); Assert.AreEqual(result.QualityOfServiceLevel, MqttQualityOfServiceLevel.AtMostOnce); } @@ -53,21 +43,15 @@ namespace MQTTnet.Core.Tests [TestMethod] public void MqttSubscriptionsManager_SubscribeTwoTimesSuccess() { - var sm = new MqttClientSubscriptionsManager(new MqttServerOptions(), ""); + var sm = new MqttClientSubscriptionsManager("", new MqttServerOptions(), new MqttServer(new IMqttServerAdapter[0], new MqttNetLogger().CreateChildLogger(""))); 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(); + sm.Subscribe(sp); - var pp = new MqttApplicationMessage - { - Topic = "A/B/C", - QualityOfServiceLevel = MqttQualityOfServiceLevel.ExactlyOnce - }; - - var result = sm.CheckSubscriptionsAsync(pp).Result; + var result = sm.CheckSubscriptions("A/B/C", MqttQualityOfServiceLevel.ExactlyOnce); Assert.IsTrue(result.IsSubscribed); Assert.AreEqual(result.QualityOfServiceLevel, MqttQualityOfServiceLevel.AtLeastOnce); } @@ -75,45 +59,33 @@ namespace MQTTnet.Core.Tests [TestMethod] public void MqttSubscriptionsManager_SubscribeSingleNoSuccess() { - var sm = new MqttClientSubscriptionsManager(new MqttServerOptions(), ""); + var sm = new MqttClientSubscriptionsManager("", new MqttServerOptions(), new MqttServer(new IMqttServerAdapter[0], new MqttNetLogger().CreateChildLogger(""))); var sp = new MqttSubscribePacket(); sp.TopicFilters.Add(new TopicFilterBuilder().WithTopic("A/B/C").Build()); - sm.SubscribeAsync(sp).Wait(); + sm.Subscribe(sp); - var pp = new MqttApplicationMessage - { - Topic = "A/B/X", - QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce - }; - - Assert.IsFalse(sm.CheckSubscriptionsAsync(pp).Result.IsSubscribed); + Assert.IsFalse(sm.CheckSubscriptions("A/B/X", MqttQualityOfServiceLevel.AtMostOnce).IsSubscribed); } [TestMethod] public void MqttSubscriptionsManager_SubscribeAndUnsubscribeSingle() { - var sm = new MqttClientSubscriptionsManager(new MqttServerOptions(), ""); + var sm = new MqttClientSubscriptionsManager("", new MqttServerOptions(), new MqttServer(new IMqttServerAdapter[0], new MqttNetLogger().CreateChildLogger(""))); var sp = new MqttSubscribePacket(); sp.TopicFilters.Add(new TopicFilterBuilder().WithTopic("A/B/C").Build()); - sm.SubscribeAsync(sp).Wait(); + sm.Subscribe(sp); - var pp = new MqttApplicationMessage - { - Topic = "A/B/C", - QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce - }; - - Assert.IsTrue(sm.CheckSubscriptionsAsync(pp).Result.IsSubscribed); + Assert.IsTrue(sm.CheckSubscriptions("A/B/C", MqttQualityOfServiceLevel.AtMostOnce).IsSubscribed); var up = new MqttUnsubscribePacket(); up.TopicFilters.Add("A/B/C"); - sm.UnsubscribeAsync(up).Wait(); + sm.Unsubscribe(up); - Assert.IsFalse(sm.CheckSubscriptionsAsync(pp).Result.IsSubscribed); + Assert.IsFalse(sm.CheckSubscriptions("A/B/C", MqttQualityOfServiceLevel.AtMostOnce).IsSubscribed); } } } diff --git a/Tests/MQTTnet.Core.Tests/TestMqttCommunicationAdapter.cs b/Tests/MQTTnet.Core.Tests/TestMqttCommunicationAdapter.cs index a898f765..9375f07d 100644 --- a/Tests/MQTTnet.Core.Tests/TestMqttCommunicationAdapter.cs +++ b/Tests/MQTTnet.Core.Tests/TestMqttCommunicationAdapter.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Concurrent; -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using MQTTnet.Adapter; @@ -15,39 +14,60 @@ namespace MQTTnet.Core.Tests public TestMqttCommunicationAdapter Partner { get; set; } + public string Endpoint { get; } + public IMqttPacketSerializer PacketSerializer { get; } = new MqttPacketSerializer(); + public event EventHandler ReadingPacketStarted; + public event EventHandler ReadingPacketCompleted; + public void Dispose() { } - public Task ConnectAsync(TimeSpan timeout) + public Task ConnectAsync(TimeSpan timeout, CancellationToken cancellationToken) { return Task.FromResult(0); } - public Task DisconnectAsync(TimeSpan timeout) + public Task DisconnectAsync(TimeSpan timeout, CancellationToken cancellationToken) { return Task.FromResult(0); } - public Task SendPacketsAsync(TimeSpan timeout, CancellationToken cancellationToken, IEnumerable packets) + public Task SendPacketAsync(MqttBasePacket packet, CancellationToken cancellationToken) { ThrowIfPartnerIsNull(); - foreach (var packet in packets) + Partner.EnqueuePacketInternal(packet); + + return Task.FromResult(0); + } + + public async Task ReceivePacketAsync(TimeSpan timeout, CancellationToken cancellationToken) + { + ThrowIfPartnerIsNull(); + + if (timeout > TimeSpan.Zero) { - Partner.EnqueuePacketInternal(packet); + using (var timeoutCts = new CancellationTokenSource(timeout)) + using (var cts = CancellationTokenSource.CreateLinkedTokenSource(timeoutCts.Token, cancellationToken)) + { + return await Task.Run(() => + { + try + { + return _incomingPackets.Take(cts.Token); + } + catch + { + return null; + } + }, cts.Token); + } } - return Task.FromResult(0); - } - - public Task ReceivePacketAsync(TimeSpan timeout, CancellationToken cancellationToken) - { - ThrowIfPartnerIsNull(); - - return Task.Run(() => + return await Task.Run(() => { try { diff --git a/Tests/MQTTnet.Core.Tests/TestMqttCommunicationAdapterFactory.cs b/Tests/MQTTnet.Core.Tests/TestMqttCommunicationAdapterFactory.cs index 4fe35373..6873c69e 100644 --- a/Tests/MQTTnet.Core.Tests/TestMqttCommunicationAdapterFactory.cs +++ b/Tests/MQTTnet.Core.Tests/TestMqttCommunicationAdapterFactory.cs @@ -13,7 +13,7 @@ namespace MQTTnet.Core.Tests _adapter = adapter; } - public IMqttChannelAdapter CreateClientAdapter(IMqttClientOptions options, IMqttNetLogger logger) + public IMqttChannelAdapter CreateClientAdapter(IMqttClientOptions options, IMqttNetChildLogger logger) { return _adapter; } diff --git a/Tests/MQTTnet.Core.Tests/TestMqttServerAdapter.cs b/Tests/MQTTnet.Core.Tests/TestMqttServerAdapter.cs index a2fec80f..2b24f42b 100644 --- a/Tests/MQTTnet.Core.Tests/TestMqttServerAdapter.cs +++ b/Tests/MQTTnet.Core.Tests/TestMqttServerAdapter.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using System.Threading.Tasks; using MQTTnet.Adapter; using MQTTnet.Client; @@ -11,7 +12,7 @@ namespace MQTTnet.Core.Tests { public event EventHandler ClientAccepted; - public async Task ConnectTestClient(IMqttServer server, string clientId, MqttApplicationMessage willMessage = null) + public async Task ConnectTestClient(string clientId, MqttApplicationMessage willMessage = null) { var adapterA = new TestMqttCommunicationAdapter(); var adapterB = new TestMqttCommunicationAdapter(); @@ -22,8 +23,6 @@ namespace MQTTnet.Core.Tests new TestMqttCommunicationAdapterFactory(adapterA), new MqttNetLogger()); - var connected = WaitForClientToConnect(server, clientId); - FireClientAcceptedEvent(adapterB); var options = new MqttClientOptions @@ -34,29 +33,11 @@ namespace MQTTnet.Core.Tests }; await client.ConnectAsync(options); - await connected; + SpinWait.SpinUntil(() => client.IsConnected); return client; } - private static Task WaitForClientToConnect(IMqttServer s, string clientId) - { - var tcs = new TaskCompletionSource(); - - void Handler(object sender, Server.MqttClientConnectedEventArgs args) - { - if (args.Client.ClientId == clientId) - { - s.ClientConnected -= Handler; - tcs.SetResult(null); - } - } - - s.ClientConnected += Handler; - - return tcs.Task; - } - private void FireClientAcceptedEvent(IMqttChannelAdapter adapter) { ClientAccepted?.Invoke(this, new MqttServerAdapterClientAcceptedEventArgs(adapter)); @@ -71,5 +52,9 @@ namespace MQTTnet.Core.Tests { return Task.FromResult(0); } + + public void Dispose() + { + } } } \ No newline at end of file diff --git a/Tests/MQTTnet.Core.Tests/TopicFilterComparerTests.cs b/Tests/MQTTnet.Core.Tests/TopicFilterComparerTests.cs index be169212..e91e9885 100644 --- a/Tests/MQTTnet.Core.Tests/TopicFilterComparerTests.cs +++ b/Tests/MQTTnet.Core.Tests/TopicFilterComparerTests.cs @@ -60,7 +60,33 @@ namespace MQTTnet.Core.Tests CompareAndAssert("A/B/C/D", "#", true); } - private void CompareAndAssert(string topic, string filter, bool expectedResult) + [TestMethod] + public void TopicFilterComparer_MultiLevel_Sport() + { + // Tests from official MQTT spec (4.7.1.2 Multi-level wildcard) + CompareAndAssert("sport/tennis/player1", "sport/tennis/player1/#", true); + CompareAndAssert("sport/tennis/player1/ranking", "sport/tennis/player1/#", true); + CompareAndAssert("sport/tennis/player1/score/wimbledon", "sport/tennis/player1/#", true); + + CompareAndAssert("sport/tennis/player1", "sport/tennis/+", true); + CompareAndAssert("sport/tennis/player2", "sport/tennis/+", true); + CompareAndAssert("sport/tennis/player1/ranking", "sport/tennis/+", false); + + CompareAndAssert("sport", "sport/#", true); + CompareAndAssert("sport", "sport/+", false); + CompareAndAssert("sport/", "sport/+", true); + } + + [TestMethod] + public void TopicFilterComparer_SingleLevel_Finance() + { + // Tests from official MQTT spec (4.7.1.3 Single level wildcard) + CompareAndAssert("/finance", "+/+", true); + CompareAndAssert("/finance", "/+", true); + CompareAndAssert("/finance", "+", false); + } + + private static void CompareAndAssert(string topic, string filter, bool expectedResult) { Assert.AreEqual(expectedResult, MqttTopicFilterComparer.IsMatch(topic, filter)); } diff --git a/Tests/MQTTnet.TestApp.AspNetCore2/MQTTnet.TestApp.AspNetCore2.csproj b/Tests/MQTTnet.TestApp.AspNetCore2/MQTTnet.TestApp.AspNetCore2.csproj index 0a720ca9..a216d728 100644 --- a/Tests/MQTTnet.TestApp.AspNetCore2/MQTTnet.TestApp.AspNetCore2.csproj +++ b/Tests/MQTTnet.TestApp.AspNetCore2/MQTTnet.TestApp.AspNetCore2.csproj @@ -1,8 +1,8 @@ - + - netcoreapp2.0 - 2.3 + netcoreapp2.1 + Latest @@ -10,11 +10,13 @@ - + + - + + diff --git a/Tests/MQTTnet.TestApp.AspNetCore2/Program.cs b/Tests/MQTTnet.TestApp.AspNetCore2/Program.cs index 38b8c12d..5248ce61 100644 --- a/Tests/MQTTnet.TestApp.AspNetCore2/Program.cs +++ b/Tests/MQTTnet.TestApp.AspNetCore2/Program.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; +using MQTTnet.AspNetCore; namespace MQTTnet.TestApp.AspNetCore2 { @@ -12,6 +13,10 @@ namespace MQTTnet.TestApp.AspNetCore2 private static IWebHost BuildWebHost(string[] args) => WebHost.CreateDefaultBuilder(args) + .UseKestrel(o => { + o.ListenAnyIP(1883, l => l.UseMqtt()); + o.ListenAnyIP(5000); // default http pipeline + }) .UseStartup() .Build(); } diff --git a/Tests/MQTTnet.TestApp.AspNetCore2/Startup.cs b/Tests/MQTTnet.TestApp.AspNetCore2/Startup.cs index 591cc87d..f52040a0 100644 --- a/Tests/MQTTnet.TestApp.AspNetCore2/Startup.cs +++ b/Tests/MQTTnet.TestApp.AspNetCore2/Startup.cs @@ -17,8 +17,12 @@ namespace MQTTnet.TestApp.AspNetCore2 public void ConfigureServices(IServiceCollection services) { - var mqttServerOptions = new MqttServerOptionsBuilder().Build(); - services.AddHostedMqttServer(mqttServerOptions); + var mqttServerOptions = new MqttServerOptionsBuilder() + .WithoutDefaultEndpoint() + .Build(); + services + .AddHostedMqttServer(mqttServerOptions) + .AddMqttConnectionHandler(); } // In class _Startup_ of the ASP.NET Core 2.0 project. diff --git a/Tests/MQTTnet.TestApp.AspNetCore2/wwwroot/app/app.ts b/Tests/MQTTnet.TestApp.AspNetCore2/wwwroot/app/app.ts index 3ce98566..b7167701 100644 --- a/Tests/MQTTnet.TestApp.AspNetCore2/wwwroot/app/app.ts +++ b/Tests/MQTTnet.TestApp.AspNetCore2/wwwroot/app/app.ts @@ -2,7 +2,7 @@ var client = connect('ws://' + location.host + '/mqtt', { - clientId: "client1", + clientId: "client" + Math.floor(Math.random() * 6) + 1 }); window.onbeforeunload = () => { @@ -23,11 +23,12 @@ publishButton.onclick = click => { client.on('connect', () => { client.subscribe('#', { qos: 0 }, (err, granted) => { - console.log(err); + console.log(err); }); client.publish('presence', 'Hello mqtt'); stateParagraph.innerText = "connected"; + showMsg("[connect]"); }); client.on("error", e => { @@ -36,6 +37,7 @@ client.on("error", e => { client.on("reconnect", () => { stateParagraph.innerText = "reconnecting"; + showMsg("[reconnect]"); }); client.on('message', (topic, message) => { @@ -50,7 +52,7 @@ function showMsg(msg: string) { msgsList.appendChild(node); - if (msgsList.childElementCount > 20) { + if (msgsList.childElementCount > 50) { msgsList.removeChild(msgsList.childNodes[0]); } } \ No newline at end of file diff --git a/Tests/MQTTnet.TestApp.NetCore/ClientFlowTest.cs b/Tests/MQTTnet.TestApp.NetCore/ClientFlowTest.cs new file mode 100644 index 00000000..4b166236 --- /dev/null +++ b/Tests/MQTTnet.TestApp.NetCore/ClientFlowTest.cs @@ -0,0 +1,47 @@ +using System; +using System.Threading.Tasks; +using MQTTnet.Client; + +namespace MQTTnet.TestApp.NetCore +{ + public static class ClientFlowTest + { + public static async Task RunAsync() + { + MqttNetConsoleLogger.ForwardToConsole(); + try + { + var factory = new MqttFactory(); + var client = factory.CreateMqttClient(); + + var options = new MqttClientOptionsBuilder() + .WithTcpServer("localhost") + .Build(); + + Console.WriteLine("BEFORE CONNECT"); + await client.ConnectAsync(options); + Console.WriteLine("AFTER CONNECT"); + + Console.WriteLine("BEFORE SUBSCRIBE"); + await client.SubscribeAsync("test/topic"); + Console.WriteLine("AFTER SUBSCRIBE"); + + Console.WriteLine("BEFORE PUBLISH"); + await client.PublishAsync("test/topic", "payload"); + Console.WriteLine("AFTER PUBLISH"); + + await Task.Delay(1000); + + Console.WriteLine("BEFORE DISCONNECT"); + await client.DisconnectAsync(); + Console.WriteLine("AFTER DISCONNECT"); + + Console.WriteLine("FINISHED"); + } + catch (Exception ex) + { + Console.WriteLine(ex); + } + } + } +} diff --git a/Tests/MQTTnet.TestApp.NetCore/ClientTest.cs b/Tests/MQTTnet.TestApp.NetCore/ClientTest.cs index dd4a1bc0..98e7f0b1 100644 --- a/Tests/MQTTnet.TestApp.NetCore/ClientTest.cs +++ b/Tests/MQTTnet.TestApp.NetCore/ClientTest.cs @@ -12,25 +12,18 @@ namespace MQTTnet.TestApp.NetCore { try { - var options = new MqttClientOptions - { - ClientId = "XYZ", - CleanSession = true, - ChannelOptions = new MqttClientTcpOptions - { - //Server = "localhost", - Server = "192.168.1.174" - }, - //ChannelOptions = new MqttClientWebSocketOptions - //{ - // Uri = "ws://localhost:59690/mqtt" - //} - }; + MqttNetConsoleLogger.ForwardToConsole(); var factory = new MqttFactory(); - var client = factory.CreateMqttClient(); - + var clientOptions = new MqttClientOptions + { + ChannelOptions = new MqttClientTcpOptions + { + Server = "127.0.0.1" + } + }; + client.ApplicationMessageReceived += (s, e) => { Console.WriteLine("### RECEIVED APPLICATION MESSAGE ###"); @@ -57,7 +50,7 @@ namespace MQTTnet.TestApp.NetCore try { - await client.ConnectAsync(options); + await client.ConnectAsync(clientOptions); } catch { @@ -67,7 +60,7 @@ namespace MQTTnet.TestApp.NetCore try { - await client.ConnectAsync(options); + await client.ConnectAsync(clientOptions); } catch (Exception exception) { diff --git a/Tests/MQTTnet.TestApp.NetCore/MQTTnet.TestApp.NetCore.csproj b/Tests/MQTTnet.TestApp.NetCore/MQTTnet.TestApp.NetCore.csproj index 1784cc0e..2a738a08 100644 --- a/Tests/MQTTnet.TestApp.NetCore/MQTTnet.TestApp.NetCore.csproj +++ b/Tests/MQTTnet.TestApp.NetCore/MQTTnet.TestApp.NetCore.csproj @@ -1,4 +1,4 @@ - + Exe @@ -11,11 +11,12 @@ - + - + + diff --git a/Tests/MQTTnet.TestApp.NetCore/ManagedClientTest.cs b/Tests/MQTTnet.TestApp.NetCore/ManagedClientTest.cs index e23e9945..60ce27d7 100644 --- a/Tests/MQTTnet.TestApp.NetCore/ManagedClientTest.cs +++ b/Tests/MQTTnet.TestApp.NetCore/ManagedClientTest.cs @@ -4,7 +4,7 @@ using System.IO; using Newtonsoft.Json; using System.Collections.Generic; using MQTTnet.Client; -using MQTTnet.ManagedClient; +using MQTTnet.Extensions.ManagedClient; using MQTTnet.Protocol; namespace MQTTnet.TestApp.NetCore @@ -39,15 +39,15 @@ namespace MQTTnet.TestApp.NetCore Console.WriteLine(">> RECEIVED: " + e.ApplicationMessage.Topic); }; - await managedClient.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("Step").WithPayload("1").Build()); - await managedClient.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("Step").WithPayload("2").WithAtLeastOnceQoS().Build()); + await managedClient.PublishAsync(builder => builder.WithTopic("Step").WithPayload("1")); + await managedClient.PublishAsync(builder => builder.WithTopic("Step").WithPayload("2").WithAtLeastOnceQoS()); 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()); + await managedClient.PublishAsync(builder => builder.WithTopic("Step").WithPayload("3")); Console.WriteLine("Managed client started."); Console.ReadLine(); @@ -76,23 +76,23 @@ namespace MQTTnet.TestApp.NetCore { private const string Filename = @"RetainedMessages.json"; - public Task SaveQueuedMessagesAsync(IList messages) + public Task SaveQueuedMessagesAsync(IList messages) { File.WriteAllText(Filename, JsonConvert.SerializeObject(messages)); return Task.FromResult(0); } - public Task> LoadQueuedMessagesAsync() + public Task> LoadQueuedMessagesAsync() { - IList retainedMessages; + IList retainedMessages; if (File.Exists(Filename)) { var json = File.ReadAllText(Filename); - retainedMessages = JsonConvert.DeserializeObject>(json); + retainedMessages = JsonConvert.DeserializeObject>(json); } else { - retainedMessages = new List(); + retainedMessages = new List(); } return Task.FromResult(retainedMessages); diff --git a/Tests/MQTTnet.TestApp.NetCore/PerformanceTest.cs b/Tests/MQTTnet.TestApp.NetCore/PerformanceTest.cs index 5f2e5adc..3f4a1d54 100644 --- a/Tests/MQTTnet.TestApp.NetCore/PerformanceTest.cs +++ b/Tests/MQTTnet.TestApp.NetCore/PerformanceTest.cs @@ -12,15 +12,87 @@ namespace MQTTnet.TestApp.NetCore { public static class PerformanceTest { - public static async Task RunAsync() + public static void RunClientOnly() { - Console.WriteLine("Press 'c' for concurrent sends. Otherwise in one batch."); - var concurrent = Console.ReadKey(true).KeyChar == 'c'; + try + { + var options = new MqttClientOptions + { + ChannelOptions = new MqttClientTcpOptions + { + Server = "127.0.0.1" + }, + CleanSession = true + }; - 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); + var client = new MqttFactory().CreateMqttClient(); + client.ConnectAsync(options).GetAwaiter().GetResult(); - await Task.WhenAll(server, client).ConfigureAwait(false); + var message = CreateMessage(); + var stopwatch = new Stopwatch(); + + for (var i = 0; i < 10; i++) + { + var sentMessagesCount = 0; + + stopwatch.Restart(); + while (stopwatch.ElapsedMilliseconds < 1000) + { + client.PublishAsync(message).GetAwaiter().GetResult(); + sentMessagesCount++; + } + + Console.WriteLine($"Sending {sentMessagesCount} messages per second. #" + (i + 1)); + + GC.Collect(); + } + } + catch (Exception exception) + { + Console.WriteLine(exception); + } + } + + public static void RunClientAndServer() + { + try + { + var mqttServer = new MqttFactory().CreateMqttServer(); + mqttServer.StartAsync(new MqttServerOptions()).GetAwaiter().GetResult(); + + var options = new MqttClientOptions + { + ChannelOptions = new MqttClientTcpOptions + { + Server = "127.0.0.1" + }, + CleanSession = true + }; + + var client = new MqttFactory().CreateMqttClient(); + client.ConnectAsync(options).GetAwaiter().GetResult(); + + var message = CreateMessage(); + var stopwatch = new Stopwatch(); + + for (var i = 0; i < 10; i++) + { + stopwatch.Restart(); + + var sentMessagesCount = 0; + while (stopwatch.ElapsedMilliseconds < 1000) + { + client.PublishAsync(message).GetAwaiter().GetResult(); + sentMessagesCount++; + } + + Console.WriteLine($"Sending {sentMessagesCount} messages per second. #" + (i + 1)); + } + } + catch (Exception exception) + { + Console.WriteLine(exception); + } } private static Task RunClientsAsync(int msgChunkSize, TimeSpan interval, bool concurrent) @@ -44,7 +116,7 @@ namespace MQTTnet.TestApp.NetCore try { - await client.ConnectAsync(options); + await client.ConnectAsync(options).ConfigureAwait(false); } catch (Exception exception) { @@ -52,29 +124,8 @@ namespace MQTTnet.TestApp.NetCore } 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; for (var i = 0; i < testMessageCount; i++) { @@ -141,38 +192,5 @@ namespace MQTTnet.TestApp.NetCore Interlocked.Increment(ref count); return Task.Run(() => client.PublishAsync(applicationMessage)); } - - private static async Task RunServerAsync() - { - try - { - 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(); - - await mqttServer.StopAsync(); - } - catch (Exception e) - { - Console.WriteLine(e); - } - - Console.ReadLine(); - } } } diff --git a/Tests/MQTTnet.TestApp.NetCore/Program.cs b/Tests/MQTTnet.TestApp.NetCore/Program.cs index 77b9ea6e..908844fa 100644 --- a/Tests/MQTTnet.TestApp.NetCore/Program.cs +++ b/Tests/MQTTnet.TestApp.NetCore/Program.cs @@ -19,6 +19,11 @@ namespace MQTTnet.TestApp.NetCore Console.WriteLine("2 = Start server"); Console.WriteLine("3 = Start performance test"); Console.WriteLine("4 = Start managed client"); + Console.WriteLine("5 = Start public broker test"); + Console.WriteLine("6 = Start server & client"); + Console.WriteLine("7 = Client flow test"); + Console.WriteLine("8 = Start performance test (client only)"); + Console.WriteLine("9 = Start server (no trace)"); var pressedKey = Console.ReadKey(true); if (pressedKey.KeyChar == '1') @@ -31,12 +36,35 @@ namespace MQTTnet.TestApp.NetCore } else if (pressedKey.KeyChar == '3') { - Task.Run(PerformanceTest.RunAsync); + PerformanceTest.RunClientAndServer(); + return; } else if (pressedKey.KeyChar == '4') { Task.Run(ManagedClientTest.RunAsync); } + else if (pressedKey.KeyChar == '5') + { + Task.Run(PublicBrokerTest.RunAsync); + } + else if (pressedKey.KeyChar == '6') + { + Task.Run(ServerAndClientTest.RunAsync); + } + else if (pressedKey.KeyChar == '7') + { + Task.Run(ClientFlowTest.RunAsync); + } + else if (pressedKey.KeyChar == '8') + { + PerformanceTest.RunClientOnly(); + return; + } + else if (pressedKey.KeyChar == '9') + { + ServerTest.RunEmptyServer(); + return; + } Thread.Sleep(Timeout.Infinite); } diff --git a/Tests/MQTTnet.TestApp.NetCore/PublicBrokerTest.cs b/Tests/MQTTnet.TestApp.NetCore/PublicBrokerTest.cs new file mode 100644 index 00000000..1265a6a2 --- /dev/null +++ b/Tests/MQTTnet.TestApp.NetCore/PublicBrokerTest.cs @@ -0,0 +1,128 @@ +using MQTTnet.Client; +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using MQTTnet.Protocol; +using Newtonsoft.Json; + +namespace MQTTnet.TestApp.NetCore +{ + public static class PublicBrokerTest + { + public static async Task RunAsync() + { + //MqttNetGlobalLogger.LogMessagePublished += (s, e) => Console.WriteLine(e.TraceMessage); + + // iot.eclipse.org + await ExecuteTestAsync("iot.eclipse.org TCP", + new MqttClientOptionsBuilder().WithTcpServer("iot.eclipse.org", 1883).Build()); + + await ExecuteTestAsync("iot.eclipse.org WS", + new MqttClientOptionsBuilder().WithWebSocketServer("iot.eclipse.org:80/mqtt").Build()); + + await ExecuteTestAsync("iot.eclipse.org WS TLS", + new MqttClientOptionsBuilder().WithWebSocketServer("iot.eclipse.org:443/mqtt").WithTls().Build()); + + // test.mosquitto.org + await ExecuteTestAsync("test.mosquitto.org TCP", + new MqttClientOptionsBuilder().WithTcpServer("test.mosquitto.org", 1883).Build()); + + await ExecuteTestAsync("test.mosquitto.org TCP TLS", + new MqttClientOptionsBuilder().WithTcpServer("test.mosquitto.org", 8883).WithTls().Build()); + + await ExecuteTestAsync("test.mosquitto.org WS", + new MqttClientOptionsBuilder().WithWebSocketServer("test.mosquitto.org:8080/mqtt").Build()); + + await ExecuteTestAsync("test.mosquitto.org WS TLS", + new MqttClientOptionsBuilder().WithWebSocketServer("test.mosquitto.org:8081/mqtt").Build()); + + // broker.hivemq.com + await ExecuteTestAsync("broker.hivemq.com TCP", + new MqttClientOptionsBuilder().WithTcpServer("broker.hivemq.com", 1883).Build()); + + await ExecuteTestAsync("broker.hivemq.com WS", + new MqttClientOptionsBuilder().WithWebSocketServer("broker.hivemq.com:8000/mqtt").Build()); + + // mqtt.swifitch.cz + await ExecuteTestAsync("mqtt.swifitch.cz", + new MqttClientOptionsBuilder().WithTcpServer("mqtt.swifitch.cz", 1883).Build()); + + // CloudMQTT + var configFile = Path.Combine("E:\\CloudMqttTestConfig.json"); + if (File.Exists(configFile)) + { + var config = JsonConvert.DeserializeObject(File.ReadAllText(configFile)); + + await ExecuteTestAsync("CloudMQTT TCP", + new MqttClientOptionsBuilder().WithTcpServer(config.Server, config.Port).WithCredentials(config.Username, config.Password).Build()); + + await ExecuteTestAsync("CloudMQTT TCP TLS", + new MqttClientOptionsBuilder().WithTcpServer(config.Server, config.SslPort).WithCredentials(config.Username, config.Password).WithTls().Build()); + + await ExecuteTestAsync("CloudMQTT WS TLS", + new MqttClientOptionsBuilder().WithWebSocketServer(config.Server + ":" + config.SslWebSocketPort + "/mqtt").WithCredentials(config.Username, config.Password).WithTls().Build()); + } + + Write("Finished.", ConsoleColor.White); + Console.ReadLine(); + } + + private static async Task ExecuteTestAsync(string name, IMqttClientOptions options) + { + try + { + Write("Testing '" + name + "'... ", ConsoleColor.Gray); + var factory = new MqttFactory(); + var client = factory.CreateMqttClient(); + var topic = Guid.NewGuid().ToString(); + + MqttApplicationMessage receivedMessage = null; + client.ApplicationMessageReceived += (s, e) => receivedMessage = e.ApplicationMessage; + + await client.ConnectAsync(options); + await client.SubscribeAsync(topic, MqttQualityOfServiceLevel.AtLeastOnce); + await client.PublishAsync(topic, "Hello_World", MqttQualityOfServiceLevel.AtLeastOnce); + + SpinWait.SpinUntil(() => receivedMessage != null, 5000); + + if (receivedMessage?.Topic != topic || receivedMessage?.ConvertPayloadToString() != "Hello_World") + { + throw new Exception("Message invalid."); + } + + await client.UnsubscribeAsync("test"); + await client.DisconnectAsync(); + + Write("[OK]\n", ConsoleColor.Green); + } + catch (Exception e) + { + Write("[FAILED] " + e.Message + "\n", ConsoleColor.Red); + } + } + + private static void Write(string message, ConsoleColor color) + { + Console.ForegroundColor = color; + Console.Write(message); + } + + public class MqttConfig + { + public string Server { get; set; } + + public string Username { get; set; } + + public string Password { get; set; } + + public int Port { get; set; } + + public int SslPort { get; set; } + + public int WebSocketPort { get; set; } + + public int SslWebSocketPort { get; set; } + } + } +} diff --git a/Tests/MQTTnet.TestApp.NetCore/ServerAndClientTest.cs b/Tests/MQTTnet.TestApp.NetCore/ServerAndClientTest.cs new file mode 100644 index 00000000..30ba9140 --- /dev/null +++ b/Tests/MQTTnet.TestApp.NetCore/ServerAndClientTest.cs @@ -0,0 +1,27 @@ +using System.Threading; +using System.Threading.Tasks; +using MQTTnet.Client; +using MQTTnet.Server; + +namespace MQTTnet.TestApp.NetCore +{ + public static class ServerAndClientTest + { + public static async Task RunAsync() + { + MqttNetConsoleLogger.ForwardToConsole(); + + var factory = new MqttFactory(); + var server = factory.CreateMqttServer(); + var client = factory.CreateMqttClient(); + + var serverOptions = new MqttServerOptionsBuilder().Build(); + await server.StartAsync(serverOptions); + + var clientOptions = new MqttClientOptionsBuilder().WithTcpServer("localhost").Build(); + await client.ConnectAsync(clientOptions); + + await Task.Delay(Timeout.Infinite); + } + } +} diff --git a/Tests/MQTTnet.TestApp.NetCore/ServerTest.cs b/Tests/MQTTnet.TestApp.NetCore/ServerTest.cs index 80a8e5a4..12fa1aa7 100644 --- a/Tests/MQTTnet.TestApp.NetCore/ServerTest.cs +++ b/Tests/MQTTnet.TestApp.NetCore/ServerTest.cs @@ -8,12 +8,19 @@ namespace MQTTnet.TestApp.NetCore { public static class ServerTest { + public static void RunEmptyServer() + { + var mqttServer = new MqttFactory().CreateMqttServer(); + mqttServer.StartAsync(new MqttServerOptions()).GetAwaiter().GetResult(); + + Console.WriteLine("Press any key to exit."); + Console.ReadLine(); + } + public static async Task RunAsync() { try { - MqttNetConsoleLogger.ForwardToConsole(); - var options = new MqttServerOptions { ConnectionValidator = p => diff --git a/Tests/MQTTnet.TestApp.UniversalWindows/MQTTnet.TestApp.UniversalWindows.csproj b/Tests/MQTTnet.TestApp.UniversalWindows/MQTTnet.TestApp.UniversalWindows.csproj index b5d2dbd9..d7090834 100644 --- a/Tests/MQTTnet.TestApp.UniversalWindows/MQTTnet.TestApp.UniversalWindows.csproj +++ b/Tests/MQTTnet.TestApp.UniversalWindows/MQTTnet.TestApp.UniversalWindows.csproj @@ -11,7 +11,7 @@ MQTTnet.TestApp.UniversalWindows en-US UAP - 10.0.16299.0 + 10.0.17134.0 10.0.16299.0 14 512 @@ -127,21 +127,25 @@ - + + {C400533A-8EBA-4F0B-BF4D-295C3708604B} + MQTTnet.Extensions.ManagedClient + + {c444e9c8-95fa-430e-9126-274129de16cd} MQTTnet.Extensions.Rpc - + {3587e506-55a2-4eb3-99c7-dc01e42d25d2} - MQTTnet.NetStandard + MQTTnet - 6.0.8 + 6.1.4 - 2.2.0 + 3.0.0 diff --git a/Tests/MQTTnet.TestApp.UniversalWindows/MainPage.xaml b/Tests/MQTTnet.TestApp.UniversalWindows/MainPage.xaml index 3698613f..41617df8 100644 --- a/Tests/MQTTnet.TestApp.UniversalWindows/MainPage.xaml +++ b/Tests/MQTTnet.TestApp.UniversalWindows/MainPage.xaml @@ -4,6 +4,8 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:server="using:MQTTnet.Server" + xmlns:interop="using:Windows.UI.Xaml.Interop" d:DesignHeight="800" d:DesignWidth="800" mc:Ignorable="d"> @@ -31,7 +33,7 @@ Keep alive interval: - + TCP WS @@ -43,8 +45,8 @@ - + Topic: @@ -69,6 +71,7 @@ + Method: @@ -103,6 +106,7 @@ + Topic: @@ -132,6 +136,7 @@ + Port: @@ -139,6 +144,7 @@ Persist retained messages in JSON format Clear previously retained messages on startup + Allow persistent sessions @@ -146,6 +152,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -159,3 +196,4 @@ + diff --git a/Tests/MQTTnet.TestApp.UniversalWindows/MainPage.xaml.cs b/Tests/MQTTnet.TestApp.UniversalWindows/MainPage.xaml.cs index da0c2f31..387364a5 100644 --- a/Tests/MQTTnet.TestApp.UniversalWindows/MainPage.xaml.cs +++ b/Tests/MQTTnet.TestApp.UniversalWindows/MainPage.xaml.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Concurrent; +using System.Collections.ObjectModel; using System.Text; using System.Threading.Tasks; using Windows.Security.Cryptography.Certificates; @@ -8,9 +9,9 @@ using Windows.UI.Xaml; using MQTTnet.Client; using MQTTnet.Diagnostics; using MQTTnet.Exceptions; +using MQTTnet.Extensions.ManagedClient; using MQTTnet.Extensions.Rpc; using MQTTnet.Implementations; -using MQTTnet.ManagedClient; using MQTTnet.Protocol; using MQTTnet.Server; using MqttClientConnectedEventArgs = MQTTnet.Client.MqttClientConnectedEventArgs; @@ -21,6 +22,7 @@ namespace MQTTnet.TestApp.UniversalWindows public sealed partial class MainPage { private readonly ConcurrentQueue _traceMessages = new ConcurrentQueue(); + private readonly ObservableCollection _sessions = new ObservableCollection(); private IMqttClient _mqttClient; private IMqttServer _mqttServer; @@ -306,6 +308,7 @@ namespace MQTTnet.TestApp.UniversalWindows var options = new MqttServerOptions(); options.DefaultEndpointOptions.Port = int.Parse(ServerPort.Text); options.Storage = storage; + options.EnablePersistentSessions = ServerAllowPersistentSessions.IsChecked == true; await _mqttServer.StartAsync(options); } @@ -350,13 +353,10 @@ namespace MQTTnet.TestApp.UniversalWindows 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)); } @@ -364,6 +364,10 @@ namespace MQTTnet.TestApp.UniversalWindows { RpcResponses.Items.Add(RpcMethod.Text + " >>> [TIMEOUT]"); } + catch (Exception exception) + { + RpcResponses.Items.Add(RpcMethod.Text + " >>> [EXCEPTION (" + exception.Message + ")]"); + } } private void ClearRpcResponses(object sender, RoutedEventArgs e) @@ -371,6 +375,29 @@ namespace MQTTnet.TestApp.UniversalWindows RpcResponses.Items.Clear(); } + private void ClearSessions(object sender, RoutedEventArgs e) + { + _sessions.Clear(); + } + + private async void RefreshSessions(object sender, RoutedEventArgs e) + { + if (_mqttServer == null) + { + return; + } + + var sessions = await _mqttServer.GetClientSessionsStatusAsync(); + _sessions.Clear(); + + foreach (var session in sessions) + { + _sessions.Add(session); + } + + ListViewSessions.DataContext = _sessions; + } + private async Task WikiCode() { { @@ -604,7 +631,6 @@ namespace MQTTnet.TestApp.UniversalWindows await mqttClient.SubscribeAsync(new TopicFilterBuilder().WithTopic("my/topic").Build()); await mqttClient.StartAsync(options); } - } } }