mirror of
https://github.com/dotnet/MQTTnet.git
synced 2026-10-16 01:03:01 +08:00
Compare commits
29
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
740d605fd2 | ||
|
|
9177b6ae68 | ||
|
|
f0ea5c9ecd | ||
|
|
76479ebabe | ||
|
|
e18a91a4b5 | ||
|
|
b95d352236 | ||
|
|
c5f4cf1f9b | ||
|
|
8650002523 | ||
|
|
25c1da6a21 | ||
|
|
4e3ede42c6 | ||
|
|
918a3c8d8b | ||
|
|
290419b3b6 | ||
|
|
32b6146532 | ||
|
|
3a73ec21f7 | ||
|
|
41d5b70344 | ||
|
|
b4769778af | ||
|
|
473f9c31b1 | ||
|
|
ae4f168066 | ||
|
|
c5578a93e8 | ||
|
|
f4b2a10616 | ||
|
|
ed63aa4910 | ||
|
|
4481869d42 | ||
|
|
abbf14b1ec | ||
|
|
4f9e54ba1d | ||
|
|
d80188167a | ||
|
|
4ab956abd1 | ||
|
|
1aa91e3828 | ||
|
|
2410d7b4c1 | ||
|
|
b7328e5fd2 |
+1
-2
@@ -40,7 +40,6 @@ dotnet_style_qualification_for_method = false:suggestion
|
||||
dotnet_style_qualification_for_property = false:suggestion
|
||||
|
||||
# ReSharper properties
|
||||
resharper_autodetect_indent_settings = true
|
||||
resharper_default_private_modifier = implicit
|
||||
resharper_place_accessorholder_attribute_on_same_line = false
|
||||
resharper_use_indent_from_vs = false
|
||||
@@ -61,7 +60,7 @@ resharper_web_config_wrong_module_highlighting = warning
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = crlf
|
||||
trim_trailing_whitespace = false
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = false
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
* [Core] Optimized packet serialization of PUBACK and PUBREC packets for protocol version 5.0.0 (#1939, thanks to @Y-Sindo).
|
||||
* [Core] The package inspector is now fully async (#1941).
|
||||
* [Client] Added a dedicated exception when the client is not connected (#1954, thanks to @marcpiulachs).
|
||||
* [ManagedClient] Added a new event (SubscriptionsChangedAsync) which is fired when a subscription or unsubscription was made (#1894, thanks to @pdufrene).
|
||||
* Core: Fixed issue when parsing AUTH packet with 0 length body (#2039).
|
||||
* nuget: Changed code signing and nuget certificate (**BREAKING CHANGE**).
|
||||
* TopicTemplates: Updated samples, parameter validation (#2022).
|
||||
* ManagedClient: Switch SubscribeAsync/UnsubscribeAsync to IEnumerable<string> (#2026).
|
||||
* Server: Fix _LoadingRetainedMessageAsync_ not executed (#2025).
|
||||
+30
-12
@@ -3,7 +3,7 @@ name: CI
|
||||
on: [push, pull_request]
|
||||
|
||||
env:
|
||||
VERSION: "4.3.4.${{github.run_number}}"
|
||||
VERSION: "4.3.7.${{github.run_number}}"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
sdk-version: 18362
|
||||
|
||||
- name: Setup .NET SDK
|
||||
uses: actions/setup-dotnet@v1.9.0
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: |
|
||||
3.1.x
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
uses: microsoft/setup-msbuild@v1.1
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Signing Certificate
|
||||
run: |
|
||||
@@ -42,14 +42,6 @@ jobs:
|
||||
- name: Build solution
|
||||
run: msbuild MQTTnet.sln /t:Build /p:Configuration="Release" /verbosity:m /p:FileVersion=${{ env.VERSION }} /p:AssemblyVersion=${{ env.VERSION }} /p:PackageVersion=${{ env.VERSION }} /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=${{ github.workspace }}\certificate.snk
|
||||
|
||||
- name: Collect nuget Packages
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: nuget Packages
|
||||
path: |
|
||||
**\*.nupkg
|
||||
**\*.snupkg
|
||||
|
||||
- name: Setup VSTest
|
||||
uses: darenm/Setup-VSTest@v1
|
||||
|
||||
@@ -59,6 +51,32 @@ jobs:
|
||||
- name: ASP.NET Tests
|
||||
run: vstest.console.exe Source\MQTTnet.AspNetCore.Tests\bin\Release\netcoreapp3.1\MQTTnet.AspNetCore.Tests.dll
|
||||
|
||||
- name: Install sign CLI tool
|
||||
run: dotnet tool install --tool-path . sign --version 0.9.0-beta.23127.3
|
||||
|
||||
- name: Sign nugets
|
||||
shell: pwsh
|
||||
run: >
|
||||
./sign code azure-key-vault `
|
||||
"**/*.nupkg" `
|
||||
--base-directory "${{ github.workspace }}/Source" `
|
||||
--publisher-name "MQTTnet" `
|
||||
--description "MQTTnet" `
|
||||
--description-url "https://github.com/dotnet/MQTTnet" `
|
||||
--azure-key-vault-tenant-id "${{ secrets.AZURE_TENANT_ID }}" `
|
||||
--azure-key-vault-client-id "${{ secrets.AZURE_CLIENT_ID }}" `
|
||||
--azure-key-vault-client-secret "${{ secrets.AZURE_CLIENT_SECRET }}" `
|
||||
--azure-key-vault-certificate "${{ secrets.KEY_VAULT_CERTIFICATE_ID }}" `
|
||||
--azure-key-vault-url "${{ secrets.KEY_VAULT_URL }}"
|
||||
|
||||
- name: Upload signed nuget packages
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: signed-nugets
|
||||
path: |
|
||||
${{ github.workspace }}/Source/**/*.nupkg
|
||||
${{ github.workspace }}/Source/**/*.snupkg
|
||||
|
||||
- name: Publish MyGet nugets
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
run: dotnet nuget push **/*.nupkg -k ${{ secrets.MYGET_API_KEY }} -s https://www.myget.org/F/mqttnet/api/v3/index.json --skip-duplicate
|
||||
run: dotnet nuget push ${{ github.workspace }}/Source/**/*.nupkg -k ${{ secrets.MYGET_API_KEY }} -s https://www.myget.org/F/mqttnet/api/v3/index.json --skip-duplicate
|
||||
|
||||
+15
-9
@@ -7,9 +7,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet", "Source\MQTTnet\M
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{B3F60ECB-45BA-4C66-8903-8BB89CA67998}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
CODE-OF-CONDUCT.md = CODE-OF-CONDUCT.md
|
||||
LICENSE = LICENSE
|
||||
README.md = README.md
|
||||
CODE-OF-CONDUCT.md = CODE-OF-CONDUCT.md
|
||||
.github\workflows\ReleaseNotes.md = .github\workflows\ReleaseNotes.md
|
||||
.github\workflows\ci.yml = .github\workflows\ci.yml
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.AspNetCore", "Source\MQTTnet.AspnetCore\MQTTnet.AspNetCore.csproj", "{F10C4060-F7EE-4A83-919F-FF723E72F94A}"
|
||||
@@ -20,17 +22,19 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Extensions.ManagedC
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Extensions.WebSocket4Net", "Source\MQTTnet.Extensions.WebSocket4Net\MQTTnet.Extensions.WebSocket4Net.csproj", "{2BD01D53-4CA5-4142-BE8D-313876395E3E}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MQTTnet.Samples", "Samples\MQTTnet.Samples.csproj", "{71CF35F5-3327-4A91-AAF4-5340F6701771}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Samples", "Samples\MQTTnet.Samples.csproj", "{71CF35F5-3327-4A91-AAF4-5340F6701771}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MQTTnet.Tests", "Source\MQTTnet.Tests\MQTTnet.Tests.csproj", "{B270F32A-9F3E-42EE-A989-813E35E29ADB}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Tests", "Source\MQTTnet.Tests\MQTTnet.Tests.csproj", "{B270F32A-9F3E-42EE-A989-813E35E29ADB}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MQTTnet.AspNetCore.Tests", "Source\MQTTnet.AspNetCore.Tests\MQTTnet.AspNetCore.Tests.csproj", "{A238BBBF-C75F-482D-9CC3-BB34ABA9B675}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.AspNetCore.Tests", "Source\MQTTnet.AspNetCore.Tests\MQTTnet.AspNetCore.Tests.csproj", "{A238BBBF-C75F-482D-9CC3-BB34ABA9B675}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MQTTnet.Benchmarks", "Source\MQTTnet.Benchmarks\MQTTnet.Benchmarks.csproj", "{2F516E76-AAC4-4219-B7D1-34CDD3CFF381}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Benchmarks", "Source\MQTTnet.Benchmarks\MQTTnet.Benchmarks.csproj", "{2F516E76-AAC4-4219-B7D1-34CDD3CFF381}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MQTTnet.TestApp", "Source\MQTTnet.TestApp\MQTTnet.TestApp.csproj", "{175D5340-CC5B-4542-939D-4E7D15A0BC8D}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.TestApp", "Source\MQTTnet.TestApp\MQTTnet.TestApp.csproj", "{175D5340-CC5B-4542-939D-4E7D15A0BC8D}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MQTTnet.AspTestApp", "Source\MQTTnet.AspTestApp\MQTTnet.AspTestApp.csproj", "{72867E4C-4E15-4E8E-8FAB-AE9253286BBC}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.AspTestApp", "Source\MQTTnet.AspTestApp\MQTTnet.AspTestApp.csproj", "{72867E4C-4E15-4E8E-8FAB-AE9253286BBC}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Extensions.TopicTemplate", "Source\MQTTnet.Extensions.TopicTemplate\MQTTnet.Extensions.TopicTemplate.csproj", "{374D5861-F7A6-42A2-8CBE-040846000EF9}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
@@ -82,12 +86,14 @@ Global
|
||||
{72867E4C-4E15-4E8E-8FAB-AE9253286BBC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{72867E4C-4E15-4E8E-8FAB-AE9253286BBC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{72867E4C-4E15-4E8E-8FAB-AE9253286BBC}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{374D5861-F7A6-42A2-8CBE-040846000EF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{374D5861-F7A6-42A2-8CBE-040846000EF9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{374D5861-F7A6-42A2-8CBE-040846000EF9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{374D5861-F7A6-42A2-8CBE-040846000EF9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {07536672-5CBC-4BE3-ACE0-708A431A7894}
|
||||
EndGlobalSection
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
// ReSharper disable UnusedMember.Local
|
||||
|
||||
using MQTTnet.Client;
|
||||
using MQTTnet.Extensions.TopicTemplate;
|
||||
using MQTTnet.Packets;
|
||||
using MQTTnet.Protocol;
|
||||
using MQTTnet.Samples.Helpers;
|
||||
@@ -16,6 +17,8 @@ namespace MQTTnet.Samples.Client;
|
||||
|
||||
public static class Client_Subscribe_Samples
|
||||
{
|
||||
static MqttTopicTemplate sampleTemplate = new MqttTopicTemplate("mqttnet/samples/topic/{id}");
|
||||
|
||||
public static async Task Handle_Received_Application_Message()
|
||||
{
|
||||
/*
|
||||
@@ -42,11 +45,7 @@ public static class Client_Subscribe_Samples
|
||||
await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None);
|
||||
|
||||
var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder()
|
||||
.WithTopicFilter(
|
||||
f =>
|
||||
{
|
||||
f.WithTopic("mqttnet/samples/topic/2");
|
||||
})
|
||||
.WithTopicTemplate(sampleTemplate.WithParameter("id", "2"))
|
||||
.Build();
|
||||
|
||||
await mqttClient.SubscribeAsync(mqttSubscribeOptions, CancellationToken.None);
|
||||
@@ -88,11 +87,8 @@ public static class Client_Subscribe_Samples
|
||||
await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None);
|
||||
|
||||
var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder()
|
||||
.WithTopicFilter(
|
||||
f =>
|
||||
{
|
||||
f.WithTopic("mqttnet/samples/topic/1");
|
||||
})
|
||||
.WithTopicTemplate(
|
||||
sampleTemplate.WithParameter("id", "1"))
|
||||
.Build();
|
||||
|
||||
var response = await mqttClient.SubscribeAsync(mqttSubscribeOptions, CancellationToken.None);
|
||||
@@ -121,21 +117,12 @@ public static class Client_Subscribe_Samples
|
||||
// Create the subscribe options including several topics with different options.
|
||||
// It is also possible to all of these topics using a dedicated call of _SubscribeAsync_ per topic.
|
||||
var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder()
|
||||
.WithTopicFilter(
|
||||
f =>
|
||||
{
|
||||
f.WithTopic("mqttnet/samples/topic/1");
|
||||
})
|
||||
.WithTopicFilter(
|
||||
f =>
|
||||
{
|
||||
f.WithTopic("mqttnet/samples/topic/2").WithNoLocal();
|
||||
})
|
||||
.WithTopicFilter(
|
||||
f =>
|
||||
{
|
||||
f.WithTopic("mqttnet/samples/topic/3").WithRetainHandling(MqttRetainHandling.SendAtSubscribe);
|
||||
})
|
||||
.WithTopicTemplate(
|
||||
sampleTemplate.WithParameter("id", "1"))
|
||||
.WithTopicTemplate(
|
||||
sampleTemplate.WithParameter("id", "2"), noLocal: true)
|
||||
.WithTopicTemplate(
|
||||
sampleTemplate.WithParameter("id", "3"), retainHandling: MqttRetainHandling.SendAtSubscribe)
|
||||
.Build();
|
||||
|
||||
var response = await mqttClient.SubscribeAsync(mqttSubscribeOptions, CancellationToken.None);
|
||||
@@ -162,11 +149,7 @@ public static class Client_Subscribe_Samples
|
||||
await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None);
|
||||
|
||||
var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder()
|
||||
.WithTopicFilter(
|
||||
f =>
|
||||
{
|
||||
f.WithTopic("mqttnet/samples/topic/1");
|
||||
})
|
||||
.WithTopicTemplate(sampleTemplate.WithParameter("id", "1"))
|
||||
.Build();
|
||||
|
||||
var response = await mqttClient.SubscribeAsync(mqttSubscribeOptions, CancellationToken.None);
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
<ProjectReference Include="..\Source\MQTTnet.AspnetCore\MQTTnet.AspNetCore.csproj" />
|
||||
<ProjectReference Include="..\Source\MQTTnet.Extensions.ManagedClient\MQTTnet.Extensions.ManagedClient.csproj" />
|
||||
<ProjectReference Include="..\Source\MQTTnet.Extensions.Rpc\MQTTnet.Extensions.Rpc.csproj" />
|
||||
<ProjectReference Include="..\Source\MQTTnet.Extensions.TopicTemplate\MQTTnet.Extensions.TopicTemplate.csproj" />
|
||||
<ProjectReference Include="..\Source\MQTTnet.Extensions.WebSocket4Net\MQTTnet.Extensions.WebSocket4Net.csproj" />
|
||||
<ProjectReference Include="..\Source\MQTTnet\MQTTnet.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -22,7 +22,7 @@ public sealed class Managed_Client_Subscribe_Samples
|
||||
* - Reconnecting when connection is lost.
|
||||
* - Storing pending messages in an internal queue so that an enqueue is possible while the client remains not connected.
|
||||
*/
|
||||
|
||||
|
||||
var mqttFactory = new MqttFactory();
|
||||
var subscribed = false;
|
||||
|
||||
@@ -43,21 +43,21 @@ public sealed class Managed_Client_Subscribe_Samples
|
||||
await managedMqttClient.EnqueueAsync("Topic", "Payload");
|
||||
|
||||
Console.WriteLine("The managed MQTT client is connected.");
|
||||
|
||||
|
||||
// Wait until the queue is fully processed.
|
||||
SpinWait.SpinUntil(() => managedMqttClient.PendingApplicationMessagesCount == 0, 10000);
|
||||
|
||||
|
||||
Console.WriteLine($"Pending messages = {managedMqttClient.PendingApplicationMessagesCount}");
|
||||
|
||||
managedMqttClient.SubscriptionsChangedAsync += args => SubscriptionsResultAsync(args, subscribed);
|
||||
managedMqttClient.SubscriptionsChangedAsync += args => SubscriptionsResultAsync(args, ref subscribed);
|
||||
await managedMqttClient.SubscribeAsync("Topic").ConfigureAwait(false);
|
||||
|
||||
SpinWait.SpinUntil(() => subscribed, 1000);
|
||||
Console.WriteLine("Subscription properly done");
|
||||
Console.WriteLine($"Subscription properly done {subscribed}");
|
||||
}
|
||||
}
|
||||
|
||||
private static Task SubscriptionsResultAsync(SubscriptionsChangedEventArgs arg, bool subscribed)
|
||||
private static Task SubscriptionsResultAsync(SubscriptionsChangedEventArgs arg, ref bool subscribed)
|
||||
{
|
||||
foreach (var mqttClientSubscribeResult in arg.SubscribeResult)
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<IsPackable>false</IsPackable>
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
<Description>MQTTnet is a high performance .NET library for MQTT based communication. It provides a MQTT client and a MQTT server (broker) and supports v3.1.0, v3.1.1 and v5.0.0 of the MQTT protocol.</Description>
|
||||
<Authors>The contributors of MQTTnet</Authors>
|
||||
<PackageId>MQTTnet.AspNetCore</PackageId>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<SignAssembly>false</SignAssembly>
|
||||
<DelaySign>false</DelaySign>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
@@ -38,8 +37,10 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\..\Images\nuget.png" Pack="True" PackagePath="\" />
|
||||
<None Include="..\..\README.md" Pack="true" PackagePath="\" />
|
||||
<None Include="..\..\Images\nuget.png">
|
||||
<Pack>True</Pack>
|
||||
<PackagePath>\</PackagePath>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -16,26 +16,51 @@ namespace MQTTnet.AspNetCore
|
||||
public sealed class MqttHostedServer : MqttServer, IHostedService
|
||||
{
|
||||
readonly MqttFactory _mqttFactory;
|
||||
|
||||
public MqttHostedServer(MqttFactory mqttFactory, MqttServerOptions options, IEnumerable<IMqttServerAdapter> adapters, IMqttNetLogger logger) : base(
|
||||
#if NETCOREAPP3_1_OR_GREATER
|
||||
readonly IHostApplicationLifetime _hostApplicationLifetime;
|
||||
public MqttHostedServer(IHostApplicationLifetime hostApplicationLifetime, MqttFactory mqttFactory,
|
||||
MqttServerOptions options, IEnumerable<IMqttServerAdapter> adapters, IMqttNetLogger logger) : base(
|
||||
options,
|
||||
adapters,
|
||||
logger)
|
||||
{
|
||||
_mqttFactory = mqttFactory ?? throw new ArgumentNullException(nameof(mqttFactory));
|
||||
_hostApplicationLifetime = hostApplicationLifetime;
|
||||
}
|
||||
#else
|
||||
public MqttHostedServer(MqttFactory mqttFactory,
|
||||
MqttServerOptions options, IEnumerable<IMqttServerAdapter> adapters, IMqttNetLogger logger) : base(
|
||||
options,
|
||||
adapters,
|
||||
logger)
|
||||
{
|
||||
_mqttFactory = mqttFactory ?? throw new ArgumentNullException(nameof(mqttFactory));
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// The yield makes sure that the hosted service is considered up and running.
|
||||
await Task.Yield();
|
||||
|
||||
#if NETCOREAPP3_1_OR_GREATER
|
||||
_hostApplicationLifetime.ApplicationStarted.Register(OnStarted);
|
||||
#else
|
||||
_ = StartAsync();
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return StopAsync(_mqttFactory.CreateMqttServerStopOptionsBuilder().Build());
|
||||
}
|
||||
|
||||
#if NETCOREAPP3_1_OR_GREATER
|
||||
private void OnStarted()
|
||||
{
|
||||
_ = StartAsync();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -40,13 +40,10 @@ namespace MQTTnet.AspNetCore
|
||||
throw new ArgumentNullException(nameof(services));
|
||||
}
|
||||
|
||||
if (configure == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(configure));
|
||||
}
|
||||
|
||||
var serverOptionsBuilder = new MqttServerOptionsBuilder();
|
||||
configure.Invoke(serverOptionsBuilder);
|
||||
|
||||
configure?.Invoke(serverOptionsBuilder);
|
||||
|
||||
var options = serverOptionsBuilder.Build();
|
||||
|
||||
return AddHostedMqttServer(services, options);
|
||||
|
||||
@@ -47,8 +47,8 @@ namespace MQTTnet.Extensions.ManagedClient
|
||||
|
||||
Task StopAsync(bool cleanDisconnect = true);
|
||||
|
||||
Task SubscribeAsync(ICollection<MqttTopicFilter> topicFilters);
|
||||
Task SubscribeAsync(IEnumerable<MqttTopicFilter> topicFilters);
|
||||
|
||||
Task UnsubscribeAsync(ICollection<string> topics);
|
||||
Task UnsubscribeAsync(IEnumerable<string> topics);
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,6 @@
|
||||
<Description>This is an extension library which provides a managed MQTT client with additional features using MQTTnet.</Description>
|
||||
<Authors>The contributors of MQTTnet</Authors>
|
||||
<PackageId>MQTTnet.Extensions.ManagedClient</PackageId>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<SignAssembly>false</SignAssembly>
|
||||
<DelaySign>false</DelaySign>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
@@ -51,8 +50,10 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\..\Images\nuget.png" Pack="True" PackagePath="\" />
|
||||
<None Include="..\..\README.md" Pack="true" PackagePath="\" />
|
||||
<None Include="..\..\Images\nuget.png">
|
||||
<Pack>True</Pack>
|
||||
<PackagePath>\</PackagePath>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -19,6 +19,8 @@ namespace MQTTnet.Extensions.ManagedClient
|
||||
{
|
||||
public sealed class ManagedMqttClient : Disposable, IManagedMqttClient
|
||||
{
|
||||
readonly MqttNetSourceLogger _logger;
|
||||
|
||||
readonly AsyncEvent<InterceptingPublishMessageEventArgs> _interceptingPublishMessageEvent = new AsyncEvent<InterceptingPublishMessageEventArgs>();
|
||||
readonly AsyncEvent<ApplicationMessageProcessedEventArgs> _applicationMessageProcessedEvent = new AsyncEvent<ApplicationMessageProcessedEventArgs>();
|
||||
readonly AsyncEvent<ApplicationMessageSkippedEventArgs> _applicationMessageSkippedEvent = new AsyncEvent<ApplicationMessageSkippedEventArgs>();
|
||||
@@ -27,9 +29,7 @@ namespace MQTTnet.Extensions.ManagedClient
|
||||
readonly AsyncEvent<ManagedProcessFailedEventArgs> _synchronizingSubscriptionsFailedEvent = new AsyncEvent<ManagedProcessFailedEventArgs>();
|
||||
readonly AsyncEvent<SubscriptionsChangedEventArgs> _subscriptionsChangedEvent = new AsyncEvent<SubscriptionsChangedEventArgs>();
|
||||
|
||||
readonly MqttNetSourceLogger _logger;
|
||||
readonly BlockingQueue<ManagedMqttApplicationMessage> _messageQueue = new BlockingQueue<ManagedMqttApplicationMessage>();
|
||||
|
||||
readonly AsyncLock _messageQueueLock = new AsyncLock();
|
||||
|
||||
/// <summary>
|
||||
@@ -83,21 +83,19 @@ namespace MQTTnet.Extensions.ManagedClient
|
||||
add => _applicationMessageSkippedEvent.AddHandler(value);
|
||||
remove => _applicationMessageSkippedEvent.RemoveHandler(value);
|
||||
}
|
||||
|
||||
|
||||
public event Func<ApplicationMessageProcessedEventArgs, Task> ApplicationMessageProcessedAsync
|
||||
{
|
||||
add => _applicationMessageProcessedEvent.AddHandler(value);
|
||||
remove => _applicationMessageProcessedEvent.RemoveHandler(value);
|
||||
}
|
||||
|
||||
|
||||
public event Func<InterceptingPublishMessageEventArgs, Task> InterceptPublishMessageAsync
|
||||
{
|
||||
add => _interceptingPublishMessageEvent.AddHandler(value);
|
||||
remove => _interceptingPublishMessageEvent.RemoveHandler(value);
|
||||
}
|
||||
|
||||
|
||||
public event Func<MqttApplicationMessageReceivedEventArgs, Task> ApplicationMessageReceivedAsync
|
||||
{
|
||||
add => InternalClient.ApplicationMessageReceivedAsync += value;
|
||||
@@ -149,7 +147,7 @@ namespace MQTTnet.Extensions.ManagedClient
|
||||
public ManagedMqttClientOptions Options { get; private set; }
|
||||
|
||||
public int PendingApplicationMessagesCount => _messageQueue.Count;
|
||||
|
||||
|
||||
public async Task EnqueueAsync(MqttApplicationMessage applicationMessage)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
@@ -277,7 +275,7 @@ namespace MQTTnet.Extensions.ManagedClient
|
||||
ThrowIfDisposed();
|
||||
|
||||
_isCleanDisconnect = cleanDisconnect;
|
||||
|
||||
|
||||
StopPublishing();
|
||||
StopMaintainingConnection();
|
||||
|
||||
@@ -290,7 +288,7 @@ namespace MQTTnet.Extensions.ManagedClient
|
||||
}
|
||||
}
|
||||
|
||||
public Task SubscribeAsync(ICollection<MqttTopicFilter> topicFilters)
|
||||
public Task SubscribeAsync(IEnumerable<MqttTopicFilter> topicFilters)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
@@ -318,7 +316,7 @@ namespace MQTTnet.Extensions.ManagedClient
|
||||
return CompletedTask.Instance;
|
||||
}
|
||||
|
||||
public Task UnsubscribeAsync(ICollection<string> topics)
|
||||
public Task UnsubscribeAsync(IEnumerable<string> topics)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
@@ -369,6 +367,13 @@ namespace MQTTnet.Extensions.ManagedClient
|
||||
return remainingTime < TimeSpan.Zero ? TimeSpan.Zero : remainingTime;
|
||||
}
|
||||
|
||||
CancellationTokenSource NewTimeoutToken(CancellationToken linkedToken)
|
||||
{
|
||||
var newTimeoutToken = CancellationTokenSource.CreateLinkedTokenSource(linkedToken);
|
||||
newTimeoutToken.CancelAfter(Options.ClientOptions.Timeout);
|
||||
return newTimeoutToken;
|
||||
}
|
||||
|
||||
async Task HandleSubscriptionExceptionAsync(Exception exception, List<MqttTopicFilter> addedSubscriptions, List<string> removedSubscriptions)
|
||||
{
|
||||
_logger.Warning(exception, "Synchronizing subscriptions failed.");
|
||||
@@ -411,7 +416,7 @@ namespace MQTTnet.Extensions.ManagedClient
|
||||
{
|
||||
if (_isCleanDisconnect)
|
||||
{
|
||||
using (var disconnectTimeout = new CancellationTokenSource(Options.ClientOptions.Timeout))
|
||||
using (var disconnectTimeout = NewTimeoutToken(CancellationToken.None))
|
||||
{
|
||||
await InternalClient.DisconnectAsync(new MqttClientDisconnectOptions(), disconnectTimeout.Token).ConfigureAwait(false);
|
||||
}
|
||||
@@ -461,7 +466,7 @@ namespace MQTTnet.Extensions.ManagedClient
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
await TryPublishQueuedMessageAsync(message).ConfigureAwait(false);
|
||||
await TryPublishQueuedMessageAsync(message, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
@@ -477,7 +482,7 @@ namespace MQTTnet.Extensions.ManagedClient
|
||||
}
|
||||
}
|
||||
|
||||
async Task PublishReconnectSubscriptionsAsync()
|
||||
async Task PublishReconnectSubscriptionsAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.Info("Publishing subscriptions at reconnect");
|
||||
|
||||
@@ -489,20 +494,20 @@ namespace MQTTnet.Extensions.ManagedClient
|
||||
{
|
||||
topicFilters = new List<MqttTopicFilter>();
|
||||
SendSubscribeUnsubscribeResult subscribeUnsubscribeResult;
|
||||
|
||||
|
||||
foreach (var sub in _reconnectSubscriptions)
|
||||
{
|
||||
topicFilters.Add(sub.Value);
|
||||
|
||||
if (topicFilters.Count == Options.MaxTopicFiltersInSubscribeUnsubscribePackets)
|
||||
{
|
||||
subscribeUnsubscribeResult = await SendSubscribeUnsubscribe(topicFilters, null).ConfigureAwait(false);
|
||||
subscribeUnsubscribeResult = await SendSubscribeUnsubscribe(topicFilters, null, cancellationToken).ConfigureAwait(false);
|
||||
topicFilters.Clear();
|
||||
await HandleSubscriptionsResultAsync(subscribeUnsubscribeResult).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
subscribeUnsubscribeResult = await SendSubscribeUnsubscribe(topicFilters, null).ConfigureAwait(false);
|
||||
subscribeUnsubscribeResult = await SendSubscribeUnsubscribe(topicFilters, null, cancellationToken).ConfigureAwait(false);
|
||||
await HandleSubscriptionsResultAsync(subscribeUnsubscribeResult).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -555,13 +560,13 @@ namespace MQTTnet.Extensions.ManagedClient
|
||||
|
||||
if (addedTopicFilters.Count == Options.MaxTopicFiltersInSubscribeUnsubscribePackets)
|
||||
{
|
||||
subscribeUnsubscribeResult = await SendSubscribeUnsubscribe(addedTopicFilters, null).ConfigureAwait(false);
|
||||
subscribeUnsubscribeResult = await SendSubscribeUnsubscribe(addedTopicFilters, null, cancellationToken).ConfigureAwait(false);
|
||||
addedTopicFilters.Clear();
|
||||
await HandleSubscriptionsResultAsync(subscribeUnsubscribeResult).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
subscribeUnsubscribeResult = await SendSubscribeUnsubscribe(addedTopicFilters, null).ConfigureAwait(false);
|
||||
|
||||
subscribeUnsubscribeResult = await SendSubscribeUnsubscribe(addedTopicFilters, null, cancellationToken).ConfigureAwait(false);
|
||||
await HandleSubscriptionsResultAsync(subscribeUnsubscribeResult).ConfigureAwait(false);
|
||||
|
||||
var removedTopicFilters = new List<string>();
|
||||
@@ -571,13 +576,13 @@ namespace MQTTnet.Extensions.ManagedClient
|
||||
|
||||
if (removedTopicFilters.Count == Options.MaxTopicFiltersInSubscribeUnsubscribePackets)
|
||||
{
|
||||
subscribeUnsubscribeResult = await SendSubscribeUnsubscribe(null, removedTopicFilters).ConfigureAwait(false);
|
||||
subscribeUnsubscribeResult = await SendSubscribeUnsubscribe(null, removedTopicFilters, cancellationToken).ConfigureAwait(false);
|
||||
removedTopicFilters.Clear();
|
||||
await HandleSubscriptionsResultAsync(subscribeUnsubscribeResult).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
subscribeUnsubscribeResult = await SendSubscribeUnsubscribe(null, removedTopicFilters).ConfigureAwait(false);
|
||||
subscribeUnsubscribeResult = await SendSubscribeUnsubscribe(null, removedTopicFilters, cancellationToken).ConfigureAwait(false);
|
||||
await HandleSubscriptionsResultAsync(subscribeUnsubscribeResult).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -592,7 +597,7 @@ namespace MQTTnet.Extensions.ManagedClient
|
||||
MqttClientConnectResult connectResult = null;
|
||||
try
|
||||
{
|
||||
using (var connectTimeout = new CancellationTokenSource(Options.ClientOptions.Timeout))
|
||||
using (var connectTimeout = NewTimeoutToken(cancellationToken))
|
||||
{
|
||||
connectResult = await InternalClient.ConnectAsync(Options.ClientOptions, connectTimeout.Token).ConfigureAwait(false);
|
||||
}
|
||||
@@ -611,7 +616,7 @@ namespace MQTTnet.Extensions.ManagedClient
|
||||
}
|
||||
}
|
||||
|
||||
async Task<SendSubscribeUnsubscribeResult> SendSubscribeUnsubscribe(List<MqttTopicFilter> addedSubscriptions, List<string> removedSubscriptions)
|
||||
async Task<SendSubscribeUnsubscribeResult> SendSubscribeUnsubscribe(List<MqttTopicFilter> addedSubscriptions, List<string> removedSubscriptions, CancellationToken cancellationToken)
|
||||
{
|
||||
var subscribeResults = new List<MqttClientSubscribeResult>();
|
||||
var unsubscribeResults = new List<MqttClientUnsubscribeResult>();
|
||||
@@ -626,8 +631,11 @@ namespace MQTTnet.Extensions.ManagedClient
|
||||
unsubscribeOptionsBuilder.WithTopicFilter(removedSubscription);
|
||||
}
|
||||
|
||||
var unsubscribeResult = await InternalClient.UnsubscribeAsync(unsubscribeOptionsBuilder.Build()).ConfigureAwait(false);
|
||||
unsubscribeResults.Add(unsubscribeResult);
|
||||
using (var unsubscribeTimeout = NewTimeoutToken(cancellationToken))
|
||||
{
|
||||
var unsubscribeResult = await InternalClient.UnsubscribeAsync(unsubscribeOptionsBuilder.Build(), unsubscribeTimeout.Token).ConfigureAwait(false);
|
||||
unsubscribeResults.Add(unsubscribeResult);
|
||||
}
|
||||
|
||||
//clear because these worked, maybe the subscribe below will fail, only report those
|
||||
removedSubscriptions.Clear();
|
||||
@@ -642,8 +650,11 @@ namespace MQTTnet.Extensions.ManagedClient
|
||||
subscribeOptionsBuilder.WithTopicFilter(addedSubscription);
|
||||
}
|
||||
|
||||
var subscribeResult = await InternalClient.SubscribeAsync(subscribeOptionsBuilder.Build()).ConfigureAwait(false);
|
||||
subscribeResults.Add(subscribeResult);
|
||||
using (var subscribeTimeout = NewTimeoutToken(cancellationToken))
|
||||
{
|
||||
var subscribeResult = await InternalClient.SubscribeAsync(subscribeOptionsBuilder.Build(), subscribeTimeout.Token).ConfigureAwait(false);
|
||||
subscribeResults.Add(subscribeResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
@@ -697,7 +708,7 @@ namespace MQTTnet.Extensions.ManagedClient
|
||||
{
|
||||
var oldConnectionState = InternalClient.IsConnected;
|
||||
var connectionState = await ReconnectIfRequiredAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
|
||||
if (connectionState == ReconnectionResult.NotConnected)
|
||||
{
|
||||
StopPublishing();
|
||||
@@ -705,7 +716,7 @@ namespace MQTTnet.Extensions.ManagedClient
|
||||
}
|
||||
else if (connectionState == ReconnectionResult.Reconnected)
|
||||
{
|
||||
await PublishReconnectSubscriptionsAsync().ConfigureAwait(false);
|
||||
await PublishReconnectSubscriptionsAsync(cancellationToken).ConfigureAwait(false);
|
||||
StartPublishing();
|
||||
}
|
||||
else if (connectionState == ReconnectionResult.Recovered)
|
||||
@@ -735,7 +746,7 @@ namespace MQTTnet.Extensions.ManagedClient
|
||||
}
|
||||
}
|
||||
|
||||
async Task TryPublishQueuedMessageAsync(ManagedMqttApplicationMessage message)
|
||||
async Task TryPublishQueuedMessageAsync(ManagedMqttApplicationMessage message, CancellationToken cancellationToken)
|
||||
{
|
||||
Exception transmitException = null;
|
||||
bool acceptPublish = true;
|
||||
@@ -750,7 +761,10 @@ namespace MQTTnet.Extensions.ManagedClient
|
||||
|
||||
if (acceptPublish)
|
||||
{
|
||||
await InternalClient.PublishAsync(message.ApplicationMessage).ConfigureAwait(false);
|
||||
using (var publishTimeout = NewTimeoutToken(cancellationToken))
|
||||
{
|
||||
await InternalClient.PublishAsync(message.ApplicationMessage, publishTimeout.Token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
using (await _messageQueueLock.EnterAsync().ConfigureAwait(false)) //lock to avoid conflict with this.PublishAsync
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
<Description>This is an extension library which allows executing synchronous device calls including a response using MQTTnet.</Description>
|
||||
<Authors>The contributors of MQTTnet</Authors>
|
||||
<PackageId>MQTTnet.Extensions.Rpc</PackageId>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<SignAssembly>false</SignAssembly>
|
||||
<DelaySign>false</DelaySign>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
@@ -52,8 +51,10 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\..\Images\nuget.png" Pack="True" PackagePath="\" />
|
||||
<None Include="..\..\README.md" Pack="true" PackagePath="\" />
|
||||
<None Include="..\..\Images\nuget.png">
|
||||
<Pack>True</Pack>
|
||||
<PackagePath>\</PackagePath>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard1.3;netstandard2.0;netstandard2.1;netcoreapp3.1;net5.0;net6.0;net7.0</TargetFrameworks>
|
||||
<TargetFrameworks Condition=" '$(OS)' == 'Windows_NT' ">$(TargetFrameworks);net452;net461;net48</TargetFrameworks>
|
||||
<TargetFrameworks Condition=" '$(OS)' == 'Windows_NT' AND '$(MSBuildRuntimeType)' != 'Core' ">$(TargetFrameworks);uap10.0</TargetFrameworks>
|
||||
|
||||
<AssemblyName>MQTTnet.Extensions.TopicTemplate</AssemblyName>
|
||||
<RootNamespace>MQTTnet.Extensions.TopicTemplate</RootNamespace>
|
||||
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
|
||||
<Company>The contributors of MQTTnet</Company>
|
||||
<Product>MQTTnet</Product>
|
||||
<Description>
|
||||
Provides mqtt topic templating logic to support dispatch,
|
||||
routing and similar functionality based on the well known moustache syntax (AsyncAPI compatible).
|
||||
</Description>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<Authors>The contributors of MQTTnet</Authors>
|
||||
<PackageId>MQTTnet.Extensions.TopicTemplate</PackageId>
|
||||
<SignAssembly>false</SignAssembly>
|
||||
<DelaySign>false</DelaySign>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
<Copyright>Copyright (c) .NET Foundation and Contributors</Copyright>
|
||||
<PackageProjectUrl>https://github.com/dotnet/MQTTnet</PackageProjectUrl>
|
||||
<RepositoryUrl>https://github.com/dotnet/MQTTnet.git</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PackageTags>MQTT Message Queue Telemetry MQTTClient Messaging Routing AsyncAPI Template</PackageTags>
|
||||
<NeutralLanguage>en-US</NeutralLanguage>
|
||||
<PackageIcon>nuget.png</PackageIcon>
|
||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<PackageReleaseNotes>For release notes please go to MQTTnet release notes (https://www.nuget.org/packages/MQTTnet/).</PackageReleaseNotes>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<NoWarn>1591;NETSDK1138</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(TargetFramework)' == 'uap10.0'">
|
||||
<CopyLocalLockFileAssemblies>false</CopyLocalLockFileAssemblies>
|
||||
<NugetTargetMoniker>UAP,Version=v10.0</NugetTargetMoniker>
|
||||
<TargetPlatformIdentifier>UAP</TargetPlatformIdentifier>
|
||||
<TargetPlatformVersion>10.0.18362.0</TargetPlatformVersion>
|
||||
<TargetPlatformMinVersion>10.0.10240.0</TargetPlatformMinVersion>
|
||||
<TargetFrameworkIdentifier>.NETCore</TargetFrameworkIdentifier>
|
||||
<TargetFrameworkVersion>v5.0</TargetFrameworkVersion>
|
||||
<DefineConstants>$(DefineConstants);WINDOWS_UWP</DefineConstants>
|
||||
<DefaultLanguage>en</DefaultLanguage>
|
||||
<LanguageTargets>$(MSBuildExtensionsPath)\Microsoft\WindowsXaml\v$(VisualStudioVersion)\Microsoft.Windows.UI.Xaml.CSharp.targets</LanguageTargets>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="README.md">
|
||||
<Pack>True</Pack>
|
||||
<PackagePath>\</PackagePath>
|
||||
</None>
|
||||
<None Include="..\..\Images\nuget.png">
|
||||
<Pack>True</Pack>
|
||||
<PackagePath>\</PackagePath>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
|
||||
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MQTTnet\MQTTnet.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,357 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using MQTTnet.Protocol;
|
||||
|
||||
namespace MQTTnet.Extensions.TopicTemplate
|
||||
{
|
||||
/// <summary>
|
||||
/// A topic template is an MQTT topic filter string that may contain
|
||||
/// segments in curly braces called parameters. This well-known
|
||||
/// 'moustache' syntax also matches AsyncAPI Channel Address Expressions.
|
||||
/// The topic template is designed to support dynamic subscription/publication,
|
||||
/// message-topic matching and routing. It is intended to be more safe and
|
||||
/// convenient than String.Format() for aforementioned purposes.
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// topic/subtopic/{parameter}/{otherParameter}
|
||||
/// </example>
|
||||
public sealed class MqttTopicTemplate : IEquatable<MqttTopicTemplate>
|
||||
{
|
||||
static readonly Regex MoustacheRegex = new Regex("{([^/]+?)}", RegexOptions.Compiled);
|
||||
|
||||
readonly string[] _parameterSegments;
|
||||
|
||||
string _topicFilter;
|
||||
|
||||
/// <summary>
|
||||
/// Create a topic template from an mqtt topic filter with moustache placeholders.
|
||||
/// </summary>
|
||||
/// <param
|
||||
/// name="topicTemplate">
|
||||
/// </param>
|
||||
/// <exception
|
||||
/// cref="ArgumentNullException">
|
||||
/// </exception>
|
||||
public MqttTopicTemplate(string topicTemplate)
|
||||
{
|
||||
if (topicTemplate == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(topicTemplate));
|
||||
}
|
||||
|
||||
MqttTopicValidator.ThrowIfInvalidSubscribe(topicTemplate);
|
||||
|
||||
Template = topicTemplate;
|
||||
_parameterSegments = topicTemplate.Split(MqttTopicFilterComparer.LevelSeparator)
|
||||
.Select(segment => MoustacheRegex.Match(segment).Groups[1].Value)
|
||||
.Select(s => s.Length > 0 ? s : null)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Yield the template parameter names.
|
||||
/// </summary>
|
||||
public IEnumerable<string> Parameters => _parameterSegments.Where(s => s != null);
|
||||
|
||||
/// <summary>
|
||||
/// The topic template string representation, e.g. A/B/{foo}/D.
|
||||
/// </summary>
|
||||
public string Template { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The topic template as an MQTT topic filter (+ substituted for all parameters). If the template
|
||||
/// ends with a multi-level wildcard (hash), this will be reflected here.
|
||||
/// </summary>
|
||||
public string TopicFilter
|
||||
{
|
||||
get
|
||||
{
|
||||
LazyInitializer.EnsureInitialized(ref _topicFilter, () => MoustacheRegex.Replace(Template, MqttTopicFilterComparer.SingleLevelWildcard.ToString()));
|
||||
return _topicFilter;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return the topic filter of this template, ending with a multi-level wildcard (hash).
|
||||
/// </summary>
|
||||
public string TopicTreeRootFilter
|
||||
{
|
||||
get
|
||||
{
|
||||
var filter = TopicFilter;
|
||||
// append slash if neccessary
|
||||
if (filter.Length > 0 && !filter.EndsWith(MqttTopicFilterComparer.LevelSeparator.ToString()) &&
|
||||
!filter.EndsWith(MqttTopicFilterComparer.MultiLevelWildcard.ToString()))
|
||||
{
|
||||
filter += MqttTopicFilterComparer.LevelSeparator;
|
||||
}
|
||||
|
||||
// append hash if neccessary
|
||||
if (!filter.EndsWith(MqttTopicFilterComparer.MultiLevelWildcard.ToString()))
|
||||
{
|
||||
filter += MqttTopicFilterComparer.MultiLevelWildcard;
|
||||
}
|
||||
|
||||
return filter;
|
||||
}
|
||||
}
|
||||
|
||||
public bool Equals(MqttTopicTemplate other)
|
||||
{
|
||||
return other != null && Template == other.Template;
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (ReferenceEquals(null, obj))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ReferenceEquals(this, obj))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (obj.GetType() != GetType())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return Equals((MqttTopicTemplate)obj);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determine the shortest common prefix of the given templates. Partial segments
|
||||
/// are not returned.
|
||||
/// </summary>
|
||||
/// <param
|
||||
/// name="templates">
|
||||
/// topic templates
|
||||
/// </param>
|
||||
/// <returns></returns>
|
||||
public static MqttTopicTemplate FindCanonicalPrefix(IEnumerable<MqttTopicTemplate> templates)
|
||||
{
|
||||
string root = null;
|
||||
|
||||
string CommonPrefix(string a, string b)
|
||||
{
|
||||
var maxIndex = Math.Min(a.Length, b.Length) - 1;
|
||||
for (var i = 0; i <= maxIndex; i++)
|
||||
{
|
||||
if (a[i] != b[i])
|
||||
{
|
||||
return a.Substring(0, i);
|
||||
}
|
||||
}
|
||||
|
||||
return a.Substring(0, maxIndex+1);
|
||||
}
|
||||
|
||||
foreach (string topic in from template in templates select template.Template)
|
||||
{
|
||||
root = root == null ? topic : CommonPrefix(root, topic);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(root))
|
||||
return new MqttTopicTemplate(MqttTopicFilterComparer.MultiLevelWildcard.ToString());
|
||||
|
||||
if (root.Contains(MqttTopicFilterComparer.LevelSeparator) &&
|
||||
!root.EndsWith(MqttTopicFilterComparer.LevelSeparator.ToString()) &&
|
||||
!root.EndsWith("}"))
|
||||
{
|
||||
root = root.Substring(0, root.LastIndexOf(MqttTopicFilterComparer.LevelSeparator)+1);
|
||||
}
|
||||
|
||||
if (root.EndsWith(MqttTopicFilterComparer.LevelSeparator.ToString()))
|
||||
root += MqttTopicFilterComparer.SingleLevelWildcard;
|
||||
|
||||
return new MqttTopicTemplate(root);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return Template.GetHashCode();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test if this topic template matches a given topic.
|
||||
/// </summary>
|
||||
/// <param
|
||||
/// name="topic">
|
||||
/// a fully specified topic
|
||||
/// </param>
|
||||
/// <param
|
||||
/// name="subtree">
|
||||
/// true to match including the subtree (multi-level wildcard)
|
||||
/// </param>
|
||||
/// <returns>true iff the topic matches the template's filter</returns>
|
||||
/// <exception
|
||||
/// cref="InvalidOperationException">
|
||||
/// </exception>
|
||||
/// <exception
|
||||
/// cref="ArgumentException">
|
||||
/// if the topic is invalid
|
||||
/// </exception>
|
||||
public bool MatchesTopic(string topic, bool subtree = false)
|
||||
{
|
||||
var comparison = MqttTopicFilterComparer.Compare(topic, subtree ? TopicTreeRootFilter : TopicFilter);
|
||||
if (comparison == MqttTopicFilterCompareResult.FilterInvalid)
|
||||
{
|
||||
throw new InvalidOperationException("Invalid filter");
|
||||
}
|
||||
|
||||
if (comparison == MqttTopicFilterCompareResult.TopicInvalid)
|
||||
{
|
||||
throw new ArgumentException("Invalid topic", nameof(topic));
|
||||
}
|
||||
|
||||
return comparison == MqttTopicFilterCompareResult.IsMatch;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extract the parameter values from a topic corresponding to the template
|
||||
/// parameters. The topic has to match this template.
|
||||
/// </summary>
|
||||
/// <param
|
||||
/// name="topic">
|
||||
/// the topic
|
||||
/// </param>
|
||||
/// <returns>an enumeration of (parameter, index, value)</returns>
|
||||
public IEnumerable<(string parameter, int index, string value)> ParseParameterValues(string topic)
|
||||
{
|
||||
if (!MatchesTopic(topic))
|
||||
{
|
||||
throw new ArgumentException("the topic has to match this template", nameof(topic));
|
||||
}
|
||||
|
||||
return parseParameterValuesInternal(topic);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extract the parameter values from the message topic corresponding to the template
|
||||
/// parameters. The message topic has to match this topic template.
|
||||
/// </summary>
|
||||
/// <param
|
||||
/// name="message">
|
||||
/// the message
|
||||
/// </param>
|
||||
/// <returns>an enumeration of (parameter, index, value)</returns>
|
||||
public IEnumerable<(string parameter, int index, string value)> ParseParameterValues(MqttApplicationMessage message)
|
||||
{
|
||||
return ParseParameterValues(message.Topic);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to set a parameter to a given value. If the parameter is not present,
|
||||
/// this is returned. The value must not contain slashes.
|
||||
/// </summary>
|
||||
/// <param
|
||||
/// name="parameter">
|
||||
/// a template parameter
|
||||
/// </param>
|
||||
/// <param
|
||||
/// name="value">
|
||||
/// a string
|
||||
/// </param>
|
||||
/// <returns></returns>
|
||||
public MqttTopicTemplate TrySetParameter(string parameter, string value)
|
||||
{
|
||||
if (parameter != null && _parameterSegments.Contains(parameter))
|
||||
{
|
||||
return WithParameter(parameter, value);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replace the given parameter with a single-level wildcard (plus sign).
|
||||
/// </summary>
|
||||
/// <param
|
||||
/// name="parameter">
|
||||
/// parameter name
|
||||
/// </param>
|
||||
/// <returns>the topic template (without the parameter)</returns>
|
||||
public MqttTopicTemplate WithoutParameter(string parameter)
|
||||
{
|
||||
if (string.IsNullOrEmpty(parameter) || !_parameterSegments.Contains(parameter))
|
||||
{
|
||||
throw new ArgumentException("topic template parameter must exist.");
|
||||
}
|
||||
|
||||
return ReplaceInternal(parameter, MqttTopicFilterComparer.SingleLevelWildcard.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Substitute a parameter with a given value, thus removing the parameter. If the parameter is not present,
|
||||
/// the method trows. The value must not contain slashes or wildcards.
|
||||
/// </summary>
|
||||
/// <param
|
||||
/// name="parameter">
|
||||
/// a template parameter
|
||||
/// </param>
|
||||
/// <param
|
||||
/// name="value">
|
||||
/// a string
|
||||
/// </param>
|
||||
/// <exception
|
||||
/// cref="ArgumentException">
|
||||
/// when the parameter is not present
|
||||
/// </exception>
|
||||
/// <returns>the topic template (without the parameter)</returns>
|
||||
public MqttTopicTemplate WithParameter(string parameter, string value)
|
||||
{
|
||||
if (value == null || string.IsNullOrEmpty(parameter) || !_parameterSegments.Contains(parameter) ||
|
||||
value.Contains(MqttTopicFilterComparer.LevelSeparator) ||
|
||||
value.Contains(MqttTopicFilterComparer.SingleLevelWildcard) ||
|
||||
value.Contains(MqttTopicFilterComparer.MultiLevelWildcard))
|
||||
{
|
||||
throw new ArgumentException("parameter must exist and value must not contain slashes or wildcard.");
|
||||
}
|
||||
|
||||
return ReplaceInternal(parameter, value);
|
||||
}
|
||||
|
||||
private MqttTopicTemplate ReplaceInternal(string parameter, string value)
|
||||
{
|
||||
var moustache = "{" + parameter + "}";
|
||||
return new MqttTopicTemplate(Template.Replace(moustache, value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reuse parameters as they are extracted using another topic template on this template
|
||||
/// when the parameter name matches. Useful
|
||||
/// for compatibility routing.
|
||||
/// </summary>
|
||||
/// <param
|
||||
/// name="parameters">
|
||||
/// </param>
|
||||
/// <returns></returns>
|
||||
public MqttTopicTemplate WithParameterValuesFrom(IEnumerable<(string parameter, int index, string value)> parameters)
|
||||
{
|
||||
return parameters.Aggregate(this, (t, p) => t.TrySetParameter(p.parameter, p.value));
|
||||
}
|
||||
|
||||
IEnumerable<(string parameter, int index, string value)> parseParameterValuesInternal(string topic)
|
||||
{
|
||||
// because we have a match, we know the segment array is at least the template's length
|
||||
var segments = topic.Split(MqttTopicFilterComparer.LevelSeparator);
|
||||
for (var i = 0; i < _parameterSegments.Length; i++)
|
||||
{
|
||||
var name = _parameterSegments[i];
|
||||
if (name != null)
|
||||
{
|
||||
yield return (name, i, segments[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
# MQTTNet.Extensions.TopicTemplate
|
||||
|
||||
Provides mqtt topic templating logic to support dispatch, routing and
|
||||
similar functionality based on the well known moustache syntax, aka
|
||||
AsyncAPI dynamic channel address.
|
||||
|
||||
Generating and parsing MQTT topics and topic filters is often done in
|
||||
an ad-hoc fashion, leading to error-prone code and subtly introduced
|
||||
showstoppers like forgetting to strip a slash from a parameter. To
|
||||
remedy, this extension aids you write safe, maintainable logic
|
||||
for topic generation, routing and dispatch.
|
||||
|
||||
|
||||
## How it works
|
||||
|
||||
The package lets you write MQTTNet code like:
|
||||
```csharp
|
||||
var template = new MqttTopicTemplate("App/v1/{sender}/message");
|
||||
|
||||
var filter = new MqttTopicFilterBuilder()
|
||||
.WithTopicTemplate(template) // (1)
|
||||
.WithAtLeastOnceQoS()
|
||||
.WithNoLocal()
|
||||
.Build();
|
||||
|
||||
// filter.Topic == "App/v1/+/message"
|
||||
|
||||
var myTopic = template
|
||||
.WithParameter("sender", "me, myself & i"); // (2)
|
||||
|
||||
var message = new MqttApplicationMessageBuilder()
|
||||
.WithTopicTemplate( // (3)
|
||||
myTopic)
|
||||
.WithPayload("Hello!")
|
||||
.Build();
|
||||
|
||||
// message.Topic == "App/v1/me, myself & i/message"
|
||||
|
||||
Assert.IsTrue(message.MatchesTopicTemplate(template)); // (4)
|
||||
```
|
||||
|
||||
At (1), the parameter "sender" was automatically replaced by a plus sign
|
||||
(single level wildcard) because the caller is creating a topic filter.
|
||||
|
||||
Using the same template (they are immutable), you can send
|
||||
a message. At (2), we replace the parameter with a known value.
|
||||
If that value contained a level separator or topic filter only
|
||||
characters, we'd get an ArgumentException. If we forgot to replace
|
||||
the parameter "sender" (e.g. when placing template not myTopic),
|
||||
we would be prevented from sending at (3).
|
||||
|
||||
On the receiving side, a message can be matched to a template
|
||||
using code like at (4).
|
||||
|
||||
These daily tasks become much safer and easier to code than
|
||||
string.Format() and Split() allow for. More elaborate computation is supported
|
||||
too. You could extract the sender using the template or route
|
||||
messages based on topic content.
|
||||
|
||||
## Features
|
||||
|
||||
- Safe handling of topic parameters in moustache syntax
|
||||
- Simplifies topic generation and parsing logic
|
||||
- Match topics and extract the values of their parameters
|
||||
- Translate between topic templates to ease dynamic routing
|
||||
- Derive a canonical topic filter from a bunch of templates
|
||||
- Integrates with MQTTNet message, subscription and filter builders
|
||||
- Extension method friendly (sealed class with one public ctor)
|
||||
|
||||
## Additional documentation
|
||||
|
||||
- Some of the [unit tests](https://github.com/dotnet/MQTTnet/blob/master/Source/MQTTnet.Tests/Extensions/MqttTopicTemplate_Tests.cs) are instructive
|
||||
- Some examples feature the extension, e.g. TopicGenerator, Client_Subscribe_Samples
|
||||
|
||||
## Feedback
|
||||
|
||||
Report any issues with the main project on https://github.com/dotnet/MQTTnet/issues
|
||||
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using MQTTnet.Client;
|
||||
using MQTTnet.Packets;
|
||||
using MQTTnet.Protocol;
|
||||
|
||||
namespace MQTTnet.Extensions.TopicTemplate
|
||||
{
|
||||
public static class TopicTemplateExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Modify this message builder to respond to a given message. The
|
||||
/// message's response topic and correlation data are included
|
||||
/// in the message builder.
|
||||
/// </summary>
|
||||
/// <param
|
||||
/// name="builder">
|
||||
/// a message builder
|
||||
/// </param>
|
||||
/// <param
|
||||
/// name="message">
|
||||
/// a message with a response topic
|
||||
/// </param>
|
||||
/// <returns>a message builder</returns>
|
||||
/// <see
|
||||
/// cref="BuildResponse" />
|
||||
public static MqttApplicationMessageBuilder AsResponseTo(this MqttApplicationMessageBuilder builder, MqttApplicationMessage message)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(message.ResponseTopic))
|
||||
{
|
||||
throw new ArgumentException("message does not have a response topic");
|
||||
}
|
||||
|
||||
return builder.WithTopic(message.ResponseTopic).WithCorrelationData(message.CorrelationData);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the filter topic according to the template, with
|
||||
/// remaining template parameters substituted by single-level
|
||||
/// wildcard.
|
||||
/// </summary>
|
||||
/// <param
|
||||
/// name="topicTemplate">
|
||||
/// a topic template
|
||||
/// </param>
|
||||
/// <param
|
||||
/// name="subscribeTreeRoot">
|
||||
/// whether to subscribe to the whole topic tree
|
||||
/// </param>
|
||||
/// <returns>the modified topic filter</returns>
|
||||
public static MqttTopicFilterBuilder BuildFilter(this MqttTopicTemplate topicTemplate, bool subscribeTreeRoot = false)
|
||||
{
|
||||
return new MqttTopicFilterBuilder().WithTopicTemplate(topicTemplate, subscribeTreeRoot);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a message builder from this template. The template must not have
|
||||
/// remaining parameters.
|
||||
/// </summary>
|
||||
/// <param
|
||||
/// name="topicTemplate">
|
||||
/// a parameterless topic template
|
||||
/// </param>
|
||||
/// <returns>a new message builder</returns>
|
||||
/// <exception
|
||||
/// cref="ArgumentException">
|
||||
/// if the topic template has parameters
|
||||
/// </exception>
|
||||
public static MqttApplicationMessageBuilder BuildMessage(this MqttTopicTemplate topicTemplate)
|
||||
{
|
||||
return new MqttApplicationMessageBuilder().WithTopicTemplate(topicTemplate);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return a message builder to respond to this message. The
|
||||
/// message's response topic and correlation data are included
|
||||
/// in the response message builder.
|
||||
/// </summary>
|
||||
/// <param
|
||||
/// name="message">
|
||||
/// a message with a response topic
|
||||
/// </param>
|
||||
/// <returns>a message builder</returns>
|
||||
/// <see
|
||||
/// cref="AsResponseTo" />
|
||||
public static MqttApplicationMessageBuilder BuildResponse(this MqttApplicationMessage message)
|
||||
{
|
||||
return new MqttApplicationMessageBuilder().AsResponseTo(message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return whether the message matches the given topic template.
|
||||
/// </summary>
|
||||
/// <param
|
||||
/// name="message">
|
||||
/// a message
|
||||
/// </param>
|
||||
/// <param
|
||||
/// name="topicTemplate">
|
||||
/// a topic template
|
||||
/// </param>
|
||||
/// <param
|
||||
/// name="subtree">
|
||||
/// whether to include the topic subtree
|
||||
/// </param>
|
||||
/// <returns></returns>
|
||||
public static bool MatchesTopicTemplate(this MqttApplicationMessage message, MqttTopicTemplate topicTemplate, bool subtree = false)
|
||||
{
|
||||
return topicTemplate.MatchesTopic(message.Topic, subtree);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the filter topic according to the template, with
|
||||
/// template parameters substituted by a single-level
|
||||
/// wildcard.
|
||||
/// </summary>
|
||||
/// <param
|
||||
/// name="builder">
|
||||
/// a filter builder
|
||||
/// </param>
|
||||
/// <param
|
||||
/// name="topicTemplate">
|
||||
/// a topic template
|
||||
/// </param>
|
||||
/// <param
|
||||
/// name="subscribeTreeRoot">
|
||||
/// whether to subscribe to the whole topic tree
|
||||
/// </param>
|
||||
/// <returns>the modified topic filter</returns>
|
||||
public static MqttTopicFilterBuilder WithTopicTemplate(this MqttTopicFilterBuilder builder, MqttTopicTemplate topicTemplate, bool subscribeTreeRoot = false)
|
||||
{
|
||||
return builder.WithTopic(subscribeTreeRoot ? topicTemplate.TopicTreeRootFilter : topicTemplate.TopicFilter);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the subscription to the template's topic filter.
|
||||
/// </summary>
|
||||
/// <returns>the builder</returns>
|
||||
public static MqttClientSubscribeOptionsBuilder WithTopicTemplate(
|
||||
this MqttClientSubscribeOptionsBuilder builder,
|
||||
MqttTopicTemplate topicTemplate,
|
||||
MqttQualityOfServiceLevel qualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce,
|
||||
bool noLocal = false,
|
||||
bool retainAsPublished = false,
|
||||
MqttRetainHandling retainHandling = MqttRetainHandling.SendAtSubscribe)
|
||||
{
|
||||
return builder.WithTopicFilter(
|
||||
new MqttTopicFilter
|
||||
{
|
||||
Topic = topicTemplate.TopicFilter,
|
||||
QualityOfServiceLevel = qualityOfServiceLevel,
|
||||
NoLocal = noLocal,
|
||||
RetainAsPublished = retainAsPublished,
|
||||
RetainHandling = retainHandling
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the publication topic according to the topic template. The template
|
||||
/// must not have remaining (unset) parameters or contain wildcards.
|
||||
/// </summary>
|
||||
/// <param
|
||||
/// name="builder">
|
||||
/// a message builder
|
||||
/// </param>
|
||||
/// <param
|
||||
/// name="topicTemplate">
|
||||
/// a parameterless topic template
|
||||
/// </param>
|
||||
/// <returns>the modified message builder</returns>
|
||||
/// <exception
|
||||
/// cref="ArgumentException">
|
||||
/// if the topic template has parameters
|
||||
/// </exception>
|
||||
public static MqttApplicationMessageBuilder WithTopicTemplate(this MqttApplicationMessageBuilder builder, MqttTopicTemplate topicTemplate)
|
||||
{
|
||||
if (topicTemplate.Parameters.Any())
|
||||
{
|
||||
throw new ArgumentException("topic templates must be parameter-less when sending " + topicTemplate.Template);
|
||||
}
|
||||
|
||||
MqttTopicValidator.ThrowIfInvalid(topicTemplate.Template);
|
||||
return builder.WithTopic(topicTemplate.Template);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,6 @@
|
||||
<Description>This is an extension library which allows using _WebSocket4Net_ as transport for MQTTnet clients.</Description>
|
||||
<Authors>The contributors of MQTTnet</Authors>
|
||||
<PackageId>MQTTnet.Extensions.WebSocket4Net</PackageId>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<SignAssembly>false</SignAssembly>
|
||||
<DelaySign>false</DelaySign>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
@@ -52,8 +51,10 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\..\Images\nuget.png" Pack="True" PackagePath="\" />
|
||||
<None Include="..\..\README.md" Pack="true" PackagePath="\" />
|
||||
<None Include="..\..\Images\nuget.png">
|
||||
<Pack>True</Pack>
|
||||
<PackagePath>\</PackagePath>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
<ProjectReference Include="..\..\Source\MQTTnet.Extensions.ManagedClient\MQTTnet.Extensions.ManagedClient.csproj" />
|
||||
<ProjectReference Include="..\..\Source\MQTTnet.Extensions.WebSocket4Net\MQTTnet.Extensions.WebSocket4Net.csproj" />
|
||||
<ProjectReference Include="..\..\Source\MQTTnet\MQTTnet.csproj" />
|
||||
<ProjectReference Include="..\MQTTnet.Extensions.TopicTemplate\MQTTnet.Extensions.TopicTemplate.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using MQTTnet.Extensions.TopicTemplate;
|
||||
|
||||
namespace MQTTnet.TestApp
|
||||
{
|
||||
@@ -17,6 +17,8 @@ namespace MQTTnet.TestApp
|
||||
singleWildcardTopicsByPublisher = new Dictionary<string, List<string>>();
|
||||
multiWildcardTopicsByPublisher = new Dictionary<string, List<string>>();
|
||||
|
||||
MqttTopicTemplate baseTemplate = new MqttTopicTemplate("{publisher}/{building}/{level}/{sensor}");
|
||||
|
||||
// Find some reasonable distribution across three topic levels
|
||||
var topicsPerLevel = (int)Math.Pow(numTopicsPerPublisher, (1.0 / 3.0));
|
||||
|
||||
@@ -40,25 +42,37 @@ namespace MQTTnet.TestApp
|
||||
int publisherTopicCount = 0;
|
||||
|
||||
var publisherName = "pub" + p;
|
||||
var publisherTemplate = baseTemplate
|
||||
.WithParameter("publisher", publisherName);
|
||||
for (var l1 = 0; l1 < numLevel1Topics; ++l1)
|
||||
{
|
||||
var l1Template = publisherTemplate
|
||||
.WithParameter("building", "building" + (l1 + 1));
|
||||
for (var l2 = 0; l2 < numLevel2Topics; ++l2)
|
||||
{
|
||||
var l2Template = l1Template
|
||||
.WithParameter("level", "level" + (l2 + 1));
|
||||
for (var l3 = 0; l3 < maxNumLevel3Topics; ++l3)
|
||||
{
|
||||
if (publisherTopicCount >= numTopicsPerPublisher)
|
||||
break;
|
||||
|
||||
var topic = string.Format("{0}/building{1}/level{2}/sensor{3}", publisherName, l1 + 1, l2 + 1, l3 + 1);
|
||||
var topic = l2Template
|
||||
.WithParameter("sensor", "sensor" + (l3 + 1))
|
||||
.TopicFilter;
|
||||
AddPublisherTopic(publisherName, topic, topicsByPublisher);
|
||||
|
||||
if (l2 == 0)
|
||||
{
|
||||
var singleWildcardTopic = string.Format("{0}/building{1}/+/sensor{2}", publisherName, l1 + 1, l3 + 1);
|
||||
var singleWildcardTopic = l1Template
|
||||
.WithParameter("sensor", "sensor" + (l3 + 1))
|
||||
.TopicFilter;
|
||||
AddPublisherTopic(publisherName, singleWildcardTopic, singleWildcardTopicsByPublisher);
|
||||
if (l1 == 0)
|
||||
{
|
||||
var multiWildcardTopic = string.Format("{0}/+/level{1}/+", publisherName, l3 + 1);
|
||||
var multiWildcardTopic = publisherTemplate
|
||||
.WithParameter("sensor", "sensor" + (l3 + 1))
|
||||
.TopicFilter;
|
||||
AddPublisherTopic(publisherName, multiWildcardTopic, multiWildcardTopicsByPublisher);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ using MQTTnet.Diagnostics;
|
||||
using MQTTnet.Extensions.ManagedClient;
|
||||
using MQTTnet.Formatter;
|
||||
using MQTTnet.Internal;
|
||||
using MQTTnet.Packets;
|
||||
using MQTTnet.Protocol;
|
||||
using MQTTnet.Server;
|
||||
using MQTTnet.Tests.Mockups;
|
||||
@@ -574,16 +575,154 @@ namespace MQTTnet.Tests.Clients.ManagedMqttClient
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task Subscribe_Does_Not_Hang_On_Server_Stop()
|
||||
{
|
||||
var timeout = TimeSpan.FromSeconds(2);
|
||||
var testTimeout = TimeSpan.FromSeconds(timeout.TotalSeconds * 2);
|
||||
const string topic = "test_topic_2";
|
||||
using (var testEnvironment = CreateTestEnvironment())
|
||||
using (var managedClient = await CreateManagedClientAsync(testEnvironment, timeout: timeout))
|
||||
{
|
||||
testEnvironment.IgnoreClientLogErrors = true;
|
||||
bool reject = true;
|
||||
var receivedOnServer = new SemaphoreSlim(0, 1);
|
||||
var failedOnClient = new SemaphoreSlim(0, 1);
|
||||
testEnvironment.Server.InterceptingInboundPacketAsync += e =>
|
||||
{
|
||||
if (e.Packet is MqttSubscribePacket)
|
||||
{
|
||||
if (reject)
|
||||
{
|
||||
e.ProcessPacket = false;
|
||||
}
|
||||
receivedOnServer.Release();
|
||||
}
|
||||
|
||||
return CompletedTask.Instance;
|
||||
};
|
||||
managedClient.SynchronizingSubscriptionsFailedAsync += e =>
|
||||
{
|
||||
failedOnClient.Release();
|
||||
return CompletedTask.Instance;
|
||||
};
|
||||
|
||||
await managedClient.SubscribeAsync(topic);
|
||||
Assert.IsTrue(await receivedOnServer.WaitAsync(testTimeout));
|
||||
Assert.IsTrue(await failedOnClient.WaitAsync(testTimeout));
|
||||
|
||||
reject = false;
|
||||
await managedClient.SubscribeAsync(topic);
|
||||
Assert.IsTrue(await receivedOnServer.WaitAsync(testTimeout));
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task Unsubscribe_Does_Not_Hang_On_Server_Stop()
|
||||
{
|
||||
var timeout = TimeSpan.FromSeconds(2);
|
||||
var testTimeout = TimeSpan.FromSeconds(timeout.TotalSeconds * 2);
|
||||
const string topic = "test_topic_2";
|
||||
using (var testEnvironment = CreateTestEnvironment())
|
||||
using (var managedClient = await CreateManagedClientAsync(testEnvironment, timeout: timeout))
|
||||
{
|
||||
testEnvironment.IgnoreClientLogErrors = true;
|
||||
bool reject = true;
|
||||
var receivedOnServer = new SemaphoreSlim(0, 1);
|
||||
var failedOnClient = new SemaphoreSlim(0, 1);
|
||||
testEnvironment.Server.InterceptingInboundPacketAsync += e =>
|
||||
{
|
||||
if (e.Packet is MqttUnsubscribePacket)
|
||||
{
|
||||
if (reject)
|
||||
{
|
||||
e.ProcessPacket = false;
|
||||
}
|
||||
receivedOnServer.Release();
|
||||
}
|
||||
else if (e.Packet is MqttSubscribePacket)
|
||||
{
|
||||
receivedOnServer.Release();
|
||||
}
|
||||
|
||||
return CompletedTask.Instance;
|
||||
};
|
||||
managedClient.SynchronizingSubscriptionsFailedAsync += e =>
|
||||
{
|
||||
failedOnClient.Release();
|
||||
return CompletedTask.Instance;
|
||||
};
|
||||
|
||||
await managedClient.SubscribeAsync(topic);
|
||||
Assert.IsTrue(await receivedOnServer.WaitAsync(testTimeout));
|
||||
|
||||
await managedClient.UnsubscribeAsync(topic);
|
||||
Assert.IsTrue(await receivedOnServer.WaitAsync(testTimeout));
|
||||
Assert.IsTrue(await failedOnClient.WaitAsync(testTimeout));
|
||||
|
||||
reject = false;
|
||||
await managedClient.UnsubscribeAsync(topic);
|
||||
Assert.IsTrue(await receivedOnServer.WaitAsync(testTimeout));
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task Publish_Does_Not_Hang_On_Server_Error()
|
||||
{
|
||||
var timeout = TimeSpan.FromSeconds(2);
|
||||
var testTimeout = TimeSpan.FromSeconds(timeout.TotalSeconds * 2);
|
||||
|
||||
const string topic = "test_topic_42";
|
||||
|
||||
using (var testEnvironment = CreateTestEnvironment())
|
||||
using (var managedClient = await CreateManagedClientAsync(testEnvironment, timeout: timeout))
|
||||
{
|
||||
testEnvironment.IgnoreClientLogErrors = true;
|
||||
bool reject = true;
|
||||
var receivedOnServer = new TaskCompletionSource<object>();
|
||||
managedClient.ApplicationMessageProcessedAsync += e => Task.FromResult(reject &= e.Exception is null);
|
||||
testEnvironment.Server.InterceptingInboundPacketAsync += e =>
|
||||
{
|
||||
if (e.Packet is MqttPublishPacket)
|
||||
{
|
||||
if (reject)
|
||||
{
|
||||
e.ProcessPacket = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
receivedOnServer.TrySetResult(null);
|
||||
}
|
||||
}
|
||||
|
||||
return CompletedTask.Instance;
|
||||
};
|
||||
|
||||
await managedClient.EnqueueAsync(new MqttApplicationMessage { Topic = topic, PayloadSegment = new ArraySegment<byte>(new byte[] { 1 }), Retain = true, QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce });
|
||||
|
||||
var timeoutTask = Task.Delay(testTimeout);
|
||||
|
||||
var firstDone = await Task.WhenAny(receivedOnServer.Task, timeoutTask);
|
||||
Assert.AreEqual(receivedOnServer.Task, firstDone, "Client is hung on publish!");
|
||||
}
|
||||
}
|
||||
|
||||
async Task<MQTTnet.Extensions.ManagedClient.ManagedMqttClient> CreateManagedClientAsync(
|
||||
TestEnvironment testEnvironment,
|
||||
IMqttClient underlyingClient = null,
|
||||
TimeSpan? connectionCheckInterval = null,
|
||||
string host = "localhost")
|
||||
string host = "localhost",
|
||||
TimeSpan? timeout = null)
|
||||
{
|
||||
await testEnvironment.StartServer();
|
||||
|
||||
var clientOptions = new MqttClientOptionsBuilder().WithTcpServer(host, testEnvironment.ServerPort);
|
||||
|
||||
if (timeout != null)
|
||||
{
|
||||
clientOptions.WithTimeout(timeout.Value);
|
||||
}
|
||||
|
||||
var managedOptions = new ManagedMqttClientOptionsBuilder().WithClientOptions(clientOptions).Build();
|
||||
|
||||
// Use a short connection check interval so that subscription operations are performed quickly
|
||||
|
||||
@@ -6,6 +6,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
@@ -86,7 +87,7 @@ namespace MQTTnet.Tests.Clients.MqttClient
|
||||
await client.ConnectAsync(clientOptions);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(InvalidOperationException))]
|
||||
public async Task Connect_Multiple_Times_Should_Fail()
|
||||
@@ -748,8 +749,8 @@ namespace MQTTnet.Tests.Clients.MqttClient
|
||||
{
|
||||
if (e.ApplicationMessage.Topic == "request")
|
||||
{
|
||||
// Use AtMostOnce here because with QoS 1 or even QoS 2 the process waits for
|
||||
// the ACK etc. The problem is that the SpinUntil below only waits until the
|
||||
// Use AtMostOnce here because with QoS 1 or even QoS 2 the process waits for
|
||||
// the ACK etc. The problem is that the SpinUntil below only waits until the
|
||||
// flag is set. It does not wait until the client has sent the ACK
|
||||
await client2.PublishStringAsync("reply");
|
||||
}
|
||||
@@ -925,5 +926,54 @@ namespace MQTTnet.Tests.Clients.MqttClient
|
||||
Assert.IsFalse(disconnectedFired);
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Backward_compatible_TCP_options()
|
||||
{
|
||||
var options = new MqttClientOptionsBuilder().WithTcpServer("host", 3).Build();
|
||||
|
||||
Assert.AreEqual("host", ((MqttClientTcpOptions)options.ChannelOptions).Server);
|
||||
Assert.AreEqual(3, ((MqttClientTcpOptions)options.ChannelOptions).Port);
|
||||
|
||||
options = new MqttClientOptions
|
||||
{
|
||||
ChannelOptions = new MqttClientTcpOptions
|
||||
{
|
||||
Server = "host",
|
||||
Port = 3
|
||||
}
|
||||
};
|
||||
|
||||
Assert.AreEqual("host:3", options.ChannelOptions.ToString());
|
||||
Assert.AreEqual("host", ((MqttClientTcpOptions)options.ChannelOptions).Server);
|
||||
Assert.AreEqual(3, ((MqttClientTcpOptions)options.ChannelOptions).Port);
|
||||
|
||||
options = new MqttClientOptionsBuilder().WithEndPoint(new DnsEndPoint("host", 3)).Build();
|
||||
Assert.AreEqual("Unspecified/host:3", options.ChannelOptions.ToString());
|
||||
Assert.AreEqual("host", ((MqttClientTcpOptions)options.ChannelOptions).Server);
|
||||
Assert.AreEqual(3, ((MqttClientTcpOptions)options.ChannelOptions).Port);
|
||||
|
||||
options = new MqttClientOptionsBuilder().WithTcpServer("host").Build();
|
||||
|
||||
Assert.AreEqual("host", ((MqttClientTcpOptions)options.ChannelOptions).Server);
|
||||
Assert.AreEqual(1883, ((MqttClientTcpOptions)options.ChannelOptions).Port);
|
||||
Assert.AreEqual("Unspecified/host:1883", options.ChannelOptions.ToString());
|
||||
|
||||
options = new MqttClientOptionsBuilder().WithTlsOptions(o => o.UseTls()).WithTcpServer("host").Build();
|
||||
|
||||
Assert.AreEqual("host", ((MqttClientTcpOptions)options.ChannelOptions).Server);
|
||||
Assert.AreEqual(8883, ((MqttClientTcpOptions)options.ChannelOptions).Port);
|
||||
|
||||
options = new MqttClientOptions
|
||||
{
|
||||
ChannelOptions = new MqttClientTcpOptions
|
||||
{
|
||||
Server = "host"
|
||||
}
|
||||
};
|
||||
|
||||
Assert.AreEqual("host", ((MqttClientTcpOptions)options.ChannelOptions).Server);
|
||||
Assert.AreEqual(null, ((MqttClientTcpOptions)options.ChannelOptions).Port);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using MQTTnet.Exceptions;
|
||||
using MQTTnet.Extensions.TopicTemplate;
|
||||
|
||||
namespace MQTTnet.Tests.Extensions
|
||||
{
|
||||
[TestClass]
|
||||
public sealed class MqttTopicTemplate_Tests
|
||||
{
|
||||
[TestMethod]
|
||||
public void BasicOps()
|
||||
{
|
||||
var template = new MqttTopicTemplate("A/B/{param}/D");
|
||||
Assert.AreEqual("A/B/C/D", template.WithParameter("param", "C").Template);
|
||||
Assert.AreEqual("A/B/+/D", template.WithoutParameter("param").Template);
|
||||
Assert.AreEqual("A/B/+/D", template.TopicFilter);
|
||||
Assert.AreEqual("A/B/+/D/#", template.TopicTreeRootFilter);
|
||||
|
||||
// corner cases
|
||||
Assert.AreEqual("#", new MqttTopicTemplate("#").TopicFilter);
|
||||
Assert.AreEqual("+", new MqttTopicTemplate("+").TopicFilter);
|
||||
Assert.AreEqual("/", new MqttTopicTemplate("/").TopicFilter);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void BasicOpsWithHash()
|
||||
{
|
||||
var template = new MqttTopicTemplate("A/B/{param}/D/#");
|
||||
Assert.AreEqual("A/B/C/D/#", template.WithParameter("param", "C").Template);
|
||||
Assert.AreEqual("A/B/+/D/#", template.WithoutParameter("param").Template);
|
||||
Assert.AreEqual("A/B/+/D/#", template.TopicFilter);
|
||||
Assert.AreEqual("A/B/+/D/#", template.TopicTreeRootFilter);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void RejectsReservedChars1()
|
||||
{
|
||||
var template = new MqttTopicTemplate("A/B/{foo}/D");
|
||||
template.WithParameter("foo", "a#");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void RejectsReservedChars2()
|
||||
{
|
||||
var template = new MqttTopicTemplate("A/B/{foo}/D");
|
||||
template.WithParameter("foo", "a+b");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void RejectsReservedChars3()
|
||||
{
|
||||
var template = new MqttTopicTemplate("A/B/{foo}/D");
|
||||
template.WithParameter("foo", "a/b");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void AcceptsEmptyValue()
|
||||
{
|
||||
var template = new MqttTopicTemplate("A/B/{foo}/D");
|
||||
template.WithParameter("foo", "");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(MqttProtocolViolationException))]
|
||||
public void RejectsEmptyTemplate()
|
||||
{
|
||||
var _ = new MqttTopicTemplate("");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public void RejectsNullTemplate()
|
||||
{
|
||||
var _ = new MqttTopicTemplate(null);
|
||||
}
|
||||
|
||||
|
||||
[TestMethod]
|
||||
public void IgnoresEmptyParameters()
|
||||
{
|
||||
var template = new MqttTopicTemplate("A/B/{}/D");
|
||||
Assert.IsFalse(template.Parameters.Any());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void AcceptsValidTopics()
|
||||
{
|
||||
_ = new MqttTopicTemplate("A");
|
||||
_ = new MqttTopicTemplate("/");
|
||||
_ = new MqttTopicTemplate("/A");
|
||||
_ = new MqttTopicTemplate("A/#");
|
||||
_ = new MqttTopicTemplate("$SYS/#");
|
||||
_ = new MqttTopicTemplate("////");
|
||||
_ = new MqttTopicTemplate("A/B/{foo}/D/");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void DynamicRoutingSupport()
|
||||
{
|
||||
var v1template = new MqttTopicTemplate("A/v1/{id}/F");
|
||||
var v2template = new MqttTopicTemplate("A/v2/foo/{id}/{bar}");
|
||||
var incoming_v1_topic = "A/v1/32/F";
|
||||
|
||||
var compat_v2_topic = v2template
|
||||
.WithParameterValuesFrom(
|
||||
v1template.ParseParameterValues(incoming_v1_topic))
|
||||
.WithParameter("bar", "unknown");
|
||||
|
||||
// id: 32 was relocated and bar: unknown added
|
||||
Assert.AreEqual("A/v2/foo/32/unknown", compat_v2_topic.Template);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SubscriptionSupport()
|
||||
{
|
||||
var template = new MqttTopicTemplate("A/v1/{param}/F");
|
||||
var filter = template.BuildFilter()
|
||||
.WithAtLeastOnceQoS()
|
||||
.WithNoLocal()
|
||||
.Build();
|
||||
Assert.AreEqual("A/v1/+/F", filter.Topic);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SubscriptionSupport2()
|
||||
{
|
||||
var template = new MqttTopicTemplate("A/v1/{param}/F");
|
||||
|
||||
var subscribeOptions = new MqttFactory().CreateSubscribeOptionsBuilder()
|
||||
.WithTopicTemplate(template)
|
||||
.WithSubscriptionIdentifier(5)
|
||||
.Build();
|
||||
Assert.AreEqual("A/v1/+/F", subscribeOptions.TopicFilters[0].Topic);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SendAndSubscribeSupport()
|
||||
{
|
||||
var template = new MqttTopicTemplate("App/v1/{sender}/message");
|
||||
|
||||
var filter = new MqttTopicFilterBuilder()
|
||||
.WithTopicTemplate(template)
|
||||
.WithAtLeastOnceQoS()
|
||||
.WithNoLocal()
|
||||
.Build();
|
||||
|
||||
Assert.AreEqual("App/v1/+/message", filter.Topic);
|
||||
|
||||
var myTopic = template.WithParameter("sender", "me, myself & i");
|
||||
|
||||
var message = new MqttApplicationMessageBuilder()
|
||||
.WithTopicTemplate(
|
||||
myTopic)
|
||||
.WithPayload("Hello!")
|
||||
.Build();
|
||||
|
||||
Assert.IsTrue(message.MatchesTopicTemplate(template));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SendAndSubscribeSupport2()
|
||||
{
|
||||
var template = new MqttTopicTemplate("App/v1/{sender}/message");
|
||||
Assert.ThrowsException<ArgumentException>(() =>
|
||||
template.BuildMessage());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CanonicalPrefixFilter()
|
||||
{
|
||||
var template1 = new MqttTopicTemplate("A/v1/{param}/F");
|
||||
var template2 = new MqttTopicTemplate("A/v1/E/F");
|
||||
var template3 = new MqttTopicTemplate("A/v1/E/F/G/H");
|
||||
var canonicalFilter = MqttTopicTemplate.FindCanonicalPrefix(new[] { template1, template2, template3 });
|
||||
// possible improvement: Assert.AreEqual("A/v1/+/F", canonicalFilter.TopicFilter);
|
||||
Assert.AreEqual("A/v1/+", canonicalFilter.TopicFilter);
|
||||
Assert.AreEqual("A/v1/+/#", canonicalFilter.TopicTreeRootFilter);
|
||||
|
||||
var template2b = new MqttTopicTemplate("A/v1/E/X");
|
||||
canonicalFilter = MqttTopicTemplate.FindCanonicalPrefix(new[] { template1, template2, template3, template2b });
|
||||
Assert.AreEqual("A/v1/+", canonicalFilter.Template);
|
||||
Assert.AreEqual("A/v1/+", canonicalFilter.TopicFilter);
|
||||
Assert.AreEqual("A/v1/+/#", canonicalFilter.TopicTreeRootFilter);
|
||||
|
||||
|
||||
var template4 = new MqttTopicTemplate("A/v2/{prefix}");
|
||||
var canonicalFilter2 = MqttTopicTemplate.FindCanonicalPrefix(new[] { template1, template2, template3, template4 });
|
||||
Assert.AreEqual("A/+", canonicalFilter2.TopicFilter);
|
||||
Assert.AreEqual("A/+/#", canonicalFilter2.TopicTreeRootFilter);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CanonicalPrefixFilterSimple1()
|
||||
{
|
||||
var template = MqttTopicTemplate.FindCanonicalPrefix(new[]
|
||||
{
|
||||
new MqttTopicTemplate("A"),
|
||||
new MqttTopicTemplate("B")
|
||||
});
|
||||
Assert.AreEqual("#", template.TopicFilter);
|
||||
Assert.AreEqual("#", template.Template);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CanonicalPrefixFilterSimple2()
|
||||
{
|
||||
var template = MqttTopicTemplate.FindCanonicalPrefix(new[]
|
||||
{
|
||||
new MqttTopicTemplate("A/v1/{param}/F/"),
|
||||
new MqttTopicTemplate("A/v1/E/X")
|
||||
});
|
||||
Assert.AreEqual("A/v1/+", template.TopicFilter);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CanonicalPrefixFilterSimple3()
|
||||
{
|
||||
var template = MqttTopicTemplate.FindCanonicalPrefix(new[]
|
||||
{
|
||||
new MqttTopicTemplate("A/v1/{param}/F"),
|
||||
new MqttTopicTemplate("A/v1/{param}/F/X")
|
||||
});
|
||||
Assert.AreEqual("A/v1/+/+", template.TopicFilter);
|
||||
Assert.AreEqual("A/v1/{param}/+", template.Template);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CanonicalPrefixFilterSimple4()
|
||||
{
|
||||
var template = MqttTopicTemplate.FindCanonicalPrefix(new[]
|
||||
{
|
||||
new MqttTopicTemplate("A/v1/{param}/FFA"),
|
||||
new MqttTopicTemplate("A/v1/{param}/FX")
|
||||
});
|
||||
Assert.AreEqual("A/v1/+/+", template.TopicFilter);
|
||||
Assert.AreEqual("A/v1/{param}/+", template.Template);
|
||||
}
|
||||
|
||||
|
||||
[TestMethod]
|
||||
public void CanonicalPrefixFilterSimple5()
|
||||
{
|
||||
var template = MqttTopicTemplate.FindCanonicalPrefix(new[]
|
||||
{
|
||||
new MqttTopicTemplate("A/v1/E/F/G/H"),
|
||||
new MqttTopicTemplate("A/v2/{param}")
|
||||
});
|
||||
Assert.AreEqual("A/+", template.TopicFilter);
|
||||
Assert.AreEqual("A/+/#", template.TopicTreeRootFilter);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CanonicalPrefixFilterSimple6()
|
||||
{
|
||||
var template = MqttTopicTemplate.FindCanonicalPrefix(new[]
|
||||
{
|
||||
new MqttTopicTemplate("A/{version}/E/F/G/H"),
|
||||
new MqttTopicTemplate("A/{version}/{param}")
|
||||
});
|
||||
Assert.AreEqual("A/+/+", template.TopicFilter);
|
||||
Assert.AreEqual("A/+/+/#", template.TopicTreeRootFilter);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -17,7 +18,7 @@ namespace MQTTnet.Tests.Internal
|
||||
[TestMethod]
|
||||
public async Task Connect_Send_Receive()
|
||||
{
|
||||
var crossPlatformSocket = new CrossPlatformSocket();
|
||||
var crossPlatformSocket = new CrossPlatformSocket(ProtocolType.Tcp);
|
||||
await crossPlatformSocket.ConnectAsync("www.google.de", 80, CancellationToken.None);
|
||||
|
||||
var requestBuffer = Encoding.UTF8.GetBytes("GET / HTTP/1.1\r\nHost: www.google.de\r\n\r\n");
|
||||
@@ -36,7 +37,7 @@ namespace MQTTnet.Tests.Internal
|
||||
[ExpectedException(typeof(OperationCanceledException))]
|
||||
public async Task Try_Connect_Invalid_Host()
|
||||
{
|
||||
var crossPlatformSocket = new CrossPlatformSocket();
|
||||
var crossPlatformSocket = new CrossPlatformSocket(ProtocolType.Tcp);
|
||||
|
||||
var cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
||||
cancellationToken.Token.Register(() => crossPlatformSocket.Dispose());
|
||||
@@ -65,7 +66,7 @@ namespace MQTTnet.Tests.Internal
|
||||
[TestMethod]
|
||||
public void Set_Options()
|
||||
{
|
||||
var crossPlatformSocket = new CrossPlatformSocket();
|
||||
var crossPlatformSocket = new CrossPlatformSocket(ProtocolType.Tcp);
|
||||
|
||||
Assert.IsFalse(crossPlatformSocket.ReuseAddress);
|
||||
crossPlatformSocket.ReuseAddress = true;
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
<ProjectReference Include="..\..\Source\MQTTnet.Extensions.ManagedClient\MQTTnet.Extensions.ManagedClient.csproj" />
|
||||
<ProjectReference Include="..\..\Source\MQTTnet.Extensions.Rpc\MQTTnet.Extensions.Rpc.csproj" />
|
||||
<ProjectReference Include="..\..\Source\MQTTnet\MQTTnet.csproj" />
|
||||
<ProjectReference Include="..\MQTTnet.Extensions.TopicTemplate\MQTTnet.Extensions.TopicTemplate.csproj" />
|
||||
<ProjectReference Include="..\MQTTnet.Extensions.WebSocket4Net\MQTTnet.Extensions.WebSocket4Net.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using MQTTnet.Implementations;
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using MQTTnet.Implementations;
|
||||
|
||||
namespace MQTTnet.Tests
|
||||
{
|
||||
@@ -18,62 +18,68 @@ namespace MQTTnet.Tests
|
||||
[TestMethod]
|
||||
public async Task Dispose_Channel_While_Used()
|
||||
{
|
||||
var ct = new CancellationTokenSource();
|
||||
var serverSocket = new CrossPlatformSocket(AddressFamily.InterNetwork);
|
||||
|
||||
try
|
||||
using (var ct = new CancellationTokenSource())
|
||||
{
|
||||
serverSocket.Bind(new IPEndPoint(IPAddress.Any, 50001));
|
||||
serverSocket.Listen(0);
|
||||
|
||||
#pragma warning disable 4014
|
||||
Task.Run(async () =>
|
||||
#pragma warning restore 4014
|
||||
using (var serverSocket = new CrossPlatformSocket(AddressFamily.InterNetwork, ProtocolType.Tcp))
|
||||
{
|
||||
while (!ct.IsCancellationRequested)
|
||||
try
|
||||
{
|
||||
var client = await serverSocket.AcceptAsync();
|
||||
var data = new byte[] { 128 };
|
||||
await client.SendAsync(new ArraySegment<byte>(data), SocketFlags.None);
|
||||
}
|
||||
}, ct.Token);
|
||||
serverSocket.Bind(new IPEndPoint(IPAddress.Any, 50001));
|
||||
serverSocket.Listen(0);
|
||||
|
||||
var clientSocket = new CrossPlatformSocket(AddressFamily.InterNetwork);
|
||||
await clientSocket.ConnectAsync("localhost", 50001, CancellationToken.None);
|
||||
|
||||
var tcpChannel = new MqttTcpChannel(clientSocket.GetStream(), "test", null);
|
||||
|
||||
await Task.Delay(100, ct.Token);
|
||||
|
||||
var buffer = new byte[1];
|
||||
await tcpChannel.ReadAsync(buffer, 0, 1, ct.Token);
|
||||
|
||||
Assert.AreEqual(128, buffer[0]);
|
||||
|
||||
// This block should fail after dispose.
|
||||
#pragma warning disable 4014
|
||||
Task.Run(() =>
|
||||
Task.Run(
|
||||
async () =>
|
||||
#pragma warning restore 4014
|
||||
{
|
||||
Task.Delay(200, ct.Token);
|
||||
tcpChannel.Dispose();
|
||||
}, ct.Token);
|
||||
{
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
var client = await serverSocket.AcceptAsync(ct.Token);
|
||||
var data = new byte[] { 128 };
|
||||
await client.SendAsync(new ArraySegment<byte>(data), SocketFlags.None);
|
||||
}
|
||||
},
|
||||
ct.Token);
|
||||
|
||||
try
|
||||
{
|
||||
await tcpChannel.ReadAsync(buffer, 0, 1, CancellationToken.None);
|
||||
var clientSocket = new CrossPlatformSocket(AddressFamily.InterNetwork, ProtocolType.Tcp);
|
||||
await clientSocket.ConnectAsync("localhost", 50001, CancellationToken.None);
|
||||
|
||||
var tcpChannel = new MqttTcpChannel(clientSocket.GetStream(), "test", null);
|
||||
|
||||
await Task.Delay(100, ct.Token);
|
||||
|
||||
var buffer = new byte[1];
|
||||
await tcpChannel.ReadAsync(buffer, 0, 1, ct.Token);
|
||||
|
||||
Assert.AreEqual(128, buffer[0]);
|
||||
|
||||
// This block should fail after dispose.
|
||||
#pragma warning disable 4014
|
||||
Task.Run(
|
||||
() =>
|
||||
#pragma warning restore 4014
|
||||
{
|
||||
Task.Delay(200, ct.Token);
|
||||
tcpChannel.Dispose();
|
||||
},
|
||||
ct.Token);
|
||||
|
||||
try
|
||||
{
|
||||
await tcpChannel.ReadAsync(buffer, 0, 1, CancellationToken.None);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Assert.IsInstanceOfType(exception, typeof(SocketException));
|
||||
Assert.AreEqual(SocketError.OperationAborted, ((SocketException)exception).SocketErrorCode);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ct.Cancel(false);
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Assert.IsInstanceOfType(exception, typeof(SocketException));
|
||||
Assert.AreEqual(SocketError.OperationAborted, ((SocketException)exception).SocketErrorCode);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ct.Cancel(false);
|
||||
serverSocket.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ namespace MQTTnet.Tests.Server
|
||||
{
|
||||
await testEnvironment.StartServer(new MqttServerOptionsBuilder().WithDefaultCommunicationTimeout(TimeSpan.FromSeconds(1)));
|
||||
|
||||
var client = new CrossPlatformSocket(AddressFamily.InterNetwork);
|
||||
var client = new CrossPlatformSocket(AddressFamily.InterNetwork, ProtocolType.Tcp);
|
||||
await client.ConnectAsync("localhost", testEnvironment.ServerPort, CancellationToken.None);
|
||||
|
||||
// Don't send anything. The server should close the connection.
|
||||
@@ -54,7 +54,7 @@ namespace MQTTnet.Tests.Server
|
||||
|
||||
// Send an invalid packet and ensure that the server will close the connection and stay in a waiting state
|
||||
// forever. This is security related.
|
||||
var client = new CrossPlatformSocket(AddressFamily.InterNetwork);
|
||||
var client = new CrossPlatformSocket(AddressFamily.InterNetwork, ProtocolType.Tcp);
|
||||
await client.ConnectAsync("localhost", testEnvironment.ServerPort, CancellationToken.None);
|
||||
|
||||
var buffer = Encoding.UTF8.GetBytes("Garbage");
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using MQTTnet.Extensions.TopicTemplate;
|
||||
|
||||
namespace MQTTnet.Tests
|
||||
{
|
||||
@@ -31,31 +32,43 @@ namespace MQTTnet.Tests
|
||||
{
|
||||
maxNumLevel3Topics = 1;
|
||||
}
|
||||
|
||||
MqttTopicTemplate baseTemplate = new MqttTopicTemplate("{publisher}/{building}/{level}/{sensor}");
|
||||
|
||||
for (var p = 0; p < numPublishers; ++p)
|
||||
{
|
||||
int publisherTopicCount = 0;
|
||||
var publisherName = "pub" + p;
|
||||
var publisherTemplate = baseTemplate
|
||||
.WithParameter("publisher", publisherName);
|
||||
|
||||
for (var l1 = 0; l1 < numLevel1Topics; ++l1)
|
||||
{
|
||||
var l1Template = publisherTemplate.WithParameter("building", "building" + (l1 + 1));
|
||||
for (var l2 = 0; l2 < numLevel2Topics; ++l2)
|
||||
{
|
||||
var l2Template = l1Template.WithParameter("level", "level" + (l2 + 1));
|
||||
for (var l3 = 0; l3 < maxNumLevel3Topics; ++l3)
|
||||
{
|
||||
if (publisherTopicCount >= numTopicsPerPublisher)
|
||||
break;
|
||||
|
||||
var topic = string.Format("{0}/building{1}/level{2}/sensor{3}", publisherName, l1 + 1, l2 + 1, l3 + 1);
|
||||
AddPublisherTopic(publisherName, topic, topicsByPublisher);
|
||||
var l3Template = l2Template
|
||||
.WithParameter("sensor", "sensor" + (l3 + 1));
|
||||
AddPublisherTopic(publisherName, l3Template.TopicFilter, topicsByPublisher);
|
||||
|
||||
if (l2 == 0)
|
||||
{
|
||||
var singleWildcardTopic = string.Format("{0}/building{1}/+/sensor{2}", publisherName, l1 + 1, l3 + 1);
|
||||
var singleWildcardTopic = l1Template
|
||||
.WithParameter("sensor", "sensor" + (l3 + 1))
|
||||
.TopicFilter;
|
||||
AddPublisherTopic(publisherName, singleWildcardTopic, singleWildcardTopicsByPublisher);
|
||||
}
|
||||
if ((l1 == 0) && (l3 == 0))
|
||||
{
|
||||
var multiWildcardTopic = string.Format("{0}/+/level{1}/+", publisherName, l2 + 1);
|
||||
var multiWildcardTopic = publisherTemplate
|
||||
.WithParameter("level", "level" + (l2 + 1))
|
||||
.TopicFilter;
|
||||
AddPublisherTopic(publisherName, multiWildcardTopic, multiWildcardTopicsByPublisher);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using MQTTnet.Exceptions;
|
||||
using MQTTnet.Packets;
|
||||
@@ -11,8 +12,9 @@ namespace MQTTnet.Client
|
||||
{
|
||||
public sealed class MqttClientUnexpectedDisconnectReceivedException : MqttCommunicationException
|
||||
{
|
||||
public MqttClientUnexpectedDisconnectReceivedException(MqttDisconnectPacket disconnectPacket)
|
||||
: base($"Unexpected DISCONNECT (Reason code={disconnectPacket.ReasonCode}) received.")
|
||||
public MqttClientUnexpectedDisconnectReceivedException(MqttDisconnectPacket disconnectPacket, Exception innerExcpetion = null) : base(
|
||||
$"Unexpected DISCONNECT (Reason code={disconnectPacket.ReasonCode}) received.",
|
||||
innerExcpetion)
|
||||
{
|
||||
ReasonCode = disconnectPacket.ReasonCode;
|
||||
SessionExpiryInterval = disconnectPacket.SessionExpiryInterval;
|
||||
@@ -23,12 +25,12 @@ namespace MQTTnet.Client
|
||||
|
||||
public MqttDisconnectReasonCode? ReasonCode { get; }
|
||||
|
||||
public uint? SessionExpiryInterval { get; }
|
||||
|
||||
public string ReasonString { get; }
|
||||
|
||||
public List<MqttUserProperty> UserProperties { get; }
|
||||
|
||||
public string ServerReference { get; }
|
||||
|
||||
public uint? SessionExpiryInterval { get; }
|
||||
|
||||
public List<MqttUserProperty> UserProperties { get; }
|
||||
}
|
||||
}
|
||||
}
|
||||
+136
-115
@@ -48,6 +48,7 @@ namespace MQTTnet.Client
|
||||
Task _packetReceiverTask;
|
||||
AsyncQueue<MqttPublishPacket> _publishPacketReceiverQueue;
|
||||
Task _publishPacketReceiverTask;
|
||||
MqttDisconnectPacket _unexpectedDisconnectPacket;
|
||||
|
||||
public MqttClient(IMqttClientAdapterFactory channelFactory, IMqttNetLogger logger)
|
||||
{
|
||||
@@ -123,6 +124,8 @@ namespace MQTTnet.Client
|
||||
var adapter = _adapterFactory.CreateClientAdapter(options, new MqttPacketInspector(_events.InspectPacketEvent, _rootLogger), _rootLogger);
|
||||
_adapter = adapter ?? throw new InvalidOperationException("The adapter factory did not provide an adapter.");
|
||||
|
||||
_unexpectedDisconnectPacket = null;
|
||||
|
||||
if (cancellationToken.CanBeCanceled)
|
||||
{
|
||||
connectResult = await ConnectInternal(adapter, cancellationToken).ConfigureAwait(false);
|
||||
@@ -215,7 +218,7 @@ namespace MQTTnet.Client
|
||||
}
|
||||
|
||||
// Sending the DISCONNECT may fail due to connection issues. The resulting exception
|
||||
// must be throw to let the caller know that the disconnect is not a clean one.
|
||||
// must be thrown to let the caller know that the disconnect was not clean.
|
||||
var disconnectPacket = MqttPacketFactories.Disconnect.Create(options);
|
||||
|
||||
if (cancellationToken.CanBeCanceled)
|
||||
@@ -239,7 +242,7 @@ namespace MQTTnet.Client
|
||||
public async Task PingAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
|
||||
ThrowIfDisposed();
|
||||
ThrowIfNotConnected();
|
||||
|
||||
@@ -330,7 +333,7 @@ namespace MQTTnet.Client
|
||||
|
||||
ThrowIfDisposed();
|
||||
ThrowIfNotConnected();
|
||||
|
||||
|
||||
if (Options.ValidateFeatures)
|
||||
{
|
||||
MqttClientSubscribeOptionsValidator.ThrowIfNotSupported(options, _adapter.PacketFormatterAdapter.ProtocolVersion);
|
||||
@@ -406,29 +409,37 @@ namespace MQTTnet.Client
|
||||
|
||||
Task AcknowledgeReceivedPublishPacket(MqttApplicationMessageReceivedEventArgs eventArgs, CancellationToken cancellationToken)
|
||||
{
|
||||
if (eventArgs.PublishPacket.QualityOfServiceLevel == MqttQualityOfServiceLevel.AtMostOnce)
|
||||
switch (eventArgs.PublishPacket.QualityOfServiceLevel)
|
||||
{
|
||||
// no response required
|
||||
}
|
||||
else if (eventArgs.PublishPacket.QualityOfServiceLevel == MqttQualityOfServiceLevel.AtLeastOnce)
|
||||
{
|
||||
if (!eventArgs.ProcessingFailed)
|
||||
case MqttQualityOfServiceLevel.AtMostOnce:
|
||||
{
|
||||
var pubAckPacket = MqttPacketFactories.PubAck.Create(eventArgs);
|
||||
return Send(pubAckPacket, cancellationToken);
|
||||
// no response required
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (eventArgs.PublishPacket.QualityOfServiceLevel == MqttQualityOfServiceLevel.ExactlyOnce)
|
||||
{
|
||||
if (!eventArgs.ProcessingFailed)
|
||||
case MqttQualityOfServiceLevel.AtLeastOnce:
|
||||
{
|
||||
var pubRecPacket = MqttPacketFactories.PubRec.Create(eventArgs);
|
||||
return Send(pubRecPacket, cancellationToken);
|
||||
if (!eventArgs.ProcessingFailed)
|
||||
{
|
||||
var pubAckPacket = MqttPacketFactories.PubAck.Create(eventArgs);
|
||||
return Send(pubAckPacket, cancellationToken);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case MqttQualityOfServiceLevel.ExactlyOnce:
|
||||
{
|
||||
if (!eventArgs.ProcessingFailed)
|
||||
{
|
||||
var pubRecPacket = MqttPacketFactories.PubRec.Create(eventArgs);
|
||||
return Send(pubRecPacket, cancellationToken);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
throw new MqttProtocolViolationException("Received a not supported QoS level.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new MqttProtocolViolationException("Received a not supported QoS level.");
|
||||
}
|
||||
|
||||
return CompletedTask.Instance;
|
||||
@@ -445,21 +456,25 @@ namespace MQTTnet.Client
|
||||
|
||||
var receivedPacket = await Receive(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (receivedPacket is MqttConnAckPacket connAckPacket)
|
||||
switch (receivedPacket)
|
||||
{
|
||||
result = MqttClientResultFactory.ConnectResult.Create(connAckPacket, channelAdapter.PacketFormatterAdapter.ProtocolVersion);
|
||||
}
|
||||
else if (receivedPacket is MqttAuthPacket)
|
||||
{
|
||||
throw new NotSupportedException("Extended authentication handler is not yet supported");
|
||||
}
|
||||
else if (receivedPacket == null)
|
||||
{
|
||||
throw new MqttCommunicationException("Connection closed.");
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException($"Received an unexpected MQTT packet ({receivedPacket}).");
|
||||
case MqttConnAckPacket connAckPacket:
|
||||
{
|
||||
result = MqttClientResultFactory.ConnectResult.Create(connAckPacket, channelAdapter.PacketFormatterAdapter.ProtocolVersion);
|
||||
break;
|
||||
}
|
||||
case MqttAuthPacket _:
|
||||
{
|
||||
throw new NotSupportedException("Extended authentication handler is not yet supported");
|
||||
}
|
||||
case null:
|
||||
{
|
||||
throw new MqttCommunicationException("Connection closed.");
|
||||
}
|
||||
default:
|
||||
{
|
||||
throw new InvalidOperationException($"Received an unexpected MQTT packet ({receivedPacket}).");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
@@ -589,8 +604,8 @@ namespace MQTTnet.Client
|
||||
_disconnectUserProperties,
|
||||
exception);
|
||||
|
||||
// This handler must be executed in a new thread because otherwise a dead lock may happen
|
||||
// when trying to reconnect in that handler etc.
|
||||
// This handler must be executed in a new thread to prevent a deadlock
|
||||
// that may occur when attempting to reconnect within that handler, etc.
|
||||
Task.Run(() => _events.DisconnectedEvent.InvokeAsync(eventArgs)).RunInBackground(_logger);
|
||||
}
|
||||
}
|
||||
@@ -599,12 +614,9 @@ namespace MQTTnet.Client
|
||||
{
|
||||
var clientWasConnected = IsConnected;
|
||||
|
||||
if (!DisconnectIsPendingOrFinished())
|
||||
{
|
||||
return DisconnectCore(sender, exception, connectResult, clientWasConnected);
|
||||
}
|
||||
|
||||
return CompletedTask.Instance;
|
||||
return !DisconnectIsPendingOrFinished() ?
|
||||
DisconnectCore(sender, exception, connectResult, clientWasConnected)
|
||||
: CompletedTask.Instance;
|
||||
}
|
||||
|
||||
bool DisconnectIsPendingOrFinished()
|
||||
@@ -657,24 +669,19 @@ namespace MQTTnet.Client
|
||||
|
||||
Task OnConnected(MqttClientConnectResult connectResult)
|
||||
{
|
||||
if (_events.ConnectedEvent.HasHandlers)
|
||||
if (!_events.ConnectedEvent.HasHandlers)
|
||||
{
|
||||
var eventArgs = new MqttClientConnectedEventArgs(connectResult);
|
||||
return _events.ConnectedEvent.InvokeAsync(eventArgs);
|
||||
return CompletedTask.Instance;
|
||||
}
|
||||
|
||||
return CompletedTask.Instance;
|
||||
var eventArgs = new MqttClientConnectedEventArgs(connectResult);
|
||||
return _events.ConnectedEvent.InvokeAsync(eventArgs);
|
||||
}
|
||||
|
||||
Task ProcessReceivedAuthPacket(MqttAuthPacket authPacket)
|
||||
{
|
||||
var extendedAuthenticationExchangeHandler = Options.ExtendedAuthenticationExchangeHandler;
|
||||
if (extendedAuthenticationExchangeHandler != null)
|
||||
{
|
||||
return extendedAuthenticationExchangeHandler.HandleRequestAsync(new MqttExtendedAuthenticationExchangeContext(authPacket, this));
|
||||
}
|
||||
|
||||
return CompletedTask.Instance;
|
||||
return extendedAuthenticationExchangeHandler != null ? extendedAuthenticationExchangeHandler.HandleRequestAsync(new MqttExtendedAuthenticationExchangeContext(authPacket, this)) : CompletedTask.Instance;
|
||||
}
|
||||
|
||||
Task ProcessReceivedDisconnectPacket(MqttDisconnectPacket disconnectPacket)
|
||||
@@ -682,9 +689,10 @@ namespace MQTTnet.Client
|
||||
_disconnectReason = (int)disconnectPacket.ReasonCode;
|
||||
_disconnectReasonString = disconnectPacket.ReasonString;
|
||||
_disconnectUserProperties = disconnectPacket.UserProperties;
|
||||
_unexpectedDisconnectPacket = disconnectPacket;
|
||||
|
||||
// Also dispatch disconnect to waiting threads to generate a proper exception.
|
||||
_packetDispatcher.Dispose(new MqttClientUnexpectedDisconnectReceivedException(disconnectPacket));
|
||||
_packetDispatcher.Dispose(new MqttClientUnexpectedDisconnectReceivedException(disconnectPacket, null));
|
||||
|
||||
return DisconnectInternal(_packetReceiverTask, null, null);
|
||||
}
|
||||
@@ -724,15 +732,15 @@ namespace MQTTnet.Client
|
||||
|
||||
Task ProcessReceivedPubRecPacket(MqttPubRecPacket pubRecPacket, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!_packetDispatcher.TryDispatch(pubRecPacket))
|
||||
if (_packetDispatcher.TryDispatch(pubRecPacket))
|
||||
{
|
||||
// The packet is unknown. Probably due to a restart of the client.
|
||||
// So wen send this to the server to trigger a full resend of the message.
|
||||
var pubRelPacket = MqttPacketFactories.PubRel.Create(pubRecPacket, MqttApplicationMessageReceivedReasonCode.PacketIdentifierNotFound);
|
||||
return Send(pubRelPacket, cancellationToken);
|
||||
return CompletedTask.Instance;
|
||||
}
|
||||
|
||||
return CompletedTask.Instance;
|
||||
// The packet is unknown, probably due to a restart of the client.
|
||||
// So we send this to the server to trigger a full resend of the message.
|
||||
var pubRelPacket = MqttPacketFactories.PubRel.Create(pubRecPacket, MqttApplicationMessageReceivedReasonCode.PacketIdentifierNotFound);
|
||||
return Send(pubRelPacket, cancellationToken);
|
||||
}
|
||||
|
||||
Task ProcessReceivedPubRelPacket(MqttPubRelPacket pubRelPacket, CancellationToken cancellationToken)
|
||||
@@ -751,10 +759,27 @@ namespace MQTTnet.Client
|
||||
|
||||
async Task<MqttClientPublishResult> PublishAtMostOnce(MqttPublishPacket publishPacket, CancellationToken cancellationToken)
|
||||
{
|
||||
// No packet identifier is used for QoS 0 [3.3.2.2 Packet Identifier]
|
||||
await Send(publishPacket, cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
// No packet identifier is used for QoS 0 [3.3.2.2 Packet Identifier]
|
||||
await Send(publishPacket, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return MqttClientResultFactory.PublishResult.Create(null);
|
||||
return MqttClientResultFactory.PublishResult.Create(null);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
// We have to check if the server has sent a disconnect packet in response to the published message.
|
||||
// Since we are in QoS 0 we do not get a direct response via an PUBACK packet and thus basically no
|
||||
// feedback at all.
|
||||
var localUnexpectedDisconnectPacket = _unexpectedDisconnectPacket;
|
||||
|
||||
if (localUnexpectedDisconnectPacket != null)
|
||||
{
|
||||
throw new MqttClientUnexpectedDisconnectReceivedException(localUnexpectedDisconnectPacket, exception);
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
async Task<MqttClientPublishResult> PublishExactlyOnce(MqttPublishPacket publishPacket, CancellationToken cancellationToken)
|
||||
@@ -952,39 +977,36 @@ namespace MQTTnet.Client
|
||||
{
|
||||
try
|
||||
{
|
||||
if (packet is MqttPublishPacket publishPacket)
|
||||
switch (packet)
|
||||
{
|
||||
EnqueueReceivedPublishPacket(publishPacket);
|
||||
}
|
||||
else if (packet is MqttPubRecPacket pubRecPacket)
|
||||
{
|
||||
await ProcessReceivedPubRecPacket(pubRecPacket, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else if (packet is MqttPubRelPacket pubRelPacket)
|
||||
{
|
||||
await ProcessReceivedPubRelPacket(pubRelPacket, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else if (packet is MqttDisconnectPacket disconnectPacket)
|
||||
{
|
||||
await ProcessReceivedDisconnectPacket(disconnectPacket).ConfigureAwait(false);
|
||||
}
|
||||
else if (packet is MqttAuthPacket authPacket)
|
||||
{
|
||||
await ProcessReceivedAuthPacket(authPacket).ConfigureAwait(false);
|
||||
}
|
||||
else if (packet is MqttPingRespPacket)
|
||||
{
|
||||
_packetDispatcher.TryDispatch(packet);
|
||||
}
|
||||
else if (packet is MqttPingReqPacket)
|
||||
{
|
||||
throw new MqttProtocolViolationException("The PINGREQ Packet is sent from a client to the server only.");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!_packetDispatcher.TryDispatch(packet))
|
||||
case MqttPublishPacket publishPacket:
|
||||
EnqueueReceivedPublishPacket(publishPacket);
|
||||
break;
|
||||
case MqttPubRecPacket pubRecPacket:
|
||||
await ProcessReceivedPubRecPacket(pubRecPacket, cancellationToken).ConfigureAwait(false);
|
||||
break;
|
||||
case MqttPubRelPacket pubRelPacket:
|
||||
await ProcessReceivedPubRelPacket(pubRelPacket, cancellationToken).ConfigureAwait(false);
|
||||
break;
|
||||
case MqttDisconnectPacket disconnectPacket:
|
||||
await ProcessReceivedDisconnectPacket(disconnectPacket).ConfigureAwait(false);
|
||||
break;
|
||||
case MqttAuthPacket authPacket:
|
||||
await ProcessReceivedAuthPacket(authPacket).ConfigureAwait(false);
|
||||
break;
|
||||
case MqttPingRespPacket _:
|
||||
_packetDispatcher.TryDispatch(packet);
|
||||
break;
|
||||
case MqttPingReqPacket _:
|
||||
throw new MqttProtocolViolationException("The PINGREQ Packet is sent from a client to the server only.");
|
||||
default:
|
||||
{
|
||||
throw new MqttProtocolViolationException($"Received packet '{packet}' at an unexpected time.");
|
||||
if (!_packetDispatcher.TryDispatch(packet))
|
||||
{
|
||||
throw new MqttProtocolViolationException($"Received packet '{packet}' at an unexpected time.");
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -995,19 +1017,19 @@ namespace MQTTnet.Client
|
||||
return;
|
||||
}
|
||||
|
||||
if (exception is OperationCanceledException)
|
||||
switch (exception)
|
||||
{
|
||||
}
|
||||
else if (exception is MqttCommunicationException)
|
||||
{
|
||||
_logger.Warning(exception, "Communication error while receiving packets");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Error(exception, "Error while processing received {0} packet", packet.GetType().Name);
|
||||
case OperationCanceledException _:
|
||||
break;
|
||||
case MqttCommunicationException _:
|
||||
_logger.Warning(exception, "Communication error while receiving packets");
|
||||
break;
|
||||
default:
|
||||
_logger.Error(exception, "Error while processing received {0} packet", packet.GetType().Name);
|
||||
break;
|
||||
}
|
||||
|
||||
// The packet dispatcher may already be gone due to disconnect etc!
|
||||
// The packet dispatcher may already be gone due to disconnect etc.!
|
||||
_packetDispatcher?.FailAll(exception);
|
||||
|
||||
await DisconnectInternal(_packetReceiverTask, exception, null).ConfigureAwait(false);
|
||||
@@ -1050,17 +1072,16 @@ namespace MQTTnet.Client
|
||||
return;
|
||||
}
|
||||
|
||||
if (exception is OperationCanceledException)
|
||||
switch (exception)
|
||||
{
|
||||
return;
|
||||
}
|
||||
else if (exception is MqttCommunicationException)
|
||||
{
|
||||
_logger.Warning(exception, "Communication error while sending/receiving keep alive packets");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Error(exception, "Error exception while sending/receiving keep alive packets");
|
||||
case OperationCanceledException _:
|
||||
return;
|
||||
case MqttCommunicationException _:
|
||||
_logger.Warning(exception, "Communication error while sending/receiving keep alive packets");
|
||||
break;
|
||||
default:
|
||||
_logger.Error(exception, "Error exception while sending/receiving keep alive packets");
|
||||
break;
|
||||
}
|
||||
|
||||
await DisconnectInternal(_keepAlivePacketsSenderTask, exception, null).ConfigureAwait(false);
|
||||
@@ -1071,4 +1092,4 @@ namespace MQTTnet.Client
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
namespace MQTTnet.Client
|
||||
{
|
||||
public sealed class MqttClientCertificateSelectionEventArgs : EventArgs
|
||||
{
|
||||
public MqttClientCertificateSelectionEventArgs(
|
||||
string targetHost,
|
||||
X509CertificateCollection localCertificates,
|
||||
X509Certificate remoteCertificate,
|
||||
string[] acceptableIssuers,
|
||||
MqttClientTcpOptions tcpOptions)
|
||||
{
|
||||
TargetHost = targetHost;
|
||||
LocalCertificates = localCertificates;
|
||||
RemoveCertificate = remoteCertificate;
|
||||
AcceptableIssuers = acceptableIssuers;
|
||||
TcpOptions = tcpOptions ?? throw new ArgumentNullException(nameof(tcpOptions));
|
||||
}
|
||||
|
||||
public string[] AcceptableIssuers { get; }
|
||||
|
||||
public X509CertificateCollection LocalCertificates { get; }
|
||||
|
||||
public X509Certificate RemoveCertificate { get; }
|
||||
|
||||
public string TargetHost { get; }
|
||||
|
||||
public MqttClientTcpOptions TcpOptions { get; }
|
||||
}
|
||||
}
|
||||
@@ -10,11 +10,20 @@ namespace MQTTnet.Client
|
||||
{
|
||||
public sealed class MqttClientCertificateValidationEventArgs : EventArgs
|
||||
{
|
||||
public X509Certificate Certificate { get; set; }
|
||||
public MqttClientCertificateValidationEventArgs(X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors, IMqttClientChannelOptions clientOptions)
|
||||
{
|
||||
Certificate = certificate;
|
||||
Chain = chain;
|
||||
SslPolicyErrors = sslPolicyErrors;
|
||||
ClientOptions = clientOptions ?? throw new ArgumentNullException(nameof(clientOptions));
|
||||
}
|
||||
|
||||
public X509Chain Chain { get; set; }
|
||||
public X509Certificate Certificate { get; }
|
||||
|
||||
public X509Chain Chain { get; }
|
||||
|
||||
public IMqttClientChannelOptions ClientOptions { get; }
|
||||
|
||||
public IMqttClientChannelOptions ClientOptions { get; set; }
|
||||
#if NET452 || NET461 || NET48
|
||||
/// <summary>
|
||||
/// Can be a host string name or an object derived from WebRequest.
|
||||
@@ -22,6 +31,6 @@ namespace MQTTnet.Client
|
||||
public object Sender { get; set; }
|
||||
#endif
|
||||
|
||||
public SslPolicyErrors SslPolicyErrors { get; set; }
|
||||
public SslPolicyErrors SslPolicyErrors { get; }
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using MQTTnet.Formatter;
|
||||
using MQTTnet.Packets;
|
||||
@@ -18,14 +19,14 @@ namespace MQTTnet.Client
|
||||
public sealed class MqttClientOptionsBuilder
|
||||
{
|
||||
readonly MqttClientOptions _options = new MqttClientOptions();
|
||||
int? _port;
|
||||
|
||||
[Obsolete] MqttClientWebSocketProxyOptions _proxyOptions;
|
||||
EndPoint _remoteEndPoint;
|
||||
|
||||
MqttClientTcpOptions _tcpOptions;
|
||||
MqttClientTlsOptions _tlsOptions;
|
||||
EndPoint _remoteEndPoint;
|
||||
int? _port;
|
||||
|
||||
|
||||
[Obsolete] MqttClientOptionsBuilderTlsParameters _tlsParameters;
|
||||
|
||||
MqttClientWebSocketOptions _webSocketOptions;
|
||||
@@ -78,18 +79,33 @@ namespace MQTTnet.Client
|
||||
{
|
||||
if (_port.HasValue)
|
||||
{
|
||||
_remoteEndPoint = new DnsEndPoint(dns.Host, _port.Value);
|
||||
_remoteEndPoint = new DnsEndPoint(dns.Host, _port.Value, dns.AddressFamily);
|
||||
}
|
||||
else
|
||||
{
|
||||
_remoteEndPoint = new DnsEndPoint(dns.Host, tlsOptions?.UseTls == false ? MqttPorts.Default : MqttPorts.Secure);
|
||||
_remoteEndPoint = new DnsEndPoint(dns.Host, tlsOptions?.UseTls == false ? MqttPorts.Default : MqttPorts.Secure, dns.AddressFamily);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_remoteEndPoint is IPEndPoint ip)
|
||||
{
|
||||
if (ip.Port == 0)
|
||||
{
|
||||
if (_port.HasValue)
|
||||
{
|
||||
_remoteEndPoint = new IPEndPoint(ip.Address, _port.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
_remoteEndPoint = new IPEndPoint(ip.Address, tlsOptions?.UseTls == false ? MqttPorts.Default : MqttPorts.Secure);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_tcpOptions.RemoteEndpoint == null)
|
||||
{
|
||||
_tcpOptions.RemoteEndpoint = _remoteEndPoint;
|
||||
_tcpOptions.RemoteEndpoint = _remoteEndPoint;
|
||||
}
|
||||
}
|
||||
else if (_webSocketOptions != null)
|
||||
@@ -114,6 +130,12 @@ namespace MQTTnet.Client
|
||||
return _options;
|
||||
}
|
||||
|
||||
public MqttClientOptionsBuilder WithAddressFamily(AddressFamily addressFamily)
|
||||
{
|
||||
_tcpOptions.AddressFamily = addressFamily;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MqttClientOptionsBuilder WithAuthentication(string method, byte[] data)
|
||||
{
|
||||
_options.AuthenticationMethod = method;
|
||||
@@ -145,7 +167,6 @@ namespace MQTTnet.Client
|
||||
return this;
|
||||
}
|
||||
|
||||
[Obsolete("Use WithTcpServer(... configure) or WithWebSocketServer(... configure) instead.")]
|
||||
public MqttClientOptionsBuilder WithConnectionUri(Uri uri)
|
||||
{
|
||||
if (uri == null)
|
||||
@@ -189,7 +210,6 @@ namespace MQTTnet.Client
|
||||
return this;
|
||||
}
|
||||
|
||||
[Obsolete("Use WithTcpServer(... configure) or WithWebSocketServer(... configure) instead.")]
|
||||
public MqttClientOptionsBuilder WithConnectionUri(string uri)
|
||||
{
|
||||
return WithConnectionUri(new Uri(uri, UriKind.Absolute));
|
||||
@@ -218,6 +238,14 @@ namespace MQTTnet.Client
|
||||
return this;
|
||||
}
|
||||
|
||||
public MqttClientOptionsBuilder WithEndPoint(EndPoint endPoint)
|
||||
{
|
||||
_remoteEndPoint = endPoint ?? throw new ArgumentNullException(nameof(endPoint));
|
||||
_tcpOptions = new MqttClientTcpOptions();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public MqttClientOptionsBuilder WithExtendedAuthenticationExchangeHandler(IMqttExtendedAuthenticationExchangeHandler handler)
|
||||
{
|
||||
_options.ExtendedAuthenticationExchangeHandler = handler;
|
||||
@@ -254,7 +282,7 @@ namespace MQTTnet.Client
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The client will not throw an exception when the MQTT server responses with a non success ACK packet.
|
||||
/// The client will not throw an exception when the MQTT server responds with a non success ACK packet.
|
||||
/// This will become the default behavior in future versions of the library.
|
||||
/// </summary>
|
||||
public MqttClientOptionsBuilder WithoutThrowOnNonSuccessfulConnectResponse()
|
||||
@@ -263,6 +291,12 @@ namespace MQTTnet.Client
|
||||
return this;
|
||||
}
|
||||
|
||||
public MqttClientOptionsBuilder WithProtocolType(ProtocolType protocolType)
|
||||
{
|
||||
_tcpOptions.ProtocolType = protocolType;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MqttClientOptionsBuilder WithProtocolVersion(MqttProtocolVersion value)
|
||||
{
|
||||
if (value == MqttProtocolVersion.Unknown)
|
||||
@@ -333,25 +367,22 @@ namespace MQTTnet.Client
|
||||
return this;
|
||||
}
|
||||
|
||||
public MqttClientOptionsBuilder WithTcpServer(string server, int? port = null)
|
||||
public MqttClientOptionsBuilder WithTcpServer(string host, int? port = null, AddressFamily addressFamily = AddressFamily.Unspecified)
|
||||
{
|
||||
if (host == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(host));
|
||||
}
|
||||
|
||||
_tcpOptions = new MqttClientTcpOptions();
|
||||
|
||||
// The value 0 will be updated when building the options.
|
||||
// This a backward compatibility feature.
|
||||
_remoteEndPoint = new DnsEndPoint(server, port ?? 0);
|
||||
_remoteEndPoint = new DnsEndPoint(host, port ?? 0, addressFamily);
|
||||
_port = port;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public MqttClientOptionsBuilder WithEndPoint(EndPoint endPoint)
|
||||
{
|
||||
_remoteEndPoint = endPoint ?? throw new ArgumentNullException(nameof(endPoint));
|
||||
_tcpOptions = new MqttClientTcpOptions();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public MqttClientOptionsBuilder WithTcpServer(Action<MqttClientTcpOptions> optionsBuilder)
|
||||
{
|
||||
|
||||
@@ -2,13 +2,16 @@
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using MQTTnet.Protocol;
|
||||
|
||||
namespace MQTTnet.Client
|
||||
{
|
||||
public sealed class MqttClientTcpOptions : IMqttClientChannelOptions
|
||||
{
|
||||
EndPoint _remoteEndpoint;
|
||||
public AddressFamily AddressFamily { get; set; } = AddressFamily.Unspecified;
|
||||
|
||||
public int BufferSize { get; set; } = 8192;
|
||||
@@ -20,6 +23,12 @@ namespace MQTTnet.Client
|
||||
/// </summary>
|
||||
public bool? DualMode { get; set; }
|
||||
|
||||
[Obsolete("Use RemoteEndpoint or MqttClientOptionsBuilder instead.")]
|
||||
public int? Port { get; set; }
|
||||
|
||||
[Obsolete("Use RemoteEndpoint or MqttClientOptionsBuilder instead.")]
|
||||
public string Server { get; set; }
|
||||
|
||||
public LingerOption LingerState { get; set; } = new LingerOption(true, 0);
|
||||
|
||||
/// <summary>
|
||||
@@ -28,15 +37,70 @@ namespace MQTTnet.Client
|
||||
/// </summary>
|
||||
public EndPoint LocalEndpoint { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables the Nagle algorithm for the socket.
|
||||
/// This is only supported for TCP.
|
||||
/// For other protocol types the value is ignored.
|
||||
/// Default: true
|
||||
/// </summary>
|
||||
public bool NoDelay { get; set; } = true;
|
||||
|
||||
public EndPoint RemoteEndpoint { get; set; }
|
||||
/// <summary>
|
||||
/// The MQTT transport is usually TCP but when using other endpoint types like
|
||||
/// unix sockets it must be changed (IP for unix sockets).
|
||||
/// </summary>
|
||||
public ProtocolType ProtocolType { get; set; } = ProtocolType.Tcp;
|
||||
|
||||
public EndPoint RemoteEndpoint
|
||||
{
|
||||
get => _remoteEndpoint;
|
||||
set
|
||||
{
|
||||
_remoteEndpoint = value;
|
||||
|
||||
if (_remoteEndpoint is DnsEndPoint dnsEndPoint)
|
||||
{
|
||||
Server = dnsEndPoint.Host;
|
||||
Port = dnsEndPoint.Port;
|
||||
}
|
||||
else if (_remoteEndpoint is IPEndPoint ipEndPoint)
|
||||
{
|
||||
Server = ipEndPoint.Address.ToString();
|
||||
Port = ipEndPoint.Port;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public MqttClientTlsOptions TlsOptions { get; set; } = new MqttClientTlsOptions();
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return RemoteEndpoint?.ToString() ?? string.Empty;
|
||||
if (RemoteEndpoint != null)
|
||||
{
|
||||
return RemoteEndpoint.ToString();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(Server))
|
||||
{
|
||||
return $"{Server}:{GetPort()}";
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
int GetPort()
|
||||
{
|
||||
if (Port.HasValue)
|
||||
{
|
||||
return Port.Value;
|
||||
}
|
||||
|
||||
if (TlsOptions?.UseTls == true)
|
||||
{
|
||||
return MqttPorts.Secure;
|
||||
}
|
||||
|
||||
return MqttPorts.Default;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,8 @@ namespace MQTTnet.Client
|
||||
{
|
||||
public Func<MqttClientCertificateValidationEventArgs, bool> CertificateValidationHandler { get; set; }
|
||||
|
||||
public Func<MqttClientCertificateSelectionEventArgs, X509Certificate> CertificateSelectionHandler { get; set; }
|
||||
|
||||
public bool UseTls { get; set; }
|
||||
|
||||
public bool IgnoreCertificateRevocationErrors { get; set; }
|
||||
|
||||
@@ -49,6 +49,17 @@ namespace MQTTnet.Client
|
||||
return this;
|
||||
}
|
||||
|
||||
public MqttClientTlsOptionsBuilder WithCertificateSelectionHandler(Func<MqttClientCertificateSelectionEventArgs, X509Certificate> certificateSelectionHandler)
|
||||
{
|
||||
if (certificateSelectionHandler == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(certificateSelectionHandler));
|
||||
}
|
||||
|
||||
_tlsOptions.CertificateSelectionHandler = certificateSelectionHandler;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MqttClientTlsOptionsBuilder WithClientCertificates(IEnumerable<X509Certificate2> certificates)
|
||||
{
|
||||
if (certificates == null)
|
||||
|
||||
@@ -68,12 +68,12 @@ namespace MQTTnet.Formatter.V5
|
||||
|
||||
MqttPacket DecodeAuthPacket(ArraySegment<byte> body)
|
||||
{
|
||||
ThrowIfBodyIsEmpty(body);
|
||||
|
||||
_bufferReader.SetBuffer(body.Array, body.Offset, body.Count);
|
||||
|
||||
var packet = new MqttAuthPacket();
|
||||
|
||||
// MQTT spec: The Reason Code and Property Length can be omitted if the Reason Code is 0x00 (Success) and there are no Properties.
|
||||
// In this case the AUTH has a Remaining Length of 0.
|
||||
if (_bufferReader.EndOfStream)
|
||||
{
|
||||
packet.ReasonCode = MqttAuthenticateReasonCode.Success;
|
||||
@@ -220,7 +220,7 @@ namespace MQTTnet.Formatter.V5
|
||||
var packet = new MqttConnectPacket
|
||||
{
|
||||
// If the Request Problem Information is absent, the value of 1 is used.
|
||||
RequestProblemInformation = true
|
||||
RequestProblemInformation = true
|
||||
};
|
||||
|
||||
var protocolName = _bufferReader.ReadString();
|
||||
@@ -352,7 +352,16 @@ namespace MQTTnet.Formatter.V5
|
||||
|
||||
MqttPacket DecodeDisconnectPacket(ArraySegment<byte> body)
|
||||
{
|
||||
ThrowIfBodyIsEmpty(body);
|
||||
// From RFC: 3.14.2.1 Disconnect Reason Code
|
||||
// Byte 1 in the Variable Header is the Disconnect Reason Code.
|
||||
// If the Remaining Length is less than 1 the value of 0x00 (Normal disconnection) is used.
|
||||
if (body.Count == 0)
|
||||
{
|
||||
return new MqttDisconnectPacket
|
||||
{
|
||||
ReasonCode = MqttDisconnectReasonCode.NormalDisconnection
|
||||
};
|
||||
}
|
||||
|
||||
_bufferReader.SetBuffer(body.Array, body.Offset, body.Count);
|
||||
|
||||
@@ -386,7 +395,7 @@ namespace MQTTnet.Formatter.V5
|
||||
|
||||
return packet;
|
||||
}
|
||||
|
||||
|
||||
MqttPacket DecodePubAckPacket(ArraySegment<byte> body)
|
||||
{
|
||||
ThrowIfBodyIsEmpty(body);
|
||||
@@ -724,7 +733,7 @@ namespace MQTTnet.Formatter.V5
|
||||
packet.UserProperties = propertiesReader.CollectedUserProperties;
|
||||
|
||||
packet.ReasonCodes = new List<MqttUnsubscribeReasonCode>(_bufferReader.BytesLeft);
|
||||
|
||||
|
||||
while (!_bufferReader.EndOfStream)
|
||||
{
|
||||
var reasonCode = (MqttUnsubscribeReasonCode)_bufferReader.ReadByte();
|
||||
@@ -770,4 +779,4 @@ namespace MQTTnet.Formatter.V5
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace MQTTnet.Formatter.V5
|
||||
public sealed class MqttV5PacketEncoder
|
||||
{
|
||||
const int FixedHeaderSize = 1;
|
||||
|
||||
|
||||
readonly MqttBufferWriter _bufferWriter;
|
||||
readonly MqttV5PropertiesWriter _propertiesWriter = new MqttV5PropertiesWriter(new MqttBufferWriter(1024, 4096));
|
||||
|
||||
@@ -64,13 +64,20 @@ namespace MQTTnet.Formatter.V5
|
||||
|
||||
byte EncodeAuthPacket(MqttAuthPacket packet)
|
||||
{
|
||||
_bufferWriter.WriteByte((byte)packet.ReasonCode);
|
||||
|
||||
_propertiesWriter.WriteAuthenticationMethod(packet.AuthenticationMethod);
|
||||
_propertiesWriter.WriteAuthenticationData(packet.AuthenticationData);
|
||||
_propertiesWriter.WriteReasonString(packet.ReasonString);
|
||||
_propertiesWriter.WriteUserProperties(packet.UserProperties);
|
||||
|
||||
// MQTT spec: The Reason Code and Property Length can be omitted if the Reason Code is 0x00 (Success) and there are no Properties.
|
||||
// In this case the AUTH has a Remaining Length of 0.
|
||||
if (packet.ReasonCode == MqttAuthenticateReasonCode.Success && _propertiesWriter.Length == 0)
|
||||
{
|
||||
return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.Auth);
|
||||
}
|
||||
|
||||
_bufferWriter.WriteByte((byte)packet.ReasonCode);
|
||||
|
||||
_propertiesWriter.WriteTo(_bufferWriter);
|
||||
_propertiesWriter.Reset();
|
||||
|
||||
@@ -526,7 +533,7 @@ namespace MQTTnet.Formatter.V5
|
||||
|
||||
static void ThrowIfPacketIdentifierIsInvalid(ushort packetIdentifier, MqttPacket packet)
|
||||
{
|
||||
// SUBSCRIBE, UNSUBSCRIBE, and PUBLISH(in cases where QoS > 0) Control Packets MUST contain a non-zero 16 - bit Packet Identifier[MQTT - 2.3.1 - 1].
|
||||
// SUBSCRIBE, UNSUBSCRIBE, and PUBLISH(in cases where QoS > 0) Control Packets MUST contain a non-zero 16 - bit Packet Identifier[MQTT - 2.3.1 - 1].
|
||||
|
||||
if (packetIdentifier == 0)
|
||||
{
|
||||
|
||||
@@ -22,20 +22,20 @@ namespace MQTTnet.Implementations
|
||||
|
||||
NetworkStream _networkStream;
|
||||
|
||||
public CrossPlatformSocket(AddressFamily addressFamily)
|
||||
public CrossPlatformSocket(AddressFamily addressFamily, ProtocolType protocolType)
|
||||
{
|
||||
_socket = new Socket(addressFamily, SocketType.Stream, ProtocolType.Tcp);
|
||||
_socket = new Socket(addressFamily, SocketType.Stream, protocolType);
|
||||
|
||||
#if !NET5_0_OR_GREATER
|
||||
_socketDisposeAction = _socket.Dispose;
|
||||
#endif
|
||||
}
|
||||
|
||||
public CrossPlatformSocket()
|
||||
public CrossPlatformSocket(ProtocolType protocolType)
|
||||
{
|
||||
// Having this constructor is important because avoiding the address family as parameter
|
||||
// will make use of dual mode in the .net framework.
|
||||
_socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
|
||||
_socket = new Socket(SocketType.Stream, protocolType);
|
||||
|
||||
#if !NET5_0_OR_GREATER
|
||||
_socketDisposeAction = _socket.Dispose;
|
||||
@@ -142,12 +142,14 @@ namespace MQTTnet.Implementations
|
||||
set => _socket.SendTimeout = value;
|
||||
}
|
||||
|
||||
public async Task<CrossPlatformSocket> AcceptAsync()
|
||||
public async Task<CrossPlatformSocket> AcceptAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
#if NET452 || NET461
|
||||
var clientSocket = await Task.Factory.FromAsync(_socket.BeginAccept, _socket.EndAccept, null).ConfigureAwait(false);
|
||||
#elif NET7_0_OR_GREATER
|
||||
var clientSocket = await _socket.AcceptAsync(cancellationToken).ConfigureAwait(false);
|
||||
#else
|
||||
var clientSocket = await _socket.AcceptAsync().ConfigureAwait(false);
|
||||
#endif
|
||||
@@ -202,9 +204,28 @@ namespace MQTTnet.Implementations
|
||||
using (cancellationToken.Register(_socketDisposeAction))
|
||||
{
|
||||
#if NET452 || NET461
|
||||
await Task.Factory.FromAsync(_socket.BeginConnect, _socket.EndConnect, endPoint, null).ConfigureAwait(false);
|
||||
// This is a fix for Mono which behaves differently than dotnet.
|
||||
// The connection will not be established when the DNS endpoint is used.
|
||||
if (endPoint is DnsEndPoint dns && dns.AddressFamily == AddressFamily.Unspecified)
|
||||
{
|
||||
await Task.Factory.FromAsync(_socket.BeginConnect, _socket.EndConnect, dns.Host, dns.Port, null).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await Task.Factory.FromAsync(_socket.BeginConnect, _socket.EndConnect, endPoint, null).ConfigureAwait(false);
|
||||
}
|
||||
#else
|
||||
await _socket.ConnectAsync(endPoint).ConfigureAwait(false);
|
||||
|
||||
// This is a fix for Mono which behaves differently than dotnet.
|
||||
// The connection will not be established when the DNS endpoint is used.
|
||||
if (endPoint is DnsEndPoint dns && dns.AddressFamily == AddressFamily.Unspecified)
|
||||
{
|
||||
await _socket.ConnectAsync(dns.Host, dns.Port).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _socket.ConnectAsync(endPoint).ConfigureAwait(false);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -16,6 +16,7 @@ using MQTTnet.Channel;
|
||||
using MQTTnet.Client;
|
||||
using MQTTnet.Exceptions;
|
||||
using MQTTnet.Internal;
|
||||
using MQTTnet.Protocol;
|
||||
|
||||
namespace MQTTnet.Implementations
|
||||
{
|
||||
@@ -63,11 +64,11 @@ namespace MQTTnet.Implementations
|
||||
{
|
||||
if (_tcpOptions.AddressFamily == AddressFamily.Unspecified)
|
||||
{
|
||||
socket = new CrossPlatformSocket();
|
||||
socket = new CrossPlatformSocket(_tcpOptions.ProtocolType);
|
||||
}
|
||||
else
|
||||
{
|
||||
socket = new CrossPlatformSocket(_tcpOptions.AddressFamily);
|
||||
socket = new CrossPlatformSocket(_tcpOptions.AddressFamily, _tcpOptions.ProtocolType);
|
||||
}
|
||||
|
||||
if (_tcpOptions.LocalEndpoint != null)
|
||||
@@ -78,7 +79,12 @@ namespace MQTTnet.Implementations
|
||||
socket.ReceiveBufferSize = _tcpOptions.BufferSize;
|
||||
socket.SendBufferSize = _tcpOptions.BufferSize;
|
||||
socket.SendTimeout = (int)_clientOptions.Timeout.TotalMilliseconds;
|
||||
socket.NoDelay = _tcpOptions.NoDelay;
|
||||
|
||||
if (_tcpOptions.ProtocolType == ProtocolType.Tcp)
|
||||
{
|
||||
// Other protocol types do not support the Nagle algorithm.
|
||||
socket.NoDelay = _tcpOptions.NoDelay;
|
||||
}
|
||||
|
||||
if (socket.LingerState != null)
|
||||
{
|
||||
@@ -93,6 +99,29 @@ namespace MQTTnet.Implementations
|
||||
socket.DualMode = _tcpOptions.DualMode.Value;
|
||||
}
|
||||
|
||||
// This block is only for backward compatibility.
|
||||
if (_tcpOptions.RemoteEndpoint == null && !string.IsNullOrEmpty(_tcpOptions.Server))
|
||||
{
|
||||
int port;
|
||||
if (_tcpOptions.Port.HasValue)
|
||||
{
|
||||
port = _tcpOptions.Port.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_tcpOptions.TlsOptions?.UseTls == true)
|
||||
{
|
||||
port = MqttPorts.Secure;
|
||||
}
|
||||
else
|
||||
{
|
||||
port = MqttPorts.Default;
|
||||
}
|
||||
}
|
||||
|
||||
_tcpOptions.RemoteEndpoint = new DnsEndPoint(_tcpOptions.Server, port, AddressFamily.Unspecified);
|
||||
}
|
||||
|
||||
await socket.ConnectAsync(_tcpOptions.RemoteEndpoint, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
@@ -106,10 +135,29 @@ namespace MQTTnet.Implementations
|
||||
{
|
||||
if (_tcpOptions.RemoteEndpoint is DnsEndPoint dns)
|
||||
{
|
||||
targetHost = dns.Host;
|
||||
targetHost = dns.Host;
|
||||
}
|
||||
}
|
||||
var sslStream = new SslStream(networkStream, false, InternalUserCertificateValidationCallback);
|
||||
|
||||
SslStream sslStream;
|
||||
if (_tcpOptions.TlsOptions.CertificateSelectionHandler != null)
|
||||
{
|
||||
sslStream = new SslStream(
|
||||
networkStream,
|
||||
false,
|
||||
InternalUserCertificateValidationCallback,
|
||||
InternalUserCertificateSelectionCallback);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Use a different constructor depending on the options for MQTTnet so that we do not have
|
||||
// to copy the exact same behavior of the selection handler.
|
||||
sslStream = new SslStream(
|
||||
networkStream,
|
||||
false,
|
||||
InternalUserCertificateValidationCallback);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
#if NETCOREAPP3_1_OR_GREATER
|
||||
@@ -134,9 +182,7 @@ namespace MQTTnet.Implementations
|
||||
{
|
||||
TrustMode = X509ChainTrustMode.CustomRootTrust,
|
||||
VerificationFlags = X509VerificationFlags.IgnoreEndRevocationUnknown,
|
||||
RevocationMode = _tcpOptions.TlsOptions.IgnoreCertificateRevocationErrors
|
||||
? X509RevocationMode.NoCheck
|
||||
: _tcpOptions.TlsOptions.RevocationMode
|
||||
RevocationMode = _tcpOptions.TlsOptions.IgnoreCertificateRevocationErrors ? X509RevocationMode.NoCheck : _tcpOptions.TlsOptions.RevocationMode
|
||||
};
|
||||
|
||||
sslOptions.CertificateChainPolicy.CustomTrustStore.AddRange(_tcpOptions.TlsOptions.TrustChain);
|
||||
@@ -292,19 +338,34 @@ namespace MQTTnet.Implementations
|
||||
}
|
||||
}
|
||||
|
||||
X509Certificate InternalUserCertificateSelectionCallback(
|
||||
object sender,
|
||||
string targetHost,
|
||||
X509CertificateCollection localCertificates,
|
||||
X509Certificate remoteCertificate,
|
||||
string[] acceptableIssuers)
|
||||
{
|
||||
var certificateSelectionHandler = _tcpOptions?.TlsOptions?.CertificateSelectionHandler;
|
||||
if (certificateSelectionHandler != null)
|
||||
{
|
||||
var eventArgs = new MqttClientCertificateSelectionEventArgs(targetHost, localCertificates, remoteCertificate, acceptableIssuers, _tcpOptions);
|
||||
return certificateSelectionHandler(eventArgs);
|
||||
}
|
||||
|
||||
if (localCertificates?.Count > 0)
|
||||
{
|
||||
return localCertificates[0];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
bool InternalUserCertificateValidationCallback(object sender, X509Certificate x509Certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
|
||||
{
|
||||
var certificateValidationHandler = _tcpOptions?.TlsOptions?.CertificateValidationHandler;
|
||||
if (certificateValidationHandler != null)
|
||||
{
|
||||
var eventArgs = new MqttClientCertificateValidationEventArgs
|
||||
{
|
||||
Certificate = x509Certificate,
|
||||
Chain = chain,
|
||||
SslPolicyErrors = sslPolicyErrors,
|
||||
ClientOptions = _tcpOptions
|
||||
};
|
||||
|
||||
var eventArgs = new MqttClientCertificateValidationEventArgs(x509Certificate, chain, sslPolicyErrors, _tcpOptions);
|
||||
return certificateValidationHandler(eventArgs);
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace MQTTnet.Implementations
|
||||
readonly MqttServerOptions _serverOptions;
|
||||
readonly MqttServerTcpEndpointBaseOptions _options;
|
||||
readonly MqttServerTlsTcpEndpointOptions _tlsOptions;
|
||||
|
||||
|
||||
CrossPlatformSocket _socket;
|
||||
IPEndPoint _localEndPoint;
|
||||
|
||||
@@ -65,7 +65,7 @@ namespace MQTTnet.Implementations
|
||||
|
||||
_logger.Info("Starting TCP listener (Endpoint={0}, TLS={1})", _localEndPoint, _tlsOptions?.CertificateProvider != null);
|
||||
|
||||
_socket = new CrossPlatformSocket(_addressFamily);
|
||||
_socket = new CrossPlatformSocket(_addressFamily, ProtocolType.Tcp);
|
||||
|
||||
// Usage of socket options is described here: https://docs.microsoft.com/en-us/dotnet/api/system.net.sockets.socket.setsocketoption?view=netcore-2.2
|
||||
if (_options.ReuseAddress)
|
||||
@@ -87,33 +87,33 @@ namespace MQTTnet.Implementations
|
||||
{
|
||||
_socket.KeepAlive = _options.KeepAlive.Value;
|
||||
}
|
||||
|
||||
|
||||
if (_options.TcpKeepAliveInterval.HasValue)
|
||||
{
|
||||
_socket.TcpKeepAliveInterval = _options.TcpKeepAliveInterval.Value;
|
||||
}
|
||||
|
||||
|
||||
if (_options.TcpKeepAliveRetryCount.HasValue)
|
||||
{
|
||||
_socket.TcpKeepAliveInterval = _options.TcpKeepAliveRetryCount.Value;
|
||||
}
|
||||
|
||||
|
||||
if (_options.TcpKeepAliveTime.HasValue)
|
||||
{
|
||||
_socket.TcpKeepAliveTime = _options.TcpKeepAliveTime.Value;
|
||||
}
|
||||
|
||||
|
||||
_socket.Bind(_localEndPoint);
|
||||
|
||||
|
||||
// Get the local endpoint back from the socket. The port may have changed.
|
||||
// This can happen when port 0 is used. Then the OS will choose the next free port.
|
||||
_localEndPoint = (IPEndPoint)_socket.LocalEndPoint;
|
||||
_options.Port = _localEndPoint.Port;
|
||||
|
||||
|
||||
_socket.Listen(_options.ConnectionBacklog);
|
||||
|
||||
|
||||
_logger.Verbose("TCP listener started (Endpoint={0})", _localEndPoint);
|
||||
|
||||
|
||||
Task.Run(() => AcceptClientConnectionsAsync(cancellationToken), cancellationToken).RunInBackground(_logger);
|
||||
|
||||
return true;
|
||||
@@ -141,7 +141,7 @@ namespace MQTTnet.Implementations
|
||||
{
|
||||
try
|
||||
{
|
||||
var clientSocket = await _socket.AcceptAsync().ConfigureAwait(false);
|
||||
var clientSocket = await _socket.AcceptAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (clientSocket == null)
|
||||
{
|
||||
continue;
|
||||
@@ -183,7 +183,7 @@ namespace MQTTnet.Implementations
|
||||
clientSocket.NoDelay = _options.NoDelay;
|
||||
stream = clientSocket.GetStream();
|
||||
var clientCertificate = _tlsOptions?.CertificateProvider?.GetCertificate();
|
||||
|
||||
|
||||
if (clientCertificate != null)
|
||||
{
|
||||
if (!clientCertificate.HasPrivateKey)
|
||||
@@ -228,7 +228,7 @@ namespace MQTTnet.Implementations
|
||||
var tcpChannel = new MqttTcpChannel(stream, remoteEndPoint, clientCertificate);
|
||||
var bufferWriter = new MqttBufferWriter(_serverOptions.WriterBufferSize, _serverOptions.WriterBufferSizeMax);
|
||||
var packetFormatterAdapter = new MqttPacketFormatterAdapter(bufferWriter);
|
||||
|
||||
|
||||
using (var clientAdapter = new MqttChannelAdapter(tcpChannel, packetFormatterAdapter, _rootLogger))
|
||||
{
|
||||
clientAdapter.AllowPacketFragmentation = _options.AllowPacketFragmentation;
|
||||
|
||||
@@ -109,7 +109,7 @@ namespace MQTTnet.Implementations
|
||||
// A single WebSocket data frame can contain multiple or partial MQTT Control Packets. The receiver MUST NOT assume that MQTT Control Packets are aligned on WebSocket frame boundaries [MQTT-6.0.0-2].
|
||||
await _webSocket.SendAsync(buffer, WebSocketMessageType.Binary, isEndOfPacket, cancellationToken).ConfigureAwait(false);
|
||||
#else
|
||||
// The lock is required because the client will throw an exception if _SendAsync_ is
|
||||
// The 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.
|
||||
if (_sendLock == null)
|
||||
@@ -156,7 +156,7 @@ namespace MQTTnet.Implementations
|
||||
#else
|
||||
var proxyUri = new Uri(_options.ProxyOptions.Address);
|
||||
WebProxy webProxy;
|
||||
|
||||
|
||||
if (!string.IsNullOrEmpty(_options.ProxyOptions.Username) && !string.IsNullOrEmpty(_options.ProxyOptions.Password))
|
||||
{
|
||||
var credentials = new NetworkCredential(_options.ProxyOptions.Username, _options.ProxyOptions.Password, _options.ProxyOptions.Domain);
|
||||
@@ -164,9 +164,9 @@ namespace MQTTnet.Implementations
|
||||
}
|
||||
else
|
||||
{
|
||||
webProxy = new WebProxy(proxyUri, _options.ProxyOptions.BypassOnLocal, _options.ProxyOptions.BypassList);
|
||||
webProxy = new WebProxy(proxyUri, _options.ProxyOptions.BypassOnLocal, _options.ProxyOptions.BypassList);
|
||||
}
|
||||
|
||||
|
||||
if (_options.ProxyOptions.UseDefaultCredentials)
|
||||
{
|
||||
// Only update the property if required because setting it to false will alter
|
||||
@@ -234,7 +234,7 @@ namespace MQTTnet.Implementations
|
||||
{
|
||||
clientWebSocket.Options.Credentials = _options.Credentials;
|
||||
}
|
||||
|
||||
|
||||
var certificateValidationHandler = _options.TlsOptions?.CertificateValidationHandler;
|
||||
if (certificateValidationHandler != null)
|
||||
{
|
||||
@@ -245,15 +245,11 @@ namespace MQTTnet.Implementations
|
||||
#elif WINDOWS_UWP
|
||||
throw new NotSupportedException("Remote certificate validation callback is not supported when using 'uap10.0'.");
|
||||
#elif NET452 || NET461 || NET48
|
||||
ServicePointManager.ServerCertificateValidationCallback += (sender, certificate, chain, sslPolicyErrors) =>
|
||||
ServicePointManager.ServerCertificateValidationCallback += (sender, certificate, chain, sslPolicyErrors) =>
|
||||
{
|
||||
var context = new MqttClientCertificateValidationEventArgs
|
||||
var context = new MqttClientCertificateValidationEventArgs(certificate, chain, sslPolicyErrors, _options)
|
||||
{
|
||||
Sender = sender,
|
||||
Certificate = certificate,
|
||||
Chain = chain,
|
||||
SslPolicyErrors = sslPolicyErrors,
|
||||
ClientOptions = _options
|
||||
Sender = sender
|
||||
};
|
||||
|
||||
return certificateValidationHandler(context);
|
||||
@@ -262,17 +258,16 @@ namespace MQTTnet.Implementations
|
||||
clientWebSocket.Options.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) =>
|
||||
{
|
||||
// TODO: Find a way to add client options to same callback. Problem is that they have a different type.
|
||||
var context = new MqttClientCertificateValidationEventArgs
|
||||
{
|
||||
Certificate = certificate,
|
||||
Chain = chain,
|
||||
SslPolicyErrors = sslPolicyErrors,
|
||||
ClientOptions = _options
|
||||
};
|
||||
|
||||
var context = new MqttClientCertificateValidationEventArgs(certificate, chain, sslPolicyErrors, _options);
|
||||
return certificateValidationHandler(context);
|
||||
};
|
||||
#endif
|
||||
|
||||
var certificateSelectionHandler = _options.TlsOptions?.CertificateSelectionHandler;
|
||||
if (certificateSelectionHandler != null)
|
||||
{
|
||||
throw new NotSupportedException("Remote certificate selection callback is not supported for WebSocket connections.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
<Description>MQTTnet is a high performance .NET library for MQTT based communication. It provides a MQTT client and a MQTT server (broker) and supports v3.1.0, v3.1.1 and v5.0.0 of the MQTT protocol.</Description>
|
||||
<Authors>The contributors of MQTTnet</Authors>
|
||||
<PackageId>MQTTnet</PackageId>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<DelaySign>false</DelaySign>
|
||||
<SignAssembly>false</SignAssembly>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
@@ -70,8 +69,10 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\..\Images\nuget.png" Pack="True" PackagePath="\" />
|
||||
<None Include="..\..\README.md" Pack="true" PackagePath="\" />
|
||||
<None Include="..\..\Images\nuget.png">
|
||||
<Pack>True</Pack>
|
||||
<PackagePath>\</PackagePath>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -257,7 +257,12 @@ namespace MQTTnet.Server
|
||||
|
||||
if (matchingSubscribersCount == 0)
|
||||
{
|
||||
reasonCode = (int)MqttPubAckReasonCode.NoMatchingSubscribers;
|
||||
if (reasonCode == (int)MqttPubAckReasonCode.Success)
|
||||
{
|
||||
// Only change the value if it was success. Otherwise, we would hide an error or not authorized status.
|
||||
reasonCode = (int)MqttPubAckReasonCode.NoMatchingSubscribers;
|
||||
}
|
||||
|
||||
await FireApplicationMessageNotConsumedEvent(applicationMessage, senderId).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user