mirror of
https://github.com/dotnet/MQTTnet.git
synced 2026-10-15 00:15:15 +08:00
Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -46,7 +46,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Execute tests
|
- name: Execute tests
|
||||||
run: dotnet test --framework net8.0 Source/MQTTnet.Tests/MQTTnet.Tests.csproj
|
run: dotnet test --framework net8.0 --configuration Release Source/MQTTnet.Tests/MQTTnet.Tests.csproj
|
||||||
|
|
||||||
sign:
|
sign:
|
||||||
needs: build
|
needs: build
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||||
|
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ConvertIfStatementToSwitchStatement/@EntryIndexedValue">DO_NOT_SHOW</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=InconsistentNaming/@EntryIndexedValue">SUGGESTION</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UncatchableException/@EntryIndexedValue">DO_NOT_SHOW</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UnthrowableException/@EntryIndexedValue">DO_NOT_SHOW</s:String>
|
||||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/BRACES_FOR_FOR/@EntryValue">Required</s:String>
|
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/BRACES_FOR_FOR/@EntryValue">Required</s:String>
|
||||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/BRACES_FOR_FOREACH/@EntryValue">Required</s:String>
|
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/BRACES_FOR_FOREACH/@EntryValue">Required</s:String>
|
||||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/BRACES_FOR_IFELSE/@EntryValue">Required</s:String>
|
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/BRACES_FOR_IFELSE/@EntryValue">Required</s:String>
|
||||||
|
|||||||
@@ -413,7 +413,7 @@ m/XriWr/Cq4h/JfB7NTsezVslgkBaoU=
|
|||||||
if (e.ClientWasConnected)
|
if (e.ClientWasConnected)
|
||||||
{
|
{
|
||||||
// Use the current options as the new options.
|
// Use the current options as the new options.
|
||||||
await mqttClient.ConnectAsync(mqttClient.Options);
|
await mqttClient.ConnectAsync(mqttClientOptions);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -23,25 +23,23 @@ public static class Client_Publish_Samples
|
|||||||
|
|
||||||
var mqttFactory = new MqttClientFactory();
|
var mqttFactory = new MqttClientFactory();
|
||||||
|
|
||||||
using (var mqttClient = mqttFactory.CreateMqttClient())
|
using var mqttClient = mqttFactory.CreateMqttClient();
|
||||||
{
|
var mqttClientOptions = new MqttClientOptionsBuilder()
|
||||||
var mqttClientOptions = new MqttClientOptionsBuilder()
|
.WithTcpServer("broker.hivemq.com")
|
||||||
.WithTcpServer("broker.hivemq.com")
|
.Build();
|
||||||
.Build();
|
|
||||||
|
|
||||||
await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None);
|
await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None);
|
||||||
|
|
||||||
var applicationMessage = new MqttApplicationMessageBuilder()
|
var applicationMessage = new MqttApplicationMessageBuilder()
|
||||||
.WithTopic("samples/temperature/living_room")
|
.WithTopic("samples/temperature/living_room")
|
||||||
.WithPayload("19.5")
|
.WithPayload("19.5")
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
await mqttClient.PublishAsync(applicationMessage, CancellationToken.None);
|
await mqttClient.PublishAsync(applicationMessage, CancellationToken.None);
|
||||||
|
|
||||||
await mqttClient.DisconnectAsync();
|
await mqttClient.DisconnectAsync();
|
||||||
|
|
||||||
Console.WriteLine("MQTT application message is published.");
|
Console.WriteLine("MQTT application message is published.");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task Publish_Multiple_Application_Messages()
|
public static async Task Publish_Multiple_Application_Messages()
|
||||||
@@ -54,38 +52,36 @@ public static class Client_Publish_Samples
|
|||||||
|
|
||||||
var mqttFactory = new MqttClientFactory();
|
var mqttFactory = new MqttClientFactory();
|
||||||
|
|
||||||
using (var mqttClient = mqttFactory.CreateMqttClient())
|
using var mqttClient = mqttFactory.CreateMqttClient();
|
||||||
{
|
var mqttClientOptions = new MqttClientOptionsBuilder()
|
||||||
var mqttClientOptions = new MqttClientOptionsBuilder()
|
.WithTcpServer("broker.hivemq.com")
|
||||||
.WithTcpServer("broker.hivemq.com")
|
.Build();
|
||||||
.Build();
|
|
||||||
|
|
||||||
await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None);
|
await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None);
|
||||||
|
|
||||||
var applicationMessage = new MqttApplicationMessageBuilder()
|
var applicationMessage = new MqttApplicationMessageBuilder()
|
||||||
.WithTopic("samples/temperature/living_room")
|
.WithTopic("samples/temperature/living_room")
|
||||||
.WithPayload("19.5")
|
.WithPayload("19.5")
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
await mqttClient.PublishAsync(applicationMessage, CancellationToken.None);
|
await mqttClient.PublishAsync(applicationMessage, CancellationToken.None);
|
||||||
|
|
||||||
applicationMessage = new MqttApplicationMessageBuilder()
|
applicationMessage = new MqttApplicationMessageBuilder()
|
||||||
.WithTopic("samples/temperature/living_room")
|
.WithTopic("samples/temperature/living_room")
|
||||||
.WithPayload("20.0")
|
.WithPayload("20.0")
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
await mqttClient.PublishAsync(applicationMessage, CancellationToken.None);
|
await mqttClient.PublishAsync(applicationMessage, CancellationToken.None);
|
||||||
|
|
||||||
applicationMessage = new MqttApplicationMessageBuilder()
|
applicationMessage = new MqttApplicationMessageBuilder()
|
||||||
.WithTopic("samples/temperature/living_room")
|
.WithTopic("samples/temperature/living_room")
|
||||||
.WithPayload("21.0")
|
.WithPayload("21.0")
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
await mqttClient.PublishAsync(applicationMessage, CancellationToken.None);
|
await mqttClient.PublishAsync(applicationMessage, CancellationToken.None);
|
||||||
|
|
||||||
await mqttClient.DisconnectAsync();
|
await mqttClient.DisconnectAsync();
|
||||||
|
|
||||||
Console.WriteLine("MQTT application message is published.");
|
Console.WriteLine("MQTT application message is published.");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -26,32 +26,30 @@ public static class Client_Subscribe_Samples
|
|||||||
|
|
||||||
var mqttFactory = new MqttClientFactory();
|
var mqttFactory = new MqttClientFactory();
|
||||||
|
|
||||||
using (var mqttClient = mqttFactory.CreateMqttClient())
|
using var mqttClient = mqttFactory.CreateMqttClient();
|
||||||
|
var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("broker.hivemq.com").Build();
|
||||||
|
|
||||||
|
// Setup message handling before connecting so that queued messages
|
||||||
|
// are also handled properly. When there is no event handler attached all
|
||||||
|
// received messages get lost.
|
||||||
|
mqttClient.ApplicationMessageReceivedAsync += e =>
|
||||||
{
|
{
|
||||||
var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("broker.hivemq.com").Build();
|
Console.WriteLine("Received application message.");
|
||||||
|
e.DumpToConsole();
|
||||||
|
|
||||||
// Setup message handling before connecting so that queued messages
|
return Task.CompletedTask;
|
||||||
// are also handled properly. When there is no event handler attached all
|
};
|
||||||
// received messages get lost.
|
|
||||||
mqttClient.ApplicationMessageReceivedAsync += e =>
|
|
||||||
{
|
|
||||||
Console.WriteLine("Received application message.");
|
|
||||||
e.DumpToConsole();
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None);
|
||||||
};
|
|
||||||
|
|
||||||
await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None);
|
var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder().WithTopicTemplate(sampleTemplate.WithParameter("id", "2")).Build();
|
||||||
|
|
||||||
var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder().WithTopicTemplate(sampleTemplate.WithParameter("id", "2")).Build();
|
await mqttClient.SubscribeAsync(mqttSubscribeOptions, CancellationToken.None);
|
||||||
|
|
||||||
await mqttClient.SubscribeAsync(mqttSubscribeOptions, CancellationToken.None);
|
Console.WriteLine("MQTT client subscribed to topic.");
|
||||||
|
|
||||||
Console.WriteLine("MQTT client subscribed to topic.");
|
Console.WriteLine("Press enter to exit.");
|
||||||
|
Console.ReadLine();
|
||||||
Console.WriteLine("Press enter to exit.");
|
|
||||||
Console.ReadLine();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task Send_Responses()
|
public static async Task Send_Responses()
|
||||||
@@ -62,36 +60,34 @@ public static class Client_Subscribe_Samples
|
|||||||
|
|
||||||
var mqttFactory = new MqttClientFactory();
|
var mqttFactory = new MqttClientFactory();
|
||||||
|
|
||||||
using (var mqttClient = mqttFactory.CreateMqttClient())
|
using var mqttClient = mqttFactory.CreateMqttClient();
|
||||||
|
mqttClient.ApplicationMessageReceivedAsync += delegate(MqttApplicationMessageReceivedEventArgs args)
|
||||||
{
|
{
|
||||||
mqttClient.ApplicationMessageReceivedAsync += delegate(MqttApplicationMessageReceivedEventArgs args)
|
// Do some work with the message...
|
||||||
{
|
|
||||||
// Do some work with the message...
|
|
||||||
|
|
||||||
// Now respond to the broker with a reason code other than success.
|
// Now respond to the broker with a reason code other than success.
|
||||||
args.ReasonCode = MqttApplicationMessageReceivedReasonCode.ImplementationSpecificError;
|
args.ReasonCode = MqttApplicationMessageReceivedReasonCode.ImplementationSpecificError;
|
||||||
args.ResponseReasonString = "That did not work!";
|
args.ResponseReasonString = "That did not work!";
|
||||||
|
|
||||||
// User properties require MQTT v5!
|
// User properties require MQTT v5!
|
||||||
args.ResponseUserProperties.Add(new MqttUserProperty("My", "Data"));
|
args.ResponseUserProperties.Add(new MqttUserProperty("My", "Data"));
|
||||||
|
|
||||||
// Now the broker will resend the message again.
|
// Now the broker will resend the message again.
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
};
|
};
|
||||||
|
|
||||||
var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("broker.hivemq.com").Build();
|
var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("broker.hivemq.com").Build();
|
||||||
|
|
||||||
await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None);
|
await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None);
|
||||||
|
|
||||||
var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder().WithTopicTemplate(sampleTemplate.WithParameter("id", "1")).Build();
|
var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder().WithTopicTemplate(sampleTemplate.WithParameter("id", "1")).Build();
|
||||||
|
|
||||||
var response = await mqttClient.SubscribeAsync(mqttSubscribeOptions, CancellationToken.None);
|
var response = await mqttClient.SubscribeAsync(mqttSubscribeOptions, CancellationToken.None);
|
||||||
|
|
||||||
Console.WriteLine("MQTT client subscribed to topic.");
|
Console.WriteLine("MQTT client subscribed to topic.");
|
||||||
|
|
||||||
// The response contains additional data sent by the server after subscribing.
|
// The response contains additional data sent by the server after subscribing.
|
||||||
response.DumpToConsole();
|
response.DumpToConsole();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task Subscribe_Multiple_Topics()
|
public static async Task Subscribe_Multiple_Topics()
|
||||||
@@ -102,27 +98,25 @@ public static class Client_Subscribe_Samples
|
|||||||
|
|
||||||
var mqttFactory = new MqttClientFactory();
|
var mqttFactory = new MqttClientFactory();
|
||||||
|
|
||||||
using (var mqttClient = mqttFactory.CreateMqttClient())
|
using var mqttClient = mqttFactory.CreateMqttClient();
|
||||||
{
|
var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("broker.hivemq.com").Build();
|
||||||
var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("broker.hivemq.com").Build();
|
|
||||||
|
|
||||||
await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None);
|
await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None);
|
||||||
|
|
||||||
// Create the subscribe options including several topics with different options.
|
// 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.
|
// It is also possible to all of these topics using a dedicated call of _SubscribeAsync_ per topic.
|
||||||
var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder()
|
var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder()
|
||||||
.WithTopicTemplate(sampleTemplate.WithParameter("id", "1"))
|
.WithTopicTemplate(sampleTemplate.WithParameter("id", "1"))
|
||||||
.WithTopicTemplate(sampleTemplate.WithParameter("id", "2"), noLocal: true)
|
.WithTopicTemplate(sampleTemplate.WithParameter("id", "2"), noLocal: true)
|
||||||
.WithTopicTemplate(sampleTemplate.WithParameter("id", "3"), retainHandling: MqttRetainHandling.SendAtSubscribe)
|
.WithTopicTemplate(sampleTemplate.WithParameter("id", "3"), retainHandling: MqttRetainHandling.SendAtSubscribe)
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
var response = await mqttClient.SubscribeAsync(mqttSubscribeOptions, CancellationToken.None);
|
var response = await mqttClient.SubscribeAsync(mqttSubscribeOptions, CancellationToken.None);
|
||||||
|
|
||||||
Console.WriteLine("MQTT client subscribed to topics.");
|
Console.WriteLine("MQTT client subscribed to topics.");
|
||||||
|
|
||||||
// The response contains additional data sent by the server after subscribing.
|
// The response contains additional data sent by the server after subscribing.
|
||||||
response.DumpToConsole();
|
response.DumpToConsole();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task Subscribe_Topic()
|
public static async Task Subscribe_Topic()
|
||||||
@@ -133,21 +127,19 @@ public static class Client_Subscribe_Samples
|
|||||||
|
|
||||||
var mqttFactory = new MqttClientFactory();
|
var mqttFactory = new MqttClientFactory();
|
||||||
|
|
||||||
using (var mqttClient = mqttFactory.CreateMqttClient())
|
using var mqttClient = mqttFactory.CreateMqttClient();
|
||||||
{
|
var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("broker.hivemq.com").Build();
|
||||||
var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("broker.hivemq.com").Build();
|
|
||||||
|
|
||||||
await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None);
|
await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None);
|
||||||
|
|
||||||
var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder().WithTopicTemplate(sampleTemplate.WithParameter("id", "1")).Build();
|
var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder().WithTopicTemplate(sampleTemplate.WithParameter("id", "1")).Build();
|
||||||
|
|
||||||
var response = await mqttClient.SubscribeAsync(mqttSubscribeOptions, CancellationToken.None);
|
var response = await mqttClient.SubscribeAsync(mqttSubscribeOptions, CancellationToken.None);
|
||||||
|
|
||||||
Console.WriteLine("MQTT client subscribed to topic.");
|
Console.WriteLine("MQTT client subscribed to topic.");
|
||||||
|
|
||||||
// The response contains additional data sent by the server after subscribing.
|
// The response contains additional data sent by the server after subscribing.
|
||||||
response.DumpToConsole();
|
response.DumpToConsole();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void ConcurrentProcessingDisableAutoAcknowledge(CancellationToken shutdownToken, IMqttClient mqttClient)
|
static void ConcurrentProcessingDisableAutoAcknowledge(CancellationToken shutdownToken, IMqttClient mqttClient)
|
||||||
|
|||||||
@@ -26,17 +26,15 @@ public static class Logger_Samples
|
|||||||
.WithTcpServer("broker.hivemq.com")
|
.WithTcpServer("broker.hivemq.com")
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
using (var mqttClient = mqttFactory.CreateMqttClient())
|
using var mqttClient = mqttFactory.CreateMqttClient();
|
||||||
{
|
await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None);
|
||||||
await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None);
|
|
||||||
|
|
||||||
Console.WriteLine("MQTT client is connected.");
|
Console.WriteLine("MQTT client is connected.");
|
||||||
|
|
||||||
var mqttClientDisconnectOptions = mqttFactory.CreateClientDisconnectOptionsBuilder()
|
var mqttClientDisconnectOptions = mqttFactory.CreateClientDisconnectOptionsBuilder()
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
await mqttClient.DisconnectAsync(mqttClientDisconnectOptions, CancellationToken.None);
|
await mqttClient.DisconnectAsync(mqttClientDisconnectOptions, CancellationToken.None);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task Use_Event_Logger()
|
public static async Task Use_Event_Logger()
|
||||||
@@ -50,7 +48,7 @@ public static class Logger_Samples
|
|||||||
// The logger ID is optional but can be set do distinguish different logger instances.
|
// The logger ID is optional but can be set do distinguish different logger instances.
|
||||||
var mqttEventLogger = new MqttNetEventLogger("MyCustomLogger");
|
var mqttEventLogger = new MqttNetEventLogger("MyCustomLogger");
|
||||||
|
|
||||||
mqttEventLogger.LogMessagePublished += (sender, args) =>
|
mqttEventLogger.LogMessagePublished += (_, args) =>
|
||||||
{
|
{
|
||||||
var output = new StringBuilder();
|
var output = new StringBuilder();
|
||||||
output.AppendLine($">> [{args.LogMessage.Timestamp:O}] [{args.LogMessage.ThreadId}] [{args.LogMessage.Source}] [{args.LogMessage.Level}]: {args.LogMessage.Message}");
|
output.AppendLine($">> [{args.LogMessage.Timestamp:O}] [{args.LogMessage.ThreadId}] [{args.LogMessage.Source}] [{args.LogMessage.Level}]: {args.LogMessage.Message}");
|
||||||
@@ -68,17 +66,15 @@ public static class Logger_Samples
|
|||||||
.WithTcpServer("broker.hivemq.com")
|
.WithTcpServer("broker.hivemq.com")
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
using (var mqttClient = mqttFactory.CreateMqttClient())
|
using var mqttClient = mqttFactory.CreateMqttClient();
|
||||||
{
|
await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None);
|
||||||
await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None);
|
|
||||||
|
|
||||||
Console.WriteLine("MQTT client is connected.");
|
Console.WriteLine("MQTT client is connected.");
|
||||||
|
|
||||||
var mqttClientDisconnectOptions = mqttFactory.CreateClientDisconnectOptionsBuilder()
|
var mqttClientDisconnectOptions = mqttFactory.CreateClientDisconnectOptionsBuilder()
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
await mqttClient.DisconnectAsync(mqttClientDisconnectOptions, CancellationToken.None);
|
await mqttClient.DisconnectAsync(mqttClientDisconnectOptions, CancellationToken.None);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class MyLogger : IMqttNetLogger
|
sealed class MyLogger : IMqttNetLogger
|
||||||
|
|||||||
@@ -20,23 +20,21 @@ public static class PackageInspection_Samples
|
|||||||
|
|
||||||
var mqttFactory = new MqttClientFactory();
|
var mqttFactory = new MqttClientFactory();
|
||||||
|
|
||||||
using (var mqttClient = mqttFactory.CreateMqttClient())
|
using var mqttClient = mqttFactory.CreateMqttClient();
|
||||||
{
|
var mqttClientOptions = mqttFactory.CreateClientOptionsBuilder()
|
||||||
var mqttClientOptions = mqttFactory.CreateClientOptionsBuilder()
|
.WithTcpServer("broker.hivemq.com")
|
||||||
.WithTcpServer("broker.hivemq.com")
|
.Build();
|
||||||
.Build();
|
|
||||||
|
|
||||||
mqttClient.InspectPacketAsync += OnInspectPacket;
|
mqttClient.InspectPacketAsync += OnInspectPacket;
|
||||||
|
|
||||||
await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None);
|
await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None);
|
||||||
|
|
||||||
Console.WriteLine("MQTT client is connected.");
|
Console.WriteLine("MQTT client is connected.");
|
||||||
|
|
||||||
var mqttClientDisconnectOptions = mqttFactory.CreateClientDisconnectOptionsBuilder()
|
var mqttClientDisconnectOptions = mqttFactory.CreateClientDisconnectOptionsBuilder()
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
await mqttClient.DisconnectAsync(mqttClientDisconnectOptions, CancellationToken.None);
|
await mqttClient.DisconnectAsync(mqttClientDisconnectOptions, CancellationToken.None);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static Task OnInspectPacket(InspectMqttPacketEventArgs eventArgs)
|
static Task OnInspectPacket(InspectMqttPacketEventArgs eventArgs)
|
||||||
|
|||||||
@@ -26,23 +26,21 @@ public static class RpcClient_Samples
|
|||||||
// The RPC client is an addon for the existing client. So we need a regular client
|
// The RPC client is an addon for the existing client. So we need a regular client
|
||||||
// which is wrapped later.
|
// which is wrapped later.
|
||||||
|
|
||||||
using (var mqttClient = mqttFactory.CreateMqttClient())
|
using var mqttClient = mqttFactory.CreateMqttClient();
|
||||||
|
var mqttClientOptions = new MqttClientOptionsBuilder()
|
||||||
|
.WithTcpServer("broker.hivemq.com")
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
await mqttClient.ConnectAsync(mqttClientOptions);
|
||||||
|
|
||||||
|
using (var mqttRpcClient = mqttFactory.CreateMqttRpcClient(mqttClient))
|
||||||
{
|
{
|
||||||
var mqttClientOptions = new MqttClientOptionsBuilder()
|
// Access to a fully featured application message is not supported for RPC calls!
|
||||||
.WithTcpServer("broker.hivemq.com")
|
// The method will throw an exception when the response was not received in time.
|
||||||
.Build();
|
await mqttRpcClient.ExecuteAsync(TimeSpan.FromSeconds(2), "ping", "", MqttQualityOfServiceLevel.AtMostOnce);
|
||||||
|
|
||||||
await mqttClient.ConnectAsync(mqttClientOptions);
|
|
||||||
|
|
||||||
using (var mqttRpcClient = mqttFactory.CreateMqttRpcClient(mqttClient))
|
|
||||||
{
|
|
||||||
// Access to a fully featured application message is not supported for RPC calls!
|
|
||||||
// The method will throw an exception when the response was not received in time.
|
|
||||||
await mqttRpcClient.ExecuteAsync(TimeSpan.FromSeconds(2), "ping", "", MqttQualityOfServiceLevel.AtMostOnce);
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.WriteLine("The RPC call was successful.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Console.WriteLine("The RPC call was successful.");
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -25,28 +25,26 @@ public static class Server_Diagnostics_Samples
|
|||||||
var mqttServerFactory = new MqttServerFactory();
|
var mqttServerFactory = new MqttServerFactory();
|
||||||
var mqttServerOptions = new MqttServerOptionsBuilder().WithDefaultEndpoint().Build();
|
var mqttServerOptions = new MqttServerOptionsBuilder().WithDefaultEndpoint().Build();
|
||||||
|
|
||||||
using (var mqttServer = mqttServerFactory.CreateMqttServer(mqttServerOptions))
|
using var mqttServer = mqttServerFactory.CreateMqttServer(mqttServerOptions);
|
||||||
|
// Attach the event handler.
|
||||||
|
mqttServer.ClientAcknowledgedPublishPacketAsync += e =>
|
||||||
{
|
{
|
||||||
// Attach the event handler.
|
Console.WriteLine($"Client '{e.ClientId}' acknowledged packet {e.PublishPacket.PacketIdentifier} with topic '{e.PublishPacket.Topic}'");
|
||||||
mqttServer.ClientAcknowledgedPublishPacketAsync += e =>
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Client '{e.ClientId}' acknowledged packet {e.PublishPacket.PacketIdentifier} with topic '{e.PublishPacket.Topic}'");
|
|
||||||
|
|
||||||
// It is also possible to read additional data from the client response. This requires casting the response packet.
|
// It is also possible to read additional data from the client response. This requires casting the response packet.
|
||||||
var qos1AcknowledgePacket = e.AcknowledgePacket as MqttPubAckPacket;
|
var qos1AcknowledgePacket = e.AcknowledgePacket as MqttPubAckPacket;
|
||||||
Console.WriteLine($"QoS 1 reason code: {qos1AcknowledgePacket?.ReasonCode}");
|
Console.WriteLine($"QoS 1 reason code: {qos1AcknowledgePacket?.ReasonCode}");
|
||||||
|
|
||||||
var qos2AcknowledgePacket = e.AcknowledgePacket as MqttPubCompPacket;
|
var qos2AcknowledgePacket = e.AcknowledgePacket as MqttPubCompPacket;
|
||||||
Console.WriteLine($"QoS 2 reason code: {qos1AcknowledgePacket?.ReasonCode}");
|
Console.WriteLine($"QoS 2 reason code: {qos2AcknowledgePacket?.ReasonCode}");
|
||||||
return CompletedTask.Instance;
|
return CompletedTask.Instance;
|
||||||
};
|
};
|
||||||
|
|
||||||
await mqttServer.StartAsync();
|
await mqttServer.StartAsync();
|
||||||
|
|
||||||
Console.WriteLine("Press Enter to exit.");
|
Console.WriteLine("Press Enter to exit.");
|
||||||
Console.ReadLine();
|
Console.ReadLine();
|
||||||
|
|
||||||
await mqttServer.StopAsync();
|
await mqttServer.StopAsync();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -25,23 +25,21 @@ public static class Server_Intercepting_Samples
|
|||||||
var mqttServerFactory = new MqttServerFactory();
|
var mqttServerFactory = new MqttServerFactory();
|
||||||
var mqttServerOptions = new MqttServerOptionsBuilder().WithDefaultEndpoint().Build();
|
var mqttServerOptions = new MqttServerOptionsBuilder().WithDefaultEndpoint().Build();
|
||||||
|
|
||||||
using (var mqttServer = mqttServerFactory.CreateMqttServer(mqttServerOptions))
|
using var mqttServer = mqttServerFactory.CreateMqttServer(mqttServerOptions);
|
||||||
|
mqttServer.InterceptingPublishAsync += args =>
|
||||||
{
|
{
|
||||||
mqttServer.InterceptingPublishAsync += args =>
|
// Here we only change the topic of the received application message.
|
||||||
{
|
// but also changing the payload etc. is required. Changing the QoS after
|
||||||
// Here we only change the topic of the received application message.
|
// transmitting is not supported and makes no sense at all.
|
||||||
// but also changing the payload etc. is required. Changing the QoS after
|
args.ApplicationMessage.Topic += "/manipulated";
|
||||||
// transmitting is not supported and makes no sense at all.
|
|
||||||
args.ApplicationMessage.Topic += "/manipulated";
|
|
||||||
|
|
||||||
return CompletedTask.Instance;
|
return CompletedTask.Instance;
|
||||||
};
|
};
|
||||||
|
|
||||||
await mqttServer.StartAsync();
|
await mqttServer.StartAsync();
|
||||||
|
|
||||||
Console.WriteLine("Press Enter to exit.");
|
Console.WriteLine("Press Enter to exit.");
|
||||||
Console.ReadLine();
|
Console.ReadLine();
|
||||||
await mqttServer.StopAsync();
|
await mqttServer.StopAsync();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -29,64 +29,62 @@ public static class Server_Retained_Messages_Samples
|
|||||||
// Due to security reasons the "default" endpoint (which is unencrypted) is not enabled by default!
|
// Due to security reasons the "default" endpoint (which is unencrypted) is not enabled by default!
|
||||||
var mqttServerOptions = mqttServerFactory.CreateServerOptionsBuilder().WithDefaultEndpoint().Build();
|
var mqttServerOptions = mqttServerFactory.CreateServerOptionsBuilder().WithDefaultEndpoint().Build();
|
||||||
|
|
||||||
using (var server = mqttServerFactory.CreateMqttServer(mqttServerOptions))
|
using var server = mqttServerFactory.CreateMqttServer(mqttServerOptions);
|
||||||
|
// Make sure that the server will load the retained messages.
|
||||||
|
server.LoadingRetainedMessageAsync += async eventArgs =>
|
||||||
{
|
{
|
||||||
// Make sure that the server will load the retained messages.
|
try
|
||||||
server.LoadingRetainedMessageAsync += async eventArgs =>
|
|
||||||
{
|
{
|
||||||
try
|
var models = await JsonSerializer.DeserializeAsync<List<MqttRetainedMessageModel>>(File.OpenRead(storePath)) ?? new List<MqttRetainedMessageModel>();
|
||||||
{
|
var retainedMessages = models.Select(m => m.ToApplicationMessage()).ToList();
|
||||||
var models = await JsonSerializer.DeserializeAsync<List<MqttRetainedMessageModel>>(File.OpenRead(storePath)) ?? new List<MqttRetainedMessageModel>();
|
|
||||||
var retainedMessages = models.Select(m => m.ToApplicationMessage()).ToList();
|
|
||||||
|
|
||||||
eventArgs.LoadedRetainedMessages = retainedMessages;
|
eventArgs.LoadedRetainedMessages = retainedMessages;
|
||||||
Console.WriteLine("Retained messages loaded.");
|
Console.WriteLine("Retained messages loaded.");
|
||||||
}
|
}
|
||||||
catch (FileNotFoundException)
|
catch (FileNotFoundException)
|
||||||
{
|
|
||||||
// Ignore because nothing is stored yet.
|
|
||||||
Console.WriteLine("No retained messages stored yet.");
|
|
||||||
}
|
|
||||||
catch (Exception exception)
|
|
||||||
{
|
|
||||||
Console.WriteLine(exception);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Make sure to persist the changed retained messages.
|
|
||||||
server.RetainedMessageChangedAsync += async eventArgs =>
|
|
||||||
{
|
{
|
||||||
try
|
// Ignore because nothing is stored yet.
|
||||||
{
|
Console.WriteLine("No retained messages stored yet.");
|
||||||
// This sample uses the property _StoredRetainedMessages_ which will contain all(!) retained messages.
|
}
|
||||||
// The event args also contain the affected retained message (property ChangedRetainedMessage). This can be
|
catch (Exception exception)
|
||||||
// used to write all retained messages to dedicated files etc. Then all files must be loaded and a full list
|
|
||||||
// of retained messages must be provided in the loaded event.
|
|
||||||
|
|
||||||
var models = eventArgs.StoredRetainedMessages.Select(MqttRetainedMessageModel.Create);
|
|
||||||
|
|
||||||
var buffer = JsonSerializer.SerializeToUtf8Bytes(models);
|
|
||||||
await File.WriteAllBytesAsync(storePath, buffer);
|
|
||||||
Console.WriteLine("Retained messages saved.");
|
|
||||||
}
|
|
||||||
catch (Exception exception)
|
|
||||||
{
|
|
||||||
Console.WriteLine(exception);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Make sure to clear the retained messages when they are all deleted via API.
|
|
||||||
server.RetainedMessagesClearedAsync += _ =>
|
|
||||||
{
|
{
|
||||||
File.Delete(storePath);
|
Console.WriteLine(exception);
|
||||||
return Task.CompletedTask;
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
await server.StartAsync();
|
// Make sure to persist the changed retained messages.
|
||||||
|
server.RetainedMessageChangedAsync += async eventArgs =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// This sample uses the property _StoredRetainedMessages_ which will contain all(!) retained messages.
|
||||||
|
// The event args also contain the affected retained message (property ChangedRetainedMessage). This can be
|
||||||
|
// used to write all retained messages to dedicated files etc. Then all files must be loaded and a full list
|
||||||
|
// of retained messages must be provided in the loaded event.
|
||||||
|
|
||||||
Console.WriteLine("Press Enter to exit.");
|
var models = eventArgs.StoredRetainedMessages.Select(MqttRetainedMessageModel.Create);
|
||||||
Console.ReadLine();
|
|
||||||
}
|
var buffer = JsonSerializer.SerializeToUtf8Bytes(models);
|
||||||
|
await File.WriteAllBytesAsync(storePath, buffer);
|
||||||
|
Console.WriteLine("Retained messages saved.");
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
Console.WriteLine(exception);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Make sure to clear the retained messages when they are all deleted via API.
|
||||||
|
server.RetainedMessagesClearedAsync += _ =>
|
||||||
|
{
|
||||||
|
File.Delete(storePath);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
};
|
||||||
|
|
||||||
|
await server.StartAsync();
|
||||||
|
|
||||||
|
Console.WriteLine("Press Enter to exit.");
|
||||||
|
Console.ReadLine();
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class MqttRetainedMessageModel
|
sealed class MqttRetainedMessageModel
|
||||||
|
|||||||
@@ -22,17 +22,15 @@ public static class Server_Simple_Samples
|
|||||||
* See _Run_Minimal_Server_ for more information.
|
* See _Run_Minimal_Server_ for more information.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
using (var mqttServer = await StartMqttServer())
|
using var mqttServer = await StartMqttServer();
|
||||||
{
|
// Let the client connect.
|
||||||
// Let the client connect.
|
await Task.Delay(TimeSpan.FromSeconds(5));
|
||||||
await Task.Delay(TimeSpan.FromSeconds(5));
|
|
||||||
|
|
||||||
// Now disconnect the client (if connected).
|
// Now disconnect the client (if connected).
|
||||||
var affectedClient = (await mqttServer.GetClientsAsync()).FirstOrDefault(c => c.Id == "MyClient");
|
var affectedClient = (await mqttServer.GetClientsAsync()).FirstOrDefault(c => c.Id == "MyClient");
|
||||||
if (affectedClient != null)
|
if (affectedClient != null)
|
||||||
{
|
{
|
||||||
await affectedClient.DisconnectAsync();
|
await affectedClient.DisconnectAsync();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,18 +42,16 @@ public static class Server_Simple_Samples
|
|||||||
* See _Run_Minimal_Server_ for more information.
|
* See _Run_Minimal_Server_ for more information.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
using (var mqttServer = await StartMqttServer())
|
using var mqttServer = await StartMqttServer();
|
||||||
{
|
// Create a new message using the builder as usual.
|
||||||
// Create a new message using the builder as usual.
|
var message = new MqttApplicationMessageBuilder().WithTopic("HelloWorld").WithPayload("Test").Build();
|
||||||
var message = new MqttApplicationMessageBuilder().WithTopic("HelloWorld").WithPayload("Test").Build();
|
|
||||||
|
|
||||||
// Now inject the new message at the broker.
|
// Now inject the new message at the broker.
|
||||||
await mqttServer.InjectApplicationMessage(
|
await mqttServer.InjectApplicationMessage(
|
||||||
new InjectedMqttApplicationMessage(message)
|
new InjectedMqttApplicationMessage(message)
|
||||||
{
|
{
|
||||||
SenderClientId = "SenderClientId"
|
SenderClientId = "SenderClientId"
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task Run_Minimal_Server()
|
public static async Task Run_Minimal_Server()
|
||||||
@@ -77,16 +73,14 @@ public static class Server_Simple_Samples
|
|||||||
// .WithDefaultEndpointPort(1234)
|
// .WithDefaultEndpointPort(1234)
|
||||||
// .Build();
|
// .Build();
|
||||||
|
|
||||||
using (var mqttServer = mqttServerFactory.CreateMqttServer(mqttServerOptions))
|
using var mqttServer = mqttServerFactory.CreateMqttServer(mqttServerOptions);
|
||||||
{
|
await mqttServer.StartAsync();
|
||||||
await mqttServer.StartAsync();
|
|
||||||
|
|
||||||
Console.WriteLine("Press Enter to exit.");
|
Console.WriteLine("Press Enter to exit.");
|
||||||
Console.ReadLine();
|
Console.ReadLine();
|
||||||
|
|
||||||
// Stop and dispose the MQTT server if it is no longer needed!
|
// Stop and dispose the MQTT server if it is no longer needed!
|
||||||
await mqttServer.StopAsync();
|
await mqttServer.StopAsync();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task Run_Server_With_Logging()
|
public static async Task Run_Server_With_Logging()
|
||||||
@@ -103,16 +97,14 @@ public static class Server_Simple_Samples
|
|||||||
|
|
||||||
var mqttServerOptions = new MqttServerOptionsBuilder().WithDefaultEndpoint().Build();
|
var mqttServerOptions = new MqttServerOptionsBuilder().WithDefaultEndpoint().Build();
|
||||||
|
|
||||||
using (var mqttServer = mqttServerFactory.CreateMqttServer(mqttServerOptions))
|
using var mqttServer = mqttServerFactory.CreateMqttServer(mqttServerOptions);
|
||||||
{
|
await mqttServer.StartAsync();
|
||||||
await mqttServer.StartAsync();
|
|
||||||
|
|
||||||
Console.WriteLine("Press Enter to exit.");
|
Console.WriteLine("Press Enter to exit.");
|
||||||
Console.ReadLine();
|
Console.ReadLine();
|
||||||
|
|
||||||
// Stop and dispose the MQTT server if it is no longer needed!
|
// Stop and dispose the MQTT server if it is no longer needed!
|
||||||
await mqttServer.StopAsync();
|
await mqttServer.StopAsync();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task Validating_Connections()
|
public static async Task Validating_Connections()
|
||||||
@@ -127,37 +119,35 @@ public static class Server_Simple_Samples
|
|||||||
|
|
||||||
var mqttServerOptions = new MqttServerOptionsBuilder().WithDefaultEndpoint().Build();
|
var mqttServerOptions = new MqttServerOptionsBuilder().WithDefaultEndpoint().Build();
|
||||||
|
|
||||||
using (var mqttServer = mqttServerFactory.CreateMqttServer(mqttServerOptions))
|
using var mqttServer = mqttServerFactory.CreateMqttServer(mqttServerOptions);
|
||||||
|
// Setup connection validation before starting the server so that there is
|
||||||
|
// no change to connect without valid credentials.
|
||||||
|
mqttServer.ValidatingConnectionAsync += e =>
|
||||||
{
|
{
|
||||||
// Setup connection validation before starting the server so that there is
|
if (e.ClientId != "ValidClientId")
|
||||||
// no change to connect without valid credentials.
|
|
||||||
mqttServer.ValidatingConnectionAsync += e =>
|
|
||||||
{
|
{
|
||||||
if (e.ClientId != "ValidClientId")
|
e.ReasonCode = MqttConnectReasonCode.ClientIdentifierNotValid;
|
||||||
{
|
}
|
||||||
e.ReasonCode = MqttConnectReasonCode.ClientIdentifierNotValid;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.UserName != "ValidUser")
|
if (e.UserName != "ValidUser")
|
||||||
{
|
{
|
||||||
e.ReasonCode = MqttConnectReasonCode.BadUserNameOrPassword;
|
e.ReasonCode = MqttConnectReasonCode.BadUserNameOrPassword;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.Password != "SecretPassword")
|
if (e.Password != "SecretPassword")
|
||||||
{
|
{
|
||||||
e.ReasonCode = MqttConnectReasonCode.BadUserNameOrPassword;
|
e.ReasonCode = MqttConnectReasonCode.BadUserNameOrPassword;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
};
|
};
|
||||||
|
|
||||||
await mqttServer.StartAsync();
|
await mqttServer.StartAsync();
|
||||||
|
|
||||||
Console.WriteLine("Press Enter to exit.");
|
Console.WriteLine("Press Enter to exit.");
|
||||||
Console.ReadLine();
|
Console.ReadLine();
|
||||||
|
|
||||||
await mqttServer.StopAsync();
|
await mqttServer.StopAsync();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async Task<MqttServer> StartMqttServer()
|
static async Task<MqttServer> StartMqttServer()
|
||||||
|
|||||||
@@ -31,16 +31,14 @@ public static class Server_TLS_Samples
|
|||||||
|
|
||||||
var mqttServerOptions = new MqttServerOptionsBuilder().WithEncryptionCertificate(certificate).WithEncryptedEndpoint().Build();
|
var mqttServerOptions = new MqttServerOptionsBuilder().WithEncryptionCertificate(certificate).WithEncryptedEndpoint().Build();
|
||||||
|
|
||||||
using (var mqttServer = mqttServerFactory.CreateMqttServer(mqttServerOptions))
|
using var mqttServer = mqttServerFactory.CreateMqttServer(mqttServerOptions);
|
||||||
{
|
await mqttServer.StartAsync();
|
||||||
await mqttServer.StartAsync();
|
|
||||||
|
|
||||||
Console.WriteLine("Press Enter to exit.");
|
Console.WriteLine("Press Enter to exit.");
|
||||||
Console.ReadLine();
|
Console.ReadLine();
|
||||||
|
|
||||||
// Stop and dispose the MQTT server if it is no longer needed!
|
// Stop and dispose the MQTT server if it is no longer needed!
|
||||||
await mqttServer.StopAsync();
|
await mqttServer.StopAsync();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static X509Certificate2 CreateSelfSignedCertificate(string oid)
|
static X509Certificate2 CreateSelfSignedCertificate(string oid)
|
||||||
@@ -50,26 +48,22 @@ public static class Server_TLS_Samples
|
|||||||
sanBuilder.AddIpAddress(IPAddress.IPv6Loopback);
|
sanBuilder.AddIpAddress(IPAddress.IPv6Loopback);
|
||||||
sanBuilder.AddDnsName("localhost");
|
sanBuilder.AddDnsName("localhost");
|
||||||
|
|
||||||
using (var rsa = RSA.Create())
|
using var rsa = RSA.Create();
|
||||||
{
|
var certRequest = new CertificateRequest("CN=localhost", rsa, HashAlgorithmName.SHA512, RSASignaturePadding.Pkcs1);
|
||||||
var certRequest = new CertificateRequest("CN=localhost", rsa, HashAlgorithmName.SHA512, RSASignaturePadding.Pkcs1);
|
|
||||||
|
|
||||||
certRequest.CertificateExtensions.Add(
|
certRequest.CertificateExtensions.Add(
|
||||||
new X509KeyUsageExtension(X509KeyUsageFlags.DataEncipherment | X509KeyUsageFlags.KeyEncipherment | X509KeyUsageFlags.DigitalSignature, false));
|
new X509KeyUsageExtension(X509KeyUsageFlags.DataEncipherment | X509KeyUsageFlags.KeyEncipherment | X509KeyUsageFlags.DigitalSignature, false));
|
||||||
|
|
||||||
certRequest.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(new OidCollection { new(oid) }, false));
|
certRequest.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(new OidCollection { new(oid) }, false));
|
||||||
|
|
||||||
certRequest.CertificateExtensions.Add(sanBuilder.Build());
|
certRequest.CertificateExtensions.Add(sanBuilder.Build());
|
||||||
|
|
||||||
using (var certificate = certRequest.CreateSelfSigned(DateTimeOffset.Now.AddMinutes(-10), DateTimeOffset.Now.AddMinutes(10)))
|
using var certificate = certRequest.CreateSelfSigned(DateTimeOffset.Now.AddMinutes(-10), DateTimeOffset.Now.AddMinutes(10));
|
||||||
{
|
var pfxCertificate = new X509Certificate2(
|
||||||
var pfxCertificate = new X509Certificate2(
|
certificate.Export(X509ContentType.Pfx),
|
||||||
certificate.Export(X509ContentType.Pfx),
|
(string)null!,
|
||||||
(string)null!,
|
X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.Exportable);
|
||||||
X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.Exportable);
|
|
||||||
|
|
||||||
return pfxCertificate;
|
return pfxCertificate;
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,20 +4,19 @@
|
|||||||
|
|
||||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
|
||||||
namespace MQTTnet.AspTestApp.Pages
|
namespace MQTTnet.AspTestApp.Pages;
|
||||||
|
|
||||||
|
public class IndexModel : PageModel
|
||||||
{
|
{
|
||||||
public class IndexModel : PageModel
|
readonly ILogger<IndexModel> _logger;
|
||||||
|
|
||||||
|
public IndexModel(ILogger<IndexModel> logger)
|
||||||
{
|
{
|
||||||
private readonly ILogger<IndexModel> _logger;
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
public IndexModel(ILogger<IndexModel> logger)
|
public void OnGet()
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger.LogDebug("OnGet");
|
||||||
}
|
|
||||||
|
|
||||||
public void OnGet()
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -33,10 +33,8 @@ public static class ApplicationBuilderExtensions
|
|||||||
}
|
}
|
||||||
|
|
||||||
var adapter = app.ApplicationServices.GetRequiredService<MqttWebSocketServerAdapter>();
|
var adapter = app.ApplicationServices.GetRequiredService<MqttWebSocketServerAdapter>();
|
||||||
using (var webSocket = await context.WebSockets.AcceptWebSocketAsync(subProtocol).ConfigureAwait(false))
|
using var webSocket = await context.WebSockets.AcceptWebSocketAsync(subProtocol).ConfigureAwait(false);
|
||||||
{
|
await adapter.RunWebSocketConnectionAsync(webSocket, context);
|
||||||
await adapter.RunWebSocketConnectionAsync(webSocket, context);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
|
|||||||
@@ -4,13 +4,12 @@
|
|||||||
|
|
||||||
using Microsoft.AspNetCore.Connections;
|
using Microsoft.AspNetCore.Connections;
|
||||||
|
|
||||||
namespace MQTTnet.AspNetCore
|
namespace MQTTnet.AspNetCore;
|
||||||
|
|
||||||
|
public static class ConnectionBuilderExtensions
|
||||||
{
|
{
|
||||||
public static class ConnectionBuilderExtensions
|
public static IConnectionBuilder UseMqtt(this IConnectionBuilder builder)
|
||||||
{
|
{
|
||||||
public static IConnectionBuilder UseMqtt(this IConnectionBuilder builder)
|
return builder.UseConnectionHandler<MqttConnectionHandler>();
|
||||||
{
|
|
||||||
return builder.UseConnectionHandler<MqttConnectionHandler>();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,19 +6,17 @@ using System;
|
|||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
|
|
||||||
namespace MQTTnet.AspNetCore
|
namespace MQTTnet.AspNetCore;
|
||||||
{
|
|
||||||
public static class EndpointRouterExtensions
|
|
||||||
{
|
|
||||||
public static void MapMqtt(this IEndpointRouteBuilder endpoints, string pattern)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(endpoints);
|
|
||||||
|
|
||||||
endpoints.MapConnectionHandler<MqttConnectionHandler>(pattern, options =>
|
public static class EndpointRouterExtensions
|
||||||
{
|
{
|
||||||
options.WebSockets.SubProtocolSelector = MqttSubProtocolSelector.SelectSubProtocol;
|
public static void MapMqtt(this IEndpointRouteBuilder endpoints, string pattern)
|
||||||
});
|
{
|
||||||
}
|
ArgumentNullException.ThrowIfNull(endpoints);
|
||||||
|
|
||||||
|
endpoints.MapConnectionHandler<MqttConnectionHandler>(pattern, options =>
|
||||||
|
{
|
||||||
|
options.WebSockets.SubProtocolSelector = MqttSubProtocolSelector.SelectSubProtocol;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,27 +7,26 @@ using MQTTnet.Adapter;
|
|||||||
using MQTTnet.Diagnostics.Logger;
|
using MQTTnet.Diagnostics.Logger;
|
||||||
using MQTTnet.Formatter;
|
using MQTTnet.Formatter;
|
||||||
|
|
||||||
namespace MQTTnet.AspNetCore
|
namespace MQTTnet.AspNetCore;
|
||||||
|
|
||||||
|
public sealed class MqttClientConnectionContextFactory : IMqttClientAdapterFactory
|
||||||
{
|
{
|
||||||
public sealed class MqttClientConnectionContextFactory : IMqttClientAdapterFactory
|
public IMqttChannelAdapter CreateClientAdapter(MqttClientOptions options, MqttPacketInspector packetInspector, IMqttNetLogger logger)
|
||||||
{
|
{
|
||||||
public IMqttChannelAdapter CreateClientAdapter(MqttClientOptions options, MqttPacketInspector packetInspector, IMqttNetLogger logger)
|
if (options == null) throw new ArgumentNullException(nameof(options));
|
||||||
|
|
||||||
|
switch (options.ChannelOptions)
|
||||||
{
|
{
|
||||||
if (options == null) throw new ArgumentNullException(nameof(options));
|
case MqttClientTcpOptions tcpOptions:
|
||||||
|
|
||||||
switch (options.ChannelOptions)
|
|
||||||
{
|
{
|
||||||
case MqttClientTcpOptions tcpOptions:
|
var tcpConnection = new SocketConnection(tcpOptions.RemoteEndpoint);
|
||||||
{
|
|
||||||
var tcpConnection = new SocketConnection(tcpOptions.RemoteEndpoint);
|
|
||||||
|
|
||||||
var formatter = new MqttPacketFormatterAdapter(options.ProtocolVersion, new MqttBufferWriter(4096, 65535));
|
var formatter = new MqttPacketFormatterAdapter(options.ProtocolVersion, new MqttBufferWriter(4096, 65535));
|
||||||
return new MqttConnectionContext(formatter, tcpConnection);
|
return new MqttConnectionContext(formatter, tcpConnection);
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
{
|
{
|
||||||
throw new NotSupportedException();
|
throw new NotSupportedException();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,13 +33,11 @@ public sealed class MqttConnectionHandler : ConnectionHandler, IMqttServerAdapte
|
|||||||
}
|
}
|
||||||
|
|
||||||
var formatter = new MqttPacketFormatterAdapter(new MqttBufferWriter(_serverOptions.WriterBufferSize, _serverOptions.WriterBufferSizeMax));
|
var formatter = new MqttPacketFormatterAdapter(new MqttBufferWriter(_serverOptions.WriterBufferSize, _serverOptions.WriterBufferSizeMax));
|
||||||
using (var adapter = new MqttConnectionContext(formatter, connection))
|
using var adapter = new MqttConnectionContext(formatter, connection);
|
||||||
|
var clientHandler = ClientHandler;
|
||||||
|
if (clientHandler != null)
|
||||||
{
|
{
|
||||||
var clientHandler = ClientHandler;
|
await clientHandler(adapter).ConfigureAwait(false);
|
||||||
if (clientHandler != null)
|
|
||||||
{
|
|
||||||
await clientHandler(adapter).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,10 +43,8 @@ public sealed class MqttWebSocketServerAdapter : IMqttServerAdapter
|
|||||||
var formatter = new MqttPacketFormatterAdapter(new MqttBufferWriter(4096, 65535));
|
var formatter = new MqttPacketFormatterAdapter(new MqttBufferWriter(4096, 65535));
|
||||||
var channel = new MqttWebSocketChannel(webSocket, remoteEndPoint, isSecureConnection, clientCertificate);
|
var channel = new MqttWebSocketChannel(webSocket, remoteEndPoint, isSecureConnection, clientCertificate);
|
||||||
|
|
||||||
using (var channelAdapter = new MqttChannelAdapter(channel, formatter, _logger))
|
using var channelAdapter = new MqttChannelAdapter(channel, formatter, _logger);
|
||||||
{
|
await clientHandler(channelAdapter).ConfigureAwait(false);
|
||||||
await clientHandler(channelAdapter).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
|
|||||||
@@ -12,34 +12,28 @@ using System.Threading.Tasks;
|
|||||||
|
|
||||||
namespace MQTTnet.AspNetCore;
|
namespace MQTTnet.AspNetCore;
|
||||||
|
|
||||||
public class SocketAwaitable : ICriticalNotifyCompletion
|
public class SocketAwaitable(PipeScheduler ioScheduler) : ICriticalNotifyCompletion
|
||||||
{
|
{
|
||||||
static readonly Action _callbackCompleted = () =>
|
static readonly Action CallbackCompleted = () =>
|
||||||
{
|
{
|
||||||
};
|
};
|
||||||
|
|
||||||
readonly PipeScheduler _ioScheduler;
|
|
||||||
int _bytesTransferred;
|
int _bytesTransferred;
|
||||||
|
|
||||||
Action _callback;
|
Action _callback;
|
||||||
SocketError _error;
|
SocketError _error;
|
||||||
|
|
||||||
public SocketAwaitable(PipeScheduler ioScheduler)
|
public bool IsCompleted => ReferenceEquals(_callback, CallbackCompleted);
|
||||||
{
|
|
||||||
_ioScheduler = ioScheduler;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsCompleted => ReferenceEquals(_callback, _callbackCompleted);
|
|
||||||
|
|
||||||
public void Complete(int bytesTransferred, SocketError socketError)
|
public void Complete(int bytesTransferred, SocketError socketError)
|
||||||
{
|
{
|
||||||
_error = socketError;
|
_error = socketError;
|
||||||
_bytesTransferred = bytesTransferred;
|
_bytesTransferred = bytesTransferred;
|
||||||
var continuation = Interlocked.Exchange(ref _callback, _callbackCompleted);
|
var continuation = Interlocked.Exchange(ref _callback, CallbackCompleted);
|
||||||
|
|
||||||
if (continuation != null)
|
if (continuation != null)
|
||||||
{
|
{
|
||||||
_ioScheduler.Schedule(state => ((Action)state)(), continuation);
|
ioScheduler.Schedule(state => ((Action)state)(), continuation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,7 +44,7 @@ public class SocketAwaitable : ICriticalNotifyCompletion
|
|||||||
|
|
||||||
public int GetResult()
|
public int GetResult()
|
||||||
{
|
{
|
||||||
Debug.Assert(ReferenceEquals(_callback, _callbackCompleted));
|
Debug.Assert(ReferenceEquals(_callback, CallbackCompleted));
|
||||||
|
|
||||||
_callback = null;
|
_callback = null;
|
||||||
|
|
||||||
@@ -64,7 +58,7 @@ public class SocketAwaitable : ICriticalNotifyCompletion
|
|||||||
|
|
||||||
public void OnCompleted(Action continuation)
|
public void OnCompleted(Action continuation)
|
||||||
{
|
{
|
||||||
if (ReferenceEquals(_callback, _callbackCompleted) || ReferenceEquals(Interlocked.CompareExchange(ref _callback, continuation, null), _callbackCompleted))
|
if (ReferenceEquals(_callback, CallbackCompleted) || ReferenceEquals(Interlocked.CompareExchange(ref _callback, continuation, null), CallbackCompleted))
|
||||||
{
|
{
|
||||||
Task.Run(continuation);
|
Task.Run(continuation);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,11 +18,11 @@ namespace MQTTnet.AspNetCore;
|
|||||||
public sealed class SocketConnection : ConnectionContext
|
public sealed class SocketConnection : ConnectionContext
|
||||||
{
|
{
|
||||||
readonly EndPoint _endPoint;
|
readonly EndPoint _endPoint;
|
||||||
|
|
||||||
volatile bool _aborted;
|
volatile bool _aborted;
|
||||||
IDuplexPipe _application;
|
IDuplexPipe _application;
|
||||||
SocketReceiver _receiver;
|
SocketReceiver _receiver;
|
||||||
SocketSender _sender;
|
SocketSender _sender;
|
||||||
|
|
||||||
Socket _socket;
|
Socket _socket;
|
||||||
|
|
||||||
public SocketConnection(EndPoint endPoint)
|
public SocketConnection(EndPoint endPoint)
|
||||||
@@ -40,10 +40,13 @@ public sealed class SocketConnection : ConnectionContext
|
|||||||
}
|
}
|
||||||
|
|
||||||
public override string ConnectionId { get; set; }
|
public override string ConnectionId { get; set; }
|
||||||
|
|
||||||
public override IFeatureCollection Features { get; }
|
public override IFeatureCollection Features { get; }
|
||||||
|
|
||||||
public bool IsConnected { get; private set; }
|
public bool IsConnected { get; private set; }
|
||||||
|
|
||||||
public override IDictionary<object, object> Items { get; set; }
|
public override IDictionary<object, object> Items { get; set; }
|
||||||
|
|
||||||
public override IDuplexPipe Transport { get; set; }
|
public override IDuplexPipe Transport { get; set; }
|
||||||
|
|
||||||
public override ValueTask DisposeAsync()
|
public override ValueTask DisposeAsync()
|
||||||
@@ -78,7 +81,7 @@ public sealed class SocketConnection : ConnectionContext
|
|||||||
IsConnected = true;
|
IsConnected = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Exception ConnectionAborted()
|
static Exception ConnectionAborted()
|
||||||
{
|
{
|
||||||
return new MqttCommunicationException("Connection Aborted");
|
return new MqttCommunicationException("Connection Aborted");
|
||||||
}
|
}
|
||||||
@@ -95,8 +98,7 @@ public sealed class SocketConnection : ConnectionContext
|
|||||||
{
|
{
|
||||||
error = new MqttCommunicationException(ex);
|
error = new MqttCommunicationException(ex);
|
||||||
}
|
}
|
||||||
catch (SocketException ex) when (ex.SocketErrorCode == SocketError.OperationAborted || ex.SocketErrorCode == SocketError.ConnectionAborted ||
|
catch (SocketException ex) when (ex.SocketErrorCode is SocketError.OperationAborted or SocketError.ConnectionAborted or SocketError.Interrupted or SocketError.InvalidArgument)
|
||||||
ex.SocketErrorCode == SocketError.Interrupted || ex.SocketErrorCode == SocketError.InvalidArgument)
|
|
||||||
{
|
{
|
||||||
if (!_aborted)
|
if (!_aborted)
|
||||||
{
|
{
|
||||||
@@ -123,10 +125,10 @@ public sealed class SocketConnection : ConnectionContext
|
|||||||
{
|
{
|
||||||
if (_aborted)
|
if (_aborted)
|
||||||
{
|
{
|
||||||
error = error ?? ConnectionAborted();
|
error ??= ConnectionAborted();
|
||||||
}
|
}
|
||||||
|
|
||||||
_application.Output.Complete(error);
|
await _application.Output.CompleteAsync(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,57 +9,54 @@ using BenchmarkDotNet.Attributes;
|
|||||||
using BenchmarkDotNet.Jobs;
|
using BenchmarkDotNet.Jobs;
|
||||||
using MQTTnet.Internal;
|
using MQTTnet.Internal;
|
||||||
|
|
||||||
namespace MQTTnet.Benchmarks
|
namespace MQTTnet.Benchmarks;
|
||||||
|
|
||||||
|
[SimpleJob(RuntimeMoniker.Net60)]
|
||||||
|
[MemoryDiagnoser]
|
||||||
|
public class AsyncLockBenchmark : BaseBenchmark
|
||||||
{
|
{
|
||||||
[SimpleJob(RuntimeMoniker.Net60)]
|
[Benchmark]
|
||||||
[MemoryDiagnoser]
|
public async Task Synchronize_100_Tasks()
|
||||||
public class AsyncLockBenchmark : BaseBenchmark
|
|
||||||
{
|
{
|
||||||
[Benchmark]
|
const int tasksCount = 100;
|
||||||
public async Task Synchronize_100_Tasks()
|
|
||||||
|
var tasks = new Task[tasksCount];
|
||||||
|
var asyncLock = new AsyncLock();
|
||||||
|
var globalI = 0;
|
||||||
|
|
||||||
|
for (var i = 0; i < tasksCount; i++)
|
||||||
{
|
{
|
||||||
const int tasksCount = 100;
|
tasks[i] = Task.Run(
|
||||||
|
async () =>
|
||||||
var tasks = new Task[tasksCount];
|
{
|
||||||
var asyncLock = new AsyncLock();
|
using (await asyncLock.EnterAsync().ConfigureAwait(false))
|
||||||
var globalI = 0;
|
|
||||||
|
|
||||||
for (var i = 0; i < tasksCount; i++)
|
|
||||||
{
|
|
||||||
tasks[i] = Task.Run(
|
|
||||||
async () =>
|
|
||||||
{
|
{
|
||||||
using (await asyncLock.EnterAsync().ConfigureAwait(false))
|
var localI = globalI;
|
||||||
{
|
await Task.Delay(5).ConfigureAwait(false); // Increase the chance for wrong data.
|
||||||
var localI = globalI;
|
localI++;
|
||||||
await Task.Delay(5).ConfigureAwait(false); // Increase the chance for wrong data.
|
globalI = localI;
|
||||||
localI++;
|
}
|
||||||
globalI = localI;
|
});
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (globalI != tasksCount)
|
|
||||||
{
|
|
||||||
throw new Exception($"Code is broken ({globalI})!");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Benchmark]
|
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||||
public async Task Wait_100_000_Times()
|
|
||||||
{
|
|
||||||
var asyncLock = new AsyncLock();
|
|
||||||
|
|
||||||
using (var cancellationToken = new CancellationTokenSource())
|
if (globalI != tasksCount)
|
||||||
|
{
|
||||||
|
throw new Exception($"Code is broken ({globalI})!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Benchmark]
|
||||||
|
public async Task Wait_100_000_Times()
|
||||||
|
{
|
||||||
|
var asyncLock = new AsyncLock();
|
||||||
|
|
||||||
|
using var cancellationToken = new CancellationTokenSource();
|
||||||
|
for (var i = 0; i < 100000; i++)
|
||||||
|
{
|
||||||
|
using (await asyncLock.EnterAsync(cancellationToken.Token).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
for (var i = 0; i < 100000; i++)
|
|
||||||
{
|
|
||||||
using (await asyncLock.EnterAsync(cancellationToken.Token).ConfigureAwait(false))
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,8 @@
|
|||||||
// The .NET Foundation licenses this file to you under the MIT license.
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
namespace MQTTnet.Benchmarks
|
namespace MQTTnet.Benchmarks;
|
||||||
|
|
||||||
|
public abstract class BaseBenchmark
|
||||||
{
|
{
|
||||||
public abstract class BaseBenchmark
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -12,79 +12,78 @@ using MQTTnet.Formatter;
|
|||||||
using MQTTnet.Packets;
|
using MQTTnet.Packets;
|
||||||
using MQTTnet.Tests.Mockups;
|
using MQTTnet.Tests.Mockups;
|
||||||
|
|
||||||
namespace MQTTnet.Benchmarks
|
namespace MQTTnet.Benchmarks;
|
||||||
|
|
||||||
|
[MemoryDiagnoser]
|
||||||
|
public sealed class ChannelAdapterBenchmark : BaseBenchmark
|
||||||
{
|
{
|
||||||
[MemoryDiagnoser]
|
MqttChannelAdapter _channelAdapter;
|
||||||
public sealed class ChannelAdapterBenchmark : BaseBenchmark
|
int _iterations;
|
||||||
|
MqttPublishPacket _packet;
|
||||||
|
MemoryStream _stream;
|
||||||
|
|
||||||
|
[Benchmark]
|
||||||
|
public void Receive_10000_Messages()
|
||||||
{
|
{
|
||||||
MqttChannelAdapter _channelAdapter;
|
_stream.Position = 0;
|
||||||
int _iterations;
|
|
||||||
MqttPublishPacket _packet;
|
|
||||||
MemoryStream _stream;
|
|
||||||
|
|
||||||
[Benchmark]
|
for (var i = 0; i < 10000; i++)
|
||||||
public void Receive_10000_Messages()
|
|
||||||
{
|
{
|
||||||
_stream.Position = 0;
|
_channelAdapter.ReceivePacketAsync(CancellationToken.None).GetAwaiter().GetResult();
|
||||||
|
|
||||||
for (var i = 0; i < 10000; i++)
|
|
||||||
{
|
|
||||||
_channelAdapter.ReceivePacketAsync(CancellationToken.None).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
_stream.Position = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Benchmark]
|
_stream.Position = 0;
|
||||||
public void Send_10000_Messages()
|
}
|
||||||
|
|
||||||
|
[Benchmark]
|
||||||
|
public void Send_10000_Messages()
|
||||||
|
{
|
||||||
|
_stream.Position = 0;
|
||||||
|
|
||||||
|
for (var i = 0; i < 10000; i++)
|
||||||
{
|
{
|
||||||
_stream.Position = 0;
|
_channelAdapter.SendPacketAsync(_packet, CancellationToken.None).GetAwaiter().GetResult();
|
||||||
|
|
||||||
for (var i = 0; i < 10000; i++)
|
|
||||||
{
|
|
||||||
_channelAdapter.SendPacketAsync(_packet, CancellationToken.None).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
_stream.Position = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[GlobalSetup]
|
_stream.Position = 0;
|
||||||
public void Setup()
|
}
|
||||||
|
|
||||||
|
[GlobalSetup]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
_packet = new MqttPublishPacket
|
||||||
{
|
{
|
||||||
_packet = new MqttPublishPacket
|
Topic = "A"
|
||||||
{
|
};
|
||||||
Topic = "A"
|
|
||||||
};
|
|
||||||
|
|
||||||
var serializer = new MqttPacketFormatterAdapter(MqttProtocolVersion.V311, new MqttBufferWriter(4096, 65535));
|
var serializer = new MqttPacketFormatterAdapter(MqttProtocolVersion.V311, new MqttBufferWriter(4096, 65535));
|
||||||
|
|
||||||
var serializedPacket = Join(serializer.Encode(_packet).Join());
|
var serializedPacket = Join(serializer.Encode(_packet).Join());
|
||||||
|
|
||||||
_iterations = 10000;
|
_iterations = 10000;
|
||||||
|
|
||||||
_stream = new MemoryStream(_iterations * serializedPacket.Length);
|
_stream = new MemoryStream(_iterations * serializedPacket.Length);
|
||||||
|
|
||||||
for (var i = 0; i < _iterations; i++)
|
for (var i = 0; i < _iterations; i++)
|
||||||
{
|
{
|
||||||
_stream.Write(serializedPacket, 0, serializedPacket.Length);
|
_stream.Write(serializedPacket, 0, serializedPacket.Length);
|
||||||
}
|
|
||||||
|
|
||||||
_stream.Position = 0;
|
|
||||||
|
|
||||||
var channel = new MemoryMqttChannel(_stream);
|
|
||||||
|
|
||||||
_channelAdapter = new MqttChannelAdapter(channel, serializer, new MqttNetEventLogger());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static byte[] Join(params ArraySegment<byte>[] chunks)
|
_stream.Position = 0;
|
||||||
{
|
|
||||||
var buffer = new MemoryStream();
|
|
||||||
foreach (var chunk in chunks)
|
|
||||||
{
|
|
||||||
buffer.Write(chunk.Array, chunk.Offset, chunk.Count);
|
|
||||||
}
|
|
||||||
|
|
||||||
return buffer.ToArray();
|
var channel = new MemoryMqttChannel(_stream);
|
||||||
|
|
||||||
|
_channelAdapter = new MqttChannelAdapter(channel, serializer, new MqttNetEventLogger());
|
||||||
|
}
|
||||||
|
|
||||||
|
static byte[] Join(params ArraySegment<byte>[] chunks)
|
||||||
|
{
|
||||||
|
var buffer = new MemoryStream();
|
||||||
|
foreach (var chunk in chunks)
|
||||||
|
{
|
||||||
|
buffer.Write(chunk.Array, chunk.Offset, chunk.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return buffer.ToArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,90 +2,90 @@
|
|||||||
// The .NET Foundation licenses this file to you under the MIT license.
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System.Diagnostics;
|
||||||
using BenchmarkDotNet.Attributes;
|
using BenchmarkDotNet.Attributes;
|
||||||
using BenchmarkDotNet.Jobs;
|
using BenchmarkDotNet.Jobs;
|
||||||
using MQTTnet.Diagnostics.Logger;
|
using MQTTnet.Diagnostics.Logger;
|
||||||
|
|
||||||
namespace MQTTnet.Benchmarks
|
namespace MQTTnet.Benchmarks;
|
||||||
|
|
||||||
|
[SimpleJob(RuntimeMoniker.Net60)]
|
||||||
|
[RPlotExporter]
|
||||||
|
[MemoryDiagnoser]
|
||||||
|
public class LoggerBenchmark : BaseBenchmark
|
||||||
{
|
{
|
||||||
[SimpleJob(RuntimeMoniker.Net60)]
|
MqttNetNullLogger _nullLogger;
|
||||||
[RPlotExporter]
|
MqttNetSourceLogger _sourceNullLogger;
|
||||||
[MemoryDiagnoser]
|
|
||||||
public class LoggerBenchmark : BaseBenchmark
|
MqttNetEventLogger _eventLogger;
|
||||||
|
MqttNetSourceLogger _sourceEventLogger;
|
||||||
|
|
||||||
|
MqttNetEventLogger _eventLoggerNoListener;
|
||||||
|
MqttNetSourceLogger _sourceEventLoggerNoListener;
|
||||||
|
|
||||||
|
bool _useHandler;
|
||||||
|
|
||||||
|
[GlobalSetup]
|
||||||
|
public void Setup()
|
||||||
{
|
{
|
||||||
MqttNetNullLogger _nullLogger;
|
_nullLogger = new MqttNetNullLogger();
|
||||||
MqttNetSourceLogger _sourceNullLogger;
|
_sourceNullLogger = _nullLogger.WithSource("Source");
|
||||||
|
|
||||||
MqttNetEventLogger _eventLogger;
|
_eventLogger = new MqttNetEventLogger();
|
||||||
MqttNetSourceLogger _sourceEventLogger;
|
_eventLogger.LogMessagePublished += OnLogMessagePublished;
|
||||||
|
_sourceEventLogger = _eventLogger.WithSource("Source");
|
||||||
|
|
||||||
MqttNetEventLogger _eventLoggerNoListener;
|
_eventLoggerNoListener = new MqttNetEventLogger();
|
||||||
MqttNetSourceLogger _sourceEventLoggerNoListener;
|
_sourceEventLoggerNoListener = _eventLoggerNoListener.WithSource("Source");
|
||||||
|
}
|
||||||
|
|
||||||
bool _useHandler;
|
void OnLogMessagePublished(object sender, MqttNetLogMessagePublishedEventArgs eventArgs)
|
||||||
|
{
|
||||||
[GlobalSetup]
|
if (_useHandler)
|
||||||
public void Setup()
|
|
||||||
{
|
{
|
||||||
_nullLogger = new MqttNetNullLogger();
|
Debug.WriteLine(eventArgs.LogMessage.ToString());
|
||||||
_sourceNullLogger = _nullLogger.WithSource("Source");
|
|
||||||
|
|
||||||
_eventLogger = new MqttNetEventLogger();
|
|
||||||
_eventLogger.LogMessagePublished += OnLogMessagePublished;
|
|
||||||
_sourceEventLogger = _eventLogger.WithSource("Source");
|
|
||||||
|
|
||||||
_eventLoggerNoListener = new MqttNetEventLogger();
|
|
||||||
_sourceEventLoggerNoListener = _eventLoggerNoListener.WithSource("Source");
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void OnLogMessagePublished(object sender, MqttNetLogMessagePublishedEventArgs eventArgs)
|
[Benchmark]
|
||||||
|
public void Log_10000_Messages_No_Listener()
|
||||||
|
{
|
||||||
|
_useHandler = false;
|
||||||
|
|
||||||
|
for (var i = 0; i < 10000; i++)
|
||||||
{
|
{
|
||||||
if (_useHandler)
|
_sourceEventLoggerNoListener.Verbose("test log message {0}", "parameter");
|
||||||
{
|
|
||||||
eventArgs.LogMessage.ToString();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Benchmark]
|
[Benchmark]
|
||||||
public void Log_10000_Messages_No_Listener()
|
public void Log_10000_Messages_With_To_String()
|
||||||
|
{
|
||||||
|
_useHandler = true;
|
||||||
|
|
||||||
|
for (var i = 0; i < 10000; i++)
|
||||||
{
|
{
|
||||||
_useHandler = false;
|
_sourceEventLogger.Verbose("test log message {0}", "parameter");
|
||||||
|
|
||||||
for (var i = 0; i < 10000; i++)
|
|
||||||
{
|
|
||||||
_sourceEventLoggerNoListener.Verbose("test log message {0}", "parameter");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Benchmark]
|
[Benchmark]
|
||||||
public void Log_10000_Messages_With_To_String()
|
public void Log_10000_Messages_Without_To_String()
|
||||||
|
{
|
||||||
|
_useHandler = false;
|
||||||
|
|
||||||
|
for (var i = 0; i < 10000; i++)
|
||||||
{
|
{
|
||||||
_useHandler = true;
|
_sourceEventLogger.Verbose("test log message {0}", "parameter");
|
||||||
|
|
||||||
for (var i = 0; i < 10000; i++)
|
|
||||||
{
|
|
||||||
_sourceEventLogger.Verbose("test log message {0}", "parameter");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Benchmark]
|
[Benchmark]
|
||||||
public void Log_10000_Messages_Without_To_String()
|
public void Log_10000_Messages_With_NullLogger()
|
||||||
|
{
|
||||||
|
for (var i = 0; i < 10000; i++)
|
||||||
{
|
{
|
||||||
_useHandler = false;
|
_sourceNullLogger.Verbose("test log message {0}", "parameter");
|
||||||
|
|
||||||
for (var i = 0; i < 10000; i++)
|
|
||||||
{
|
|
||||||
_sourceEventLogger.Verbose("test log message {0}", "parameter");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Benchmark]
|
|
||||||
public void Log_10000_Messages_With_NullLogger()
|
|
||||||
{
|
|
||||||
for (var i = 0; i < 10000; i++)
|
|
||||||
{
|
|
||||||
_sourceNullLogger.Verbose("test log message {0}", "parameter");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,38 +3,38 @@ using BenchmarkDotNet.Jobs;
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
|
|
||||||
namespace MQTTnet.Benchmarks
|
namespace MQTTnet.Benchmarks;
|
||||||
|
|
||||||
|
[SimpleJob(RuntimeMoniker.Net60)]
|
||||||
|
[RPlotExporter, RankColumn]
|
||||||
|
[MemoryDiagnoser]
|
||||||
|
public class MemoryCopyBenchmark
|
||||||
{
|
{
|
||||||
[SimpleJob(RuntimeMoniker.Net60)]
|
const int MaxLength = 1024 * 8;
|
||||||
[RPlotExporter, RankColumn]
|
|
||||||
[MemoryDiagnoser]
|
byte[] _source;
|
||||||
public class MemoryCopyBenchmark
|
byte[] _target;
|
||||||
|
|
||||||
|
[Params(64 - 1, 128 - 1, 256 - 1, 512 - 1, 1024 - 1, 2048 - 1, 5096 - 1)]
|
||||||
|
public int Length { get; set; }
|
||||||
|
|
||||||
|
[GlobalSetup]
|
||||||
|
public void Setup()
|
||||||
{
|
{
|
||||||
const int max_length = 1024 * 8;
|
_source = new byte[MaxLength];
|
||||||
private byte[] source;
|
_target = new byte[MaxLength];
|
||||||
private byte[] target;
|
|
||||||
|
|
||||||
[Params(64 - 1, 128 - 1, 256 - 1, 512 - 1, 1024 - 1, 2048 - 1, 5096 - 1)]
|
|
||||||
public int Length { get; set; }
|
|
||||||
|
|
||||||
[GlobalSetup]
|
|
||||||
public void Setup()
|
|
||||||
{
|
|
||||||
source = new byte[max_length];
|
|
||||||
target = new byte[max_length];
|
|
||||||
}
|
|
||||||
|
|
||||||
[Benchmark(Baseline = true)]
|
|
||||||
public void Array_Copy()
|
|
||||||
{
|
|
||||||
Array.Copy(source, 0, target, 0, Length);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Benchmark]
|
|
||||||
public void Memory_Copy()
|
|
||||||
{
|
|
||||||
MQTTnet.Internal.MqttMemoryHelper.Copy(source, 0, target, 0, Length);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Benchmark(Baseline = true)]
|
||||||
|
public void Array_Copy()
|
||||||
|
{
|
||||||
|
Array.Copy(_source, 0, _target, 0, Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Benchmark]
|
||||||
|
public void Memory_Copy()
|
||||||
|
{
|
||||||
|
Internal.MqttMemoryHelper.Copy(_source, 0, _target, 0, Length);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,6 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BenchmarkDotNet.Attributes;
|
using BenchmarkDotNet.Attributes;
|
||||||
@@ -23,22 +22,19 @@ public class MessageDeliveryBenchmark : BaseBenchmark
|
|||||||
int _messagesExpectedCount;
|
int _messagesExpectedCount;
|
||||||
int _messagesReceivedCount;
|
int _messagesReceivedCount;
|
||||||
Dictionary<string, IMqttClient> _mqttPublisherClientsByPublisherName;
|
Dictionary<string, IMqttClient> _mqttPublisherClientsByPublisherName;
|
||||||
|
|
||||||
MqttServer _mqttServer;
|
MqttServer _mqttServer;
|
||||||
List<IMqttClient> _mqttSubscriberClients;
|
List<IMqttClient> _mqttSubscriberClients;
|
||||||
|
|
||||||
|
[Params(1000, 10000)] public int _numPublishers;
|
||||||
|
|
||||||
|
[Params(5, 10, 20, 50)] public int _numSubscribedTopicsPerSubscriber;
|
||||||
|
|
||||||
|
[Params(10)] public int _numSubscribers;
|
||||||
|
|
||||||
|
[Params(1, 5)] public int _numTopicsPerPublisher;
|
||||||
Dictionary<string, string> _publisherByTopic;
|
Dictionary<string, string> _publisherByTopic;
|
||||||
List<MqttApplicationMessage> _topicPublishMessages;
|
|
||||||
|
|
||||||
Dictionary<string, List<string>> _topicsByPublisher;
|
Dictionary<string, List<string>> _topicsByPublisher;
|
||||||
|
|
||||||
[Params(1000, 10000)] public int NumPublishers;
|
|
||||||
|
|
||||||
[Params(5, 10, 20, 50)] public int NumSubscribedTopicsPerSubscriber;
|
|
||||||
|
|
||||||
[Params(10)] public int NumSubscribers;
|
|
||||||
|
|
||||||
[Params(1, 5)] public int NumTopicsPerPublisher;
|
|
||||||
|
|
||||||
[GlobalCleanup]
|
[GlobalCleanup]
|
||||||
public void Cleanup()
|
public void Cleanup()
|
||||||
{
|
{
|
||||||
@@ -64,14 +60,11 @@ public class MessageDeliveryBenchmark : BaseBenchmark
|
|||||||
_mqttServer = null;
|
_mqttServer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Publish messages and wait for messages sent to subscribers
|
|
||||||
/// </summary>
|
|
||||||
[Benchmark]
|
[Benchmark]
|
||||||
public void DeliverMessages()
|
public void DeliverMessages()
|
||||||
{
|
{
|
||||||
// There should be one message received per publish for each subscribed topic
|
// There should be one message received per publish for each subscribed topic
|
||||||
_messagesExpectedCount = NumSubscribedTopicsPerSubscriber * NumSubscribers;
|
_messagesExpectedCount = _numSubscribedTopicsPerSubscriber * _numSubscribers;
|
||||||
|
|
||||||
// Loop for a while and exchange messages
|
// Loop for a while and exchange messages
|
||||||
|
|
||||||
@@ -82,8 +75,6 @@ public class MessageDeliveryBenchmark : BaseBenchmark
|
|||||||
// same payload for all messages
|
// same payload for all messages
|
||||||
var payload = new byte[] { 1, 2, 3, 4 };
|
var payload = new byte[] { 1, 2, 3, 4 };
|
||||||
|
|
||||||
var tasks = new List<Task>();
|
|
||||||
|
|
||||||
// publish a message for each subscribed topic
|
// publish a message for each subscribed topic
|
||||||
foreach (var topic in _allSubscribedTopics)
|
foreach (var topic in _allSubscribedTopics)
|
||||||
{
|
{
|
||||||
@@ -101,35 +92,23 @@ public class MessageDeliveryBenchmark : BaseBenchmark
|
|||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
// Ignore all errors.
|
||||||
}
|
}
|
||||||
|
|
||||||
_cancellationTokenSource.Dispose();
|
_cancellationTokenSource.Dispose();
|
||||||
|
|
||||||
if (_messagesReceivedCount < _messagesExpectedCount)
|
if (_messagesReceivedCount < _messagesExpectedCount)
|
||||||
{
|
{
|
||||||
throw new Exception(string.Format("Messages Received Count mismatch, expected {0}, received {1}", _messagesExpectedCount, _messagesReceivedCount));
|
throw new Exception($"Messages Received Count mismatch, expected {_messagesExpectedCount}, received {_messagesReceivedCount}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[GlobalSetup]
|
[GlobalSetup]
|
||||||
public void Setup()
|
public void Setup()
|
||||||
{
|
{
|
||||||
_lockMsgCount = new object();
|
_lockMsgCount = new object();
|
||||||
|
|
||||||
Dictionary<string, List<string>> singleWildcardTopicsByPublisher;
|
TopicGenerator.Generate(_numPublishers, _numTopicsPerPublisher, out _topicsByPublisher, out _, out _);
|
||||||
Dictionary<string, List<string>> multiWildcardTopicsByPublisher;
|
|
||||||
|
|
||||||
TopicGenerator.Generate(NumPublishers, NumTopicsPerPublisher, out _topicsByPublisher, out singleWildcardTopicsByPublisher, out multiWildcardTopicsByPublisher);
|
|
||||||
|
|
||||||
var topics = _topicsByPublisher.First().Value;
|
|
||||||
_topicPublishMessages = new List<MqttApplicationMessage>();
|
|
||||||
// Prepare messages, same for each publisher
|
|
||||||
foreach (var topic in topics)
|
|
||||||
{
|
|
||||||
var message = new MqttApplicationMessageBuilder().WithTopic(topic).WithPayload(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }).Build();
|
|
||||||
_topicPublishMessages.Add(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create server
|
// Create server
|
||||||
var serverFactory = new MqttServerFactory();
|
var serverFactory = new MqttServerFactory();
|
||||||
@@ -139,7 +118,7 @@ public class MessageDeliveryBenchmark : BaseBenchmark
|
|||||||
_mqttServer.StartAsync().GetAwaiter().GetResult();
|
_mqttServer.StartAsync().GetAwaiter().GetResult();
|
||||||
|
|
||||||
// Create publisher clients
|
// Create publisher clients
|
||||||
_mqttPublisherClientsByPublisherName = new Dictionary<string, IMqttClient>();
|
_mqttPublisherClientsByPublisherName = [];
|
||||||
foreach (var pt in _topicsByPublisher)
|
foreach (var pt in _topicsByPublisher)
|
||||||
{
|
{
|
||||||
var publisherName = pt.Key;
|
var publisherName = pt.Key;
|
||||||
@@ -151,13 +130,13 @@ public class MessageDeliveryBenchmark : BaseBenchmark
|
|||||||
|
|
||||||
// Create subscriber clients
|
// Create subscriber clients
|
||||||
_mqttSubscriberClients = new List<IMqttClient>();
|
_mqttSubscriberClients = new List<IMqttClient>();
|
||||||
for (var i = 0; i < NumSubscribers; i++)
|
for (var i = 0; i < _numSubscribers; i++)
|
||||||
{
|
{
|
||||||
var mqttSubscriberClient = clientFactory.CreateMqttClient();
|
var mqttSubscriberClient = clientFactory.CreateMqttClient();
|
||||||
_mqttSubscriberClients.Add(mqttSubscriberClient);
|
_mqttSubscriberClients.Add(mqttSubscriberClient);
|
||||||
|
|
||||||
var subscriberOptions = new MqttClientOptionsBuilder().WithTcpServer("localhost").WithClientId("subscriber" + i).Build();
|
var subscriberOptions = new MqttClientOptionsBuilder().WithTcpServer("localhost").WithClientId("subscriber" + i).Build();
|
||||||
mqttSubscriberClient.ApplicationMessageReceivedAsync += r =>
|
mqttSubscriberClient.ApplicationMessageReceivedAsync += _ =>
|
||||||
{
|
{
|
||||||
// count messages and signal cancellation when expected message count is reached
|
// count messages and signal cancellation when expected message count is reached
|
||||||
lock (_lockMsgCount)
|
lock (_lockMsgCount)
|
||||||
@@ -171,12 +150,13 @@ public class MessageDeliveryBenchmark : BaseBenchmark
|
|||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
};
|
};
|
||||||
|
|
||||||
mqttSubscriberClient.ConnectAsync(subscriberOptions).GetAwaiter().GetResult();
|
mqttSubscriberClient.ConnectAsync(subscriberOptions).GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
var allTopics = new List<string>();
|
var allTopics = new List<string>();
|
||||||
_publisherByTopic = new Dictionary<string, string>();
|
_publisherByTopic = [];
|
||||||
foreach (var t in _topicsByPublisher)
|
foreach (var t in _topicsByPublisher)
|
||||||
{
|
{
|
||||||
foreach (var topic in t.Value)
|
foreach (var topic in t.Value)
|
||||||
@@ -189,21 +169,18 @@ public class MessageDeliveryBenchmark : BaseBenchmark
|
|||||||
// Subscribe to NumSubscribedTopics topics spread across all topics
|
// Subscribe to NumSubscribedTopics topics spread across all topics
|
||||||
_allSubscribedTopics = new List<string>();
|
_allSubscribedTopics = new List<string>();
|
||||||
|
|
||||||
var totalNumTopics = NumPublishers * NumTopicsPerPublisher;
|
var totalNumTopics = _numPublishers * _numTopicsPerPublisher;
|
||||||
var topicIndexStep = totalNumTopics / (NumSubscribedTopicsPerSubscriber * NumSubscribers);
|
var topicIndexStep = totalNumTopics / (_numSubscribedTopicsPerSubscriber * _numSubscribers);
|
||||||
if (topicIndexStep * NumSubscribedTopicsPerSubscriber * NumSubscribers != totalNumTopics)
|
if (topicIndexStep * _numSubscribedTopicsPerSubscriber * _numSubscribers != totalNumTopics)
|
||||||
{
|
{
|
||||||
throw new Exception(
|
throw new Exception(
|
||||||
string.Format(
|
$"The total number of topics must be divisible by the number of subscribed topics across all subscribers. Total number of topics: {totalNumTopics}, topic step: {topicIndexStep}");
|
||||||
"The total number of topics must be divisible by the number of subscribed topics across all subscribers. Total number of topics: {0}, topic step: {1}",
|
|
||||||
totalNumTopics,
|
|
||||||
topicIndexStep));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var topicIndex = 0;
|
var topicIndex = 0;
|
||||||
foreach (var mqttSubscriber in _mqttSubscriberClients)
|
foreach (var mqttSubscriber in _mqttSubscriberClients)
|
||||||
{
|
{
|
||||||
for (var i = 0; i < NumSubscribedTopicsPerSubscriber; ++i, topicIndex += topicIndexStep)
|
for (var i = 0; i < _numSubscribedTopicsPerSubscriber; ++i, topicIndex += topicIndexStep)
|
||||||
{
|
{
|
||||||
var topic = allTopics[topicIndex];
|
var topic = allTopics[topicIndex];
|
||||||
_allSubscribedTopics.Add(topic);
|
_allSubscribedTopics.Add(topic);
|
||||||
|
|||||||
@@ -9,65 +9,64 @@ using Microsoft.AspNetCore.Hosting;
|
|||||||
using MQTTnet.AspNetCore;
|
using MQTTnet.AspNetCore;
|
||||||
using MQTTnet.Diagnostics.Logger;
|
using MQTTnet.Diagnostics.Logger;
|
||||||
|
|
||||||
namespace MQTTnet.Benchmarks
|
namespace MQTTnet.Benchmarks;
|
||||||
|
|
||||||
|
[SimpleJob(RuntimeMoniker.Net60)]
|
||||||
|
[MemoryDiagnoser]
|
||||||
|
public class MessageProcessingMqttConnectionContextBenchmark : BaseBenchmark
|
||||||
{
|
{
|
||||||
[SimpleJob(RuntimeMoniker.Net60)]
|
IWebHost _host;
|
||||||
[MemoryDiagnoser]
|
IMqttClient _mqttClient;
|
||||||
public class MessageProcessingMqttConnectionContextBenchmark : BaseBenchmark
|
MqttApplicationMessage _message;
|
||||||
|
|
||||||
|
[GlobalSetup]
|
||||||
|
public void Setup()
|
||||||
{
|
{
|
||||||
IWebHost _host;
|
_host = WebHost.CreateDefaultBuilder()
|
||||||
IMqttClient _mqttClient;
|
.UseKestrel(o => o.ListenAnyIP(1883, l => l.UseMqtt()))
|
||||||
MqttApplicationMessage _message;
|
.ConfigureServices(services => {
|
||||||
|
services
|
||||||
|
.AddHostedMqttServer(mqttServerOptions => mqttServerOptions.WithoutDefaultEndpoint())
|
||||||
|
.AddMqttConnectionHandler();
|
||||||
|
})
|
||||||
|
.Configure(app => {
|
||||||
|
app.UseMqttServer(_ => {
|
||||||
|
|
||||||
[GlobalSetup]
|
});
|
||||||
public void Setup()
|
})
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
var factory = new MqttClientFactory();
|
||||||
|
_mqttClient = factory.CreateMqttClient(new MqttNetEventLogger(), new MqttClientConnectionContextFactory());
|
||||||
|
|
||||||
|
_host.StartAsync().GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
var clientOptions = new MqttClientOptionsBuilder()
|
||||||
|
.WithTcpServer("localhost").Build();
|
||||||
|
|
||||||
|
_mqttClient.ConnectAsync(clientOptions).GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
_message = new MqttApplicationMessageBuilder()
|
||||||
|
.WithTopic("A")
|
||||||
|
.Build();
|
||||||
|
}
|
||||||
|
|
||||||
|
[GlobalCleanup]
|
||||||
|
public void Cleanup()
|
||||||
|
{
|
||||||
|
_mqttClient.DisconnectAsync().GetAwaiter().GetResult();
|
||||||
|
_mqttClient.Dispose();
|
||||||
|
|
||||||
|
_host.StopAsync().GetAwaiter().GetResult();
|
||||||
|
_host.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Benchmark]
|
||||||
|
public void Send_10000_Messages()
|
||||||
|
{
|
||||||
|
for (var i = 0; i < 10000; i++)
|
||||||
{
|
{
|
||||||
_host = WebHost.CreateDefaultBuilder()
|
_mqttClient.PublishAsync(_message).GetAwaiter().GetResult();
|
||||||
.UseKestrel(o => o.ListenAnyIP(1883, l => l.UseMqtt()))
|
|
||||||
.ConfigureServices(services => {
|
|
||||||
services
|
|
||||||
.AddHostedMqttServer(mqttServerOptions => mqttServerOptions.WithoutDefaultEndpoint())
|
|
||||||
.AddMqttConnectionHandler();
|
|
||||||
})
|
|
||||||
.Configure(app => {
|
|
||||||
app.UseMqttServer(s => {
|
|
||||||
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.Build();
|
|
||||||
|
|
||||||
var factory = new MqttClientFactory();
|
|
||||||
_mqttClient = factory.CreateMqttClient(new MqttNetEventLogger(), new MqttClientConnectionContextFactory());
|
|
||||||
|
|
||||||
_host.StartAsync().GetAwaiter().GetResult();
|
|
||||||
|
|
||||||
var clientOptions = new MqttClientOptionsBuilder()
|
|
||||||
.WithTcpServer("localhost").Build();
|
|
||||||
|
|
||||||
_mqttClient.ConnectAsync(clientOptions).GetAwaiter().GetResult();
|
|
||||||
|
|
||||||
_message = new MqttApplicationMessageBuilder()
|
|
||||||
.WithTopic("A")
|
|
||||||
.Build();
|
|
||||||
}
|
|
||||||
|
|
||||||
[GlobalCleanup]
|
|
||||||
public void Cleanup()
|
|
||||||
{
|
|
||||||
_mqttClient.DisconnectAsync().GetAwaiter().GetResult();
|
|
||||||
_mqttClient.Dispose();
|
|
||||||
|
|
||||||
_host.StopAsync().GetAwaiter().GetResult();
|
|
||||||
_host.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Benchmark]
|
|
||||||
public void Send_10000_Messages()
|
|
||||||
{
|
|
||||||
for (var i = 0; i < 10000; i++)
|
|
||||||
{
|
|
||||||
_mqttClient.PublishAsync(_message).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,36 +8,35 @@ using BenchmarkDotNet.Attributes;
|
|||||||
using BenchmarkDotNet.Jobs;
|
using BenchmarkDotNet.Jobs;
|
||||||
using MQTTnet.Formatter;
|
using MQTTnet.Formatter;
|
||||||
|
|
||||||
namespace MQTTnet.Benchmarks
|
namespace MQTTnet.Benchmarks;
|
||||||
|
|
||||||
|
[SimpleJob(RuntimeMoniker.Net60)]
|
||||||
|
[MemoryDiagnoser]
|
||||||
|
public class MqttBufferReaderBenchmark
|
||||||
{
|
{
|
||||||
[SimpleJob(RuntimeMoniker.Net60)]
|
byte[] _buffer;
|
||||||
[MemoryDiagnoser]
|
int _bufferLength;
|
||||||
public class MqttBufferReaderBenchmark
|
|
||||||
|
[GlobalSetup]
|
||||||
|
public void GlobalSetup()
|
||||||
{
|
{
|
||||||
byte[] _buffer;
|
var writer = new MqttBufferWriter(1024, 1024);
|
||||||
int _bufferLength;
|
writer.WriteString("hgfjkdfkjlghfdjghdfljkdfhgdlkjfshgsldkfjghsdflkjghdsflkjhrstiuoghlkfjbhnfbutghjoiöjhklötnbhtroliöuhbjntluiobkjzbhtdrlskbhtruhjkfthgbkftgjhgfiklhotriuöhbjtrsioöbtrsötrhträhtrühjtriüoätrhjtsrölbktrbnhtrulöbionhströloubinströoliubhnsöotrunbtöroisntröointröioujhgötiohjgötorshjnbgtöorihbnjtröoihbjntröobntröoibntrjhötrohjbtröoihntröoibnrtoiöbtrjnboöitrhjtnriohötrhjtöroihjtroöihjtroösibntsroönbotöirsbntöoihjntröoihntroöbtrboöitrnhoöitrhjntröoishbnjtröosbhtröbntriohjtröoijtöoitbjöotibjnhöotirhbjntroöibhnjrtoöibnhtroöibnhtörsbnhtöoirbnhtöroibntoörhjnbträöbtrbträbtrbtirbätrsibohjntrsöiobthnjiohjsrtoib");
|
||||||
|
|
||||||
[GlobalSetup]
|
_buffer = writer.GetBuffer();
|
||||||
public void GlobalSetup()
|
_bufferLength = writer.Length;
|
||||||
{
|
}
|
||||||
var writer = new MqttBufferWriter(1024, 1024);
|
|
||||||
writer.WriteString("hgfjkdfkjlghfdjghdfljkdfhgdlkjfshgsldkfjghsdflkjghdsflkjhrstiuoghlkfjbhnfbutghjoiöjhklötnbhtroliöuhbjntluiobkjzbhtdrlskbhtruhjkfthgbkftgjhgfiklhotriuöhbjtrsioöbtrsötrhträhtrühjtriüoätrhjtsrölbktrbnhtrulöbionhströloubinströoliubhnsöotrunbtöroisntröointröioujhgötiohjgötorshjnbgtöorihbnjtröoihbjntröobntröoibntrjhötrohjbtröoihntröoibnrtoiöbtrjnboöitrhjtnriohötrhjtöroihjtroöihjtroösibntsroönbotöirsbntöoihjntröoihntroöbtrboöitrnhoöitrhjntröoishbnjtröosbhtröbntriohjtröoijtöoitbjöotibjnhöotirhbjntroöibhnjrtoöibnhtroöibnhtörsbnhtöoirbnhtöroibntoörhjnbträöbtrbträbtrbtirbätrsibohjntrsöiobthnjiohjsrtoib");
|
|
||||||
|
|
||||||
_buffer = writer.GetBuffer();
|
[Benchmark]
|
||||||
_bufferLength = writer.Length;
|
public void Use_Span()
|
||||||
}
|
{
|
||||||
|
var span = _buffer.AsSpan(0, _bufferLength);
|
||||||
|
Encoding.UTF8.GetString(span);
|
||||||
|
}
|
||||||
|
|
||||||
[Benchmark]
|
[Benchmark]
|
||||||
public void Use_Span()
|
public void Use_Encoding()
|
||||||
{
|
{
|
||||||
var span = _buffer.AsSpan(0, _bufferLength);
|
Encoding.UTF8.GetString(_buffer, 0, _bufferLength);
|
||||||
Encoding.UTF8.GetString(span);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Benchmark]
|
|
||||||
public void Use_Encoding()
|
|
||||||
{
|
|
||||||
Encoding.UTF8.GetString(_buffer, 0, _bufferLength);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,89 +8,88 @@ using BenchmarkDotNet.Jobs;
|
|||||||
using MQTTnet.Formatter;
|
using MQTTnet.Formatter;
|
||||||
using MQTTnet.Tests.Mockups;
|
using MQTTnet.Tests.Mockups;
|
||||||
|
|
||||||
namespace MQTTnet.Benchmarks
|
namespace MQTTnet.Benchmarks;
|
||||||
|
|
||||||
|
[SimpleJob(RuntimeMoniker.Net60)]
|
||||||
|
[MemoryDiagnoser]
|
||||||
|
public class MqttPacketReaderWriterBenchmark : BaseBenchmark
|
||||||
{
|
{
|
||||||
[SimpleJob(RuntimeMoniker.Net60)]
|
readonly byte[] _demoPayload = new byte[1024];
|
||||||
[MemoryDiagnoser]
|
|
||||||
public class MqttPacketReaderWriterBenchmark : BaseBenchmark
|
byte[] _readPayload;
|
||||||
|
|
||||||
|
[GlobalCleanup]
|
||||||
|
public void GlobalCleanup()
|
||||||
{
|
{
|
||||||
readonly byte[] _demoPayload = new byte[1024];
|
}
|
||||||
|
|
||||||
byte[] _readPayload;
|
[GlobalSetup]
|
||||||
|
public void GlobalSetup()
|
||||||
|
{
|
||||||
|
TestEnvironment.EnableLogger = false;
|
||||||
|
|
||||||
[GlobalCleanup]
|
var writer = new MqttBufferWriter(4096, 65535);
|
||||||
public void GlobalCleanup()
|
writer.WriteString("A relative short string.");
|
||||||
|
writer.WriteBinary(_demoPayload);
|
||||||
|
writer.WriteByte(0x01);
|
||||||
|
writer.WriteByte(0x02);
|
||||||
|
writer.WriteVariableByteInteger(5647382);
|
||||||
|
writer.WriteString("A relative short string.");
|
||||||
|
writer.WriteVariableByteInteger(8574489);
|
||||||
|
writer.WriteBinary(_demoPayload);
|
||||||
|
writer.WriteByte(2);
|
||||||
|
writer.WriteByte(0x02);
|
||||||
|
writer.WriteString("fjgffiogfhgfhoihgoireghreghreguhreguireoghreouighreouighreughreguiorehreuiohruiorehreuioghreug");
|
||||||
|
writer.WriteBinary(_demoPayload);
|
||||||
|
|
||||||
|
_readPayload = new ArraySegment<byte>(writer.GetBuffer(), 0, writer.Length).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Benchmark]
|
||||||
|
public void Read_100_000_Messages()
|
||||||
|
{
|
||||||
|
var reader = new MqttBufferReader();
|
||||||
|
reader.SetBuffer(_readPayload, 0, _readPayload.Length);
|
||||||
|
|
||||||
|
for (var i = 0; i < 100000; i++)
|
||||||
{
|
{
|
||||||
|
reader.Seek(0);
|
||||||
|
|
||||||
|
reader.ReadString();
|
||||||
|
reader.ReadBinaryData();
|
||||||
|
reader.ReadByte();
|
||||||
|
reader.ReadByte();
|
||||||
|
reader.ReadVariableByteInteger();
|
||||||
|
reader.ReadString();
|
||||||
|
reader.ReadVariableByteInteger();
|
||||||
|
reader.ReadBinaryData();
|
||||||
|
reader.ReadByte();
|
||||||
|
reader.ReadByte();
|
||||||
|
reader.ReadString();
|
||||||
|
reader.ReadBinaryData();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[GlobalSetup]
|
[Benchmark]
|
||||||
public void GlobalSetup()
|
public void Write_100_000_Messages()
|
||||||
|
{
|
||||||
|
var writer = new MqttBufferWriter(4096, 65535);
|
||||||
|
|
||||||
|
for (var i = 0; i < 100000; i++)
|
||||||
{
|
{
|
||||||
TestEnvironment.EnableLogger = false;
|
|
||||||
|
|
||||||
var writer = new MqttBufferWriter(4096, 65535);
|
|
||||||
writer.WriteString("A relative short string.");
|
writer.WriteString("A relative short string.");
|
||||||
writer.WriteBinary(_demoPayload);
|
|
||||||
writer.WriteByte(0x01);
|
writer.WriteByte(0x01);
|
||||||
writer.WriteByte(0x02);
|
writer.WriteByte(0x02);
|
||||||
writer.WriteVariableByteInteger(5647382);
|
writer.WriteVariableByteInteger(5647382);
|
||||||
writer.WriteString("A relative short string.");
|
writer.WriteString("A relative short string.");
|
||||||
writer.WriteVariableByteInteger(8574489);
|
writer.WriteVariableByteInteger(8574589);
|
||||||
writer.WriteBinary(_demoPayload);
|
writer.WriteBinary(_demoPayload);
|
||||||
writer.WriteByte(2);
|
writer.WriteByte(2);
|
||||||
writer.WriteByte(0x02);
|
writer.WriteByte(0x02);
|
||||||
writer.WriteString("fjgffiogfhgfhoihgoireghreghreguhreguireoghreouighreouighreughreguiorehreuiohruiorehreuioghreug");
|
writer.WriteString("fjgffiogfhgfhoihgoireghreghreguhreguireoghreouighreouighreughreguiorehreuiohruiorehreuioghreug");
|
||||||
writer.WriteBinary(_demoPayload);
|
writer.WriteBinary(_demoPayload);
|
||||||
|
|
||||||
_readPayload = new ArraySegment<byte>(writer.GetBuffer(), 0, writer.Length).ToArray();
|
writer.Reset(0);
|
||||||
}
|
|
||||||
|
|
||||||
[Benchmark]
|
|
||||||
public void Read_100_000_Messages()
|
|
||||||
{
|
|
||||||
var reader = new MqttBufferReader();
|
|
||||||
reader.SetBuffer(_readPayload, 0, _readPayload.Length);
|
|
||||||
|
|
||||||
for (var i = 0; i < 100000; i++)
|
|
||||||
{
|
|
||||||
reader.Seek(0);
|
|
||||||
|
|
||||||
reader.ReadString();
|
|
||||||
reader.ReadBinaryData();
|
|
||||||
reader.ReadByte();
|
|
||||||
reader.ReadByte();
|
|
||||||
reader.ReadVariableByteInteger();
|
|
||||||
reader.ReadString();
|
|
||||||
reader.ReadVariableByteInteger();
|
|
||||||
reader.ReadBinaryData();
|
|
||||||
reader.ReadByte();
|
|
||||||
reader.ReadByte();
|
|
||||||
reader.ReadString();
|
|
||||||
reader.ReadBinaryData();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Benchmark]
|
|
||||||
public void Write_100_000_Messages()
|
|
||||||
{
|
|
||||||
var writer = new MqttBufferWriter(4096, 65535);
|
|
||||||
|
|
||||||
for (var i = 0; i < 100000; i++)
|
|
||||||
{
|
|
||||||
writer.WriteString("A relative short string.");
|
|
||||||
writer.WriteByte(0x01);
|
|
||||||
writer.WriteByte(0x02);
|
|
||||||
writer.WriteVariableByteInteger(5647382);
|
|
||||||
writer.WriteString("A relative short string.");
|
|
||||||
writer.WriteVariableByteInteger(8574589);
|
|
||||||
writer.WriteBinary(_demoPayload);
|
|
||||||
writer.WriteByte(2);
|
|
||||||
writer.WriteByte(0x02);
|
|
||||||
writer.WriteString("fjgffiogfhgfhoihgoireghreghreguhreguireoghreouighreouighreughreguiorehreuiohruiorehreuioghreug");
|
|
||||||
writer.WriteBinary(_demoPayload);
|
|
||||||
|
|
||||||
writer.Reset(0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
// The .NET Foundation licenses this file to you under the MIT license.
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|||||||
@@ -5,138 +5,135 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using BenchmarkDotNet.Loggers;
|
|
||||||
using BenchmarkDotNet.Running;
|
using BenchmarkDotNet.Running;
|
||||||
using MQTTnet.Diagnostics;
|
|
||||||
|
|
||||||
namespace MQTTnet.Benchmarks
|
namespace MQTTnet.Benchmarks;
|
||||||
|
|
||||||
|
public static class Program
|
||||||
{
|
{
|
||||||
public static class Program
|
static int _selectedBenchmarkIndex;
|
||||||
|
static List<Type> _benchmarks;
|
||||||
|
|
||||||
|
public static void Main(string[] arguments)
|
||||||
{
|
{
|
||||||
static int _selectedBenchmarkIndex;
|
_benchmarks = CollectBenchmarks();
|
||||||
static List<Type> _benchmarks;
|
HandleArguments(arguments);
|
||||||
|
|
||||||
public static void Main(string[] arguments)
|
while (true)
|
||||||
{
|
{
|
||||||
_benchmarks = CollectBenchmarks();
|
RenderMenu();
|
||||||
HandleArguments(arguments);
|
|
||||||
|
|
||||||
while (true)
|
var key = Console.ReadKey(true);
|
||||||
|
|
||||||
|
if (key.Key == ConsoleKey.DownArrow)
|
||||||
{
|
{
|
||||||
RenderMenu();
|
_selectedBenchmarkIndex++;
|
||||||
|
if (_selectedBenchmarkIndex > _benchmarks.Count - 1)
|
||||||
var key = Console.ReadKey(true);
|
|
||||||
|
|
||||||
if (key.Key == ConsoleKey.DownArrow)
|
|
||||||
{
|
{
|
||||||
_selectedBenchmarkIndex++;
|
_selectedBenchmarkIndex = 0;
|
||||||
if (_selectedBenchmarkIndex > _benchmarks.Count - 1)
|
|
||||||
{
|
|
||||||
_selectedBenchmarkIndex = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (key.Key == ConsoleKey.UpArrow)
|
|
||||||
{
|
|
||||||
_selectedBenchmarkIndex--;
|
|
||||||
if (_selectedBenchmarkIndex < 0)
|
|
||||||
{
|
|
||||||
_selectedBenchmarkIndex = _benchmarks.Count - 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (key.Key == ConsoleKey.Escape)
|
|
||||||
{
|
|
||||||
Environment.Exit(0);
|
|
||||||
}
|
|
||||||
else if (key.Key == ConsoleKey.Enter)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (key.Key == ConsoleKey.UpArrow)
|
||||||
BenchmarkRunner.Run(_benchmarks[_selectedBenchmarkIndex]);
|
|
||||||
|
|
||||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
|
||||||
Console.WriteLine("Press any key to exit");
|
|
||||||
Console.ReadLine();
|
|
||||||
}
|
|
||||||
|
|
||||||
static List<Type> CollectBenchmarks()
|
|
||||||
{
|
|
||||||
var benchmarks = new List<Type>();
|
|
||||||
|
|
||||||
var types = typeof(Program).Assembly.GetExportedTypes();
|
|
||||||
foreach (var type in types)
|
|
||||||
{
|
{
|
||||||
if (typeof(BaseBenchmark).IsAssignableFrom(type))
|
_selectedBenchmarkIndex--;
|
||||||
|
if (_selectedBenchmarkIndex < 0)
|
||||||
{
|
{
|
||||||
if (type != typeof(BaseBenchmark))
|
_selectedBenchmarkIndex = _benchmarks.Count - 1;
|
||||||
{
|
|
||||||
benchmarks.Add(type);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (key.Key == ConsoleKey.Escape)
|
||||||
return benchmarks.OrderBy(b => b.Name).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
static void HandleArguments(string[] arguments)
|
|
||||||
{
|
|
||||||
if (arguments.Length == 0)
|
|
||||||
{
|
{
|
||||||
return;
|
Environment.Exit(0);
|
||||||
}
|
}
|
||||||
|
else if (key.Key == ConsoleKey.Enter)
|
||||||
// Allow for preselection to avoid developer frustration.
|
|
||||||
|
|
||||||
if (int.TryParse(arguments[0], out var benchmarkIndex))
|
|
||||||
{
|
{
|
||||||
_selectedBenchmarkIndex = benchmarkIndex;
|
break;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_selectedBenchmarkIndex = _benchmarks.FindIndex(b => b.Name.Equals(arguments[0]));
|
|
||||||
|
|
||||||
if (_selectedBenchmarkIndex < 0)
|
|
||||||
{
|
|
||||||
_selectedBenchmarkIndex = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_selectedBenchmarkIndex > _benchmarks.Count - 1)
|
|
||||||
{
|
|
||||||
_selectedBenchmarkIndex = _benchmarks.Count - 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void RenderMenu()
|
BenchmarkRunner.Run(_benchmarks[_selectedBenchmarkIndex]);
|
||||||
|
|
||||||
|
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||||
|
Console.WriteLine("Press any key to exit");
|
||||||
|
Console.ReadLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<Type> CollectBenchmarks()
|
||||||
|
{
|
||||||
|
var benchmarks = new List<Type>();
|
||||||
|
|
||||||
|
var types = typeof(Program).Assembly.GetExportedTypes();
|
||||||
|
foreach (var type in types)
|
||||||
{
|
{
|
||||||
Console.Clear();
|
if (typeof(BaseBenchmark).IsAssignableFrom(type))
|
||||||
|
|
||||||
Console.WriteLine($"MQTTnet - Benchmarks");
|
|
||||||
Console.WriteLine("-----------------------------------------------");
|
|
||||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
|
||||||
Console.WriteLine("Press arrow keys for benchmark selection");
|
|
||||||
Console.WriteLine("Press Enter for benchmark execution");
|
|
||||||
Console.WriteLine("Press Esc for exit");
|
|
||||||
Console.ForegroundColor = ConsoleColor.White;
|
|
||||||
Console.WriteLine();
|
|
||||||
|
|
||||||
for (var index = 0; index < _benchmarks.Count; index++)
|
|
||||||
{
|
{
|
||||||
var benchmark = _benchmarks[index];
|
if (type != typeof(BaseBenchmark))
|
||||||
|
|
||||||
if (_selectedBenchmarkIndex == index)
|
|
||||||
{
|
{
|
||||||
Console.Write("-> ");
|
benchmarks.Add(type);
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
Console.Write(" ");
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.WriteLine(benchmark.Name);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Console.WriteLine();
|
return benchmarks.OrderBy(b => b.Name).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void HandleArguments(string[] arguments)
|
||||||
|
{
|
||||||
|
if (arguments.Length == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow for preselection to avoid developer frustration.
|
||||||
|
|
||||||
|
if (int.TryParse(arguments[0], out var benchmarkIndex))
|
||||||
|
{
|
||||||
|
_selectedBenchmarkIndex = benchmarkIndex;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_selectedBenchmarkIndex = _benchmarks.FindIndex(b => b.Name.Equals(arguments[0]));
|
||||||
|
|
||||||
|
if (_selectedBenchmarkIndex < 0)
|
||||||
|
{
|
||||||
|
_selectedBenchmarkIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_selectedBenchmarkIndex > _benchmarks.Count - 1)
|
||||||
|
{
|
||||||
|
_selectedBenchmarkIndex = _benchmarks.Count - 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void RenderMenu()
|
||||||
|
{
|
||||||
|
Console.Clear();
|
||||||
|
|
||||||
|
Console.WriteLine($"MQTTnet - Benchmarks");
|
||||||
|
Console.WriteLine("-----------------------------------------------");
|
||||||
|
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||||
|
Console.WriteLine("Press arrow keys for benchmark selection");
|
||||||
|
Console.WriteLine("Press Enter for benchmark execution");
|
||||||
|
Console.WriteLine("Press Esc for exit");
|
||||||
|
Console.ForegroundColor = ConsoleColor.White;
|
||||||
|
Console.WriteLine();
|
||||||
|
|
||||||
|
for (var index = 0; index < _benchmarks.Count; index++)
|
||||||
|
{
|
||||||
|
var benchmark = _benchmarks[index];
|
||||||
|
|
||||||
|
if (_selectedBenchmarkIndex == index)
|
||||||
|
{
|
||||||
|
Console.Write("-> ");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.Write(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine(benchmark.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,240 +1,86 @@
|
|||||||
using BenchmarkDotNet.Attributes;
|
using BenchmarkDotNet.Attributes;
|
||||||
using BenchmarkDotNet.Jobs;
|
using BenchmarkDotNet.Jobs;
|
||||||
using MQTTnet.Adapter;
|
|
||||||
using MQTTnet.AspNetCore;
|
using MQTTnet.AspNetCore;
|
||||||
using MQTTnet.Exceptions;
|
using MQTTnet.Exceptions;
|
||||||
using MQTTnet.Formatter;
|
using MQTTnet.Formatter;
|
||||||
using MQTTnet.Packets;
|
|
||||||
using System;
|
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.IO.Pipelines;
|
using System.IO.Pipelines;
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace MQTTnet.Benchmarks
|
namespace MQTTnet.Benchmarks;
|
||||||
{
|
|
||||||
[SimpleJob(RuntimeMoniker.Net60)]
|
|
||||||
[RPlotExporter, RankColumn]
|
|
||||||
[MemoryDiagnoser]
|
|
||||||
public class ReaderExtensionsBenchmark
|
|
||||||
{
|
|
||||||
MqttPacketFormatterAdapter mqttPacketFormatter;
|
|
||||||
MemoryStream stream;
|
|
||||||
|
|
||||||
[GlobalSetup]
|
[SimpleJob(RuntimeMoniker.Net60)]
|
||||||
public void GlobalSetup()
|
[RPlotExporter, RankColumn]
|
||||||
{
|
[MemoryDiagnoser]
|
||||||
mqttPacketFormatter = new MqttPacketFormatterAdapter(MqttProtocolVersion.V311, new MqttBufferWriter(4096, 65535));
|
public class ReaderExtensionsBenchmark
|
||||||
var mqttMessage = new MqttApplicationMessageBuilder()
|
{
|
||||||
|
MqttPacketFormatterAdapter _mqttPacketFormatter;
|
||||||
|
MemoryStream _stream;
|
||||||
|
|
||||||
|
[GlobalSetup]
|
||||||
|
public void GlobalSetup()
|
||||||
|
{
|
||||||
|
_mqttPacketFormatter = new MqttPacketFormatterAdapter(MqttProtocolVersion.V311, new MqttBufferWriter(4096, 65535));
|
||||||
|
var mqttMessage = new MqttApplicationMessageBuilder()
|
||||||
.WithTopic("topic")
|
.WithTopic("topic")
|
||||||
.WithPayload(new byte[10 * 1024])
|
.WithPayload(new byte[10 * 1024])
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
var packet = MqttPublishPacketFactory.Create(mqttMessage);
|
var packet = MqttPublishPacketFactory.Create(mqttMessage);
|
||||||
|
|
||||||
var buffer = mqttPacketFormatter.Encode(packet);
|
var buffer = _mqttPacketFormatter.Encode(packet);
|
||||||
stream = new MemoryStream();
|
_stream = new MemoryStream();
|
||||||
stream.Write(buffer.Packet);
|
_stream.Write(buffer.Packet);
|
||||||
stream.Write(buffer.Payload.ToArray());
|
_stream.Write(buffer.Payload.ToArray());
|
||||||
mqttPacketFormatter.Cleanup();
|
_mqttPacketFormatter.Cleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Benchmark(Baseline = true)]
|
[Benchmark]
|
||||||
public async Task Before()
|
public async Task After()
|
||||||
|
{
|
||||||
|
_stream.Position = 0;
|
||||||
|
var input = PipeReader.Create(_stream);
|
||||||
|
|
||||||
|
while (true)
|
||||||
{
|
{
|
||||||
stream.Position = 0;
|
ReadResult readResult;
|
||||||
var input = PipeReader.Create(stream);
|
var readTask = input.ReadAsync();
|
||||||
|
if (readTask.IsCompleted)
|
||||||
while (true)
|
|
||||||
{
|
{
|
||||||
ReadResult readResult;
|
readResult = readTask.Result;
|
||||||
var readTask = input.ReadAsync();
|
}
|
||||||
if (readTask.IsCompleted)
|
else
|
||||||
{
|
{
|
||||||
readResult = readTask.Result;
|
readResult = await readTask.ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
readResult = await readTask.ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
var buffer = readResult.Buffer;
|
var buffer = readResult.Buffer;
|
||||||
|
|
||||||
var consumed = buffer.Start;
|
var consumed = buffer.Start;
|
||||||
var observed = buffer.Start;
|
var observed = buffer.Start;
|
||||||
|
|
||||||
try
|
try
|
||||||
|
{
|
||||||
|
if (!buffer.IsEmpty)
|
||||||
{
|
{
|
||||||
if (!buffer.IsEmpty)
|
if (_mqttPacketFormatter.TryDecode(buffer, out _, out consumed, out observed, out _))
|
||||||
{
|
{
|
||||||
if (ReaderExtensions_Before.TryDecode(mqttPacketFormatter, buffer, out var packet, out consumed, out observed, out var received))
|
break;
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (readResult.IsCompleted)
|
|
||||||
{
|
|
||||||
throw new MqttCommunicationException("Connection Aborted");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
else if (readResult.IsCompleted)
|
||||||
{
|
{
|
||||||
// The buffer was sliced up to where it was consumed, so we can just advance to the start.
|
throw new MqttCommunicationException("Connection Aborted");
|
||||||
// We mark examined as buffer.End so that if we didn't receive a full frame, we'll wait for more data
|
|
||||||
// before yielding the read again.
|
|
||||||
input.AdvanceTo(consumed, observed);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// The buffer was sliced up to where it was consumed, so we can just advance to the start.
|
||||||
|
// We mark examined as buffer.End so that if we didn't receive a full frame, we'll wait for more data
|
||||||
|
// before yielding the read again.
|
||||||
|
input.AdvanceTo(consumed, observed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Benchmark]
|
|
||||||
public async Task After()
|
|
||||||
{
|
|
||||||
stream.Position = 0;
|
|
||||||
var input = PipeReader.Create(stream);
|
|
||||||
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
ReadResult readResult;
|
|
||||||
var readTask = input.ReadAsync();
|
|
||||||
if (readTask.IsCompleted)
|
|
||||||
{
|
|
||||||
readResult = readTask.Result;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
readResult = await readTask.ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
var buffer = readResult.Buffer;
|
|
||||||
|
|
||||||
var consumed = buffer.Start;
|
|
||||||
var observed = buffer.Start;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!buffer.IsEmpty)
|
|
||||||
{
|
|
||||||
if (ReaderExtensions.TryDecode(mqttPacketFormatter, buffer, out var packet, out consumed, out observed, out var received))
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (readResult.IsCompleted)
|
|
||||||
{
|
|
||||||
throw new MqttCommunicationException("Connection Aborted");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
// The buffer was sliced up to where it was consumed, so we can just advance to the start.
|
|
||||||
// We mark examined as buffer.End so that if we didn't receive a full frame, we'll wait for more data
|
|
||||||
// before yielding the read again.
|
|
||||||
input.AdvanceTo(consumed, observed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class ReaderExtensions_Before
|
|
||||||
{
|
|
||||||
public static bool TryDecode(MqttPacketFormatterAdapter formatter,
|
|
||||||
in ReadOnlySequence<byte> input,
|
|
||||||
out MqttPacket packet,
|
|
||||||
out SequencePosition consumed,
|
|
||||||
out SequencePosition observed,
|
|
||||||
out int bytesRead)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(formatter);
|
|
||||||
|
|
||||||
packet = null;
|
|
||||||
consumed = input.Start;
|
|
||||||
observed = input.End;
|
|
||||||
bytesRead = 0;
|
|
||||||
var copy = input;
|
|
||||||
|
|
||||||
if (copy.Length < 2)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var fixedHeader = copy.First.Span[0];
|
|
||||||
if (!TryReadBodyLength(ref copy, out int headerLength, out var bodyLength))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (copy.Length < bodyLength)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var bodySlice = copy.Slice(0, bodyLength);
|
|
||||||
var buffer = GetMemory(bodySlice).ToArray();
|
|
||||||
|
|
||||||
var receivedMqttPacket = new ReceivedMqttPacket(fixedHeader, new ArraySegment<byte>(buffer, 0, buffer.Length), buffer.Length + 2);
|
|
||||||
|
|
||||||
if (formatter.ProtocolVersion == MqttProtocolVersion.Unknown)
|
|
||||||
{
|
|
||||||
formatter.DetectProtocolVersion(receivedMqttPacket);
|
|
||||||
}
|
|
||||||
|
|
||||||
packet = formatter.Decode(receivedMqttPacket);
|
|
||||||
consumed = bodySlice.End;
|
|
||||||
observed = bodySlice.End;
|
|
||||||
bytesRead = headerLength + bodyLength;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static ReadOnlyMemory<byte> GetMemory(in ReadOnlySequence<byte> input)
|
|
||||||
{
|
|
||||||
if (input.IsSingleSegment)
|
|
||||||
{
|
|
||||||
return input.First;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Should be rare
|
|
||||||
return input.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool TryReadBodyLength(ref ReadOnlySequence<byte> input, out int headerLength, out int bodyLength)
|
|
||||||
{
|
|
||||||
// Alorithm taken from https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/errata01/os/mqtt-v3.1.1-errata01-os-complete.html.
|
|
||||||
var multiplier = 1;
|
|
||||||
var value = 0;
|
|
||||||
byte encodedByte;
|
|
||||||
var index = 1;
|
|
||||||
headerLength = 0;
|
|
||||||
bodyLength = 0;
|
|
||||||
|
|
||||||
var temp = GetMemory(input.Slice(0, Math.Min(5, input.Length))).Span;
|
|
||||||
|
|
||||||
do
|
|
||||||
{
|
|
||||||
if (index == temp.Length)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
encodedByte = temp[index];
|
|
||||||
index++;
|
|
||||||
|
|
||||||
value += (byte)(encodedByte & 127) * multiplier;
|
|
||||||
if (multiplier > 128 * 128 * 128)
|
|
||||||
{
|
|
||||||
throw new MqttProtocolViolationException($"Remaining length is invalid (Data={string.Join(",", temp.Slice(1, index).ToArray())}).");
|
|
||||||
}
|
|
||||||
|
|
||||||
multiplier *= 128;
|
|
||||||
} while ((encodedByte & 128) != 0);
|
|
||||||
|
|
||||||
input = input.Slice(index);
|
|
||||||
|
|
||||||
headerLength = index;
|
|
||||||
bodyLength = value;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,28 +3,27 @@ using BenchmarkDotNet.Jobs;
|
|||||||
using MQTTnet.Tests.Mockups;
|
using MQTTnet.Tests.Mockups;
|
||||||
using MQTTnet.Tests.Server;
|
using MQTTnet.Tests.Server;
|
||||||
|
|
||||||
namespace MQTTnet.Benchmarks
|
namespace MQTTnet.Benchmarks;
|
||||||
|
|
||||||
|
[SimpleJob(RuntimeMoniker.Net60)]
|
||||||
|
[RPlotExporter, RankColumn]
|
||||||
|
[MemoryDiagnoser]
|
||||||
|
public class RoundtripProcessingBenchmark : BaseBenchmark
|
||||||
{
|
{
|
||||||
[SimpleJob(RuntimeMoniker.Net60)]
|
[GlobalSetup]
|
||||||
[RPlotExporter, RankColumn]
|
public void GlobalSetup()
|
||||||
[MemoryDiagnoser]
|
|
||||||
public class RoundtripProcessingBenchmark : BaseBenchmark
|
|
||||||
{
|
{
|
||||||
[GlobalSetup]
|
TestEnvironment.EnableLogger = false;
|
||||||
public void GlobalSetup()
|
}
|
||||||
{
|
|
||||||
TestEnvironment.EnableLogger = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
[GlobalCleanup]
|
[GlobalCleanup]
|
||||||
public void GlobalCleanup()
|
public void GlobalCleanup()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
[Benchmark]
|
[Benchmark]
|
||||||
public void Handle_100_000_Messages_In_Receiving_Client()
|
public void Handle_100_000_Messages_In_Receiving_Client()
|
||||||
{
|
{
|
||||||
new Load_Tests().Handle_100_000_Messages_In_Receiving_Client().GetAwaiter().GetResult();
|
new Load_Tests().Handle_100_000_Messages_In_Receiving_Client().GetAwaiter().GetResult();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,63 +7,61 @@ using System.IO;
|
|||||||
using System.IO.Pipelines;
|
using System.IO.Pipelines;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace MQTTnet.Benchmarks
|
namespace MQTTnet.Benchmarks;
|
||||||
|
|
||||||
|
[SimpleJob(RuntimeMoniker.Net60)]
|
||||||
|
[RPlotExporter, RankColumn]
|
||||||
|
[MemoryDiagnoser]
|
||||||
|
public class SendPacketAsyncBenchmark : BaseBenchmark
|
||||||
{
|
{
|
||||||
[SimpleJob(RuntimeMoniker.Net60)]
|
MemoryStream _stream;
|
||||||
[RPlotExporter, RankColumn]
|
MqttPacketBuffer _buffer;
|
||||||
[MemoryDiagnoser]
|
|
||||||
public class SendPacketAsyncBenchmark : BaseBenchmark
|
[GlobalSetup]
|
||||||
|
public void GlobalSetup()
|
||||||
{
|
{
|
||||||
MemoryStream stream;
|
_stream = new MemoryStream(1024);
|
||||||
MqttPacketBuffer buffer;
|
var packet = new ArraySegment<byte>(new byte[10]);
|
||||||
|
_buffer = new MqttPacketBuffer(packet);
|
||||||
|
}
|
||||||
|
|
||||||
[GlobalSetup]
|
[Benchmark(Baseline = true)]
|
||||||
public void GlobalSetup()
|
public async ValueTask Before()
|
||||||
|
{
|
||||||
|
_stream.Position = 0;
|
||||||
|
var output = PipeWriter.Create(_stream);
|
||||||
|
|
||||||
|
WritePacketBuffer(output, _buffer);
|
||||||
|
await output.FlushAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Benchmark]
|
||||||
|
public async ValueTask After()
|
||||||
|
{
|
||||||
|
_stream.Position = 0;
|
||||||
|
var output = PipeWriter.Create(_stream);
|
||||||
|
|
||||||
|
if (_buffer.Payload.Length == 0)
|
||||||
{
|
{
|
||||||
stream = new MemoryStream(1024);
|
await output.WriteAsync(_buffer.Packet).ConfigureAwait(false);
|
||||||
var packet = new ArraySegment<byte>(new byte[10]);
|
|
||||||
buffer = new MqttPacketBuffer(packet);
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
[Benchmark(Baseline = true)]
|
|
||||||
public async ValueTask Before()
|
|
||||||
{
|
{
|
||||||
stream.Position = 0;
|
WritePacketBuffer(output, _buffer);
|
||||||
var output = PipeWriter.Create(stream);
|
await output.FlushAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
WritePacketBuffer(output, buffer);
|
|
||||||
await output.FlushAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Benchmark]
|
|
||||||
public async ValueTask After()
|
|
||||||
{
|
|
||||||
stream.Position = 0;
|
|
||||||
var output = PipeWriter.Create(stream);
|
|
||||||
|
|
||||||
if (buffer.Payload.Length == 0)
|
|
||||||
{
|
|
||||||
await output.WriteAsync(buffer.Packet).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
WritePacketBuffer(output, buffer);
|
|
||||||
await output.FlushAsync().ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static void WritePacketBuffer(PipeWriter output, MqttPacketBuffer buffer)
|
|
||||||
{
|
|
||||||
// copy MqttPacketBuffer's Packet and Payload to the same buffer block of PipeWriter
|
|
||||||
// MqttPacket will be transmitted within the bounds of a WebSocket frame after PipeWriter.FlushAsync
|
|
||||||
|
|
||||||
var span = output.GetSpan(buffer.Length);
|
|
||||||
|
|
||||||
buffer.Packet.AsSpan().CopyTo(span);
|
|
||||||
buffer.Payload.CopyTo(span.Slice(buffer.Packet.Count));
|
|
||||||
|
|
||||||
output.Advance(buffer.Length);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void WritePacketBuffer(PipeWriter output, MqttPacketBuffer buffer)
|
||||||
|
{
|
||||||
|
// copy MqttPacketBuffer's Packet and Payload to the same buffer block of PipeWriter
|
||||||
|
// MqttPacket will be transmitted within the bounds of a WebSocket frame after PipeWriter.FlushAsync
|
||||||
|
|
||||||
|
var span = output.GetSpan(buffer.Length);
|
||||||
|
|
||||||
|
buffer.Packet.AsSpan().CopyTo(span);
|
||||||
|
buffer.Payload.CopyTo(span[buffer.Packet.Count..]);
|
||||||
|
|
||||||
|
output.Advance(buffer.Length);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -17,103 +17,102 @@ using MQTTnet.Diagnostics.Logger;
|
|||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
|
||||||
namespace MQTTnet.Benchmarks
|
namespace MQTTnet.Benchmarks;
|
||||||
|
|
||||||
|
[SimpleJob(RuntimeMoniker.Net60)]
|
||||||
|
[RPlotExporter]
|
||||||
|
[MemoryDiagnoser]
|
||||||
|
public class SerializerBenchmark : BaseBenchmark
|
||||||
{
|
{
|
||||||
[SimpleJob(RuntimeMoniker.Net60)]
|
MqttPacket _packet;
|
||||||
[RPlotExporter]
|
ArraySegment<byte> _serializedPacket;
|
||||||
[MemoryDiagnoser]
|
IMqttPacketFormatter _serializer;
|
||||||
public class SerializerBenchmark : BaseBenchmark
|
MqttBufferWriter _bufferWriter;
|
||||||
|
|
||||||
|
[GlobalSetup]
|
||||||
|
public void GlobalSetup()
|
||||||
{
|
{
|
||||||
MqttPacket _packet;
|
_packet = new MqttPublishPacket
|
||||||
ArraySegment<byte> _serializedPacket;
|
|
||||||
IMqttPacketFormatter _serializer;
|
|
||||||
MqttBufferWriter _bufferWriter;
|
|
||||||
|
|
||||||
[GlobalSetup]
|
|
||||||
public void GlobalSetup()
|
|
||||||
{
|
{
|
||||||
_packet = new MqttPublishPacket
|
Topic = "A"
|
||||||
{
|
};
|
||||||
Topic = "A"
|
|
||||||
};
|
|
||||||
|
|
||||||
_bufferWriter = new MqttBufferWriter(4096, 65535);
|
_bufferWriter = new MqttBufferWriter(4096, 65535);
|
||||||
_serializer = new MqttV3PacketFormatter(_bufferWriter, MqttProtocolVersion.V311);
|
_serializer = new MqttV3PacketFormatter(_bufferWriter, MqttProtocolVersion.V311);
|
||||||
_serializedPacket = _serializer.Encode(_packet).Join();
|
_serializedPacket = _serializer.Encode(_packet).Join();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Benchmark]
|
||||||
|
public void Serialize_10000_Messages()
|
||||||
|
{
|
||||||
|
for (var i = 0; i < 10000; i++)
|
||||||
|
{
|
||||||
|
_serializer.Encode(_packet);
|
||||||
|
_bufferWriter.Cleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Benchmark]
|
||||||
|
public void Deserialize_10000_Messages()
|
||||||
|
{
|
||||||
|
var channel = new BenchmarkMqttChannel(_serializedPacket);
|
||||||
|
var reader = new MqttChannelAdapter(channel, new MqttPacketFormatterAdapter(new MqttBufferWriter(4096, 65535)), new MqttNetEventLogger());
|
||||||
|
|
||||||
|
for (var i = 0; i < 10000; i++)
|
||||||
|
{
|
||||||
|
channel.Reset();
|
||||||
|
|
||||||
|
reader.ReceivePacketAsync(CancellationToken.None).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BenchmarkMqttChannel : IMqttChannel
|
||||||
|
{
|
||||||
|
readonly ArraySegment<byte> _buffer;
|
||||||
|
int _position;
|
||||||
|
|
||||||
|
public BenchmarkMqttChannel(ArraySegment<byte> buffer)
|
||||||
|
{
|
||||||
|
_buffer = buffer;
|
||||||
|
_position = _buffer.Offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Benchmark]
|
public EndPoint RemoteEndPoint { get; set; }
|
||||||
public void Serialize_10000_Messages()
|
|
||||||
|
public bool IsSecureConnection { get; } = false;
|
||||||
|
|
||||||
|
public X509Certificate2 ClientCertificate { get; set; }
|
||||||
|
|
||||||
|
public void Reset()
|
||||||
{
|
{
|
||||||
for (var i = 0; i < 10000; i++)
|
_position = _buffer.Offset;
|
||||||
{
|
|
||||||
_serializer.Encode(_packet);
|
|
||||||
_bufferWriter.Cleanup();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Benchmark]
|
public Task ConnectAsync(CancellationToken cancellationToken)
|
||||||
public void Deserialize_10000_Messages()
|
|
||||||
{
|
{
|
||||||
var channel = new BenchmarkMqttChannel(_serializedPacket);
|
throw new NotSupportedException();
|
||||||
var reader = new MqttChannelAdapter(channel, new MqttPacketFormatterAdapter(new MqttBufferWriter(4096, 65535)), new MqttNetEventLogger());
|
|
||||||
|
|
||||||
for (var i = 0; i < 10000; i++)
|
|
||||||
{
|
|
||||||
channel.Reset();
|
|
||||||
|
|
||||||
var header = reader.ReceivePacketAsync(CancellationToken.None).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class BenchmarkMqttChannel : IMqttChannel
|
public Task DisconnectAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
readonly ArraySegment<byte> _buffer;
|
throw new NotSupportedException();
|
||||||
int _position;
|
}
|
||||||
|
|
||||||
public BenchmarkMqttChannel(ArraySegment<byte> buffer)
|
public Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
_buffer = buffer;
|
Array.Copy(_buffer!.Array!, _position, buffer, offset, count);
|
||||||
_position = _buffer.Offset;
|
_position += count;
|
||||||
}
|
|
||||||
|
|
||||||
public EndPoint RemoteEndPoint { get; set; }
|
return Task.FromResult(count);
|
||||||
|
}
|
||||||
|
|
||||||
public bool IsSecureConnection { get; } = false;
|
public Task WriteAsync(ReadOnlySequence<byte> buffer, bool isEndOfPacket, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
public X509Certificate2 ClientCertificate { get; }
|
public void Dispose()
|
||||||
|
{
|
||||||
public void Reset()
|
|
||||||
{
|
|
||||||
_position = _buffer.Offset;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task ConnectAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
throw new NotSupportedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task DisconnectAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
throw new NotSupportedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
Array.Copy(_buffer.Array, _position, buffer, offset, count);
|
|
||||||
_position += count;
|
|
||||||
|
|
||||||
return Task.FromResult(count);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task WriteAsync(ReadOnlySequence<byte> buffer, bool isEndOfPacket, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
throw new NotSupportedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,34 +7,33 @@ using BenchmarkDotNet.Jobs;
|
|||||||
using MQTTnet.Tests.Mockups;
|
using MQTTnet.Tests.Mockups;
|
||||||
using MQTTnet.Tests.Server;
|
using MQTTnet.Tests.Server;
|
||||||
|
|
||||||
namespace MQTTnet.Benchmarks
|
namespace MQTTnet.Benchmarks;
|
||||||
|
|
||||||
|
[SimpleJob(RuntimeMoniker.Net60)]
|
||||||
|
[RPlotExporter, RankColumn]
|
||||||
|
[MemoryDiagnoser]
|
||||||
|
public class ServerProcessingBenchmark : BaseBenchmark
|
||||||
{
|
{
|
||||||
[SimpleJob(RuntimeMoniker.Net60)]
|
[GlobalSetup]
|
||||||
[RPlotExporter, RankColumn]
|
public void GlobalSetup()
|
||||||
[MemoryDiagnoser]
|
|
||||||
public class ServerProcessingBenchmark : BaseBenchmark
|
|
||||||
{
|
{
|
||||||
[GlobalSetup]
|
TestEnvironment.EnableLogger = false;
|
||||||
public void GlobalSetup()
|
}
|
||||||
{
|
|
||||||
TestEnvironment.EnableLogger = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
[GlobalCleanup]
|
[GlobalCleanup]
|
||||||
public void GlobalCleanup()
|
public void GlobalCleanup()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
[Benchmark]
|
[Benchmark]
|
||||||
public void Handle_100_000_Messages_In_Server_MqttClient()
|
public void Handle_100_000_Messages_In_Server_MqttClient()
|
||||||
{
|
{
|
||||||
new Load_Tests().Handle_100_000_Messages_In_Server().GetAwaiter().GetResult();
|
new Load_Tests().Handle_100_000_Messages_In_Server().GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
//[Benchmark]
|
//[Benchmark]
|
||||||
public void Handle_100_000_Messages_In_Server_LowLevelMqttClient()
|
public void Handle_100_000_Messages_In_Server_LowLevelMqttClient()
|
||||||
{
|
{
|
||||||
new Load_Tests().Handle_100_000_Messages_In_Low_Level_Client().GetAwaiter().GetResult();
|
new Load_Tests().Handle_100_000_Messages_In_Low_Level_Client().GetAwaiter().GetResult();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -30,7 +30,7 @@ public class SubscribeBenchmark : BaseBenchmark
|
|||||||
[GlobalSetup]
|
[GlobalSetup]
|
||||||
public void Setup()
|
public void Setup()
|
||||||
{
|
{
|
||||||
TopicGenerator.Generate(NumPublishers, NumTopicsPerPublisher, out var topicsByPublisher, out var singleWildcardTopicsByPublisher, out var multiWildcardTopicsByPublisher);
|
TopicGenerator.Generate(NumPublishers, NumTopicsPerPublisher, out var topicsByPublisher, out _, out _);
|
||||||
_topics = topicsByPublisher.Values.First();
|
_topics = topicsByPublisher.Values.First();
|
||||||
|
|
||||||
var serverOptions = new MqttServerOptionsBuilder().WithDefaultEndpoint().Build();
|
var serverOptions = new MqttServerOptionsBuilder().WithDefaultEndpoint().Build();
|
||||||
|
|||||||
@@ -11,70 +11,69 @@ using BenchmarkDotNet.Attributes;
|
|||||||
using BenchmarkDotNet.Jobs;
|
using BenchmarkDotNet.Jobs;
|
||||||
using MQTTnet.AspNetCore;
|
using MQTTnet.AspNetCore;
|
||||||
|
|
||||||
namespace MQTTnet.Benchmarks
|
namespace MQTTnet.Benchmarks;
|
||||||
|
|
||||||
|
[SimpleJob(RuntimeMoniker.Net60)]
|
||||||
|
[MemoryDiagnoser]
|
||||||
|
public class TcpPipesBenchmark : BaseBenchmark
|
||||||
{
|
{
|
||||||
[SimpleJob(RuntimeMoniker.Net60)]
|
IDuplexPipe _client;
|
||||||
[MemoryDiagnoser]
|
IDuplexPipe _server;
|
||||||
public class TcpPipesBenchmark : BaseBenchmark
|
|
||||||
|
[GlobalSetup]
|
||||||
|
public void Setup()
|
||||||
{
|
{
|
||||||
IDuplexPipe _client;
|
var server = new TcpListener(IPAddress.Any, 1883);
|
||||||
IDuplexPipe _server;
|
|
||||||
|
|
||||||
[GlobalSetup]
|
server.Start(1);
|
||||||
public void Setup()
|
|
||||||
|
var task = Task.Run(() => server.AcceptSocket());
|
||||||
|
|
||||||
|
var clientConnection = new SocketConnection(new IPEndPoint(IPAddress.Loopback, 1883));
|
||||||
|
|
||||||
|
clientConnection.StartAsync().GetAwaiter().GetResult();
|
||||||
|
_client = clientConnection.Transport;
|
||||||
|
|
||||||
|
var serverConnection = new SocketConnection(task.GetAwaiter().GetResult());
|
||||||
|
serverConnection.StartAsync().GetAwaiter().GetResult();
|
||||||
|
_server = serverConnection.Transport;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[Benchmark]
|
||||||
|
public async Task Send_10000_Chunks_Pipe()
|
||||||
|
{
|
||||||
|
var size = 5;
|
||||||
|
var iterations = 10000;
|
||||||
|
|
||||||
|
await Task.WhenAll(WriteAsync(iterations, size), ReadAsync(iterations, size));
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task ReadAsync(int iterations, int size)
|
||||||
|
{
|
||||||
|
await Task.Yield();
|
||||||
|
|
||||||
|
var expected = iterations * size;
|
||||||
|
long read = 0;
|
||||||
|
var input = _client.Input;
|
||||||
|
|
||||||
|
while (read < expected)
|
||||||
{
|
{
|
||||||
var server = new TcpListener(IPAddress.Any, 1883);
|
var readresult = await input.ReadAsync(CancellationToken.None).ConfigureAwait(false);
|
||||||
|
input.AdvanceTo(readresult.Buffer.End);
|
||||||
server.Start(1);
|
read += readresult.Buffer.Length;
|
||||||
|
|
||||||
var task = Task.Run(() => server.AcceptSocket());
|
|
||||||
|
|
||||||
var clientConnection = new SocketConnection(new IPEndPoint(IPAddress.Loopback, 1883));
|
|
||||||
|
|
||||||
clientConnection.StartAsync().GetAwaiter().GetResult();
|
|
||||||
_client = clientConnection.Transport;
|
|
||||||
|
|
||||||
var serverConnection = new SocketConnection(task.GetAwaiter().GetResult());
|
|
||||||
serverConnection.StartAsync().GetAwaiter().GetResult();
|
|
||||||
_server = serverConnection.Transport;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task WriteAsync(int iterations, int size)
|
||||||
|
{
|
||||||
|
await Task.Yield();
|
||||||
|
|
||||||
[Benchmark]
|
var output = _server.Output;
|
||||||
public async Task Send_10000_Chunks_Pipe()
|
|
||||||
|
for (var i = 0; i < iterations; i++)
|
||||||
{
|
{
|
||||||
var size = 5;
|
await output.WriteAsync(new byte[size], CancellationToken.None).ConfigureAwait(false);
|
||||||
var iterations = 10000;
|
|
||||||
|
|
||||||
await Task.WhenAll(WriteAsync(iterations, size), ReadAsync(iterations, size));
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task ReadAsync(int iterations, int size)
|
|
||||||
{
|
|
||||||
await Task.Yield();
|
|
||||||
|
|
||||||
var expected = iterations * size;
|
|
||||||
long read = 0;
|
|
||||||
var input = _client.Input;
|
|
||||||
|
|
||||||
while (read < expected)
|
|
||||||
{
|
|
||||||
var readresult = await input.ReadAsync(CancellationToken.None).ConfigureAwait(false);
|
|
||||||
input.AdvanceTo(readresult.Buffer.End);
|
|
||||||
read += readresult.Buffer.Length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task WriteAsync(int iterations, int size)
|
|
||||||
{
|
|
||||||
await Task.Yield();
|
|
||||||
|
|
||||||
var output = _server.Output;
|
|
||||||
|
|
||||||
for (var i = 0; i < iterations; i++)
|
|
||||||
{
|
|
||||||
await output.WriteAsync(new byte[size], CancellationToken.None).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,269 +2,32 @@
|
|||||||
// The .NET Foundation licenses this file to you under the MIT license.
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System;
|
|
||||||
using BenchmarkDotNet.Attributes;
|
using BenchmarkDotNet.Attributes;
|
||||||
using BenchmarkDotNet.Jobs;
|
using BenchmarkDotNet.Jobs;
|
||||||
|
|
||||||
namespace MQTTnet.Benchmarks
|
namespace MQTTnet.Benchmarks;
|
||||||
|
|
||||||
|
[SimpleJob(RuntimeMoniker.Net60)]
|
||||||
|
[RPlotExporter]
|
||||||
|
[MemoryDiagnoser]
|
||||||
|
public class TopicFilterComparerBenchmark : BaseBenchmark
|
||||||
{
|
{
|
||||||
[SimpleJob(RuntimeMoniker.Net60)]
|
readonly string _longTopic =
|
||||||
[RPlotExporter]
|
"AAAAAAAAAAAAAssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssshhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC";
|
||||||
[MemoryDiagnoser]
|
|
||||||
public class TopicFilterComparerBenchmark : BaseBenchmark
|
[Benchmark]
|
||||||
|
public void MqttTopicFilterComparer_10000_LoopMethod()
|
||||||
{
|
{
|
||||||
static readonly char[] TopicLevelSeparator = { '/' };
|
for (var i = 0; i < 100000; i++)
|
||||||
|
|
||||||
readonly string _longTopic =
|
|
||||||
"AAAAAAAAAAAAAssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssshhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC";
|
|
||||||
|
|
||||||
[Benchmark]
|
|
||||||
public void MqttTopicFilterComparer_10000_LoopMethod()
|
|
||||||
{
|
{
|
||||||
for (var i = 0; i < 100000; i++)
|
MqttTopicFilterComparer.Compare("sport/tennis/player1", "sport/#");
|
||||||
{
|
MqttTopicFilterComparer.Compare("sport/tennis/player1/ranking", "sport/#/ranking");
|
||||||
MqttTopicFilterComparer.Compare("sport/tennis/player1", "sport/#");
|
MqttTopicFilterComparer.Compare("sport/tennis/player1/score/wimbledon", "sport/+/player1/#");
|
||||||
MqttTopicFilterComparer.Compare("sport/tennis/player1/ranking", "sport/#/ranking");
|
MqttTopicFilterComparer.Compare("sport/tennis/player1", "sport/tennis/+");
|
||||||
MqttTopicFilterComparer.Compare("sport/tennis/player1/score/wimbledon", "sport/+/player1/#");
|
MqttTopicFilterComparer.Compare("/finance", "+/+");
|
||||||
MqttTopicFilterComparer.Compare("sport/tennis/player1", "sport/tennis/+");
|
MqttTopicFilterComparer.Compare("/finance", "/+");
|
||||||
MqttTopicFilterComparer.Compare("/finance", "+/+");
|
MqttTopicFilterComparer.Compare("/finance", "+");
|
||||||
MqttTopicFilterComparer.Compare("/finance", "/+");
|
MqttTopicFilterComparer.Compare(_longTopic, _longTopic);
|
||||||
MqttTopicFilterComparer.Compare("/finance", "+");
|
|
||||||
MqttTopicFilterComparer.Compare(_longTopic, _longTopic);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Benchmark]
|
|
||||||
public void MqttTopicFilterComparer_10000_LoopMethod_Without_Pointer()
|
|
||||||
{
|
|
||||||
for (var i = 0; i < 100000; i++)
|
|
||||||
{
|
|
||||||
MqttTopicFilterComparerWithoutPointer.Compare("sport/tennis/player1", "sport/#");
|
|
||||||
MqttTopicFilterComparerWithoutPointer.Compare("sport/tennis/player1/ranking", "sport/#/ranking");
|
|
||||||
MqttTopicFilterComparerWithoutPointer.Compare("sport/tennis/player1/score/wimbledon", "sport/+/player1/#");
|
|
||||||
MqttTopicFilterComparerWithoutPointer.Compare("sport/tennis/player1", "sport/tennis/+");
|
|
||||||
MqttTopicFilterComparerWithoutPointer.Compare("/finance", "+/+");
|
|
||||||
MqttTopicFilterComparerWithoutPointer.Compare("/finance", "/+");
|
|
||||||
MqttTopicFilterComparerWithoutPointer.Compare("/finance", "+");
|
|
||||||
MqttTopicFilterComparerWithoutPointer.Compare(_longTopic, _longTopic);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Benchmark]
|
|
||||||
public void MqttTopicFilterComparer_10000_StringSplitMethod()
|
|
||||||
{
|
|
||||||
for (var i = 0; i < 100000; i++)
|
|
||||||
{
|
|
||||||
LegacyMethodByStringSplit("sport/tennis/player1", "sport/#");
|
|
||||||
LegacyMethodByStringSplit("sport/tennis/player1/ranking", "sport/#/ranking");
|
|
||||||
LegacyMethodByStringSplit("sport/tennis/player1/score/wimbledon", "sport/+/player1/#");
|
|
||||||
LegacyMethodByStringSplit("sport/tennis/player1", "sport/tennis/+");
|
|
||||||
LegacyMethodByStringSplit("/finance", "+/+");
|
|
||||||
LegacyMethodByStringSplit("/finance", "/+");
|
|
||||||
LegacyMethodByStringSplit("/finance", "+");
|
|
||||||
MqttTopicFilterComparer.Compare(_longTopic, _longTopic);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[GlobalSetup]
|
|
||||||
public void Setup()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool LegacyMethodByStringSplit(string topic, string filter)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(topic);
|
|
||||||
ArgumentNullException.ThrowIfNull(filter);
|
|
||||||
|
|
||||||
if (string.Equals(topic, filter, StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var fragmentsTopic = topic.Split(TopicLevelSeparator, StringSplitOptions.None);
|
|
||||||
var fragmentsFilter = filter.Split(TopicLevelSeparator, StringSplitOptions.None);
|
|
||||||
|
|
||||||
// # > In either case it MUST be the last character specified in the Topic Filter [MQTT-4.7.1-2].
|
|
||||||
for (var i = 0; i < fragmentsFilter.Length; i++)
|
|
||||||
{
|
|
||||||
if (fragmentsFilter[i] == "+")
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fragmentsFilter[i] == "#")
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i >= fragmentsTopic.Length)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.Equals(fragmentsFilter[i], fragmentsTopic[i], StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return fragmentsTopic.Length == fragmentsFilter.Length;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class MqttTopicFilterComparerWithoutPointer
|
|
||||||
{
|
|
||||||
public const char LevelSeparator = '/';
|
|
||||||
public const char MultiLevelWildcard = '#';
|
|
||||||
public const char SingleLevelWildcard = '+';
|
|
||||||
public const char ReservedTopicPrefix = '$';
|
|
||||||
|
|
||||||
public static MqttTopicFilterCompareResult Compare(string topic, string filter)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(topic))
|
|
||||||
{
|
|
||||||
return MqttTopicFilterCompareResult.TopicInvalid;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(filter))
|
|
||||||
{
|
|
||||||
return MqttTopicFilterCompareResult.FilterInvalid;
|
|
||||||
}
|
|
||||||
|
|
||||||
var filterOffset = 0;
|
|
||||||
var filterLength = filter.Length;
|
|
||||||
|
|
||||||
var topicOffset = 0;
|
|
||||||
var topicLength = topic.Length;
|
|
||||||
|
|
||||||
var topicPointer = topic;
|
|
||||||
var filterPointer = filter;
|
|
||||||
|
|
||||||
var isMultiLevelFilter = filterPointer[filterLength - 1] == MultiLevelWildcard;
|
|
||||||
var isReservedTopic = topicPointer[0] == ReservedTopicPrefix;
|
|
||||||
|
|
||||||
if (isReservedTopic && filterLength == 1 && isMultiLevelFilter)
|
|
||||||
{
|
|
||||||
// It is not allowed to receive i.e. '$foo/bar' with filter '#'.
|
|
||||||
return MqttTopicFilterCompareResult.NoMatch;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isReservedTopic && filterPointer[0] == SingleLevelWildcard)
|
|
||||||
{
|
|
||||||
// It is not allowed to receive i.e. '$SYS/monitor/Clients' with filter '+/monitor/Clients'.
|
|
||||||
return MqttTopicFilterCompareResult.NoMatch;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filterLength == 1 && isMultiLevelFilter)
|
|
||||||
{
|
|
||||||
// Filter '#' matches basically everything.
|
|
||||||
return MqttTopicFilterCompareResult.IsMatch;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Go through the filter char by char.
|
|
||||||
while (filterOffset < filterLength && topicOffset < topicLength)
|
|
||||||
{
|
|
||||||
// Check if the current char is a multi level wildcard. The char is only allowed
|
|
||||||
// at the very las position.
|
|
||||||
if (filterPointer[filterOffset] == MultiLevelWildcard && filterOffset != filterLength - 1)
|
|
||||||
{
|
|
||||||
return MqttTopicFilterCompareResult.FilterInvalid;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filterPointer[filterOffset] == topicPointer[topicOffset])
|
|
||||||
{
|
|
||||||
if (topicOffset == topicLength - 1)
|
|
||||||
{
|
|
||||||
// Check for e.g. "foo" matching "foo/#"
|
|
||||||
if (filterOffset == filterLength - 3 && filterPointer[filterOffset + 1] == LevelSeparator && isMultiLevelFilter)
|
|
||||||
{
|
|
||||||
return MqttTopicFilterCompareResult.IsMatch;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for e.g. "foo/" matching "foo/#"
|
|
||||||
if (filterOffset == filterLength - 2 && filterPointer[filterOffset] == LevelSeparator && isMultiLevelFilter)
|
|
||||||
{
|
|
||||||
return MqttTopicFilterCompareResult.IsMatch;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
filterOffset++;
|
|
||||||
topicOffset++;
|
|
||||||
|
|
||||||
// Check if the end was reached and i.e. "foo/bar" matches "foo/bar"
|
|
||||||
if (filterOffset == filterLength && topicOffset == topicLength)
|
|
||||||
{
|
|
||||||
return MqttTopicFilterCompareResult.IsMatch;
|
|
||||||
}
|
|
||||||
|
|
||||||
var endOfTopic = topicOffset == topicLength;
|
|
||||||
|
|
||||||
if (endOfTopic && filterOffset == filterLength - 1 && filterPointer[filterOffset] == SingleLevelWildcard)
|
|
||||||
{
|
|
||||||
if (filterOffset > 0 && filterPointer[filterOffset - 1] != LevelSeparator)
|
|
||||||
{
|
|
||||||
return MqttTopicFilterCompareResult.FilterInvalid;
|
|
||||||
}
|
|
||||||
|
|
||||||
return MqttTopicFilterCompareResult.IsMatch;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (filterPointer[filterOffset] == SingleLevelWildcard)
|
|
||||||
{
|
|
||||||
// Check for invalid "+foo" or "a/+foo" subscription
|
|
||||||
if (filterOffset > 0 && filterPointer[filterOffset - 1] != LevelSeparator)
|
|
||||||
{
|
|
||||||
return MqttTopicFilterCompareResult.FilterInvalid;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for bad "foo+" or "foo+/a" subscription
|
|
||||||
if (filterOffset < filterLength - 1 && filterPointer[filterOffset + 1] != LevelSeparator)
|
|
||||||
{
|
|
||||||
return MqttTopicFilterCompareResult.FilterInvalid;
|
|
||||||
}
|
|
||||||
|
|
||||||
filterOffset++;
|
|
||||||
while (topicOffset < topicLength && topicPointer[topicOffset] != LevelSeparator)
|
|
||||||
{
|
|
||||||
topicOffset++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (topicOffset == topicLength && filterOffset == filterLength)
|
|
||||||
{
|
|
||||||
return MqttTopicFilterCompareResult.IsMatch;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (filterPointer[filterOffset] == MultiLevelWildcard)
|
|
||||||
{
|
|
||||||
if (filterOffset > 0 && filterPointer[filterOffset - 1] != LevelSeparator)
|
|
||||||
{
|
|
||||||
return MqttTopicFilterCompareResult.FilterInvalid;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filterOffset + 1 != filterLength)
|
|
||||||
{
|
|
||||||
return MqttTopicFilterCompareResult.FilterInvalid;
|
|
||||||
}
|
|
||||||
|
|
||||||
return MqttTopicFilterCompareResult.IsMatch;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Check for e.g. "foo/bar" matching "foo/+/#".
|
|
||||||
if (filterOffset > 0 && filterOffset + 2 == filterLength && topicOffset == topicLength && filterPointer[filterOffset - 1] == SingleLevelWildcard &&
|
|
||||||
filterPointer[filterOffset] == LevelSeparator && isMultiLevelFilter)
|
|
||||||
{
|
|
||||||
return MqttTopicFilterCompareResult.IsMatch;
|
|
||||||
}
|
|
||||||
|
|
||||||
return MqttTopicFilterCompareResult.NoMatch;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return MqttTopicFilterCompareResult.NoMatch;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,84 +1,83 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace MQTTnet.Benchmarks
|
namespace MQTTnet.Benchmarks;
|
||||||
|
|
||||||
|
public class TopicGenerator
|
||||||
{
|
{
|
||||||
public class TopicGenerator
|
public static void Generate(
|
||||||
|
int numPublishers,
|
||||||
|
int numTopicsPerPublisher,
|
||||||
|
out Dictionary<string, List<string>> topicsByPublisher,
|
||||||
|
out Dictionary<string, List<string>> singleWildcardTopicsByPublisher,
|
||||||
|
out Dictionary<string, List<string>> multiWildcardTopicsByPublisher)
|
||||||
{
|
{
|
||||||
public static void Generate(
|
topicsByPublisher = new Dictionary<string, List<string>>();
|
||||||
int numPublishers,
|
singleWildcardTopicsByPublisher = new Dictionary<string, List<string>>();
|
||||||
int numTopicsPerPublisher,
|
multiWildcardTopicsByPublisher = new Dictionary<string, List<string>>();
|
||||||
out Dictionary<string, List<string>> topicsByPublisher,
|
|
||||||
out Dictionary<string, List<string>> singleWildcardTopicsByPublisher,
|
// Find some reasonable distribution across three topic levels
|
||||||
out Dictionary<string, List<string>> multiWildcardTopicsByPublisher)
|
var topicsPerLevel = (int)Math.Pow(numTopicsPerPublisher, 1.0 / 3.0);
|
||||||
|
if (topicsPerLevel <= 0)
|
||||||
{
|
{
|
||||||
topicsByPublisher = new Dictionary<string, List<string>>();
|
topicsPerLevel = 1;
|
||||||
singleWildcardTopicsByPublisher = new Dictionary<string, List<string>>();
|
}
|
||||||
multiWildcardTopicsByPublisher = new Dictionary<string, List<string>>();
|
|
||||||
|
|
||||||
// Find some reasonable distribution across three topic levels
|
var numLevel1Topics = topicsPerLevel;
|
||||||
var topicsPerLevel = (int)Math.Pow(numTopicsPerPublisher, 1.0 / 3.0);
|
var numLevel2Topics = topicsPerLevel;
|
||||||
if (topicsPerLevel <= 0)
|
|
||||||
|
var maxNumLevel3Topics = 1 + (int)((double)numTopicsPerPublisher / numLevel1Topics / numLevel2Topics);
|
||||||
|
if (maxNumLevel3Topics <= 0)
|
||||||
|
{
|
||||||
|
maxNumLevel3Topics = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var p = 0; p < numPublishers; ++p)
|
||||||
|
{
|
||||||
|
var publisherTopicCount = 0;
|
||||||
|
var publisherName = "pub" + p;
|
||||||
|
for (var l1 = 0; l1 < numLevel1Topics; ++l1)
|
||||||
{
|
{
|
||||||
topicsPerLevel = 1;
|
for (var l2 = 0; l2 < numLevel2Topics; ++l2)
|
||||||
}
|
|
||||||
|
|
||||||
var numLevel1Topics = topicsPerLevel;
|
|
||||||
var numLevel2Topics = topicsPerLevel;
|
|
||||||
|
|
||||||
var maxNumLevel3Topics = 1 + (int)((double)numTopicsPerPublisher / numLevel1Topics / numLevel2Topics);
|
|
||||||
if (maxNumLevel3Topics <= 0)
|
|
||||||
{
|
|
||||||
maxNumLevel3Topics = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var p = 0; p < numPublishers; ++p)
|
|
||||||
{
|
|
||||||
var publisherTopicCount = 0;
|
|
||||||
var publisherName = "pub" + p;
|
|
||||||
for (var l1 = 0; l1 < numLevel1Topics; ++l1)
|
|
||||||
{
|
{
|
||||||
for (var l2 = 0; l2 < numLevel2Topics; ++l2)
|
for (var l3 = 0; l3 < maxNumLevel3Topics; ++l3)
|
||||||
{
|
{
|
||||||
for (var l3 = 0; l3 < maxNumLevel3Topics; ++l3)
|
if (publisherTopicCount >= numTopicsPerPublisher)
|
||||||
{
|
{
|
||||||
if (publisherTopicCount >= numTopicsPerPublisher)
|
break;
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
var topic = $"{publisherName}/building{l1 + 1}/level{l2 + 1}/sensor{l3 + 1}";
|
|
||||||
AddPublisherTopic(publisherName, topic, topicsByPublisher);
|
|
||||||
|
|
||||||
if (l2 == 0)
|
|
||||||
{
|
|
||||||
var singleWildcardTopic = $"{publisherName}/building{l1 + 1}/+/sensor{l3 + 1}";
|
|
||||||
AddPublisherTopic(publisherName, singleWildcardTopic, singleWildcardTopicsByPublisher);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (l1 == 0 && l3 == 0)
|
|
||||||
{
|
|
||||||
var multiWildcardTopic = $"{publisherName}/+/level{l2 + 1}/+";
|
|
||||||
AddPublisherTopic(publisherName, multiWildcardTopic, multiWildcardTopicsByPublisher);
|
|
||||||
}
|
|
||||||
|
|
||||||
++publisherTopicCount;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var topic = $"{publisherName}/building{l1 + 1}/level{l2 + 1}/sensor{l3 + 1}";
|
||||||
|
AddPublisherTopic(publisherName, topic, topicsByPublisher);
|
||||||
|
|
||||||
|
if (l2 == 0)
|
||||||
|
{
|
||||||
|
var singleWildcardTopic = $"{publisherName}/building{l1 + 1}/+/sensor{l3 + 1}";
|
||||||
|
AddPublisherTopic(publisherName, singleWildcardTopic, singleWildcardTopicsByPublisher);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (l1 == 0 && l3 == 0)
|
||||||
|
{
|
||||||
|
var multiWildcardTopic = $"{publisherName}/+/level{l2 + 1}/+";
|
||||||
|
AddPublisherTopic(publisherName, multiWildcardTopic, multiWildcardTopicsByPublisher);
|
||||||
|
}
|
||||||
|
|
||||||
|
++publisherTopicCount;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static void AddPublisherTopic(string publisherName, string topic, Dictionary<string, List<string>> topicsByPublisher)
|
static void AddPublisherTopic(string publisherName, string topic, Dictionary<string, List<string>> topicsByPublisher)
|
||||||
|
{
|
||||||
|
List<string> topicList;
|
||||||
|
if (!topicsByPublisher.TryGetValue(publisherName, out topicList))
|
||||||
{
|
{
|
||||||
List<string> topicList;
|
topicList = new List<string>();
|
||||||
if (!topicsByPublisher.TryGetValue(publisherName, out topicList))
|
topicsByPublisher.Add(publisherName, topicList);
|
||||||
{
|
|
||||||
topicList = new List<string>();
|
|
||||||
topicsByPublisher.Add(publisherName, topicList);
|
|
||||||
}
|
|
||||||
|
|
||||||
topicList.Add(topic);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
topicList.Add(topic);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -14,15 +14,15 @@ public class UnsubscribeBenchmark : BaseBenchmark
|
|||||||
{
|
{
|
||||||
const int NumPublishers = 1;
|
const int NumPublishers = 1;
|
||||||
const int NumTopicsPerPublisher = 10000;
|
const int NumTopicsPerPublisher = 10000;
|
||||||
|
|
||||||
IMqttClient _mqttClient;
|
IMqttClient _mqttClient;
|
||||||
MqttServer _mqttServer;
|
MqttServer _mqttServer;
|
||||||
|
|
||||||
List<string> _topics;
|
List<string> _topics;
|
||||||
|
|
||||||
[GlobalSetup]
|
[GlobalSetup]
|
||||||
public void Setup()
|
public void Setup()
|
||||||
{
|
{
|
||||||
TopicGenerator.Generate(NumPublishers, NumTopicsPerPublisher, out var topicsByPublisher, out var singleWildcardTopicsByPublisher, out var multiWildcardTopicsByPublisher);
|
TopicGenerator.Generate(NumPublishers, NumTopicsPerPublisher, out var topicsByPublisher, out _, out _);
|
||||||
_topics = topicsByPublisher.Values.First();
|
_topics = topicsByPublisher.Values.First();
|
||||||
|
|
||||||
var serverOptions = new MqttServerOptionsBuilder().WithDefaultEndpoint().Build();
|
var serverOptions = new MqttServerOptionsBuilder().WithDefaultEndpoint().Build();
|
||||||
|
|||||||
@@ -4,27 +4,26 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace MQTTnet.Extensions.Rpc
|
namespace MQTTnet.Extensions.Rpc;
|
||||||
|
|
||||||
|
public sealed class DefaultMqttRpcClientTopicGenerationStrategy : IMqttRpcClientTopicGenerationStrategy
|
||||||
{
|
{
|
||||||
public sealed class DefaultMqttRpcClientTopicGenerationStrategy : IMqttRpcClientTopicGenerationStrategy
|
public MqttRpcTopicPair CreateRpcTopics(TopicGenerationContext context)
|
||||||
{
|
{
|
||||||
public MqttRpcTopicPair CreateRpcTopics(TopicGenerationContext context)
|
ArgumentNullException.ThrowIfNull(context);
|
||||||
|
|
||||||
|
if (context.MethodName.Contains("/") || context.MethodName.Contains("+") || context.MethodName.Contains("#"))
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(context);
|
throw new ArgumentException("The method name cannot contain /, + or #.");
|
||||||
|
|
||||||
if (context.MethodName.Contains("/") || context.MethodName.Contains("+") || context.MethodName.Contains("#"))
|
|
||||||
{
|
|
||||||
throw new ArgumentException("The method name cannot contain /, + or #.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var requestTopic = $"MQTTnet.RPC/{Guid.NewGuid():N}/{context.MethodName}";
|
|
||||||
var responseTopic = requestTopic + "/response";
|
|
||||||
|
|
||||||
return new MqttRpcTopicPair
|
|
||||||
{
|
|
||||||
RequestTopic = requestTopic,
|
|
||||||
ResponseTopic = responseTopic
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var requestTopic = $"MQTTnet.RPC/{Guid.NewGuid():N}/{context.MethodName}";
|
||||||
|
var responseTopic = requestTopic + "/response";
|
||||||
|
|
||||||
|
return new MqttRpcTopicPair
|
||||||
|
{
|
||||||
|
RequestTopic = requestTopic,
|
||||||
|
ResponseTopic = responseTopic
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,12 +8,11 @@ using System.Threading;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MQTTnet.Protocol;
|
using MQTTnet.Protocol;
|
||||||
|
|
||||||
namespace MQTTnet.Extensions.Rpc
|
namespace MQTTnet.Extensions.Rpc;
|
||||||
{
|
|
||||||
public interface IMqttRpcClient : IDisposable
|
|
||||||
{
|
|
||||||
Task<byte[]> ExecuteAsync(TimeSpan timeout, string methodName, byte[] payload, MqttQualityOfServiceLevel qualityOfServiceLevel, IDictionary<string,object> parameters = null);
|
|
||||||
|
|
||||||
Task<byte[]> ExecuteAsync(string methodName, byte[] payload, MqttQualityOfServiceLevel qualityOfServiceLevel, IDictionary<string, object> parameters = null, CancellationToken cancellationToken = default);
|
public interface IMqttRpcClient : IDisposable
|
||||||
}
|
{
|
||||||
|
Task<byte[]> ExecuteAsync(TimeSpan timeout, string methodName, byte[] payload, MqttQualityOfServiceLevel qualityOfServiceLevel, IDictionary<string,object> parameters = null);
|
||||||
|
|
||||||
|
Task<byte[]> ExecuteAsync(string methodName, byte[] payload, MqttQualityOfServiceLevel qualityOfServiceLevel, IDictionary<string, object> parameters = null, CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
@@ -2,10 +2,9 @@
|
|||||||
// The .NET Foundation licenses this file to you under the MIT license.
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
namespace MQTTnet.Extensions.Rpc
|
namespace MQTTnet.Extensions.Rpc;
|
||||||
|
|
||||||
|
public interface IMqttRpcClientTopicGenerationStrategy
|
||||||
{
|
{
|
||||||
public interface IMqttRpcClientTopicGenerationStrategy
|
MqttRpcTopicPair CreateRpcTopics(TopicGenerationContext context);
|
||||||
{
|
|
||||||
MqttRpcTopicPair CreateRpcTopics(TopicGenerationContext context);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -4,26 +4,25 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace MQTTnet.Extensions.Rpc
|
namespace MQTTnet.Extensions.Rpc;
|
||||||
|
|
||||||
|
public static class MqttFactoryExtensions
|
||||||
{
|
{
|
||||||
public static class MqttFactoryExtensions
|
public static IMqttRpcClient CreateMqttRpcClient(this MqttClientFactory clientFactory, IMqttClient mqttClient)
|
||||||
{
|
{
|
||||||
public static IMqttRpcClient CreateMqttRpcClient(this MqttClientFactory clientFactory, IMqttClient mqttClient)
|
return clientFactory.CreateMqttRpcClient(
|
||||||
{
|
mqttClient,
|
||||||
return clientFactory.CreateMqttRpcClient(
|
new MqttRpcClientOptions
|
||||||
mqttClient,
|
{
|
||||||
new MqttRpcClientOptions
|
TopicGenerationStrategy = new DefaultMqttRpcClientTopicGenerationStrategy()
|
||||||
{
|
});
|
||||||
TopicGenerationStrategy = new DefaultMqttRpcClientTopicGenerationStrategy()
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IMqttRpcClient CreateMqttRpcClient(this MqttClientFactory _, IMqttClient mqttClient, MqttRpcClientOptions rpcClientOptions)
|
public static IMqttRpcClient CreateMqttRpcClient(this MqttClientFactory _, IMqttClient mqttClient, MqttRpcClientOptions rpcClientOptions)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(mqttClient);
|
ArgumentNullException.ThrowIfNull(mqttClient);
|
||||||
ArgumentNullException.ThrowIfNull(rpcClientOptions);
|
ArgumentNullException.ThrowIfNull(rpcClientOptions);
|
||||||
|
|
||||||
return new MqttRpcClient(mqttClient, rpcClientOptions);
|
return new MqttRpcClient(mqttClient, rpcClientOptions);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,6 @@ using System;
|
|||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MQTTnet.Exceptions;
|
using MQTTnet.Exceptions;
|
||||||
@@ -14,128 +13,125 @@ using MQTTnet.Formatter;
|
|||||||
using MQTTnet.Internal;
|
using MQTTnet.Internal;
|
||||||
using MQTTnet.Protocol;
|
using MQTTnet.Protocol;
|
||||||
|
|
||||||
namespace MQTTnet.Extensions.Rpc
|
namespace MQTTnet.Extensions.Rpc;
|
||||||
|
|
||||||
|
public sealed class MqttRpcClient : IMqttRpcClient
|
||||||
{
|
{
|
||||||
public sealed class MqttRpcClient : IMqttRpcClient
|
readonly IMqttClient _mqttClient;
|
||||||
|
readonly MqttRpcClientOptions _options;
|
||||||
|
|
||||||
|
readonly ConcurrentDictionary<string, AsyncTaskCompletionSource<byte[]>> _waitingCalls = new ConcurrentDictionary<string, AsyncTaskCompletionSource<byte[]>>();
|
||||||
|
|
||||||
|
public MqttRpcClient(IMqttClient mqttClient, MqttRpcClientOptions options)
|
||||||
{
|
{
|
||||||
readonly IMqttClient _mqttClient;
|
_mqttClient = mqttClient ?? throw new ArgumentNullException(nameof(mqttClient));
|
||||||
readonly MqttRpcClientOptions _options;
|
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||||
|
|
||||||
readonly ConcurrentDictionary<string, AsyncTaskCompletionSource<byte[]>> _waitingCalls = new ConcurrentDictionary<string, AsyncTaskCompletionSource<byte[]>>();
|
_mqttClient.ApplicationMessageReceivedAsync += HandleApplicationMessageReceivedAsync;
|
||||||
|
}
|
||||||
|
|
||||||
public MqttRpcClient(IMqttClient mqttClient, MqttRpcClientOptions options)
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_mqttClient.ApplicationMessageReceivedAsync -= HandleApplicationMessageReceivedAsync;
|
||||||
|
|
||||||
|
foreach (var tcs in _waitingCalls)
|
||||||
{
|
{
|
||||||
_mqttClient = mqttClient ?? throw new ArgumentNullException(nameof(mqttClient));
|
tcs.Value.TrySetCanceled();
|
||||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
|
||||||
|
|
||||||
_mqttClient.ApplicationMessageReceivedAsync += HandleApplicationMessageReceivedAsync;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
_waitingCalls.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<byte[]> ExecuteAsync(TimeSpan timeout, string methodName, byte[] payload, MqttQualityOfServiceLevel qualityOfServiceLevel, IDictionary<string, object> parameters = null)
|
||||||
|
{
|
||||||
|
using var timeoutToken = new CancellationTokenSource(timeout);
|
||||||
|
try
|
||||||
{
|
{
|
||||||
_mqttClient.ApplicationMessageReceivedAsync -= HandleApplicationMessageReceivedAsync;
|
return await ExecuteAsync(methodName, payload, qualityOfServiceLevel, parameters, timeoutToken.Token).ConfigureAwait(false);
|
||||||
|
|
||||||
foreach (var tcs in _waitingCalls)
|
|
||||||
{
|
|
||||||
tcs.Value.TrySetCanceled();
|
|
||||||
}
|
|
||||||
|
|
||||||
_waitingCalls.Clear();
|
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException exception)
|
||||||
public async Task<byte[]> ExecuteAsync(TimeSpan timeout, string methodName, byte[] payload, MqttQualityOfServiceLevel qualityOfServiceLevel, IDictionary<string, object> parameters = null)
|
|
||||||
{
|
{
|
||||||
using (var timeoutToken = new CancellationTokenSource(timeout))
|
if (timeoutToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
try
|
throw new MqttCommunicationTimedOutException(exception);
|
||||||
{
|
|
||||||
return await ExecuteAsync(methodName, payload, qualityOfServiceLevel, parameters, timeoutToken.Token).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException exception)
|
|
||||||
{
|
|
||||||
if (timeoutToken.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
throw new MqttCommunicationTimedOutException(exception);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<byte[]> ExecuteAsync(string methodName, byte[] payload, MqttQualityOfServiceLevel qualityOfServiceLevel, IDictionary<string, object> parameters = null, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(methodName);
|
|
||||||
|
|
||||||
var context = new TopicGenerationContext(_mqttClient, _options, methodName, parameters, qualityOfServiceLevel);
|
|
||||||
var topicNames = _options.TopicGenerationStrategy.CreateRpcTopics(context);
|
|
||||||
|
|
||||||
var requestTopic = topicNames.RequestTopic;
|
|
||||||
var responseTopic = topicNames.ResponseTopic;
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(requestTopic))
|
|
||||||
{
|
|
||||||
throw new MqttProtocolViolationException("RPC request topic is empty.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(responseTopic))
|
throw;
|
||||||
{
|
|
||||||
throw new MqttProtocolViolationException("RPC response topic is empty.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var requestMessageBuilder = new MqttApplicationMessageBuilder().WithTopic(requestTopic).WithPayload(payload).WithQualityOfServiceLevel(qualityOfServiceLevel);
|
|
||||||
|
|
||||||
if (_mqttClient.Options.ProtocolVersion == MqttProtocolVersion.V500)
|
|
||||||
{
|
|
||||||
requestMessageBuilder.WithResponseTopic(responseTopic);
|
|
||||||
}
|
|
||||||
|
|
||||||
var requestMessage = requestMessageBuilder.Build();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var awaitable = new AsyncTaskCompletionSource<byte[]>();
|
|
||||||
|
|
||||||
if (!_waitingCalls.TryAdd(responseTopic, awaitable))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
var subscribeOptions = new MqttClientSubscribeOptionsBuilder().WithTopicFilter(responseTopic, qualityOfServiceLevel).Build();
|
|
||||||
|
|
||||||
await _mqttClient.SubscribeAsync(subscribeOptions, cancellationToken).ConfigureAwait(false);
|
|
||||||
await _mqttClient.PublishAsync(requestMessage, cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
using (cancellationToken.Register(
|
|
||||||
() =>
|
|
||||||
{
|
|
||||||
awaitable.TrySetCanceled();
|
|
||||||
}))
|
|
||||||
{
|
|
||||||
return await awaitable.Task.ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_waitingCalls.TryRemove(responseTopic, out _);
|
|
||||||
await _mqttClient.UnsubscribeAsync(responseTopic, CancellationToken.None).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Task HandleApplicationMessageReceivedAsync(MqttApplicationMessageReceivedEventArgs eventArgs)
|
|
||||||
{
|
|
||||||
if (!_waitingCalls.TryRemove(eventArgs.ApplicationMessage.Topic, out var awaitable))
|
|
||||||
{
|
|
||||||
return CompletedTask.Instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
var payloadBuffer = eventArgs.ApplicationMessage.Payload.ToArray();
|
|
||||||
awaitable.TrySetResult(payloadBuffer);
|
|
||||||
|
|
||||||
// Set this message to handled to that other code can avoid execution etc.
|
|
||||||
eventArgs.IsHandled = true;
|
|
||||||
|
|
||||||
return CompletedTask.Instance;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<byte[]> ExecuteAsync(string methodName, byte[] payload, MqttQualityOfServiceLevel qualityOfServiceLevel, IDictionary<string, object> parameters = null, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(methodName);
|
||||||
|
|
||||||
|
var context = new TopicGenerationContext(_mqttClient, _options, methodName, parameters, qualityOfServiceLevel);
|
||||||
|
var topicNames = _options.TopicGenerationStrategy.CreateRpcTopics(context);
|
||||||
|
|
||||||
|
var requestTopic = topicNames.RequestTopic;
|
||||||
|
var responseTopic = topicNames.ResponseTopic;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(requestTopic))
|
||||||
|
{
|
||||||
|
throw new MqttProtocolViolationException("RPC request topic is empty.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(responseTopic))
|
||||||
|
{
|
||||||
|
throw new MqttProtocolViolationException("RPC response topic is empty.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var requestMessageBuilder = new MqttApplicationMessageBuilder().WithTopic(requestTopic).WithPayload(payload).WithQualityOfServiceLevel(qualityOfServiceLevel);
|
||||||
|
|
||||||
|
if (_mqttClient.Options.ProtocolVersion == MqttProtocolVersion.V500)
|
||||||
|
{
|
||||||
|
requestMessageBuilder.WithResponseTopic(responseTopic);
|
||||||
|
}
|
||||||
|
|
||||||
|
var requestMessage = requestMessageBuilder.Build();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var awaitable = new AsyncTaskCompletionSource<byte[]>();
|
||||||
|
|
||||||
|
if (!_waitingCalls.TryAdd(responseTopic, awaitable))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var subscribeOptions = new MqttClientSubscribeOptionsBuilder().WithTopicFilter(responseTopic, qualityOfServiceLevel).Build();
|
||||||
|
|
||||||
|
await _mqttClient.SubscribeAsync(subscribeOptions, cancellationToken).ConfigureAwait(false);
|
||||||
|
await _mqttClient.PublishAsync(requestMessage, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
using (cancellationToken.Register(
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
awaitable.TrySetCanceled();
|
||||||
|
}))
|
||||||
|
{
|
||||||
|
return await awaitable.Task.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_waitingCalls.TryRemove(responseTopic, out _);
|
||||||
|
await _mqttClient.UnsubscribeAsync(responseTopic, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Task HandleApplicationMessageReceivedAsync(MqttApplicationMessageReceivedEventArgs eventArgs)
|
||||||
|
{
|
||||||
|
if (!_waitingCalls.TryRemove(eventArgs.ApplicationMessage.Topic, out var awaitable))
|
||||||
|
{
|
||||||
|
return CompletedTask.Instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
var payloadBuffer = eventArgs.ApplicationMessage.Payload.ToArray();
|
||||||
|
awaitable.TrySetResult(payloadBuffer);
|
||||||
|
|
||||||
|
// Set this message to handled to that other code can avoid execution etc.
|
||||||
|
eventArgs.IsHandled = true;
|
||||||
|
|
||||||
|
return CompletedTask.Instance;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -8,17 +8,16 @@ using System.Text;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MQTTnet.Protocol;
|
using MQTTnet.Protocol;
|
||||||
|
|
||||||
namespace MQTTnet.Extensions.Rpc
|
namespace MQTTnet.Extensions.Rpc;
|
||||||
|
|
||||||
|
public static class MqttRpcClientExtensions
|
||||||
{
|
{
|
||||||
public static class MqttRpcClientExtensions
|
public static Task<byte[]> ExecuteAsync(this IMqttRpcClient client, TimeSpan timeout, string methodName, string payload, MqttQualityOfServiceLevel qualityOfServiceLevel, IDictionary<string,object> parameters = null)
|
||||||
{
|
{
|
||||||
public static Task<byte[]> ExecuteAsync(this IMqttRpcClient client, TimeSpan timeout, string methodName, string payload, MqttQualityOfServiceLevel qualityOfServiceLevel, IDictionary<string,object> parameters = null)
|
if (client == null) throw new ArgumentNullException(nameof(client));
|
||||||
{
|
|
||||||
if (client == null) throw new ArgumentNullException(nameof(client));
|
|
||||||
|
|
||||||
var buffer = Encoding.UTF8.GetBytes(payload ?? string.Empty);
|
var buffer = Encoding.UTF8.GetBytes(payload ?? string.Empty);
|
||||||
|
|
||||||
return client.ExecuteAsync(timeout, methodName, buffer, qualityOfServiceLevel, parameters);
|
return client.ExecuteAsync(timeout, methodName, buffer, qualityOfServiceLevel, parameters);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,10 +2,9 @@
|
|||||||
// The .NET Foundation licenses this file to you under the MIT license.
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
namespace MQTTnet.Extensions.Rpc
|
namespace MQTTnet.Extensions.Rpc;
|
||||||
|
|
||||||
|
public sealed class MqttRpcClientOptions
|
||||||
{
|
{
|
||||||
public sealed class MqttRpcClientOptions
|
public IMqttRpcClientTopicGenerationStrategy TopicGenerationStrategy { get; set; } = new DefaultMqttRpcClientTopicGenerationStrategy();
|
||||||
{
|
|
||||||
public IMqttRpcClientTopicGenerationStrategy TopicGenerationStrategy { get; set; } = new DefaultMqttRpcClientTopicGenerationStrategy();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -4,25 +4,24 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace MQTTnet.Extensions.Rpc
|
namespace MQTTnet.Extensions.Rpc;
|
||||||
|
|
||||||
|
public sealed class MqttRpcClientOptionsBuilder
|
||||||
{
|
{
|
||||||
public sealed class MqttRpcClientOptionsBuilder
|
IMqttRpcClientTopicGenerationStrategy _topicGenerationStrategy = new DefaultMqttRpcClientTopicGenerationStrategy();
|
||||||
|
|
||||||
|
public MqttRpcClientOptions Build()
|
||||||
{
|
{
|
||||||
IMqttRpcClientTopicGenerationStrategy _topicGenerationStrategy = new DefaultMqttRpcClientTopicGenerationStrategy();
|
return new MqttRpcClientOptions
|
||||||
|
|
||||||
public MqttRpcClientOptions Build()
|
|
||||||
{
|
{
|
||||||
return new MqttRpcClientOptions
|
TopicGenerationStrategy = _topicGenerationStrategy
|
||||||
{
|
};
|
||||||
TopicGenerationStrategy = _topicGenerationStrategy
|
}
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public MqttRpcClientOptionsBuilder WithTopicGenerationStrategy(IMqttRpcClientTopicGenerationStrategy value)
|
public MqttRpcClientOptionsBuilder WithTopicGenerationStrategy(IMqttRpcClientTopicGenerationStrategy value)
|
||||||
{
|
{
|
||||||
_topicGenerationStrategy = value ?? throw new ArgumentNullException(nameof(value));
|
_topicGenerationStrategy = value ?? throw new ArgumentNullException(nameof(value));
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,12 +2,11 @@
|
|||||||
// The .NET Foundation licenses this file to you under the MIT license.
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
namespace MQTTnet.Extensions.Rpc
|
namespace MQTTnet.Extensions.Rpc;
|
||||||
{
|
|
||||||
public sealed class MqttRpcTopicPair
|
|
||||||
{
|
|
||||||
public string RequestTopic { get; set; }
|
|
||||||
|
|
||||||
public string ResponseTopic { get; set; }
|
public sealed class MqttRpcTopicPair
|
||||||
}
|
{
|
||||||
|
public string RequestTopic { get; set; }
|
||||||
|
|
||||||
|
public string ResponseTopic { get; set; }
|
||||||
}
|
}
|
||||||
@@ -6,37 +6,36 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using MQTTnet.Protocol;
|
using MQTTnet.Protocol;
|
||||||
|
|
||||||
namespace MQTTnet.Extensions.Rpc
|
namespace MQTTnet.Extensions.Rpc;
|
||||||
|
|
||||||
|
public sealed class TopicGenerationContext
|
||||||
{
|
{
|
||||||
public sealed class TopicGenerationContext
|
public TopicGenerationContext(IMqttClient mqttClient, MqttRpcClientOptions options, string methodName,
|
||||||
|
IDictionary<string, object> parameters, MqttQualityOfServiceLevel qualityOfServiceLevel)
|
||||||
{
|
{
|
||||||
public TopicGenerationContext(IMqttClient mqttClient, MqttRpcClientOptions options, string methodName,
|
MethodName = methodName ?? throw new ArgumentNullException(nameof(methodName));
|
||||||
IDictionary<string, object> parameters, MqttQualityOfServiceLevel qualityOfServiceLevel)
|
Parameters = parameters;
|
||||||
{
|
QualityOfServiceLevel = qualityOfServiceLevel;
|
||||||
MethodName = methodName ?? throw new ArgumentNullException(nameof(methodName));
|
MqttClient = mqttClient ?? throw new ArgumentNullException(nameof(mqttClient));
|
||||||
Parameters = parameters;
|
Options = options ?? throw new ArgumentNullException(nameof(options));
|
||||||
QualityOfServiceLevel = qualityOfServiceLevel;
|
|
||||||
MqttClient = mqttClient ?? throw new ArgumentNullException(nameof(mqttClient));
|
|
||||||
Options = options ?? throw new ArgumentNullException(nameof(options));
|
|
||||||
}
|
|
||||||
|
|
||||||
public string MethodName { get; }
|
|
||||||
|
|
||||||
public IDictionary<string, object> Parameters { get; }
|
|
||||||
|
|
||||||
public IMqttClient MqttClient { get; }
|
|
||||||
|
|
||||||
public MqttRpcClientOptions Options { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the quality of service level.
|
|
||||||
/// The Quality of Service (QoS) level is an agreement between the sender of a message and the receiver of a message
|
|
||||||
/// that defines the guarantee of delivery for a specific message.
|
|
||||||
/// There are 3 QoS levels in MQTT:
|
|
||||||
/// - At most once (0): Message gets delivered no time, once or multiple times.
|
|
||||||
/// - At least once (1): Message gets delivered at least once (one time or more often).
|
|
||||||
/// - Exactly once (2): Message gets delivered exactly once (It's ensured that the message only comes once).
|
|
||||||
/// </summary>
|
|
||||||
public MqttQualityOfServiceLevel QualityOfServiceLevel { get; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string MethodName { get; }
|
||||||
|
|
||||||
|
public IDictionary<string, object> Parameters { get; }
|
||||||
|
|
||||||
|
public IMqttClient MqttClient { get; }
|
||||||
|
|
||||||
|
public MqttRpcClientOptions Options { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the quality of service level.
|
||||||
|
/// The Quality of Service (QoS) level is an agreement between the sender of a message and the receiver of a message
|
||||||
|
/// that defines the guarantee of delivery for a specific message.
|
||||||
|
/// There are 3 QoS levels in MQTT:
|
||||||
|
/// - At most once (0): Message gets delivered no time, once or multiple times.
|
||||||
|
/// - At least once (1): Message gets delivered at least once (one time or more often).
|
||||||
|
/// - Exactly once (2): Message gets delivered exactly once (It's ensured that the message only comes once).
|
||||||
|
/// </summary>
|
||||||
|
public MqttQualityOfServiceLevel QualityOfServiceLevel { get; }
|
||||||
}
|
}
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
|
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
|
||||||
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
<PackageReference Include="System.ValueTuple" Version="4.6.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ public sealed class MqttTopicTemplate : IEquatable<MqttTopicTemplate>
|
|||||||
throw new ArgumentException("the topic has to match this template", nameof(topic));
|
throw new ArgumentException("the topic has to match this template", nameof(topic));
|
||||||
}
|
}
|
||||||
|
|
||||||
return parseParameterValuesInternal(topic);
|
return ParseParameterValuesInternal(topic);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -330,7 +330,7 @@ public sealed class MqttTopicTemplate : IEquatable<MqttTopicTemplate>
|
|||||||
return parameters.Aggregate(this, (t, p) => t.TrySetParameter(p.parameter, p.value));
|
return parameters.Aggregate(this, (t, p) => t.TrySetParameter(p.parameter, p.value));
|
||||||
}
|
}
|
||||||
|
|
||||||
IEnumerable<(string parameter, int index, string value)> parseParameterValuesInternal(string topic)
|
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
|
// because we have a match, we know the segment array is at least the template's length
|
||||||
var segments = topic.Split(MqttTopicFilterComparer.LevelSeparator);
|
var segments = topic.Split(MqttTopicFilterComparer.LevelSeparator);
|
||||||
|
|||||||
@@ -5,28 +5,27 @@
|
|||||||
using MQTTnet.Packets;
|
using MQTTnet.Packets;
|
||||||
using MQTTnet.Protocol;
|
using MQTTnet.Protocol;
|
||||||
|
|
||||||
namespace MQTTnet.Server
|
namespace MQTTnet.Server;
|
||||||
|
|
||||||
|
public sealed class MqttServerClientDisconnectOptions
|
||||||
{
|
{
|
||||||
public sealed class MqttServerClientDisconnectOptions
|
public MqttDisconnectReasonCode ReasonCode { get; set; } = MqttDisconnectReasonCode.NormalDisconnection;
|
||||||
{
|
|
||||||
public MqttDisconnectReasonCode ReasonCode { get; set; } = MqttDisconnectReasonCode.NormalDisconnection;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The reason string is sent to every client via a DISCONNECT packet.
|
/// The reason string is sent to every client via a DISCONNECT packet.
|
||||||
/// <remarks>MQTT 5.0.0+ feature.</remarks>
|
/// <remarks>MQTT 5.0.0+ feature.</remarks>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string ReasonString { get; set; }
|
public string ReasonString { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The server reference is sent to every client via a DISCONNECT packet.
|
/// The server reference is sent to every client via a DISCONNECT packet.
|
||||||
/// <remarks>MQTT 5.0.0+ feature.</remarks>
|
/// <remarks>MQTT 5.0.0+ feature.</remarks>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string ServerReference { get; set; }
|
public string ServerReference { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// These user properties are sent to every client via a DISCONNECT packet.
|
/// These user properties are sent to every client via a DISCONNECT packet.
|
||||||
/// <remarks>MQTT 5.0.0+ feature.</remarks>
|
/// <remarks>MQTT 5.0.0+ feature.</remarks>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public List<MqttUserProperty> UserProperties { get; set; }
|
public List<MqttUserProperty> UserProperties { get; set; }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -5,50 +5,49 @@
|
|||||||
using MQTTnet.Packets;
|
using MQTTnet.Packets;
|
||||||
using MQTTnet.Protocol;
|
using MQTTnet.Protocol;
|
||||||
|
|
||||||
namespace MQTTnet.Server
|
namespace MQTTnet.Server;
|
||||||
|
|
||||||
|
public sealed class MqttServerClientDisconnectOptionsBuilder
|
||||||
{
|
{
|
||||||
public sealed class MqttServerClientDisconnectOptionsBuilder
|
readonly MqttServerClientDisconnectOptions _options = new MqttServerClientDisconnectOptions();
|
||||||
|
|
||||||
|
public MqttServerClientDisconnectOptions Build()
|
||||||
{
|
{
|
||||||
readonly MqttServerClientDisconnectOptions _options = new MqttServerClientDisconnectOptions();
|
return _options;
|
||||||
|
}
|
||||||
|
|
||||||
public MqttServerClientDisconnectOptions Build()
|
public MqttServerClientDisconnectOptionsBuilder WithReasonCode(MqttDisconnectReasonCode value)
|
||||||
|
{
|
||||||
|
_options.ReasonCode = value;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MqttServerClientDisconnectOptionsBuilder WithReasonString(string value)
|
||||||
|
{
|
||||||
|
_options.ReasonString = value;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MqttServerClientDisconnectOptionsBuilder WithServerReference(string value)
|
||||||
|
{
|
||||||
|
_options.ServerReference = value;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MqttServerClientDisconnectOptionsBuilder WithUserProperties(List<MqttUserProperty> value)
|
||||||
|
{
|
||||||
|
_options.UserProperties = value;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MqttServerClientDisconnectOptionsBuilder WithUserProperty(string name, string value)
|
||||||
|
{
|
||||||
|
if (_options.UserProperties == null)
|
||||||
{
|
{
|
||||||
return _options;
|
_options.UserProperties = new List<MqttUserProperty>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public MqttServerClientDisconnectOptionsBuilder WithReasonCode(MqttDisconnectReasonCode value)
|
_options.UserProperties.Add(new MqttUserProperty(name, value));
|
||||||
{
|
return this;
|
||||||
_options.ReasonCode = value;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MqttServerClientDisconnectOptionsBuilder WithReasonString(string value)
|
|
||||||
{
|
|
||||||
_options.ReasonString = value;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MqttServerClientDisconnectOptionsBuilder WithServerReference(string value)
|
|
||||||
{
|
|
||||||
_options.ServerReference = value;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MqttServerClientDisconnectOptionsBuilder WithUserProperties(List<MqttUserProperty> value)
|
|
||||||
{
|
|
||||||
_options.UserProperties = value;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MqttServerClientDisconnectOptionsBuilder WithUserProperty(string name, string value)
|
|
||||||
{
|
|
||||||
if (_options.UserProperties == null)
|
|
||||||
{
|
|
||||||
_options.UserProperties = new List<MqttUserProperty>();
|
|
||||||
}
|
|
||||||
|
|
||||||
_options.UserProperties.Add(new MqttUserProperty(name, value));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,26 +2,23 @@
|
|||||||
// The .NET Foundation licenses this file to you under the MIT license.
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System;
|
namespace MQTTnet.Server;
|
||||||
|
|
||||||
namespace MQTTnet.Server
|
public sealed class ApplicationMessageEnqueuedEventArgs : EventArgs
|
||||||
{
|
{
|
||||||
public sealed class ApplicationMessageEnqueuedEventArgs : EventArgs
|
public ApplicationMessageEnqueuedEventArgs(string senderClientId, string receiverClientId, MqttApplicationMessage applicationMessage, bool isDropped)
|
||||||
{
|
{
|
||||||
public ApplicationMessageEnqueuedEventArgs(string senderClientId, string receiverClientId, MqttApplicationMessage applicationMessage, bool isDropped)
|
SenderClientId = senderClientId ?? throw new ArgumentNullException( nameof(senderClientId));
|
||||||
{
|
ReceiverClientId = receiverClientId ?? throw new ArgumentNullException(nameof(receiverClientId));
|
||||||
SenderClientId = senderClientId ?? throw new ArgumentNullException( nameof(senderClientId));
|
ApplicationMessage = applicationMessage ?? throw new ArgumentNullException(nameof(applicationMessage));
|
||||||
ReceiverClientId = receiverClientId ?? throw new ArgumentNullException(nameof(receiverClientId));
|
IsDropped = isDropped;
|
||||||
ApplicationMessage = applicationMessage ?? throw new ArgumentNullException(nameof(applicationMessage));
|
|
||||||
IsDropped = isDropped;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string SenderClientId { get; }
|
|
||||||
|
|
||||||
public string ReceiverClientId { get; }
|
|
||||||
|
|
||||||
public bool IsDropped { get; }
|
|
||||||
|
|
||||||
public MqttApplicationMessage ApplicationMessage { get; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string SenderClientId { get; }
|
||||||
|
|
||||||
|
public string ReceiverClientId { get; }
|
||||||
|
|
||||||
|
public bool IsDropped { get; }
|
||||||
|
|
||||||
|
public MqttApplicationMessage ApplicationMessage { get; }
|
||||||
}
|
}
|
||||||
@@ -2,26 +2,23 @@
|
|||||||
// The .NET Foundation licenses this file to you under the MIT license.
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System;
|
namespace MQTTnet.Server;
|
||||||
|
|
||||||
namespace MQTTnet.Server
|
public sealed class ApplicationMessageNotConsumedEventArgs : EventArgs
|
||||||
{
|
{
|
||||||
public sealed class ApplicationMessageNotConsumedEventArgs : EventArgs
|
public ApplicationMessageNotConsumedEventArgs(MqttApplicationMessage applicationMessage, string senderId)
|
||||||
{
|
{
|
||||||
public ApplicationMessageNotConsumedEventArgs(MqttApplicationMessage applicationMessage, string senderId)
|
ApplicationMessage = applicationMessage ?? throw new ArgumentNullException(nameof(applicationMessage));
|
||||||
{
|
SenderId = senderId;
|
||||||
ApplicationMessage = applicationMessage ?? throw new ArgumentNullException(nameof(applicationMessage));
|
|
||||||
SenderId = senderId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the application message which was not consumed by any client.
|
|
||||||
/// </summary>
|
|
||||||
public MqttApplicationMessage ApplicationMessage { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the ID of the client which has sent the affected application message.
|
|
||||||
/// </summary>
|
|
||||||
public string SenderId { get; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the application message which was not consumed by any client.
|
||||||
|
/// </summary>
|
||||||
|
public MqttApplicationMessage ApplicationMessage { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the ID of the client which has sent the affected application message.
|
||||||
|
/// </summary>
|
||||||
|
public string SenderId { get; }
|
||||||
}
|
}
|
||||||
@@ -2,51 +2,49 @@
|
|||||||
// The .NET Foundation licenses this file to you under the MIT license.
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
using MQTTnet.Packets;
|
using MQTTnet.Packets;
|
||||||
|
|
||||||
namespace MQTTnet.Server
|
namespace MQTTnet.Server;
|
||||||
|
|
||||||
|
public sealed class ClientAcknowledgedPublishPacketEventArgs : EventArgs
|
||||||
{
|
{
|
||||||
public sealed class ClientAcknowledgedPublishPacketEventArgs : EventArgs
|
public ClientAcknowledgedPublishPacketEventArgs(string clientId, string userName, IDictionary sessionItems, MqttPublishPacket publishPacket, MqttPacketWithIdentifier acknowledgePacket)
|
||||||
{
|
{
|
||||||
public ClientAcknowledgedPublishPacketEventArgs(string clientId, string userName, IDictionary sessionItems, MqttPublishPacket publishPacket, MqttPacketWithIdentifier acknowledgePacket)
|
ClientId = clientId ?? throw new ArgumentNullException(nameof(clientId));
|
||||||
{
|
UserName = userName;
|
||||||
ClientId = clientId ?? throw new ArgumentNullException(nameof(clientId));
|
SessionItems = sessionItems ?? throw new ArgumentNullException(nameof(sessionItems));
|
||||||
UserName = userName;
|
PublishPacket = publishPacket ?? throw new ArgumentNullException(nameof(publishPacket));
|
||||||
SessionItems = sessionItems ?? throw new ArgumentNullException(nameof(sessionItems));
|
AcknowledgePacket = acknowledgePacket ?? throw new ArgumentNullException(nameof(acknowledgePacket));
|
||||||
PublishPacket = publishPacket ?? throw new ArgumentNullException(nameof(publishPacket));
|
|
||||||
AcknowledgePacket = acknowledgePacket ?? throw new ArgumentNullException(nameof(acknowledgePacket));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the packet which was used for acknowledge. This can be a PubAck or PubComp packet.
|
|
||||||
/// </summary>
|
|
||||||
public MqttPacketWithIdentifier AcknowledgePacket { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the ID of the client which acknowledged a PUBLISH packet.
|
|
||||||
/// </summary>
|
|
||||||
public string ClientId { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the user name of the client.
|
|
||||||
/// </summary>
|
|
||||||
public string UserName { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets whether the PUBLISH packet is fully acknowledged. This is the case for PUBACK (QoS 1) and PUBCOMP (QoS 2.
|
|
||||||
/// </summary>
|
|
||||||
public bool IsCompleted => AcknowledgePacket is MqttPubAckPacket || AcknowledgePacket is MqttPubCompPacket;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the PUBLISH packet which was acknowledged.
|
|
||||||
/// </summary>
|
|
||||||
public MqttPublishPacket PublishPacket { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the session items which contain custom user data per session.
|
|
||||||
/// </summary>
|
|
||||||
public IDictionary SessionItems { get; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the packet which was used for acknowledge. This can be a PubAck or PubComp packet.
|
||||||
|
/// </summary>
|
||||||
|
public MqttPacketWithIdentifier AcknowledgePacket { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the ID of the client which acknowledged a PUBLISH packet.
|
||||||
|
/// </summary>
|
||||||
|
public string ClientId { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the user name of the client.
|
||||||
|
/// </summary>
|
||||||
|
public string UserName { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets whether the PUBLISH packet is fully acknowledged. This is the case for PUBACK (QoS 1) and PUBCOMP (QoS 2.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsCompleted => AcknowledgePacket is MqttPubAckPacket || AcknowledgePacket is MqttPubCompPacket;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the PUBLISH packet which was acknowledged.
|
||||||
|
/// </summary>
|
||||||
|
public MqttPublishPacket PublishPacket { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the session items which contain custom user data per session.
|
||||||
|
/// </summary>
|
||||||
|
public IDictionary SessionItems { get; }
|
||||||
}
|
}
|
||||||
@@ -2,70 +2,67 @@
|
|||||||
// The .NET Foundation licenses this file to you under the MIT license.
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using MQTTnet.Formatter;
|
using MQTTnet.Formatter;
|
||||||
using MQTTnet.Packets;
|
using MQTTnet.Packets;
|
||||||
|
|
||||||
namespace MQTTnet.Server
|
namespace MQTTnet.Server;
|
||||||
|
|
||||||
|
public sealed class ClientConnectedEventArgs : EventArgs
|
||||||
{
|
{
|
||||||
public sealed class ClientConnectedEventArgs : EventArgs
|
readonly MqttConnectPacket _connectPacket;
|
||||||
|
|
||||||
|
public ClientConnectedEventArgs(MqttConnectPacket connectPacket, MqttProtocolVersion protocolVersion, EndPoint remoteEndPoint, IDictionary sessionItems)
|
||||||
{
|
{
|
||||||
readonly MqttConnectPacket _connectPacket;
|
_connectPacket = connectPacket ?? throw new ArgumentNullException(nameof(connectPacket));
|
||||||
|
ProtocolVersion = protocolVersion;
|
||||||
public ClientConnectedEventArgs(MqttConnectPacket connectPacket, MqttProtocolVersion protocolVersion, EndPoint remoteEndPoint, IDictionary sessionItems)
|
RemoteEndPoint = remoteEndPoint;
|
||||||
{
|
SessionItems = sessionItems ?? throw new ArgumentNullException(nameof(sessionItems));
|
||||||
_connectPacket = connectPacket ?? throw new ArgumentNullException(nameof(connectPacket));
|
|
||||||
ProtocolVersion = protocolVersion;
|
|
||||||
RemoteEndPoint = remoteEndPoint;
|
|
||||||
SessionItems = sessionItems ?? throw new ArgumentNullException(nameof(sessionItems));
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] AuthenticationData => _connectPacket.AuthenticationData;
|
|
||||||
|
|
||||||
public string AuthenticationMethod => _connectPacket.AuthenticationMethod;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the client identifier of the connected client.
|
|
||||||
/// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues.
|
|
||||||
/// </summary>
|
|
||||||
public string ClientId => _connectPacket.ClientId;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the endpoint of the connected client.
|
|
||||||
/// </summary>
|
|
||||||
public EndPoint RemoteEndPoint { get; }
|
|
||||||
|
|
||||||
[Obsolete("Use RemoteEndPoint instead.")]
|
|
||||||
public string Endpoint => RemoteEndPoint?.ToString();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the protocol version which is used by the connected client.
|
|
||||||
/// </summary>
|
|
||||||
public MqttProtocolVersion ProtocolVersion { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets a key/value collection that can be used to share data within the scope of this session.
|
|
||||||
/// </summary>
|
|
||||||
public IDictionary SessionItems { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the user name of the connected client.
|
|
||||||
/// </summary>
|
|
||||||
public string UserName => _connectPacket.Username;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the password of the connected client.
|
|
||||||
/// </summary>
|
|
||||||
public string Password => Encoding.UTF8.GetString(_connectPacket.Password.AsSpan());
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the user properties sent by the client.
|
|
||||||
/// <remarks>MQTT 5.0.0+ feature.</remarks>
|
|
||||||
/// </summary>
|
|
||||||
public List<MqttUserProperty> UserProperties => _connectPacket?.UserProperties;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public byte[] AuthenticationData => _connectPacket.AuthenticationData;
|
||||||
|
|
||||||
|
public string AuthenticationMethod => _connectPacket.AuthenticationMethod;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the client identifier of the connected client.
|
||||||
|
/// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues.
|
||||||
|
/// </summary>
|
||||||
|
public string ClientId => _connectPacket.ClientId;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the endpoint of the connected client.
|
||||||
|
/// </summary>
|
||||||
|
public EndPoint RemoteEndPoint { get; }
|
||||||
|
|
||||||
|
[Obsolete("Use RemoteEndPoint instead.")]
|
||||||
|
public string Endpoint => RemoteEndPoint?.ToString();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the protocol version which is used by the connected client.
|
||||||
|
/// </summary>
|
||||||
|
public MqttProtocolVersion ProtocolVersion { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a key/value collection that can be used to share data within the scope of this session.
|
||||||
|
/// </summary>
|
||||||
|
public IDictionary SessionItems { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the user name of the connected client.
|
||||||
|
/// </summary>
|
||||||
|
public string UserName => _connectPacket.Username;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the password of the connected client.
|
||||||
|
/// </summary>
|
||||||
|
public string Password => Encoding.UTF8.GetString(_connectPacket.Password.AsSpan());
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the user properties sent by the client.
|
||||||
|
/// <remarks>MQTT 5.0.0+ feature.</remarks>
|
||||||
|
/// </summary>
|
||||||
|
public List<MqttUserProperty> UserProperties => _connectPacket?.UserProperties;
|
||||||
}
|
}
|
||||||
@@ -2,91 +2,88 @@
|
|||||||
// The .NET Foundation licenses this file to you under the MIT license.
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using MQTTnet.Packets;
|
using MQTTnet.Packets;
|
||||||
using MQTTnet.Protocol;
|
using MQTTnet.Protocol;
|
||||||
|
|
||||||
namespace MQTTnet.Server
|
namespace MQTTnet.Server;
|
||||||
|
|
||||||
|
public sealed class ClientDisconnectedEventArgs : EventArgs
|
||||||
{
|
{
|
||||||
public sealed class ClientDisconnectedEventArgs : EventArgs
|
readonly MqttConnectPacket _connectPacket;
|
||||||
|
readonly MqttDisconnectPacket _disconnectPacket;
|
||||||
|
|
||||||
|
public ClientDisconnectedEventArgs(
|
||||||
|
MqttConnectPacket connectPacket,
|
||||||
|
MqttDisconnectPacket disconnectPacket,
|
||||||
|
MqttClientDisconnectType disconnectType,
|
||||||
|
EndPoint remoteEndPoint,
|
||||||
|
IDictionary sessionItems)
|
||||||
{
|
{
|
||||||
readonly MqttConnectPacket _connectPacket;
|
DisconnectType = disconnectType;
|
||||||
readonly MqttDisconnectPacket _disconnectPacket;
|
RemoteEndPoint = remoteEndPoint;
|
||||||
|
SessionItems = sessionItems ?? throw new ArgumentNullException(nameof(sessionItems));
|
||||||
|
|
||||||
public ClientDisconnectedEventArgs(
|
_connectPacket = connectPacket;
|
||||||
MqttConnectPacket connectPacket,
|
// The DISCONNECT packet can be null in case of a non clean disconnect or session takeover.
|
||||||
MqttDisconnectPacket disconnectPacket,
|
_disconnectPacket = disconnectPacket;
|
||||||
MqttClientDisconnectType disconnectType,
|
|
||||||
EndPoint remoteEndPoint,
|
|
||||||
IDictionary sessionItems)
|
|
||||||
{
|
|
||||||
DisconnectType = disconnectType;
|
|
||||||
RemoteEndPoint = remoteEndPoint;
|
|
||||||
SessionItems = sessionItems ?? throw new ArgumentNullException(nameof(sessionItems));
|
|
||||||
|
|
||||||
_connectPacket = connectPacket;
|
|
||||||
// The DISCONNECT packet can be null in case of a non clean disconnect or session takeover.
|
|
||||||
_disconnectPacket = disconnectPacket;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the client identifier.
|
|
||||||
/// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues.
|
|
||||||
/// </summary>
|
|
||||||
public string ClientId => _connectPacket.ClientId;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the user name of the client.
|
|
||||||
/// </summary>
|
|
||||||
public string UserName => _connectPacket.Username;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the password of the client.
|
|
||||||
/// </summary>
|
|
||||||
public string Password => Encoding.UTF8.GetString(_connectPacket.Password.AsSpan());
|
|
||||||
|
|
||||||
public MqttClientDisconnectType DisconnectType { get; }
|
|
||||||
|
|
||||||
public EndPoint RemoteEndPoint { get; }
|
|
||||||
|
|
||||||
[Obsolete("Use RemoteEndPoint instead.")]
|
|
||||||
public string Endpoint => RemoteEndPoint?.ToString();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the reason code sent by the client.
|
|
||||||
/// Only available for clean disconnects.
|
|
||||||
/// <remarks>MQTT 5.0.0+ feature.</remarks>
|
|
||||||
/// </summary>
|
|
||||||
public MqttDisconnectReasonCode? ReasonCode => _disconnectPacket?.ReasonCode;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the reason string sent by the client.
|
|
||||||
/// Only available for clean disconnects.
|
|
||||||
/// <remarks>MQTT 5.0.0+ feature.</remarks>
|
|
||||||
/// </summary>
|
|
||||||
public string ReasonString => _disconnectPacket?.ReasonString;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the session expiry interval sent by the client.
|
|
||||||
/// Only available for clean disconnects.
|
|
||||||
/// <remarks>MQTT 5.0.0+ feature.</remarks>
|
|
||||||
/// </summary>
|
|
||||||
public uint SessionExpiryInterval => _disconnectPacket?.SessionExpiryInterval ?? 0;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets a key/value collection that can be used to share data within the scope of this session.
|
|
||||||
/// </summary>
|
|
||||||
public IDictionary SessionItems { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the user properties sent by the client.
|
|
||||||
/// Only available for clean disconnects.
|
|
||||||
/// <remarks>MQTT 5.0.0+ feature.</remarks>
|
|
||||||
/// </summary>
|
|
||||||
public List<MqttUserProperty> UserProperties => _disconnectPacket?.UserProperties;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the client identifier.
|
||||||
|
/// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues.
|
||||||
|
/// </summary>
|
||||||
|
public string ClientId => _connectPacket.ClientId;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the user name of the client.
|
||||||
|
/// </summary>
|
||||||
|
public string UserName => _connectPacket.Username;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the password of the client.
|
||||||
|
/// </summary>
|
||||||
|
public string Password => Encoding.UTF8.GetString(_connectPacket.Password.AsSpan());
|
||||||
|
|
||||||
|
public MqttClientDisconnectType DisconnectType { get; }
|
||||||
|
|
||||||
|
public EndPoint RemoteEndPoint { get; }
|
||||||
|
|
||||||
|
[Obsolete("Use RemoteEndPoint instead.")]
|
||||||
|
public string Endpoint => RemoteEndPoint?.ToString();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the reason code sent by the client.
|
||||||
|
/// Only available for clean disconnects.
|
||||||
|
/// <remarks>MQTT 5.0.0+ feature.</remarks>
|
||||||
|
/// </summary>
|
||||||
|
public MqttDisconnectReasonCode? ReasonCode => _disconnectPacket?.ReasonCode;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the reason string sent by the client.
|
||||||
|
/// Only available for clean disconnects.
|
||||||
|
/// <remarks>MQTT 5.0.0+ feature.</remarks>
|
||||||
|
/// </summary>
|
||||||
|
public string ReasonString => _disconnectPacket?.ReasonString;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the session expiry interval sent by the client.
|
||||||
|
/// Only available for clean disconnects.
|
||||||
|
/// <remarks>MQTT 5.0.0+ feature.</remarks>
|
||||||
|
/// </summary>
|
||||||
|
public uint SessionExpiryInterval => _disconnectPacket?.SessionExpiryInterval ?? 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a key/value collection that can be used to share data within the scope of this session.
|
||||||
|
/// </summary>
|
||||||
|
public IDictionary SessionItems { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the user properties sent by the client.
|
||||||
|
/// Only available for clean disconnects.
|
||||||
|
/// <remarks>MQTT 5.0.0+ feature.</remarks>
|
||||||
|
/// </summary>
|
||||||
|
public List<MqttUserProperty> UserProperties => _disconnectPacket?.UserProperties;
|
||||||
}
|
}
|
||||||
@@ -2,42 +2,40 @@
|
|||||||
// The .NET Foundation licenses this file to you under the MIT license.
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
using MQTTnet.Packets;
|
using MQTTnet.Packets;
|
||||||
|
|
||||||
namespace MQTTnet.Server
|
namespace MQTTnet.Server;
|
||||||
|
|
||||||
|
public sealed class ClientSubscribedTopicEventArgs : EventArgs
|
||||||
{
|
{
|
||||||
public sealed class ClientSubscribedTopicEventArgs : EventArgs
|
public ClientSubscribedTopicEventArgs(string clientId, string userName, MqttTopicFilter topicFilter, IDictionary sessionItems)
|
||||||
{
|
{
|
||||||
public ClientSubscribedTopicEventArgs(string clientId, string userName, MqttTopicFilter topicFilter, IDictionary sessionItems)
|
ClientId = clientId ?? throw new ArgumentNullException(nameof(clientId));
|
||||||
{
|
UserName = userName;
|
||||||
ClientId = clientId ?? throw new ArgumentNullException(nameof(clientId));
|
TopicFilter = topicFilter ?? throw new ArgumentNullException(nameof(topicFilter));
|
||||||
UserName = userName;
|
SessionItems = sessionItems ?? throw new ArgumentNullException(nameof(sessionItems));
|
||||||
TopicFilter = topicFilter ?? throw new ArgumentNullException(nameof(topicFilter));
|
|
||||||
SessionItems = sessionItems ?? throw new ArgumentNullException(nameof(sessionItems));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the client identifier.
|
|
||||||
/// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues.
|
|
||||||
/// </summary>
|
|
||||||
public string ClientId { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the user name of the client.
|
|
||||||
/// </summary>
|
|
||||||
public string UserName { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets a key/value collection that can be used to share data within the scope of this session.
|
|
||||||
/// </summary>
|
|
||||||
public IDictionary SessionItems { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the topic filter.
|
|
||||||
/// The topic filter can contain topics and wildcards.
|
|
||||||
/// </summary>
|
|
||||||
public MqttTopicFilter TopicFilter { get; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the client identifier.
|
||||||
|
/// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues.
|
||||||
|
/// </summary>
|
||||||
|
public string ClientId { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the user name of the client.
|
||||||
|
/// </summary>
|
||||||
|
public string UserName { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a key/value collection that can be used to share data within the scope of this session.
|
||||||
|
/// </summary>
|
||||||
|
public IDictionary SessionItems { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the topic filter.
|
||||||
|
/// The topic filter can contain topics and wildcards.
|
||||||
|
/// </summary>
|
||||||
|
public MqttTopicFilter TopicFilter { get; }
|
||||||
}
|
}
|
||||||
@@ -2,41 +2,39 @@
|
|||||||
// The .NET Foundation licenses this file to you under the MIT license.
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
|
|
||||||
namespace MQTTnet.Server
|
namespace MQTTnet.Server;
|
||||||
|
|
||||||
|
public sealed class ClientUnsubscribedTopicEventArgs : EventArgs
|
||||||
{
|
{
|
||||||
public sealed class ClientUnsubscribedTopicEventArgs : EventArgs
|
public ClientUnsubscribedTopicEventArgs(string clientId, string userName, string topicFilter, IDictionary sessionItems)
|
||||||
{
|
{
|
||||||
public ClientUnsubscribedTopicEventArgs(string clientId, string userName, string topicFilter, IDictionary sessionItems)
|
ClientId = clientId ?? throw new ArgumentNullException(nameof(clientId));
|
||||||
{
|
UserName = userName;
|
||||||
ClientId = clientId ?? throw new ArgumentNullException(nameof(clientId));
|
TopicFilter = topicFilter ?? throw new ArgumentNullException(nameof(topicFilter));
|
||||||
UserName = userName;
|
SessionItems = sessionItems ?? throw new ArgumentNullException(nameof(sessionItems));
|
||||||
TopicFilter = topicFilter ?? throw new ArgumentNullException(nameof(topicFilter));
|
|
||||||
SessionItems = sessionItems ?? throw new ArgumentNullException(nameof(sessionItems));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the client identifier.
|
|
||||||
/// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues.
|
|
||||||
/// </summary>
|
|
||||||
public string ClientId { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the user name of the client.
|
|
||||||
/// </summary>
|
|
||||||
public string UserName { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets a key/value collection that can be used to share data within the scope of this session.
|
|
||||||
/// </summary>
|
|
||||||
public IDictionary SessionItems { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the topic filter.
|
|
||||||
/// The topic filter can contain topics and wildcards.
|
|
||||||
/// </summary>
|
|
||||||
public string TopicFilter { get; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the client identifier.
|
||||||
|
/// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues.
|
||||||
|
/// </summary>
|
||||||
|
public string ClientId { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the user name of the client.
|
||||||
|
/// </summary>
|
||||||
|
public string UserName { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a key/value collection that can be used to share data within the scope of this session.
|
||||||
|
/// </summary>
|
||||||
|
public IDictionary SessionItems { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the topic filter.
|
||||||
|
/// The topic filter can contain topics and wildcards.
|
||||||
|
/// </summary>
|
||||||
|
public string TopicFilter { get; }
|
||||||
}
|
}
|
||||||
+23
-26
@@ -2,34 +2,31 @@
|
|||||||
// The .NET Foundation licenses this file to you under the MIT license.
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System;
|
namespace MQTTnet.Server;
|
||||||
|
|
||||||
namespace MQTTnet.Server
|
public sealed class InterceptingClientApplicationMessageEnqueueEventArgs : EventArgs
|
||||||
{
|
{
|
||||||
public sealed class InterceptingClientApplicationMessageEnqueueEventArgs : EventArgs
|
public InterceptingClientApplicationMessageEnqueueEventArgs(string senderClientId, string receiverClientId, MqttApplicationMessage applicationMessage)
|
||||||
{
|
{
|
||||||
public InterceptingClientApplicationMessageEnqueueEventArgs(string senderClientId, string receiverClientId, MqttApplicationMessage applicationMessage)
|
SenderClientId = senderClientId ?? throw new ArgumentNullException(nameof(senderClientId));
|
||||||
{
|
ReceiverClientId = receiverClientId ?? throw new ArgumentNullException(nameof(receiverClientId));
|
||||||
SenderClientId = senderClientId ?? throw new ArgumentNullException(nameof(senderClientId));
|
ApplicationMessage = applicationMessage ?? throw new ArgumentNullException(nameof(applicationMessage));
|
||||||
ReceiverClientId = receiverClientId ?? throw new ArgumentNullException(nameof(receiverClientId));
|
|
||||||
ApplicationMessage = applicationMessage ?? throw new ArgumentNullException(nameof(applicationMessage));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets whether the enqueue of the application message should be performed or not.
|
|
||||||
/// If set to _False_ the client will not receive the application message.
|
|
||||||
/// </summary>
|
|
||||||
public bool AcceptEnqueue { get; set; } = true;
|
|
||||||
|
|
||||||
public MqttApplicationMessage ApplicationMessage { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Indicates if the connection with the sender should be closed.
|
|
||||||
/// </summary>
|
|
||||||
public bool CloseSenderConnection { get; set; }
|
|
||||||
|
|
||||||
public string ReceiverClientId { get; }
|
|
||||||
|
|
||||||
public string SenderClientId { get; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets whether the enqueue of the application message should be performed or not.
|
||||||
|
/// If set to _False_ the client will not receive the application message.
|
||||||
|
/// </summary>
|
||||||
|
public bool AcceptEnqueue { get; set; } = true;
|
||||||
|
|
||||||
|
public MqttApplicationMessage ApplicationMessage { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates if the connection with the sender should be closed.
|
||||||
|
/// </summary>
|
||||||
|
public bool CloseSenderConnection { get; set; }
|
||||||
|
|
||||||
|
public string ReceiverClientId { get; }
|
||||||
|
|
||||||
|
public string SenderClientId { get; }
|
||||||
}
|
}
|
||||||
@@ -2,63 +2,60 @@
|
|||||||
// The .NET Foundation licenses this file to you under the MIT license.
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Threading;
|
|
||||||
using MQTTnet.Packets;
|
using MQTTnet.Packets;
|
||||||
|
|
||||||
namespace MQTTnet.Server
|
namespace MQTTnet.Server;
|
||||||
|
|
||||||
|
public sealed class InterceptingPacketEventArgs : EventArgs
|
||||||
{
|
{
|
||||||
public sealed class InterceptingPacketEventArgs : EventArgs
|
public InterceptingPacketEventArgs(CancellationToken cancellationToken, string clientId, string userName, EndPoint remoteEndPoint, MqttPacket packet, IDictionary sessionItems)
|
||||||
{
|
{
|
||||||
public InterceptingPacketEventArgs(CancellationToken cancellationToken, string clientId, string userName, EndPoint remoteEndPoint, MqttPacket packet, IDictionary sessionItems)
|
CancellationToken = cancellationToken;
|
||||||
{
|
ClientId = clientId ?? throw new ArgumentNullException(nameof(clientId));
|
||||||
CancellationToken = cancellationToken;
|
UserName = userName;
|
||||||
ClientId = clientId ?? throw new ArgumentNullException(nameof(clientId));
|
RemoteEndPoint = remoteEndPoint;
|
||||||
UserName = userName;
|
Packet = packet ?? throw new ArgumentNullException(nameof(packet));
|
||||||
RemoteEndPoint = remoteEndPoint;
|
SessionItems = sessionItems;
|
||||||
Packet = packet ?? throw new ArgumentNullException(nameof(packet));
|
|
||||||
SessionItems = sessionItems;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the cancellation token from the connection managing thread.
|
|
||||||
/// Use this in further event processing.
|
|
||||||
/// </summary>
|
|
||||||
public CancellationToken CancellationToken { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the client ID which has sent the packet or will receive the packet.
|
|
||||||
/// </summary>
|
|
||||||
public string ClientId { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the user name of the client.
|
|
||||||
/// </summary>
|
|
||||||
public string UserName { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the endpoint of the sending or receiving client.
|
|
||||||
/// </summary>
|
|
||||||
public EndPoint RemoteEndPoint { get; }
|
|
||||||
|
|
||||||
[Obsolete("Use RemoteEndPoint instead.")]
|
|
||||||
public string Endpoint => RemoteEndPoint?.ToString();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the MQTT packet which was received or will be sent.
|
|
||||||
/// </summary>
|
|
||||||
public MqttPacket Packet { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets whether the packet should be processed or not.
|
|
||||||
/// </summary>
|
|
||||||
public bool ProcessPacket { get; set; } = true;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets a key/value collection that can be used to share data within the scope of this session.
|
|
||||||
/// </summary>
|
|
||||||
public IDictionary SessionItems { get; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the cancellation token from the connection managing thread.
|
||||||
|
/// Use this in further event processing.
|
||||||
|
/// </summary>
|
||||||
|
public CancellationToken CancellationToken { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the client ID which has sent the packet or will receive the packet.
|
||||||
|
/// </summary>
|
||||||
|
public string ClientId { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the user name of the client.
|
||||||
|
/// </summary>
|
||||||
|
public string UserName { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the endpoint of the sending or receiving client.
|
||||||
|
/// </summary>
|
||||||
|
public EndPoint RemoteEndPoint { get; }
|
||||||
|
|
||||||
|
[Obsolete("Use RemoteEndPoint instead.")]
|
||||||
|
public string Endpoint => RemoteEndPoint?.ToString();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the MQTT packet which was received or will be sent.
|
||||||
|
/// </summary>
|
||||||
|
public MqttPacket Packet { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets whether the packet should be processed or not.
|
||||||
|
/// </summary>
|
||||||
|
public bool ProcessPacket { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a key/value collection that can be used to share data within the scope of this session.
|
||||||
|
/// </summary>
|
||||||
|
public IDictionary SessionItems { get; }
|
||||||
}
|
}
|
||||||
@@ -2,56 +2,53 @@
|
|||||||
// The .NET Foundation licenses this file to you under the MIT license.
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Threading;
|
|
||||||
|
|
||||||
namespace MQTTnet.Server
|
namespace MQTTnet.Server;
|
||||||
|
|
||||||
|
public sealed class InterceptingPublishEventArgs : EventArgs
|
||||||
{
|
{
|
||||||
public sealed class InterceptingPublishEventArgs : EventArgs
|
public InterceptingPublishEventArgs(MqttApplicationMessage applicationMessage, CancellationToken cancellationToken, string clientId, string userName, IDictionary sessionItems)
|
||||||
{
|
{
|
||||||
public InterceptingPublishEventArgs(MqttApplicationMessage applicationMessage, CancellationToken cancellationToken, string clientId, string userName, IDictionary sessionItems)
|
ApplicationMessage = applicationMessage ?? throw new ArgumentNullException(nameof(applicationMessage));
|
||||||
{
|
CancellationToken = cancellationToken;
|
||||||
ApplicationMessage = applicationMessage ?? throw new ArgumentNullException(nameof(applicationMessage));
|
ClientId = clientId ?? throw new ArgumentNullException(nameof(clientId));
|
||||||
CancellationToken = cancellationToken;
|
UserName = userName;
|
||||||
ClientId = clientId ?? throw new ArgumentNullException(nameof(clientId));
|
SessionItems = sessionItems ?? throw new ArgumentNullException(nameof(sessionItems));
|
||||||
UserName = userName;
|
|
||||||
SessionItems = sessionItems ?? throw new ArgumentNullException(nameof(sessionItems));
|
|
||||||
}
|
|
||||||
|
|
||||||
public MqttApplicationMessage ApplicationMessage { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the cancellation token which can indicate that the client connection gets down.
|
|
||||||
/// </summary>
|
|
||||||
public CancellationToken CancellationToken { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the client identifier.
|
|
||||||
/// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues.
|
|
||||||
/// </summary>
|
|
||||||
public string ClientId { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the user name of the client.
|
|
||||||
/// </summary>
|
|
||||||
public string UserName { get; }
|
|
||||||
|
|
||||||
public bool CloseConnection { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets whether the publish should be processed internally.
|
|
||||||
/// </summary>
|
|
||||||
public bool ProcessPublish { get; set; } = true;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the response which will be sent to the client via the PUBACK etc. packets.
|
|
||||||
/// </summary>
|
|
||||||
public PublishResponse Response { get; } = new PublishResponse();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets a key/value collection that can be used to share data within the scope of this session.
|
|
||||||
/// </summary>
|
|
||||||
public IDictionary SessionItems { get; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public MqttApplicationMessage ApplicationMessage { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the cancellation token which can indicate that the client connection gets down.
|
||||||
|
/// </summary>
|
||||||
|
public CancellationToken CancellationToken { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the client identifier.
|
||||||
|
/// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues.
|
||||||
|
/// </summary>
|
||||||
|
public string ClientId { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the user name of the client.
|
||||||
|
/// </summary>
|
||||||
|
public string UserName { get; }
|
||||||
|
|
||||||
|
public bool CloseConnection { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets whether the publish should be processed internally.
|
||||||
|
/// </summary>
|
||||||
|
public bool ProcessPublish { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the response which will be sent to the client via the PUBACK etc. packets.
|
||||||
|
/// </summary>
|
||||||
|
public PublishResponse Response { get; } = new PublishResponse();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a key/value collection that can be used to share data within the scope of this session.
|
||||||
|
/// </summary>
|
||||||
|
public IDictionary SessionItems { get; }
|
||||||
}
|
}
|
||||||
@@ -2,90 +2,86 @@
|
|||||||
// The .NET Foundation licenses this file to you under the MIT license.
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading;
|
|
||||||
using MQTTnet.Packets;
|
using MQTTnet.Packets;
|
||||||
|
|
||||||
namespace MQTTnet.Server
|
namespace MQTTnet.Server;
|
||||||
|
|
||||||
|
public sealed class InterceptingSubscriptionEventArgs : EventArgs
|
||||||
{
|
{
|
||||||
public sealed class InterceptingSubscriptionEventArgs : EventArgs
|
public InterceptingSubscriptionEventArgs(
|
||||||
|
CancellationToken cancellationToken,
|
||||||
|
string clientId,
|
||||||
|
string userName,
|
||||||
|
MqttSessionStatus session,
|
||||||
|
MqttTopicFilter topicFilter,
|
||||||
|
List<MqttUserProperty> userProperties)
|
||||||
{
|
{
|
||||||
public InterceptingSubscriptionEventArgs(
|
CancellationToken = cancellationToken;
|
||||||
CancellationToken cancellationToken,
|
ClientId = clientId;
|
||||||
string clientId,
|
UserName = userName;
|
||||||
string userName,
|
Session = session;
|
||||||
MqttSessionStatus session,
|
TopicFilter = topicFilter;
|
||||||
MqttTopicFilter topicFilter,
|
UserProperties = userProperties;
|
||||||
List<MqttUserProperty> userProperties)
|
|
||||||
{
|
|
||||||
CancellationToken = cancellationToken;
|
|
||||||
ClientId = clientId;
|
|
||||||
UserName = userName;
|
|
||||||
Session = session;
|
|
||||||
TopicFilter = topicFilter;
|
|
||||||
UserProperties = userProperties;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the cancellation token which can indicate that the client connection gets down.
|
|
||||||
/// </summary>
|
|
||||||
public CancellationToken CancellationToken { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the client identifier.
|
|
||||||
/// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues.
|
|
||||||
/// </summary>
|
|
||||||
public string ClientId { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the user name of the client.
|
|
||||||
/// </summary>
|
|
||||||
public string UserName { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets whether the broker should close the client connection.
|
|
||||||
/// </summary>
|
|
||||||
public bool CloseConnection { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets whether the broker should create an internal subscription for the client.
|
|
||||||
/// The broker can also avoid this and return "success" to the client.
|
|
||||||
/// This feature allows using the MQTT Broker as the Frontend and another system as the backend.
|
|
||||||
/// </summary>
|
|
||||||
public bool ProcessSubscription { get; set; } = true;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the reason string which will be sent to the client in the SUBACK packet.
|
|
||||||
/// </summary>
|
|
||||||
public string ReasonString { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the response which will be sent to the client via the SUBACK packet.
|
|
||||||
/// </summary>
|
|
||||||
public SubscribeResponse Response { get; } = new SubscribeResponse();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the current client session.
|
|
||||||
/// </summary>
|
|
||||||
public MqttSessionStatus Session { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets a key/value collection that can be used to share data within the scope of this session.
|
|
||||||
/// </summary>
|
|
||||||
public IDictionary SessionItems => Session.Items;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the topic filter.
|
|
||||||
/// The topic filter can contain topics and wildcards.
|
|
||||||
/// </summary>
|
|
||||||
public MqttTopicFilter TopicFilter { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the user properties.
|
|
||||||
/// <remarks>MQTT 5.0.0+ feature.</remarks>
|
|
||||||
/// </summary>
|
|
||||||
public List<MqttUserProperty> UserProperties { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the cancellation token which can indicate that the client connection gets down.
|
||||||
|
/// </summary>
|
||||||
|
public CancellationToken CancellationToken { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the client identifier.
|
||||||
|
/// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues.
|
||||||
|
/// </summary>
|
||||||
|
public string ClientId { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the user name of the client.
|
||||||
|
/// </summary>
|
||||||
|
public string UserName { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets whether the broker should close the client connection.
|
||||||
|
/// </summary>
|
||||||
|
public bool CloseConnection { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets whether the broker should create an internal subscription for the client.
|
||||||
|
/// The broker can also avoid this and return "success" to the client.
|
||||||
|
/// This feature allows using the MQTT Broker as the Frontend and another system as the backend.
|
||||||
|
/// </summary>
|
||||||
|
public bool ProcessSubscription { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the reason string which will be sent to the client in the SUBACK packet.
|
||||||
|
/// </summary>
|
||||||
|
public string ReasonString { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the response which will be sent to the client via the SUBACK packet.
|
||||||
|
/// </summary>
|
||||||
|
public SubscribeResponse Response { get; } = new SubscribeResponse();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the current client session.
|
||||||
|
/// </summary>
|
||||||
|
public MqttSessionStatus Session { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a key/value collection that can be used to share data within the scope of this session.
|
||||||
|
/// </summary>
|
||||||
|
public IDictionary SessionItems => Session.Items;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the topic filter.
|
||||||
|
/// The topic filter can contain topics and wildcards.
|
||||||
|
/// </summary>
|
||||||
|
public MqttTopicFilter TopicFilter { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the user properties.
|
||||||
|
/// <remarks>MQTT 5.0.0+ feature.</remarks>
|
||||||
|
/// </summary>
|
||||||
|
public List<MqttUserProperty> UserProperties { get; set; }
|
||||||
}
|
}
|
||||||
@@ -2,77 +2,73 @@
|
|||||||
// The .NET Foundation licenses this file to you under the MIT license.
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading;
|
|
||||||
using MQTTnet.Packets;
|
using MQTTnet.Packets;
|
||||||
|
|
||||||
namespace MQTTnet.Server
|
namespace MQTTnet.Server;
|
||||||
|
|
||||||
|
public sealed class InterceptingUnsubscriptionEventArgs : EventArgs
|
||||||
{
|
{
|
||||||
public sealed class InterceptingUnsubscriptionEventArgs : EventArgs
|
public InterceptingUnsubscriptionEventArgs(CancellationToken cancellationToken, string clientId, string userName, IDictionary sessionItems, string topic, List<MqttUserProperty> userProperties)
|
||||||
{
|
{
|
||||||
public InterceptingUnsubscriptionEventArgs(CancellationToken cancellationToken, string clientId, string userName, IDictionary sessionItems, string topic, List<MqttUserProperty> userProperties)
|
CancellationToken = cancellationToken;
|
||||||
{
|
ClientId = clientId;
|
||||||
CancellationToken = cancellationToken;
|
UserName = userName;
|
||||||
ClientId = clientId;
|
SessionItems = sessionItems;
|
||||||
UserName = userName;
|
Topic = topic;
|
||||||
SessionItems = sessionItems;
|
UserProperties = userProperties;
|
||||||
Topic = topic;
|
|
||||||
UserProperties = userProperties;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the cancellation token which can indicate that the client connection gets down.
|
|
||||||
/// </summary>
|
|
||||||
public CancellationToken CancellationToken { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the client identifier.
|
|
||||||
/// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues.
|
|
||||||
/// </summary>
|
|
||||||
public string ClientId { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the user name of the client.
|
|
||||||
/// </summary>
|
|
||||||
public string UserName { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets whether the broker should close the client connection.
|
|
||||||
/// </summary>
|
|
||||||
public bool CloseConnection { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets whether the broker should remove an internal subscription for the client.
|
|
||||||
/// The broker can also avoid this and return "success" to the client.
|
|
||||||
/// This feature allows using the MQTT Broker as the Frontend and another system as the backend.
|
|
||||||
/// </summary>
|
|
||||||
public bool ProcessUnsubscription { get; set; } = true;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the response which will be sent to the client via the UNSUBACK pocket.
|
|
||||||
/// </summary>
|
|
||||||
public UnsubscribeResponse Response { get; } = new UnsubscribeResponse();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets a key/value collection that can be used to share data within the scope of this session.
|
|
||||||
/// </summary>
|
|
||||||
public IDictionary SessionItems { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the MQTT topic.
|
|
||||||
/// In MQTT, the word topic refers to an UTF-8 string that the broker uses to filter messages for each connected
|
|
||||||
/// client.
|
|
||||||
/// The topic consists of one or more topic levels. Each topic level is separated by a forward slash (topic level
|
|
||||||
/// separator).
|
|
||||||
/// </summary>
|
|
||||||
public string Topic { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the user properties.
|
|
||||||
/// <remarks>MQTT 5.0.0+ feature.</remarks>
|
|
||||||
/// </summary>
|
|
||||||
public List<MqttUserProperty> UserProperties { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the cancellation token which can indicate that the client connection gets down.
|
||||||
|
/// </summary>
|
||||||
|
public CancellationToken CancellationToken { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the client identifier.
|
||||||
|
/// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues.
|
||||||
|
/// </summary>
|
||||||
|
public string ClientId { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the user name of the client.
|
||||||
|
/// </summary>
|
||||||
|
public string UserName { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets whether the broker should close the client connection.
|
||||||
|
/// </summary>
|
||||||
|
public bool CloseConnection { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets whether the broker should remove an internal subscription for the client.
|
||||||
|
/// The broker can also avoid this and return "success" to the client.
|
||||||
|
/// This feature allows using the MQTT Broker as the Frontend and another system as the backend.
|
||||||
|
/// </summary>
|
||||||
|
public bool ProcessUnsubscription { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the response which will be sent to the client via the UNSUBACK pocket.
|
||||||
|
/// </summary>
|
||||||
|
public UnsubscribeResponse Response { get; } = new UnsubscribeResponse();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a key/value collection that can be used to share data within the scope of this session.
|
||||||
|
/// </summary>
|
||||||
|
public IDictionary SessionItems { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the MQTT topic.
|
||||||
|
/// In MQTT, the word topic refers to an UTF-8 string that the broker uses to filter messages for each connected
|
||||||
|
/// client.
|
||||||
|
/// The topic consists of one or more topic levels. Each topic level is separated by a forward slash (topic level
|
||||||
|
/// separator).
|
||||||
|
/// </summary>
|
||||||
|
public string Topic { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the user properties.
|
||||||
|
/// <remarks>MQTT 5.0.0+ feature.</remarks>
|
||||||
|
/// </summary>
|
||||||
|
public List<MqttUserProperty> UserProperties { get; set; }
|
||||||
}
|
}
|
||||||
@@ -2,13 +2,9 @@
|
|||||||
// The .NET Foundation licenses this file to you under the MIT license.
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System;
|
namespace MQTTnet.Server;
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace MQTTnet.Server
|
public sealed class LoadingRetainedMessagesEventArgs : EventArgs
|
||||||
{
|
{
|
||||||
public sealed class LoadingRetainedMessagesEventArgs : EventArgs
|
public List<MqttApplicationMessage> LoadedRetainedMessages { get; set; } = new List<MqttApplicationMessage>();
|
||||||
{
|
|
||||||
public List<MqttApplicationMessage> LoadedRetainedMessages { get; set; } = new List<MqttApplicationMessage>();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -2,50 +2,47 @@
|
|||||||
// The .NET Foundation licenses this file to you under the MIT license.
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using MQTTnet.Packets;
|
using MQTTnet.Packets;
|
||||||
using MQTTnet.Server.Internal;
|
using MQTTnet.Server.Internal;
|
||||||
|
|
||||||
namespace MQTTnet.Server
|
namespace MQTTnet.Server;
|
||||||
|
|
||||||
|
public sealed class PreparingSessionEventArgs : EventArgs
|
||||||
{
|
{
|
||||||
public sealed class PreparingSessionEventArgs : EventArgs
|
public string Id { get; set; }
|
||||||
{
|
|
||||||
public string Id { get; set; }
|
|
||||||
|
|
||||||
// TODO: Allow adding of packets to the queue etc.
|
// TODO: Allow adding of packets to the queue etc.
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* The Session State in the Server consists of:
|
* The Session State in the Server consists of:
|
||||||
· The existence of a Session, even if the rest of the Session State is empty.
|
· The existence of a Session, even if the rest of the Session State is empty.
|
||||||
· The Clients subscriptions, including any Subscription Identifiers.
|
· The Clients subscriptions, including any Subscription Identifiers.
|
||||||
· QoS 1 and QoS 2 messages which have been sent to the Client, but have not been completely acknowledged.
|
· QoS 1 and QoS 2 messages which have been sent to the Client, but have not been completely acknowledged.
|
||||||
· QoS 1 and QoS 2 messages pending transmission to the Client and OPTIONALLY QoS 0 messages pending transmission to the Client.
|
· QoS 1 and QoS 2 messages pending transmission to the Client and OPTIONALLY QoS 0 messages pending transmission to the Client.
|
||||||
· QoS 2 messages which have been received from the Client, but have not been completely acknowledged.The Will Message and the Will Delay Interval
|
· QoS 2 messages which have been received from the Client, but have not been completely acknowledged.The Will Message and the Will Delay Interval
|
||||||
· If the Session is currently not connected, the time at which the Session will end and Session State will be discarded.
|
· If the Session is currently not connected, the time at which the Session will end and Session State will be discarded.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public bool IsExistingSession { get; set; }
|
public bool IsExistingSession { get; set; }
|
||||||
|
|
||||||
public IDictionary<object, object> Items { get; set; }
|
public IDictionary<object, object> Items { get; set; }
|
||||||
|
|
||||||
public List<MqttPublishPacket> PublishPackets { get; } = new List<MqttPublishPacket>();
|
public List<MqttPublishPacket> PublishPackets { get; } = new List<MqttPublishPacket>();
|
||||||
|
|
||||||
DateTime? SessionExpiryTimestamp { get; set; }
|
DateTime? SessionExpiryTimestamp { get; set; }
|
||||||
|
|
||||||
public List<MqttSubscription> Subscriptions { get; } = new List<MqttSubscription>();
|
public List<MqttSubscription> Subscriptions { get; } = new List<MqttSubscription>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the will delay interval.
|
/// Gets the will delay interval.
|
||||||
/// This is the time between the client disconnect and the time the will message will be sent.
|
/// This is the time between the client disconnect and the time the will message will be sent.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public uint? WillDelayInterval { get; set; }
|
public uint? WillDelayInterval { get; set; }
|
||||||
|
|
||||||
// <summary>
|
// <summary>
|
||||||
// Gets the last will message.
|
// Gets the last will message.
|
||||||
// In MQTT, you use the last will message feature to notify other clients about an ungracefully disconnected client.
|
// In MQTT, you use the last will message feature to notify other clients about an ungracefully disconnected client.
|
||||||
// </summary>
|
// </summary>
|
||||||
// TODO: Use single properties. No entire will message.
|
// TODO: Use single properties. No entire will message.
|
||||||
//MqttApplicationMessage WillMessage { get; set; }
|
//MqttApplicationMessage WillMessage { get; set; }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -2,21 +2,19 @@
|
|||||||
// The .NET Foundation licenses this file to you under the MIT license.
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System;
|
|
||||||
using MQTTnet.Packets;
|
using MQTTnet.Packets;
|
||||||
|
|
||||||
namespace MQTTnet.Server
|
namespace MQTTnet.Server;
|
||||||
|
|
||||||
|
public sealed class QueueMessageOverwrittenEventArgs : EventArgs
|
||||||
{
|
{
|
||||||
public sealed class QueueMessageOverwrittenEventArgs : EventArgs
|
public QueueMessageOverwrittenEventArgs(string receiverClientId, MqttPacket packet)
|
||||||
{
|
{
|
||||||
public QueueMessageOverwrittenEventArgs(string receiverClientId, MqttPacket packet)
|
ReceiverClientId = receiverClientId ?? throw new ArgumentNullException(nameof(receiverClientId));
|
||||||
{
|
Packet = packet ?? throw new ArgumentNullException(nameof(packet));
|
||||||
ReceiverClientId = receiverClientId ?? throw new ArgumentNullException(nameof(receiverClientId));
|
|
||||||
Packet = packet ?? throw new ArgumentNullException(nameof(packet));
|
|
||||||
}
|
|
||||||
|
|
||||||
public MqttPacket Packet { get; }
|
|
||||||
|
|
||||||
public string ReceiverClientId { get; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public MqttPacket Packet { get; }
|
||||||
|
|
||||||
|
public string ReceiverClientId { get; }
|
||||||
}
|
}
|
||||||
@@ -2,24 +2,20 @@
|
|||||||
// The .NET Foundation licenses this file to you under the MIT license.
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System;
|
namespace MQTTnet.Server;
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace MQTTnet.Server
|
public sealed class RetainedMessageChangedEventArgs : EventArgs
|
||||||
{
|
{
|
||||||
public sealed class RetainedMessageChangedEventArgs : EventArgs
|
public RetainedMessageChangedEventArgs(string clientId, MqttApplicationMessage changedRetainedMessage, List<MqttApplicationMessage> storedRetainedMessages)
|
||||||
{
|
{
|
||||||
public RetainedMessageChangedEventArgs(string clientId, MqttApplicationMessage changedRetainedMessage, List<MqttApplicationMessage> storedRetainedMessages)
|
ClientId = clientId ?? throw new ArgumentNullException(nameof(clientId));
|
||||||
{
|
ChangedRetainedMessage = changedRetainedMessage ?? throw new ArgumentNullException(nameof(changedRetainedMessage));
|
||||||
ClientId = clientId ?? throw new ArgumentNullException(nameof(clientId));
|
StoredRetainedMessages = storedRetainedMessages ?? throw new ArgumentNullException(nameof(storedRetainedMessages));
|
||||||
ChangedRetainedMessage = changedRetainedMessage ?? throw new ArgumentNullException(nameof(changedRetainedMessage));
|
|
||||||
StoredRetainedMessages = storedRetainedMessages ?? throw new ArgumentNullException(nameof(storedRetainedMessages));
|
|
||||||
}
|
|
||||||
|
|
||||||
public MqttApplicationMessage ChangedRetainedMessage { get; }
|
|
||||||
|
|
||||||
public string ClientId { get; }
|
|
||||||
|
|
||||||
public List<MqttApplicationMessage> StoredRetainedMessages { get; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public MqttApplicationMessage ChangedRetainedMessage { get; }
|
||||||
|
|
||||||
|
public string ClientId { get; }
|
||||||
|
|
||||||
|
public List<MqttApplicationMessage> StoredRetainedMessages { get; }
|
||||||
}
|
}
|
||||||
@@ -2,33 +2,31 @@
|
|||||||
// The .NET Foundation licenses this file to you under the MIT license.
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
|
|
||||||
namespace MQTTnet.Server
|
namespace MQTTnet.Server;
|
||||||
|
|
||||||
|
public sealed class SessionDeletedEventArgs : EventArgs
|
||||||
{
|
{
|
||||||
public sealed class SessionDeletedEventArgs : EventArgs
|
public SessionDeletedEventArgs(string id, string userName, IDictionary sessionItems)
|
||||||
{
|
{
|
||||||
public SessionDeletedEventArgs(string id, string userName, IDictionary sessionItems)
|
Id = id ?? throw new ArgumentNullException(nameof(id));
|
||||||
{
|
UserName = userName;
|
||||||
Id = id ?? throw new ArgumentNullException(nameof(id));
|
SessionItems = sessionItems ?? throw new ArgumentNullException(nameof(sessionItems));
|
||||||
UserName = userName;
|
|
||||||
SessionItems = sessionItems ?? throw new ArgumentNullException(nameof(sessionItems));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the ID of the session.
|
|
||||||
/// </summary>
|
|
||||||
public string Id { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the user name of the session.
|
|
||||||
/// </summary>
|
|
||||||
public string UserName { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets a key/value collection that can be used to share data within the scope of this session.
|
|
||||||
/// </summary>
|
|
||||||
public IDictionary SessionItems { get; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the ID of the session.
|
||||||
|
/// </summary>
|
||||||
|
public string Id { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the user name of the session.
|
||||||
|
/// </summary>
|
||||||
|
public string UserName { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a key/value collection that can be used to share data within the scope of this session.
|
||||||
|
/// </summary>
|
||||||
|
public IDictionary SessionItems { get; }
|
||||||
}
|
}
|
||||||
@@ -8,117 +8,116 @@ using MQTTnet.Adapter;
|
|||||||
using MQTTnet.Diagnostics.Logger;
|
using MQTTnet.Diagnostics.Logger;
|
||||||
using MQTTnet.Internal;
|
using MQTTnet.Internal;
|
||||||
|
|
||||||
namespace MQTTnet.Server.Internal.Adapter
|
namespace MQTTnet.Server.Internal.Adapter;
|
||||||
|
|
||||||
|
public sealed class MqttTcpServerAdapter : IMqttServerAdapter
|
||||||
{
|
{
|
||||||
public sealed class MqttTcpServerAdapter : IMqttServerAdapter
|
readonly List<MqttTcpServerListener> _listeners = new List<MqttTcpServerListener>();
|
||||||
|
|
||||||
|
CancellationTokenSource _cancellationTokenSource;
|
||||||
|
|
||||||
|
MqttServerOptions _serverOptions;
|
||||||
|
|
||||||
|
public Func<IMqttChannelAdapter, Task> ClientHandler { get; set; }
|
||||||
|
|
||||||
|
public bool TreatSocketOpeningErrorAsWarning { get; set; }
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
{
|
{
|
||||||
readonly List<MqttTcpServerListener> _listeners = new List<MqttTcpServerListener>();
|
Cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
CancellationTokenSource _cancellationTokenSource;
|
public Task StartAsync(MqttServerOptions options, IMqttNetLogger logger)
|
||||||
|
{
|
||||||
MqttServerOptions _serverOptions;
|
if (_cancellationTokenSource != null)
|
||||||
|
|
||||||
public Func<IMqttChannelAdapter, Task> ClientHandler { get; set; }
|
|
||||||
|
|
||||||
public bool TreatSocketOpeningErrorAsWarning { get; set; }
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
{
|
||||||
Cleanup();
|
throw new InvalidOperationException("Server is already started.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task StartAsync(MqttServerOptions options, IMqttNetLogger logger)
|
_serverOptions = options;
|
||||||
|
|
||||||
|
_cancellationTokenSource = new CancellationTokenSource();
|
||||||
|
|
||||||
|
if (options.DefaultEndpointOptions.IsEnabled)
|
||||||
{
|
{
|
||||||
if (_cancellationTokenSource != null)
|
RegisterListeners(options.DefaultEndpointOptions, logger, _cancellationTokenSource.Token);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.TlsEndpointOptions?.IsEnabled == true)
|
||||||
|
{
|
||||||
|
if (options.TlsEndpointOptions.CertificateProvider == null)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Server is already started.");
|
throw new ArgumentException("TLS certificate is not set.");
|
||||||
}
|
}
|
||||||
|
|
||||||
_serverOptions = options;
|
RegisterListeners(options.TlsEndpointOptions, logger, _cancellationTokenSource.Token);
|
||||||
|
}
|
||||||
|
|
||||||
_cancellationTokenSource = new CancellationTokenSource();
|
return CompletedTask.Instance;
|
||||||
|
}
|
||||||
|
|
||||||
if (options.DefaultEndpointOptions.IsEnabled)
|
public Task StopAsync()
|
||||||
|
{
|
||||||
|
Cleanup();
|
||||||
|
return CompletedTask.Instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Cleanup()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_cancellationTokenSource?.Cancel(false);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_cancellationTokenSource?.Dispose();
|
||||||
|
_cancellationTokenSource = null;
|
||||||
|
|
||||||
|
foreach (var listener in _listeners)
|
||||||
{
|
{
|
||||||
RegisterListeners(options.DefaultEndpointOptions, logger, _cancellationTokenSource.Token);
|
listener.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.TlsEndpointOptions?.IsEnabled == true)
|
_listeners.Clear();
|
||||||
{
|
}
|
||||||
if (options.TlsEndpointOptions.CertificateProvider == null)
|
}
|
||||||
{
|
|
||||||
throw new ArgumentException("TLS certificate is not set.");
|
|
||||||
}
|
|
||||||
|
|
||||||
RegisterListeners(options.TlsEndpointOptions, logger, _cancellationTokenSource.Token);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
Task OnClientAcceptedAsync(IMqttChannelAdapter channelAdapter)
|
||||||
|
{
|
||||||
|
var clientHandler = ClientHandler;
|
||||||
|
if (clientHandler == null)
|
||||||
|
{
|
||||||
return CompletedTask.Instance;
|
return CompletedTask.Instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task StopAsync()
|
return clientHandler(channelAdapter);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RegisterListeners(MqttServerTcpEndpointBaseOptions tcpEndpointOptions, IMqttNetLogger logger, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (!tcpEndpointOptions.BoundInterNetworkAddress.Equals(IPAddress.None))
|
||||||
{
|
{
|
||||||
Cleanup();
|
var listenerV4 = new MqttTcpServerListener(AddressFamily.InterNetwork, _serverOptions, tcpEndpointOptions, logger)
|
||||||
return CompletedTask.Instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Cleanup()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
_cancellationTokenSource?.Cancel(false);
|
ClientHandler = OnClientAcceptedAsync
|
||||||
}
|
};
|
||||||
finally
|
|
||||||
|
if (listenerV4.Start(TreatSocketOpeningErrorAsWarning, cancellationToken))
|
||||||
{
|
{
|
||||||
_cancellationTokenSource?.Dispose();
|
_listeners.Add(listenerV4);
|
||||||
_cancellationTokenSource = null;
|
|
||||||
|
|
||||||
foreach (var listener in _listeners)
|
|
||||||
{
|
|
||||||
listener.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
_listeners.Clear();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Task OnClientAcceptedAsync(IMqttChannelAdapter channelAdapter)
|
if (!tcpEndpointOptions.BoundInterNetworkV6Address.Equals(IPAddress.None))
|
||||||
{
|
{
|
||||||
var clientHandler = ClientHandler;
|
var listenerV6 = new MqttTcpServerListener(AddressFamily.InterNetworkV6, _serverOptions, tcpEndpointOptions, logger)
|
||||||
if (clientHandler == null)
|
|
||||||
{
|
{
|
||||||
return CompletedTask.Instance;
|
ClientHandler = OnClientAcceptedAsync
|
||||||
}
|
};
|
||||||
|
|
||||||
return clientHandler(channelAdapter);
|
if (listenerV6.Start(TreatSocketOpeningErrorAsWarning, cancellationToken))
|
||||||
}
|
|
||||||
|
|
||||||
void RegisterListeners(MqttServerTcpEndpointBaseOptions tcpEndpointOptions, IMqttNetLogger logger, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
if (!tcpEndpointOptions.BoundInterNetworkAddress.Equals(IPAddress.None))
|
|
||||||
{
|
{
|
||||||
var listenerV4 = new MqttTcpServerListener(AddressFamily.InterNetwork, _serverOptions, tcpEndpointOptions, logger)
|
_listeners.Add(listenerV6);
|
||||||
{
|
|
||||||
ClientHandler = OnClientAcceptedAsync
|
|
||||||
};
|
|
||||||
|
|
||||||
if (listenerV4.Start(TreatSocketOpeningErrorAsWarning, cancellationToken))
|
|
||||||
{
|
|
||||||
_listeners.Add(listenerV4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tcpEndpointOptions.BoundInterNetworkV6Address.Equals(IPAddress.None))
|
|
||||||
{
|
|
||||||
var listenerV6 = new MqttTcpServerListener(AddressFamily.InterNetworkV6, _serverOptions, tcpEndpointOptions, logger)
|
|
||||||
{
|
|
||||||
ClientHandler = OnClientAcceptedAsync
|
|
||||||
};
|
|
||||||
|
|
||||||
if (listenerV6.Start(TreatSocketOpeningErrorAsWarning, cancellationToken))
|
|
||||||
{
|
|
||||||
_listeners.Add(listenerV6);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,248 +12,247 @@ using MQTTnet.Formatter;
|
|||||||
using MQTTnet.Implementations;
|
using MQTTnet.Implementations;
|
||||||
using MQTTnet.Internal;
|
using MQTTnet.Internal;
|
||||||
|
|
||||||
namespace MQTTnet.Server.Internal.Adapter
|
namespace MQTTnet.Server.Internal.Adapter;
|
||||||
|
|
||||||
|
public sealed class MqttTcpServerListener : IDisposable
|
||||||
{
|
{
|
||||||
public sealed class MqttTcpServerListener : IDisposable
|
readonly MqttNetSourceLogger _logger;
|
||||||
|
readonly IMqttNetLogger _rootLogger;
|
||||||
|
readonly AddressFamily _addressFamily;
|
||||||
|
readonly MqttServerOptions _serverOptions;
|
||||||
|
readonly MqttServerTcpEndpointBaseOptions _options;
|
||||||
|
readonly MqttServerTlsTcpEndpointOptions _tlsOptions;
|
||||||
|
|
||||||
|
CrossPlatformSocket _socket;
|
||||||
|
IPEndPoint _localEndPoint;
|
||||||
|
|
||||||
|
public MqttTcpServerListener(
|
||||||
|
AddressFamily addressFamily,
|
||||||
|
MqttServerOptions serverOptions,
|
||||||
|
MqttServerTcpEndpointBaseOptions tcpEndpointOptions,
|
||||||
|
IMqttNetLogger logger)
|
||||||
{
|
{
|
||||||
readonly MqttNetSourceLogger _logger;
|
_addressFamily = addressFamily;
|
||||||
readonly IMqttNetLogger _rootLogger;
|
_serverOptions = serverOptions ?? throw new ArgumentNullException(nameof(serverOptions));
|
||||||
readonly AddressFamily _addressFamily;
|
_options = tcpEndpointOptions ?? throw new ArgumentNullException(nameof(tcpEndpointOptions));
|
||||||
readonly MqttServerOptions _serverOptions;
|
_rootLogger = logger;
|
||||||
readonly MqttServerTcpEndpointBaseOptions _options;
|
_logger = logger.WithSource(nameof(MqttTcpServerListener));
|
||||||
readonly MqttServerTlsTcpEndpointOptions _tlsOptions;
|
|
||||||
|
|
||||||
CrossPlatformSocket _socket;
|
if (_options is MqttServerTlsTcpEndpointOptions tlsOptions)
|
||||||
IPEndPoint _localEndPoint;
|
|
||||||
|
|
||||||
public MqttTcpServerListener(
|
|
||||||
AddressFamily addressFamily,
|
|
||||||
MqttServerOptions serverOptions,
|
|
||||||
MqttServerTcpEndpointBaseOptions tcpEndpointOptions,
|
|
||||||
IMqttNetLogger logger)
|
|
||||||
{
|
{
|
||||||
_addressFamily = addressFamily;
|
_tlsOptions = tlsOptions;
|
||||||
_serverOptions = serverOptions ?? throw new ArgumentNullException(nameof(serverOptions));
|
|
||||||
_options = tcpEndpointOptions ?? throw new ArgumentNullException(nameof(tcpEndpointOptions));
|
|
||||||
_rootLogger = logger;
|
|
||||||
_logger = logger.WithSource(nameof(MqttTcpServerListener));
|
|
||||||
|
|
||||||
if (_options is MqttServerTlsTcpEndpointOptions tlsOptions)
|
|
||||||
{
|
|
||||||
_tlsOptions = tlsOptions;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public Func<IMqttChannelAdapter, Task> ClientHandler { get; set; }
|
public Func<IMqttChannelAdapter, Task> ClientHandler { get; set; }
|
||||||
|
|
||||||
public bool Start(bool treatErrorsAsWarning, CancellationToken cancellationToken)
|
public bool Start(bool treatErrorsAsWarning, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var boundIp = _options.BoundInterNetworkAddress;
|
||||||
|
if (_addressFamily == AddressFamily.InterNetworkV6)
|
||||||
|
{
|
||||||
|
boundIp = _options.BoundInterNetworkV6Address;
|
||||||
|
}
|
||||||
|
|
||||||
|
_localEndPoint = new IPEndPoint(boundIp, _options.Port);
|
||||||
|
|
||||||
|
_logger.Info("Starting TCP listener (Endpoint={0}, TLS={1})", _localEndPoint, _tlsOptions?.CertificateProvider != null);
|
||||||
|
|
||||||
|
_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)
|
||||||
|
{
|
||||||
|
_socket.ReuseAddress = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_options.NoDelay)
|
||||||
|
{
|
||||||
|
_socket.NoDelay = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_options.LingerState != null)
|
||||||
|
{
|
||||||
|
_socket.LingerState = _options.LingerState;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_options.KeepAlive.HasValue)
|
||||||
|
{
|
||||||
|
_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;
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
if (!treatErrorsAsWarning)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.Warning(exception, "Error while starting TCP listener (Endpoint={0})", _localEndPoint);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_socket?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task AcceptClientConnectionsAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
while (!cancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var boundIp = _options.BoundInterNetworkAddress;
|
var clientSocket = await _socket.AcceptAsync(cancellationToken).ConfigureAwait(false);
|
||||||
if (_addressFamily == AddressFamily.InterNetworkV6)
|
if (clientSocket == null)
|
||||||
{
|
{
|
||||||
boundIp = _options.BoundInterNetworkV6Address;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
_localEndPoint = new IPEndPoint(boundIp, _options.Port);
|
_ = Task.Factory.StartNew(() => TryHandleClientConnectionAsync(clientSocket), cancellationToken, TaskCreationOptions.PreferFairness, TaskScheduler.Default).ConfigureAwait(false);
|
||||||
|
}
|
||||||
_logger.Info("Starting TCP listener (Endpoint={0}, TLS={1})", _localEndPoint, _tlsOptions?.CertificateProvider != null);
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
_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)
|
|
||||||
{
|
|
||||||
_socket.ReuseAddress = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_options.NoDelay)
|
|
||||||
{
|
|
||||||
_socket.NoDelay = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_options.LingerState != null)
|
|
||||||
{
|
|
||||||
_socket.LingerState = _options.LingerState;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_options.KeepAlive.HasValue)
|
|
||||||
{
|
|
||||||
_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;
|
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
if (!treatErrorsAsWarning)
|
if (exception is SocketException socketException)
|
||||||
{
|
{
|
||||||
throw;
|
if (socketException.SocketErrorCode is SocketError.ConnectionAborted or SocketError.OperationAborted)
|
||||||
}
|
|
||||||
|
|
||||||
_logger.Warning(exception, "Error while starting TCP listener (Endpoint={0})", _localEndPoint);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
_socket?.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task AcceptClientConnectionsAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
while (!cancellationToken.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var clientSocket = await _socket.AcceptAsync(cancellationToken).ConfigureAwait(false);
|
|
||||||
if (clientSocket == null)
|
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_ = Task.Factory.StartNew(() => TryHandleClientConnectionAsync(clientSocket), cancellationToken, TaskCreationOptions.PreferFairness, TaskScheduler.Default).ConfigureAwait(false);
|
_logger.Error(exception, "Error while accepting TCP connection (Endpoint={0})", _localEndPoint);
|
||||||
}
|
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false);
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
catch (Exception exception)
|
|
||||||
{
|
|
||||||
if (exception is SocketException socketException)
|
|
||||||
{
|
|
||||||
if (socketException.SocketErrorCode == SocketError.ConnectionAborted ||
|
|
||||||
socketException.SocketErrorCode == SocketError.OperationAborted)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.Error(exception, "Error while accepting TCP connection (Endpoint={0})", _localEndPoint);
|
|
||||||
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
async Task TryHandleClientConnectionAsync(CrossPlatformSocket clientSocket)
|
|
||||||
{
|
async Task TryHandleClientConnectionAsync(CrossPlatformSocket clientSocket)
|
||||||
Stream stream = null;
|
{
|
||||||
EndPoint remoteEndPoint = null;
|
Stream stream = null;
|
||||||
|
EndPoint remoteEndPoint = null;
|
||||||
try
|
|
||||||
{
|
try
|
||||||
remoteEndPoint = clientSocket.RemoteEndPoint;
|
{
|
||||||
|
remoteEndPoint = clientSocket.RemoteEndPoint;
|
||||||
_logger.Verbose("TCP client '{0}' accepted (Local endpoint={1})", remoteEndPoint, _localEndPoint);
|
|
||||||
|
_logger.Verbose("TCP client '{0}' accepted (Local endpoint={1})", remoteEndPoint, _localEndPoint);
|
||||||
clientSocket.NoDelay = _options.NoDelay;
|
|
||||||
stream = clientSocket.GetStream();
|
clientSocket.NoDelay = _options.NoDelay;
|
||||||
var clientCertificate = _tlsOptions?.CertificateProvider?.GetCertificate();
|
stream = clientSocket.GetStream();
|
||||||
|
var clientCertificate = _tlsOptions?.CertificateProvider?.GetCertificate();
|
||||||
if (clientCertificate != null)
|
|
||||||
{
|
if (clientCertificate != null)
|
||||||
if (!clientCertificate.HasPrivateKey)
|
{
|
||||||
{
|
if (!clientCertificate.HasPrivateKey)
|
||||||
throw new InvalidOperationException("The certificate for TLS encryption must contain the private key.");
|
{
|
||||||
}
|
throw new InvalidOperationException("The certificate for TLS encryption must contain the private key.");
|
||||||
|
}
|
||||||
var sslStream = new SslStream(stream, false, _tlsOptions.RemoteCertificateValidationCallback);
|
|
||||||
|
var sslStream = new SslStream(stream, false, _tlsOptions.RemoteCertificateValidationCallback);
|
||||||
await sslStream.AuthenticateAsServerAsync(
|
|
||||||
new SslServerAuthenticationOptions
|
await sslStream.AuthenticateAsServerAsync(
|
||||||
{
|
new SslServerAuthenticationOptions
|
||||||
ServerCertificate = clientCertificate,
|
{
|
||||||
ClientCertificateRequired = _tlsOptions.ClientCertificateRequired,
|
ServerCertificate = clientCertificate,
|
||||||
EnabledSslProtocols = _tlsOptions.SslProtocol,
|
ClientCertificateRequired = _tlsOptions.ClientCertificateRequired,
|
||||||
CertificateRevocationCheckMode = _tlsOptions.CheckCertificateRevocation ? X509RevocationMode.Online : X509RevocationMode.NoCheck,
|
EnabledSslProtocols = _tlsOptions.SslProtocol,
|
||||||
EncryptionPolicy = EncryptionPolicy.RequireEncryption,
|
CertificateRevocationCheckMode = _tlsOptions.CheckCertificateRevocation ? X509RevocationMode.Online : X509RevocationMode.NoCheck,
|
||||||
CipherSuitesPolicy = _tlsOptions.CipherSuitesPolicy
|
EncryptionPolicy = EncryptionPolicy.RequireEncryption,
|
||||||
}).ConfigureAwait(false);
|
CipherSuitesPolicy = _tlsOptions.CipherSuitesPolicy
|
||||||
|
}).ConfigureAwait(false);
|
||||||
stream = sslStream;
|
|
||||||
|
stream = sslStream;
|
||||||
clientCertificate = sslStream.RemoteCertificate as X509Certificate2;
|
|
||||||
|
clientCertificate = sslStream.RemoteCertificate as X509Certificate2;
|
||||||
if (clientCertificate == null && sslStream.RemoteCertificate != null)
|
|
||||||
{
|
if (clientCertificate == null && sslStream.RemoteCertificate != null)
|
||||||
clientCertificate = new X509Certificate2(sslStream.RemoteCertificate.Export(X509ContentType.Cert));
|
{
|
||||||
}
|
clientCertificate = new X509Certificate2(sslStream.RemoteCertificate.Export(X509ContentType.Cert));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
var clientHandler = ClientHandler;
|
|
||||||
if (clientHandler != null)
|
var clientHandler = ClientHandler;
|
||||||
{
|
if (clientHandler != null)
|
||||||
var tcpChannel = new MqttTcpChannel(stream, remoteEndPoint, clientCertificate);
|
{
|
||||||
var bufferWriter = new MqttBufferWriter(_serverOptions.WriterBufferSize, _serverOptions.WriterBufferSizeMax);
|
var tcpChannel = new MqttTcpChannel(stream, remoteEndPoint, clientCertificate);
|
||||||
var packetFormatterAdapter = new MqttPacketFormatterAdapter(bufferWriter);
|
var bufferWriter = new MqttBufferWriter(_serverOptions.WriterBufferSize, _serverOptions.WriterBufferSizeMax);
|
||||||
|
var packetFormatterAdapter = new MqttPacketFormatterAdapter(bufferWriter);
|
||||||
using (var clientAdapter = new MqttChannelAdapter(tcpChannel, packetFormatterAdapter, _rootLogger))
|
|
||||||
{
|
using var clientAdapter = new MqttChannelAdapter(tcpChannel, packetFormatterAdapter, _rootLogger);
|
||||||
clientAdapter.AllowPacketFragmentation = _options.AllowPacketFragmentation;
|
clientAdapter.AllowPacketFragmentation = _options.AllowPacketFragmentation;
|
||||||
await clientHandler(clientAdapter).ConfigureAwait(false);
|
await clientHandler(clientAdapter).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
catch (Exception exception)
|
||||||
catch (Exception exception)
|
{
|
||||||
{
|
if (exception is ObjectDisposedException)
|
||||||
if (exception is ObjectDisposedException)
|
{
|
||||||
{
|
// It can happen that the listener socket is accessed after the cancellation token is already set and the listener socket is disposed.
|
||||||
// It can happen that the listener socket is accessed after the cancellation token is already set and the listener socket is disposed.
|
return;
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
if (exception is SocketException socketException &&
|
||||||
if (exception is SocketException socketException &&
|
socketException.SocketErrorCode == SocketError.OperationAborted)
|
||||||
socketException.SocketErrorCode == SocketError.OperationAborted)
|
{
|
||||||
{
|
return;
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
_logger.Error(exception, "Error while handling TCP client connection");
|
||||||
_logger.Error(exception, "Error while handling TCP client connection");
|
}
|
||||||
}
|
finally
|
||||||
finally
|
{
|
||||||
{
|
try
|
||||||
try
|
{
|
||||||
{
|
if (stream != null)
|
||||||
// ReSharper disable once MethodHasAsyncOverload
|
{
|
||||||
stream?.Dispose();
|
await stream.DisposeAsync().ConfigureAwait(false);
|
||||||
clientSocket?.Dispose();
|
}
|
||||||
}
|
|
||||||
catch (Exception disposeException)
|
clientSocket?.Dispose();
|
||||||
{
|
}
|
||||||
_logger.Error(disposeException, "Error while cleaning up client connection");
|
catch (Exception disposeException)
|
||||||
}
|
{
|
||||||
}
|
_logger.Error(disposeException, "Error while cleaning up client connection");
|
||||||
|
}
|
||||||
_logger.Verbose("TCP client '{0}' disconnected (Local endpoint={1})", remoteEndPoint, _localEndPoint);
|
}
|
||||||
}
|
|
||||||
|
_logger.Verbose("TCP client '{0}' disconnected (Local endpoint={1})", remoteEndPoint, _localEndPoint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,18 +4,17 @@
|
|||||||
|
|
||||||
using MQTTnet.Protocol;
|
using MQTTnet.Protocol;
|
||||||
|
|
||||||
namespace MQTTnet.Server.Internal
|
namespace MQTTnet.Server.Internal;
|
||||||
|
|
||||||
|
public sealed class CheckSubscriptionsResult
|
||||||
{
|
{
|
||||||
public sealed class CheckSubscriptionsResult
|
public static CheckSubscriptionsResult NotSubscribed { get; } = new CheckSubscriptionsResult();
|
||||||
{
|
|
||||||
public static CheckSubscriptionsResult NotSubscribed { get; } = new CheckSubscriptionsResult();
|
|
||||||
|
|
||||||
public bool IsSubscribed { get; set; }
|
public bool IsSubscribed { get; set; }
|
||||||
|
|
||||||
public bool RetainAsPublished { get; set; }
|
public bool RetainAsPublished { get; set; }
|
||||||
|
|
||||||
public List<uint> SubscriptionIdentifiers { get; set; }
|
public List<uint> SubscriptionIdentifiers { get; set; }
|
||||||
|
|
||||||
public MqttQualityOfServiceLevel QualityOfServiceLevel { get; set; }
|
public MqttQualityOfServiceLevel QualityOfServiceLevel { get; set; }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -4,24 +4,23 @@
|
|||||||
|
|
||||||
using MQTTnet.Packets;
|
using MQTTnet.Packets;
|
||||||
|
|
||||||
namespace MQTTnet.Server.Internal
|
namespace MQTTnet.Server.Internal;
|
||||||
|
|
||||||
|
public sealed class DispatchApplicationMessageResult
|
||||||
{
|
{
|
||||||
public sealed class DispatchApplicationMessageResult
|
public DispatchApplicationMessageResult(int reasonCode, bool closeConnection, string reasonString, List<MqttUserProperty> userProperties)
|
||||||
{
|
{
|
||||||
public DispatchApplicationMessageResult(int reasonCode, bool closeConnection, string reasonString, List<MqttUserProperty> userProperties)
|
ReasonCode = reasonCode;
|
||||||
{
|
CloseConnection = closeConnection;
|
||||||
ReasonCode = reasonCode;
|
ReasonString = reasonString;
|
||||||
CloseConnection = closeConnection;
|
UserProperties = userProperties;
|
||||||
ReasonString = reasonString;
|
|
||||||
UserProperties = userProperties;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool CloseConnection { get; }
|
|
||||||
|
|
||||||
public int ReasonCode { get; }
|
|
||||||
|
|
||||||
public string ReasonString { get; }
|
|
||||||
|
|
||||||
public List<MqttUserProperty> UserProperties { get; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool CloseConnection { get; }
|
||||||
|
|
||||||
|
public int ReasonCode { get; }
|
||||||
|
|
||||||
|
public string ReasonString { get; }
|
||||||
|
|
||||||
|
public List<MqttUserProperty> UserProperties { get; }
|
||||||
}
|
}
|
||||||
@@ -2,11 +2,10 @@
|
|||||||
// The .NET Foundation licenses this file to you under the MIT license.
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
namespace MQTTnet.Server.Internal
|
namespace MQTTnet.Server.Internal;
|
||||||
|
|
||||||
|
public enum EnqueueDataPacketResult
|
||||||
{
|
{
|
||||||
public enum EnqueueDataPacketResult
|
Enqueued,
|
||||||
{
|
Dropped
|
||||||
Enqueued,
|
|
||||||
Dropped
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -4,24 +4,23 @@
|
|||||||
|
|
||||||
using MQTTnet.Packets;
|
using MQTTnet.Packets;
|
||||||
|
|
||||||
namespace MQTTnet.Server.Internal.Formatter
|
namespace MQTTnet.Server.Internal.Formatter;
|
||||||
|
|
||||||
|
public static class MqttUnsubAckPacketFactory
|
||||||
{
|
{
|
||||||
public static class MqttUnsubAckPacketFactory
|
public static MqttUnsubAckPacket Create(MqttUnsubscribePacket unsubscribePacket, UnsubscribeResult unsubscribeResult)
|
||||||
{
|
{
|
||||||
public static MqttUnsubAckPacket Create(MqttUnsubscribePacket unsubscribePacket, UnsubscribeResult unsubscribeResult)
|
ArgumentNullException.ThrowIfNull(unsubscribePacket);
|
||||||
|
ArgumentNullException.ThrowIfNull(unsubscribeResult);
|
||||||
|
|
||||||
|
var unsubAckPacket = new MqttUnsubAckPacket
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(unsubscribePacket);
|
PacketIdentifier = unsubscribePacket.PacketIdentifier
|
||||||
ArgumentNullException.ThrowIfNull(unsubscribeResult);
|
};
|
||||||
|
|
||||||
var unsubAckPacket = new MqttUnsubAckPacket
|
// MQTTv5.0.0 only.
|
||||||
{
|
unsubAckPacket.ReasonCodes = unsubscribeResult.ReasonCodes;
|
||||||
PacketIdentifier = unsubscribePacket.PacketIdentifier
|
|
||||||
};
|
|
||||||
|
|
||||||
// MQTTv5.0.0 only.
|
return unsubAckPacket;
|
||||||
unsubAckPacket.ReasonCodes = unsubscribeResult.ReasonCodes;
|
|
||||||
|
|
||||||
return unsubAckPacket;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
namespace MQTTnet.Server.Internal
|
namespace MQTTnet.Server.Internal;
|
||||||
{
|
|
||||||
public interface ISubscriptionChangedNotification
|
|
||||||
{
|
|
||||||
void OnSubscriptionsAdded(MqttSession clientSession, List<string> subscriptionsTopics);
|
|
||||||
|
|
||||||
void OnSubscriptionsRemoved(MqttSession clientSession, List<string> subscriptionTopics);
|
public interface ISubscriptionChangedNotification
|
||||||
}
|
{
|
||||||
|
void OnSubscriptionsAdded(MqttSession clientSession, List<string> subscriptionsTopics);
|
||||||
|
|
||||||
|
void OnSubscriptionsRemoved(MqttSession clientSession, List<string> subscriptionTopics);
|
||||||
}
|
}
|
||||||
@@ -439,10 +439,8 @@ public sealed class MqttClientSessionsManager : ISubscriptionChangedNotification
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
using (var timeout = new CancellationTokenSource(_options.DefaultCommunicationTimeout))
|
using var timeout = new CancellationTokenSource(_options.DefaultCommunicationTimeout);
|
||||||
{
|
await channelAdapter.DisconnectAsync(timeout.Token).ConfigureAwait(false);
|
||||||
await channelAdapter.DisconnectAsync(timeout.Token).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -673,14 +671,12 @@ public sealed class MqttClientSessionsManager : ISubscriptionChangedNotification
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using (var timeoutToken = new CancellationTokenSource(_options.DefaultCommunicationTimeout))
|
using var timeoutToken = new CancellationTokenSource(_options.DefaultCommunicationTimeout);
|
||||||
using (var effectiveCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(timeoutToken.Token, cancellationToken))
|
using var effectiveCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(timeoutToken.Token, cancellationToken);
|
||||||
|
var firstPacket = await channelAdapter.ReceivePacketAsync(effectiveCancellationToken.Token).ConfigureAwait(false);
|
||||||
|
if (firstPacket is MqttConnectPacket connectPacket)
|
||||||
{
|
{
|
||||||
var firstPacket = await channelAdapter.ReceivePacketAsync(effectiveCancellationToken.Token).ConfigureAwait(false);
|
return connectPacket;
|
||||||
if (firstPacket is MqttConnectPacket connectPacket)
|
|
||||||
{
|
|
||||||
return connectPacket;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
|
|||||||
@@ -4,101 +4,100 @@
|
|||||||
|
|
||||||
using MQTTnet.Packets;
|
using MQTTnet.Packets;
|
||||||
|
|
||||||
namespace MQTTnet.Server.Internal
|
namespace MQTTnet.Server.Internal;
|
||||||
|
|
||||||
|
public sealed class MqttClientStatistics
|
||||||
{
|
{
|
||||||
public sealed class MqttClientStatistics
|
Statistics _statistics = new Statistics // mutable struct, don't make readonly!
|
||||||
{
|
{
|
||||||
Statistics _statistics = new Statistics // mutable struct, don't make readonly!
|
// Start with 1 because the CONNACK packet is not counted here.
|
||||||
|
_receivedPacketsCount = 1,
|
||||||
|
|
||||||
|
// Start with 1 because the CONNECT packet is not counted here.
|
||||||
|
_sentPacketsCount = 1
|
||||||
|
};
|
||||||
|
|
||||||
|
public MqttClientStatistics()
|
||||||
|
{
|
||||||
|
ConnectedTimestamp = DateTime.UtcNow;
|
||||||
|
|
||||||
|
LastPacketReceivedTimestamp = ConnectedTimestamp;
|
||||||
|
LastPacketSentTimestamp = ConnectedTimestamp;
|
||||||
|
|
||||||
|
LastNonKeepAlivePacketReceivedTimestamp = ConnectedTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DateTime ConnectedTimestamp { get; }
|
||||||
|
public DateTime LastNonKeepAlivePacketReceivedTimestamp { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Timestamp of the last package that has been sent to the client ("received" from the client's perspective)
|
||||||
|
/// </summary>
|
||||||
|
public DateTime LastPacketReceivedTimestamp { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Timestamp of the last package that has been received from the client ("sent" from the client's perspective)
|
||||||
|
/// </summary>
|
||||||
|
public DateTime LastPacketSentTimestamp { get; private set; }
|
||||||
|
|
||||||
|
public long SentApplicationMessagesCount => Volatile.Read(ref _statistics._sentApplicationMessagesCount);
|
||||||
|
|
||||||
|
public long ReceivedApplicationMessagesCount => Volatile.Read(ref _statistics._receivedApplicationMessagesCount);
|
||||||
|
|
||||||
|
public long SentPacketsCount => Volatile.Read(ref _statistics._sentPacketsCount);
|
||||||
|
|
||||||
|
public long ReceivedPacketsCount => Volatile.Read(ref _statistics._receivedPacketsCount);
|
||||||
|
|
||||||
|
public void HandleReceivedPacket(MqttPacket packet)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(packet);
|
||||||
|
|
||||||
|
// This class is tracking all values from Clients perspective!
|
||||||
|
LastPacketSentTimestamp = DateTime.UtcNow;
|
||||||
|
|
||||||
|
Interlocked.Increment(ref _statistics._sentPacketsCount);
|
||||||
|
|
||||||
|
if (packet is MqttPublishPacket)
|
||||||
{
|
{
|
||||||
// Start with 1 because the CONNACK packet is not counted here.
|
Interlocked.Increment(ref _statistics._sentApplicationMessagesCount);
|
||||||
_receivedPacketsCount = 1,
|
|
||||||
|
|
||||||
// Start with 1 because the CONNECT packet is not counted here.
|
|
||||||
_sentPacketsCount = 1
|
|
||||||
};
|
|
||||||
|
|
||||||
public MqttClientStatistics()
|
|
||||||
{
|
|
||||||
ConnectedTimestamp = DateTime.UtcNow;
|
|
||||||
|
|
||||||
LastPacketReceivedTimestamp = ConnectedTimestamp;
|
|
||||||
LastPacketSentTimestamp = ConnectedTimestamp;
|
|
||||||
|
|
||||||
LastNonKeepAlivePacketReceivedTimestamp = ConnectedTimestamp;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public DateTime ConnectedTimestamp { get; }
|
if (!(packet is MqttPingReqPacket || packet is MqttPingRespPacket))
|
||||||
public DateTime LastNonKeepAlivePacketReceivedTimestamp { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Timestamp of the last package that has been sent to the client ("received" from the client's perspective)
|
|
||||||
/// </summary>
|
|
||||||
public DateTime LastPacketReceivedTimestamp { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Timestamp of the last package that has been received from the client ("sent" from the client's perspective)
|
|
||||||
/// </summary>
|
|
||||||
public DateTime LastPacketSentTimestamp { get; private set; }
|
|
||||||
|
|
||||||
public long SentApplicationMessagesCount => Volatile.Read(ref _statistics._sentApplicationMessagesCount);
|
|
||||||
|
|
||||||
public long ReceivedApplicationMessagesCount => Volatile.Read(ref _statistics._receivedApplicationMessagesCount);
|
|
||||||
|
|
||||||
public long SentPacketsCount => Volatile.Read(ref _statistics._sentPacketsCount);
|
|
||||||
|
|
||||||
public long ReceivedPacketsCount => Volatile.Read(ref _statistics._receivedPacketsCount);
|
|
||||||
|
|
||||||
public void HandleReceivedPacket(MqttPacket packet)
|
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(packet);
|
LastNonKeepAlivePacketReceivedTimestamp = LastPacketReceivedTimestamp;
|
||||||
|
|
||||||
// This class is tracking all values from Clients perspective!
|
|
||||||
LastPacketSentTimestamp = DateTime.UtcNow;
|
|
||||||
|
|
||||||
Interlocked.Increment(ref _statistics._sentPacketsCount);
|
|
||||||
|
|
||||||
if (packet is MqttPublishPacket)
|
|
||||||
{
|
|
||||||
Interlocked.Increment(ref _statistics._sentApplicationMessagesCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(packet is MqttPingReqPacket || packet is MqttPingRespPacket))
|
|
||||||
{
|
|
||||||
LastNonKeepAlivePacketReceivedTimestamp = LastPacketReceivedTimestamp;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
public void ResetStatistics() => _statistics.Reset();
|
}
|
||||||
|
public void ResetStatistics() => _statistics.Reset();
|
||||||
|
|
||||||
public void HandleSentPacket(MqttPacket packet)
|
public void HandleSentPacket(MqttPacket packet)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(packet);
|
||||||
|
|
||||||
|
// This class is tracking all values from Clients perspective!
|
||||||
|
LastPacketReceivedTimestamp = DateTime.UtcNow;
|
||||||
|
|
||||||
|
Interlocked.Increment(ref _statistics._receivedPacketsCount);
|
||||||
|
|
||||||
|
if (packet is MqttPublishPacket)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(packet);
|
Interlocked.Increment(ref _statistics._receivedApplicationMessagesCount);
|
||||||
|
|
||||||
// This class is tracking all values from Clients perspective!
|
|
||||||
LastPacketReceivedTimestamp = DateTime.UtcNow;
|
|
||||||
|
|
||||||
Interlocked.Increment(ref _statistics._receivedPacketsCount);
|
|
||||||
|
|
||||||
if (packet is MqttPublishPacket)
|
|
||||||
{
|
|
||||||
Interlocked.Increment(ref _statistics._receivedApplicationMessagesCount);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct Statistics
|
struct Statistics
|
||||||
|
{
|
||||||
|
public long _receivedPacketsCount;
|
||||||
|
public long _sentPacketsCount;
|
||||||
|
|
||||||
|
public long _receivedApplicationMessagesCount;
|
||||||
|
public long _sentApplicationMessagesCount;
|
||||||
|
|
||||||
|
public void Reset()
|
||||||
{
|
{
|
||||||
public long _receivedPacketsCount;
|
Volatile.Write(ref _receivedPacketsCount, 0);
|
||||||
public long _sentPacketsCount;
|
Volatile.Write(ref _sentPacketsCount, 0);
|
||||||
|
Volatile.Write(ref _receivedApplicationMessagesCount, 0);
|
||||||
public long _receivedApplicationMessagesCount;
|
Volatile.Write(ref _sentApplicationMessagesCount, 0);
|
||||||
public long _sentApplicationMessagesCount;
|
|
||||||
|
|
||||||
public void Reset()
|
|
||||||
{
|
|
||||||
Volatile.Write(ref _receivedPacketsCount, 0);
|
|
||||||
Volatile.Write(ref _sentPacketsCount, 0);
|
|
||||||
Volatile.Write(ref _receivedApplicationMessagesCount, 0);
|
|
||||||
Volatile.Write(ref _sentApplicationMessagesCount, 0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,180 +5,237 @@
|
|||||||
using MQTTnet.Packets;
|
using MQTTnet.Packets;
|
||||||
using MQTTnet.Protocol;
|
using MQTTnet.Protocol;
|
||||||
|
|
||||||
namespace MQTTnet.Server.Internal
|
namespace MQTTnet.Server.Internal;
|
||||||
|
|
||||||
|
public sealed class MqttClientSubscriptionsManager : IDisposable
|
||||||
{
|
{
|
||||||
public sealed class MqttClientSubscriptionsManager : IDisposable
|
static readonly List<uint> EmptySubscriptionIdentifiers = new List<uint>();
|
||||||
|
|
||||||
|
readonly MqttServerEventContainer _eventContainer;
|
||||||
|
readonly Dictionary<ulong, HashSet<MqttSubscription>> _noWildcardSubscriptionsByTopicHash = new Dictionary<ulong, HashSet<MqttSubscription>>();
|
||||||
|
readonly MqttRetainedMessagesManager _retainedMessagesManager;
|
||||||
|
|
||||||
|
readonly MqttSession _session;
|
||||||
|
|
||||||
|
// Callback to maintain list of subscriber clients
|
||||||
|
readonly ISubscriptionChangedNotification _subscriptionChangedNotification;
|
||||||
|
|
||||||
|
// Subscriptions are stored in various dictionaries and use a "topic hash"; see the MqttSubscription object for a detailed explanation.
|
||||||
|
// The additional lock is important to coordinate complex update logic with multiple steps, checks and interceptors.
|
||||||
|
readonly Dictionary<string, MqttSubscription> _subscriptions = new Dictionary<string, MqttSubscription>();
|
||||||
|
|
||||||
|
// Use subscription lock to maintain consistency across subscriptions and topic hash dictionaries
|
||||||
|
readonly ReaderWriterLockSlim _subscriptionsLock = new ReaderWriterLockSlim();
|
||||||
|
readonly Dictionary<ulong, TopicHashMaskSubscriptions> _wildcardSubscriptionsByTopicHash = new Dictionary<ulong, TopicHashMaskSubscriptions>();
|
||||||
|
|
||||||
|
public MqttClientSubscriptionsManager(
|
||||||
|
MqttSession session,
|
||||||
|
MqttServerEventContainer eventContainer,
|
||||||
|
MqttRetainedMessagesManager retainedMessagesManager,
|
||||||
|
ISubscriptionChangedNotification subscriptionChangedNotification)
|
||||||
{
|
{
|
||||||
static readonly List<uint> EmptySubscriptionIdentifiers = new List<uint>();
|
_session = session ?? throw new ArgumentNullException(nameof(session));
|
||||||
|
_eventContainer = eventContainer ?? throw new ArgumentNullException(nameof(eventContainer));
|
||||||
|
_retainedMessagesManager = retainedMessagesManager ?? throw new ArgumentNullException(nameof(retainedMessagesManager));
|
||||||
|
_subscriptionChangedNotification = subscriptionChangedNotification;
|
||||||
|
}
|
||||||
|
|
||||||
readonly MqttServerEventContainer _eventContainer;
|
public CheckSubscriptionsResult CheckSubscriptions(string topic, ulong topicHash, MqttQualityOfServiceLevel qualityOfServiceLevel, string senderId)
|
||||||
readonly Dictionary<ulong, HashSet<MqttSubscription>> _noWildcardSubscriptionsByTopicHash = new Dictionary<ulong, HashSet<MqttSubscription>>();
|
{
|
||||||
readonly MqttRetainedMessagesManager _retainedMessagesManager;
|
var possibleSubscriptions = new List<MqttSubscription>();
|
||||||
|
|
||||||
readonly MqttSession _session;
|
// Check for possible subscriptions. They might have collisions but this is fine.
|
||||||
|
_subscriptionsLock.EnterReadLock();
|
||||||
// Callback to maintain list of subscriber clients
|
try
|
||||||
readonly ISubscriptionChangedNotification _subscriptionChangedNotification;
|
|
||||||
|
|
||||||
// Subscriptions are stored in various dictionaries and use a "topic hash"; see the MqttSubscription object for a detailed explanation.
|
|
||||||
// The additional lock is important to coordinate complex update logic with multiple steps, checks and interceptors.
|
|
||||||
readonly Dictionary<string, MqttSubscription> _subscriptions = new Dictionary<string, MqttSubscription>();
|
|
||||||
|
|
||||||
// Use subscription lock to maintain consistency across subscriptions and topic hash dictionaries
|
|
||||||
readonly ReaderWriterLockSlim _subscriptionsLock = new ReaderWriterLockSlim();
|
|
||||||
readonly Dictionary<ulong, TopicHashMaskSubscriptions> _wildcardSubscriptionsByTopicHash = new Dictionary<ulong, TopicHashMaskSubscriptions>();
|
|
||||||
|
|
||||||
public MqttClientSubscriptionsManager(
|
|
||||||
MqttSession session,
|
|
||||||
MqttServerEventContainer eventContainer,
|
|
||||||
MqttRetainedMessagesManager retainedMessagesManager,
|
|
||||||
ISubscriptionChangedNotification subscriptionChangedNotification)
|
|
||||||
{
|
{
|
||||||
_session = session ?? throw new ArgumentNullException(nameof(session));
|
if (_noWildcardSubscriptionsByTopicHash.TryGetValue(topicHash, out var noWildcardSubscriptions))
|
||||||
_eventContainer = eventContainer ?? throw new ArgumentNullException(nameof(eventContainer));
|
|
||||||
_retainedMessagesManager = retainedMessagesManager ?? throw new ArgumentNullException(nameof(retainedMessagesManager));
|
|
||||||
_subscriptionChangedNotification = subscriptionChangedNotification;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CheckSubscriptionsResult CheckSubscriptions(string topic, ulong topicHash, MqttQualityOfServiceLevel qualityOfServiceLevel, string senderId)
|
|
||||||
{
|
|
||||||
var possibleSubscriptions = new List<MqttSubscription>();
|
|
||||||
|
|
||||||
// Check for possible subscriptions. They might have collisions but this is fine.
|
|
||||||
_subscriptionsLock.EnterReadLock();
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
if (_noWildcardSubscriptionsByTopicHash.TryGetValue(topicHash, out var noWildcardSubscriptions))
|
possibleSubscriptions.AddRange(noWildcardSubscriptions);
|
||||||
{
|
}
|
||||||
possibleSubscriptions.AddRange(noWildcardSubscriptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var wcs in _wildcardSubscriptionsByTopicHash)
|
foreach (var wcs in _wildcardSubscriptionsByTopicHash)
|
||||||
|
{
|
||||||
|
var subscriptionHash = wcs.Key;
|
||||||
|
var subscriptionsByHashMask = wcs.Value.SubscriptionsByHashMask;
|
||||||
|
foreach (var shm in subscriptionsByHashMask)
|
||||||
{
|
{
|
||||||
var subscriptionHash = wcs.Key;
|
var subscriptionHashMask = shm.Key;
|
||||||
var subscriptionsByHashMask = wcs.Value.SubscriptionsByHashMask;
|
if ((topicHash & subscriptionHashMask) == subscriptionHash)
|
||||||
foreach (var shm in subscriptionsByHashMask)
|
|
||||||
{
|
{
|
||||||
var subscriptionHashMask = shm.Key;
|
var subscriptions = shm.Value;
|
||||||
if ((topicHash & subscriptionHashMask) == subscriptionHash)
|
possibleSubscriptions.AddRange(subscriptions);
|
||||||
{
|
|
||||||
var subscriptions = shm.Value;
|
|
||||||
possibleSubscriptions.AddRange(subscriptions);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
}
|
||||||
{
|
finally
|
||||||
_subscriptionsLock.ExitReadLock();
|
{
|
||||||
}
|
_subscriptionsLock.ExitReadLock();
|
||||||
|
|
||||||
// The pre check has evaluated that nothing is subscribed.
|
|
||||||
// If there were some possible candidates they get checked below
|
|
||||||
// again to avoid collisions.
|
|
||||||
if (possibleSubscriptions.Count == 0)
|
|
||||||
{
|
|
||||||
return CheckSubscriptionsResult.NotSubscribed;
|
|
||||||
}
|
|
||||||
|
|
||||||
var senderIsReceiver = string.Equals(senderId, _session.Id);
|
|
||||||
var maxQoSLevel = -1; // Not subscribed.
|
|
||||||
|
|
||||||
HashSet<uint> subscriptionIdentifiers = null;
|
|
||||||
var retainAsPublished = false;
|
|
||||||
|
|
||||||
foreach (var subscription in possibleSubscriptions)
|
|
||||||
{
|
|
||||||
if (subscription.NoLocal && senderIsReceiver)
|
|
||||||
{
|
|
||||||
// This is a MQTTv5 feature!
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (MqttTopicFilterComparer.Compare(topic, subscription.Topic) != MqttTopicFilterCompareResult.IsMatch)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (subscription.RetainAsPublished)
|
|
||||||
{
|
|
||||||
// This is a MQTTv5 feature!
|
|
||||||
retainAsPublished = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((int)subscription.GrantedQualityOfServiceLevel > maxQoSLevel)
|
|
||||||
{
|
|
||||||
maxQoSLevel = (int)subscription.GrantedQualityOfServiceLevel;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (subscription.Identifier > 0)
|
|
||||||
{
|
|
||||||
if (subscriptionIdentifiers == null)
|
|
||||||
{
|
|
||||||
subscriptionIdentifiers = new HashSet<uint>();
|
|
||||||
}
|
|
||||||
|
|
||||||
subscriptionIdentifiers.Add(subscription.Identifier);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (maxQoSLevel == -1)
|
|
||||||
{
|
|
||||||
return CheckSubscriptionsResult.NotSubscribed;
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = new CheckSubscriptionsResult
|
|
||||||
{
|
|
||||||
IsSubscribed = true,
|
|
||||||
RetainAsPublished = retainAsPublished,
|
|
||||||
SubscriptionIdentifiers = subscriptionIdentifiers?.ToList() ?? EmptySubscriptionIdentifiers,
|
|
||||||
|
|
||||||
// Start with the same QoS as the publisher.
|
|
||||||
QualityOfServiceLevel = qualityOfServiceLevel
|
|
||||||
};
|
|
||||||
|
|
||||||
// Now downgrade if required.
|
|
||||||
//
|
|
||||||
// If a subscribing Client has been granted maximum QoS 1 for a particular Topic Filter, then a QoS 0 Application Message matching the filter is delivered
|
|
||||||
// to the Client at QoS 0. This means that at most one copy of the message is received by the Client. On the other hand, a QoS 2 Message published to
|
|
||||||
// the same topic is downgraded by the Server to QoS 1 for delivery to the Client, so that Client might receive duplicate copies of the Message.
|
|
||||||
|
|
||||||
// Subscribing to a Topic Filter at QoS 2 is equivalent to saying "I would like to receive Messages matching this filter at the QoS with which they were published".
|
|
||||||
// This means a publisher is responsible for determining the maximum QoS a Message can be delivered at, but a subscriber is able to require that the Server
|
|
||||||
// downgrades the QoS to one more suitable for its usage.
|
|
||||||
if (maxQoSLevel < (int)qualityOfServiceLevel)
|
|
||||||
{
|
|
||||||
result.QualityOfServiceLevel = (MqttQualityOfServiceLevel)maxQoSLevel;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
// The pre check has evaluated that nothing is subscribed.
|
||||||
|
// If there were some possible candidates they get checked below
|
||||||
|
// again to avoid collisions.
|
||||||
|
if (possibleSubscriptions.Count == 0)
|
||||||
{
|
{
|
||||||
_subscriptionsLock?.Dispose();
|
return CheckSubscriptionsResult.NotSubscribed;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<SubscribeResult> Subscribe(MqttSubscribePacket subscribePacket, CancellationToken cancellationToken)
|
var senderIsReceiver = string.Equals(senderId, _session.Id);
|
||||||
|
var maxQoSLevel = -1; // Not subscribed.
|
||||||
|
|
||||||
|
HashSet<uint> subscriptionIdentifiers = null;
|
||||||
|
var retainAsPublished = false;
|
||||||
|
|
||||||
|
foreach (var subscription in possibleSubscriptions)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(subscribePacket);
|
if (subscription.NoLocal && senderIsReceiver)
|
||||||
|
|
||||||
var retainedApplicationMessages = await _retainedMessagesManager.GetMessages().ConfigureAwait(false);
|
|
||||||
var result = new SubscribeResult(subscribePacket.TopicFilters.Count);
|
|
||||||
|
|
||||||
var addedSubscriptions = new List<string>();
|
|
||||||
var finalTopicFilters = new List<MqttTopicFilter>();
|
|
||||||
|
|
||||||
// The topic filters are order by its QoS so that the higher QoS will win over a
|
|
||||||
// lower one.
|
|
||||||
foreach (var topicFilterItem in subscribePacket.TopicFilters.OrderByDescending(f => f.QualityOfServiceLevel))
|
|
||||||
{
|
{
|
||||||
var interceptorEventArgs = await InterceptSubscribe(topicFilterItem, subscribePacket.UserProperties, cancellationToken).ConfigureAwait(false);
|
// This is a MQTTv5 feature!
|
||||||
var topicFilter = interceptorEventArgs.TopicFilter;
|
continue;
|
||||||
var processSubscription = interceptorEventArgs.ProcessSubscription && interceptorEventArgs.Response.ReasonCode <= MqttSubscribeReasonCode.GrantedQoS2;
|
}
|
||||||
|
|
||||||
|
if (MqttTopicFilterComparer.Compare(topic, subscription.Topic) != MqttTopicFilterCompareResult.IsMatch)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subscription.RetainAsPublished)
|
||||||
|
{
|
||||||
|
// This is a MQTTv5 feature!
|
||||||
|
retainAsPublished = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((int)subscription.GrantedQualityOfServiceLevel > maxQoSLevel)
|
||||||
|
{
|
||||||
|
maxQoSLevel = (int)subscription.GrantedQualityOfServiceLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subscription.Identifier > 0)
|
||||||
|
{
|
||||||
|
if (subscriptionIdentifiers == null)
|
||||||
|
{
|
||||||
|
subscriptionIdentifiers = new HashSet<uint>();
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriptionIdentifiers.Add(subscription.Identifier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxQoSLevel == -1)
|
||||||
|
{
|
||||||
|
return CheckSubscriptionsResult.NotSubscribed;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = new CheckSubscriptionsResult
|
||||||
|
{
|
||||||
|
IsSubscribed = true,
|
||||||
|
RetainAsPublished = retainAsPublished,
|
||||||
|
SubscriptionIdentifiers = subscriptionIdentifiers?.ToList() ?? EmptySubscriptionIdentifiers,
|
||||||
|
|
||||||
|
// Start with the same QoS as the publisher.
|
||||||
|
QualityOfServiceLevel = qualityOfServiceLevel
|
||||||
|
};
|
||||||
|
|
||||||
|
// Now downgrade if required.
|
||||||
|
//
|
||||||
|
// If a subscribing Client has been granted maximum QoS 1 for a particular Topic Filter, then a QoS 0 Application Message matching the filter is delivered
|
||||||
|
// to the Client at QoS 0. This means that at most one copy of the message is received by the Client. On the other hand, a QoS 2 Message published to
|
||||||
|
// the same topic is downgraded by the Server to QoS 1 for delivery to the Client, so that Client might receive duplicate copies of the Message.
|
||||||
|
|
||||||
|
// Subscribing to a Topic Filter at QoS 2 is equivalent to saying "I would like to receive Messages matching this filter at the QoS with which they were published".
|
||||||
|
// This means a publisher is responsible for determining the maximum QoS a Message can be delivered at, but a subscriber is able to require that the Server
|
||||||
|
// downgrades the QoS to one more suitable for its usage.
|
||||||
|
if (maxQoSLevel < (int)qualityOfServiceLevel)
|
||||||
|
{
|
||||||
|
result.QualityOfServiceLevel = (MqttQualityOfServiceLevel)maxQoSLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_subscriptionsLock?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SubscribeResult> Subscribe(MqttSubscribePacket subscribePacket, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(subscribePacket);
|
||||||
|
|
||||||
|
var retainedApplicationMessages = await _retainedMessagesManager.GetMessages().ConfigureAwait(false);
|
||||||
|
var result = new SubscribeResult(subscribePacket.TopicFilters.Count);
|
||||||
|
|
||||||
|
var addedSubscriptions = new List<string>();
|
||||||
|
var finalTopicFilters = new List<MqttTopicFilter>();
|
||||||
|
|
||||||
|
// The topic filters are order by its QoS so that the higher QoS will win over a
|
||||||
|
// lower one.
|
||||||
|
foreach (var topicFilterItem in subscribePacket.TopicFilters.OrderByDescending(f => f.QualityOfServiceLevel))
|
||||||
|
{
|
||||||
|
var interceptorEventArgs = await InterceptSubscribe(topicFilterItem, subscribePacket.UserProperties, cancellationToken).ConfigureAwait(false);
|
||||||
|
var topicFilter = interceptorEventArgs.TopicFilter;
|
||||||
|
var processSubscription = interceptorEventArgs.ProcessSubscription && interceptorEventArgs.Response.ReasonCode <= MqttSubscribeReasonCode.GrantedQoS2;
|
||||||
|
|
||||||
|
result.UserProperties = interceptorEventArgs.UserProperties;
|
||||||
|
result.ReasonString = interceptorEventArgs.ReasonString;
|
||||||
|
result.ReasonCodes.Add(interceptorEventArgs.Response.ReasonCode);
|
||||||
|
|
||||||
|
if (interceptorEventArgs.CloseConnection)
|
||||||
|
{
|
||||||
|
// When any of the interceptor calls leads to a connection close the connection
|
||||||
|
// must be closed. So do not revert to false!
|
||||||
|
result.CloseConnection = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!processSubscription || string.IsNullOrEmpty(topicFilter.Topic))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var createSubscriptionResult = CreateSubscription(topicFilter, subscribePacket.SubscriptionIdentifier, interceptorEventArgs.Response.ReasonCode);
|
||||||
|
|
||||||
|
addedSubscriptions.Add(topicFilter.Topic);
|
||||||
|
finalTopicFilters.Add(topicFilter);
|
||||||
|
|
||||||
|
FilterRetainedApplicationMessages(retainedApplicationMessages, createSubscriptionResult, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This call will add the new subscription to the internal storage.
|
||||||
|
// So the event _ClientSubscribedTopicEvent_ must be called afterwards.
|
||||||
|
_subscriptionChangedNotification?.OnSubscriptionsAdded(_session, addedSubscriptions);
|
||||||
|
|
||||||
|
if (_eventContainer.ClientSubscribedTopicEvent.HasHandlers)
|
||||||
|
{
|
||||||
|
foreach (var finalTopicFilter in finalTopicFilters)
|
||||||
|
{
|
||||||
|
var eventArgs = new ClientSubscribedTopicEventArgs(_session.Id, _session.UserName, finalTopicFilter, _session.Items);
|
||||||
|
await _eventContainer.ClientSubscribedTopicEvent.InvokeAsync(eventArgs).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UnsubscribeResult> Unsubscribe(MqttUnsubscribePacket unsubscribePacket, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(unsubscribePacket);
|
||||||
|
|
||||||
|
var result = new UnsubscribeResult();
|
||||||
|
|
||||||
|
var removedSubscriptions = new List<string>();
|
||||||
|
|
||||||
|
_subscriptionsLock.EnterWriteLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var topicFilter in unsubscribePacket.TopicFilters)
|
||||||
|
{
|
||||||
|
_subscriptions.TryGetValue(topicFilter, out var existingSubscription);
|
||||||
|
|
||||||
|
var interceptorEventArgs = await InterceptUnsubscribe(topicFilter, existingSubscription, unsubscribePacket.UserProperties, cancellationToken).ConfigureAwait(false);
|
||||||
|
var acceptUnsubscription = interceptorEventArgs.Response.ReasonCode == MqttUnsubscribeReasonCode.Success;
|
||||||
|
|
||||||
result.UserProperties = interceptorEventArgs.UserProperties;
|
result.UserProperties = interceptorEventArgs.UserProperties;
|
||||||
result.ReasonString = interceptorEventArgs.ReasonString;
|
|
||||||
result.ReasonCodes.Add(interceptorEventArgs.Response.ReasonCode);
|
result.ReasonCodes.Add(interceptorEventArgs.Response.ReasonCode);
|
||||||
|
|
||||||
if (interceptorEventArgs.CloseConnection)
|
if (interceptorEventArgs.CloseConnection)
|
||||||
@@ -188,329 +245,271 @@ namespace MQTTnet.Server.Internal
|
|||||||
result.CloseConnection = true;
|
result.CloseConnection = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!processSubscription || string.IsNullOrEmpty(topicFilter.Topic))
|
if (!acceptUnsubscription)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var createSubscriptionResult = CreateSubscription(topicFilter, subscribePacket.SubscriptionIdentifier, interceptorEventArgs.Response.ReasonCode);
|
if (interceptorEventArgs.ProcessUnsubscription)
|
||||||
|
|
||||||
addedSubscriptions.Add(topicFilter.Topic);
|
|
||||||
finalTopicFilters.Add(topicFilter);
|
|
||||||
|
|
||||||
FilterRetainedApplicationMessages(retainedApplicationMessages, createSubscriptionResult, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
// This call will add the new subscription to the internal storage.
|
|
||||||
// So the event _ClientSubscribedTopicEvent_ must be called afterwards.
|
|
||||||
_subscriptionChangedNotification?.OnSubscriptionsAdded(_session, addedSubscriptions);
|
|
||||||
|
|
||||||
if (_eventContainer.ClientSubscribedTopicEvent.HasHandlers)
|
|
||||||
{
|
|
||||||
foreach (var finalTopicFilter in finalTopicFilters)
|
|
||||||
{
|
{
|
||||||
var eventArgs = new ClientSubscribedTopicEventArgs(_session.Id, _session.UserName, finalTopicFilter, _session.Items);
|
_subscriptions.Remove(topicFilter);
|
||||||
await _eventContainer.ClientSubscribedTopicEvent.InvokeAsync(eventArgs).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
// must remove subscription object from topic hash dictionary also
|
||||||
}
|
if (existingSubscription != null)
|
||||||
|
|
||||||
public async Task<UnsubscribeResult> Unsubscribe(MqttUnsubscribePacket unsubscribePacket, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(unsubscribePacket);
|
|
||||||
|
|
||||||
var result = new UnsubscribeResult();
|
|
||||||
|
|
||||||
var removedSubscriptions = new List<string>();
|
|
||||||
|
|
||||||
_subscriptionsLock.EnterWriteLock();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
foreach (var topicFilter in unsubscribePacket.TopicFilters)
|
|
||||||
{
|
|
||||||
_subscriptions.TryGetValue(topicFilter, out var existingSubscription);
|
|
||||||
|
|
||||||
var interceptorEventArgs = await InterceptUnsubscribe(topicFilter, existingSubscription, unsubscribePacket.UserProperties, cancellationToken).ConfigureAwait(false);
|
|
||||||
var acceptUnsubscription = interceptorEventArgs.Response.ReasonCode == MqttUnsubscribeReasonCode.Success;
|
|
||||||
|
|
||||||
result.UserProperties = interceptorEventArgs.UserProperties;
|
|
||||||
result.ReasonCodes.Add(interceptorEventArgs.Response.ReasonCode);
|
|
||||||
|
|
||||||
if (interceptorEventArgs.CloseConnection)
|
|
||||||
{
|
{
|
||||||
// When any of the interceptor calls leads to a connection close the connection
|
var topicHash = existingSubscription.TopicHash;
|
||||||
// must be closed. So do not revert to false!
|
|
||||||
result.CloseConnection = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!acceptUnsubscription)
|
if (existingSubscription.TopicHasWildcard)
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (interceptorEventArgs.ProcessUnsubscription)
|
|
||||||
{
|
|
||||||
_subscriptions.Remove(topicFilter);
|
|
||||||
|
|
||||||
// must remove subscription object from topic hash dictionary also
|
|
||||||
if (existingSubscription != null)
|
|
||||||
{
|
{
|
||||||
var topicHash = existingSubscription.TopicHash;
|
if (_wildcardSubscriptionsByTopicHash.TryGetValue(topicHash, out var subscriptions))
|
||||||
|
|
||||||
if (existingSubscription.TopicHasWildcard)
|
|
||||||
{
|
{
|
||||||
if (_wildcardSubscriptionsByTopicHash.TryGetValue(topicHash, out var subscriptions))
|
subscriptions.RemoveSubscription(existingSubscription);
|
||||||
|
if (subscriptions.SubscriptionsByHashMask.Count == 0)
|
||||||
{
|
{
|
||||||
subscriptions.RemoveSubscription(existingSubscription);
|
_wildcardSubscriptionsByTopicHash.Remove(topicHash);
|
||||||
if (subscriptions.SubscriptionsByHashMask.Count == 0)
|
}
|
||||||
{
|
}
|
||||||
_wildcardSubscriptionsByTopicHash.Remove(topicHash);
|
}
|
||||||
}
|
else
|
||||||
}
|
{
|
||||||
}
|
if (_noWildcardSubscriptionsByTopicHash.TryGetValue(topicHash, out var subscriptions))
|
||||||
else
|
{
|
||||||
{
|
subscriptions.Remove(existingSubscription);
|
||||||
if (_noWildcardSubscriptionsByTopicHash.TryGetValue(topicHash, out var subscriptions))
|
if (subscriptions.Count == 0)
|
||||||
{
|
{
|
||||||
subscriptions.Remove(existingSubscription);
|
_noWildcardSubscriptionsByTopicHash.Remove(topicHash);
|
||||||
if (subscriptions.Count == 0)
|
|
||||||
{
|
|
||||||
_noWildcardSubscriptionsByTopicHash.Remove(topicHash);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
removedSubscriptions.Add(topicFilter);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
removedSubscriptions.Add(topicFilter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
}
|
||||||
{
|
finally
|
||||||
_subscriptionsLock.ExitWriteLock();
|
{
|
||||||
_subscriptionChangedNotification?.OnSubscriptionsRemoved(_session, removedSubscriptions);
|
_subscriptionsLock.ExitWriteLock();
|
||||||
}
|
_subscriptionChangedNotification?.OnSubscriptionsRemoved(_session, removedSubscriptions);
|
||||||
|
|
||||||
if (_eventContainer.ClientUnsubscribedTopicEvent.HasHandlers)
|
|
||||||
{
|
|
||||||
foreach (var topicFilter in unsubscribePacket.TopicFilters)
|
|
||||||
{
|
|
||||||
var eventArgs = new ClientUnsubscribedTopicEventArgs(_session.Id, _session.UserName, topicFilter, _session.Items);
|
|
||||||
await _eventContainer.ClientUnsubscribedTopicEvent.InvokeAsync(eventArgs).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CreateSubscriptionResult CreateSubscription(MqttTopicFilter topicFilter, uint subscriptionIdentifier, MqttSubscribeReasonCode reasonCode)
|
if (_eventContainer.ClientUnsubscribedTopicEvent.HasHandlers)
|
||||||
{
|
{
|
||||||
MqttQualityOfServiceLevel grantedQualityOfServiceLevel;
|
foreach (var topicFilter in unsubscribePacket.TopicFilters)
|
||||||
|
|
||||||
if (reasonCode == MqttSubscribeReasonCode.GrantedQoS0)
|
|
||||||
{
|
{
|
||||||
grantedQualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce;
|
var eventArgs = new ClientUnsubscribedTopicEventArgs(_session.Id, _session.UserName, topicFilter, _session.Items);
|
||||||
|
await _eventContainer.ClientUnsubscribedTopicEvent.InvokeAsync(eventArgs).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
else if (reasonCode == MqttSubscribeReasonCode.GrantedQoS1)
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
CreateSubscriptionResult CreateSubscription(MqttTopicFilter topicFilter, uint subscriptionIdentifier, MqttSubscribeReasonCode reasonCode)
|
||||||
|
{
|
||||||
|
MqttQualityOfServiceLevel grantedQualityOfServiceLevel;
|
||||||
|
|
||||||
|
if (reasonCode == MqttSubscribeReasonCode.GrantedQoS0)
|
||||||
|
{
|
||||||
|
grantedQualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce;
|
||||||
|
}
|
||||||
|
else if (reasonCode == MqttSubscribeReasonCode.GrantedQoS1)
|
||||||
|
{
|
||||||
|
grantedQualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce;
|
||||||
|
}
|
||||||
|
else if (reasonCode == MqttSubscribeReasonCode.GrantedQoS2)
|
||||||
|
{
|
||||||
|
grantedQualityOfServiceLevel = MqttQualityOfServiceLevel.ExactlyOnce;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var subscription = new MqttSubscription(
|
||||||
|
topicFilter.Topic,
|
||||||
|
topicFilter.NoLocal,
|
||||||
|
topicFilter.RetainHandling,
|
||||||
|
topicFilter.RetainAsPublished,
|
||||||
|
grantedQualityOfServiceLevel,
|
||||||
|
subscriptionIdentifier);
|
||||||
|
|
||||||
|
bool isNewSubscription;
|
||||||
|
|
||||||
|
// Add to subscriptions and maintain topic hash dictionaries
|
||||||
|
|
||||||
|
_subscriptionsLock.EnterWriteLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
MqttTopicHash.Calculate(topicFilter.Topic, out var topicHash, out _, out var hasWildcard);
|
||||||
|
|
||||||
|
if (_subscriptions.TryGetValue(topicFilter.Topic, out var existingSubscription))
|
||||||
{
|
{
|
||||||
grantedQualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce;
|
// must remove object from topic hash dictionary first
|
||||||
}
|
|
||||||
else if (reasonCode == MqttSubscribeReasonCode.GrantedQoS2)
|
|
||||||
{
|
|
||||||
grantedQualityOfServiceLevel = MqttQualityOfServiceLevel.ExactlyOnce;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
var subscription = new MqttSubscription(
|
|
||||||
topicFilter.Topic,
|
|
||||||
topicFilter.NoLocal,
|
|
||||||
topicFilter.RetainHandling,
|
|
||||||
topicFilter.RetainAsPublished,
|
|
||||||
grantedQualityOfServiceLevel,
|
|
||||||
subscriptionIdentifier);
|
|
||||||
|
|
||||||
bool isNewSubscription;
|
|
||||||
|
|
||||||
// Add to subscriptions and maintain topic hash dictionaries
|
|
||||||
|
|
||||||
_subscriptionsLock.EnterWriteLock();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
MqttTopicHash.Calculate(topicFilter.Topic, out var topicHash, out _, out var hasWildcard);
|
|
||||||
|
|
||||||
if (_subscriptions.TryGetValue(topicFilter.Topic, out var existingSubscription))
|
|
||||||
{
|
|
||||||
// must remove object from topic hash dictionary first
|
|
||||||
if (hasWildcard)
|
|
||||||
{
|
|
||||||
if (_wildcardSubscriptionsByTopicHash.TryGetValue(topicHash, out var subscriptions))
|
|
||||||
{
|
|
||||||
subscriptions.RemoveSubscription(existingSubscription);
|
|
||||||
// no need to remove empty entry because we'll be adding subscription again below
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (_noWildcardSubscriptionsByTopicHash.TryGetValue(topicHash, out var subscriptions))
|
|
||||||
{
|
|
||||||
subscriptions.Remove(existingSubscription);
|
|
||||||
// no need to remove empty entry because we'll be adding subscription again below
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isNewSubscription = existingSubscription == null;
|
|
||||||
_subscriptions[topicFilter.Topic] = subscription;
|
|
||||||
|
|
||||||
// Add or re-add to topic hash dictionary
|
|
||||||
if (hasWildcard)
|
if (hasWildcard)
|
||||||
{
|
{
|
||||||
if (!_wildcardSubscriptionsByTopicHash.TryGetValue(topicHash, out var subscriptions))
|
if (_wildcardSubscriptionsByTopicHash.TryGetValue(topicHash, out var subscriptions))
|
||||||
{
|
{
|
||||||
subscriptions = new TopicHashMaskSubscriptions();
|
subscriptions.RemoveSubscription(existingSubscription);
|
||||||
_wildcardSubscriptionsByTopicHash.Add(topicHash, subscriptions);
|
// no need to remove empty entry because we'll be adding subscription again below
|
||||||
}
|
}
|
||||||
|
|
||||||
subscriptions.AddSubscription(subscription);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (!_noWildcardSubscriptionsByTopicHash.TryGetValue(topicHash, out var subscriptions))
|
if (_noWildcardSubscriptionsByTopicHash.TryGetValue(topicHash, out var subscriptions))
|
||||||
{
|
{
|
||||||
subscriptions = new HashSet<MqttSubscription>();
|
subscriptions.Remove(existingSubscription);
|
||||||
_noWildcardSubscriptionsByTopicHash.Add(topicHash, subscriptions);
|
// no need to remove empty entry because we'll be adding subscription again below
|
||||||
}
|
}
|
||||||
|
|
||||||
subscriptions.Add(subscription);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
|
||||||
{
|
|
||||||
_subscriptionsLock.ExitWriteLock();
|
|
||||||
}
|
|
||||||
|
|
||||||
return new CreateSubscriptionResult
|
isNewSubscription = existingSubscription == null;
|
||||||
{
|
_subscriptions[topicFilter.Topic] = subscription;
|
||||||
IsNewSubscription = isNewSubscription,
|
|
||||||
Subscription = subscription
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static void FilterRetainedApplicationMessages(
|
// Add or re-add to topic hash dictionary
|
||||||
IList<MqttApplicationMessage> retainedMessages,
|
if (hasWildcard)
|
||||||
CreateSubscriptionResult createSubscriptionResult,
|
|
||||||
SubscribeResult subscribeResult)
|
|
||||||
{
|
|
||||||
if (createSubscriptionResult.Subscription.RetainHandling == MqttRetainHandling.DoNotSendOnSubscribe)
|
|
||||||
{
|
{
|
||||||
// This is a MQTT V5+ feature.
|
if (!_wildcardSubscriptionsByTopicHash.TryGetValue(topicHash, out var subscriptions))
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (createSubscriptionResult.Subscription.RetainHandling == MqttRetainHandling.SendAtSubscribeIfNewSubscriptionOnly && !createSubscriptionResult.IsNewSubscription)
|
|
||||||
{
|
|
||||||
// This is a MQTT V5+ feature.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var index = retainedMessages.Count - 1; index >= 0; index--)
|
|
||||||
{
|
|
||||||
var retainedMessage = retainedMessages[index];
|
|
||||||
if (retainedMessage == null)
|
|
||||||
{
|
{
|
||||||
continue;
|
subscriptions = new TopicHashMaskSubscriptions();
|
||||||
|
_wildcardSubscriptionsByTopicHash.Add(topicHash, subscriptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (MqttTopicFilterComparer.Compare(retainedMessage.Topic, createSubscriptionResult.Subscription.Topic) != MqttTopicFilterCompareResult.IsMatch)
|
subscriptions.AddSubscription(subscription);
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var retainedMessageMatch = new MqttRetainedMessageMatch(retainedMessage, createSubscriptionResult.Subscription.GrantedQualityOfServiceLevel);
|
|
||||||
if (retainedMessageMatch.SubscriptionQualityOfServiceLevel > retainedMessageMatch.ApplicationMessage.QualityOfServiceLevel)
|
|
||||||
{
|
|
||||||
// UPGRADING the QoS is not allowed!
|
|
||||||
// From MQTT spec: Subscribing to a Topic Filter at QoS 2 is equivalent to saying
|
|
||||||
// "I would like to receive Messages matching this filter at the QoS with which they were published".
|
|
||||||
// This means a publisher is responsible for determining the maximum QoS a Message can be delivered at,
|
|
||||||
// but a subscriber is able to require that the Server downgrades the QoS to one more suitable for its usage.
|
|
||||||
retainedMessageMatch.SubscriptionQualityOfServiceLevel = retainedMessageMatch.ApplicationMessage.QualityOfServiceLevel;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (subscribeResult.RetainedMessages == null)
|
|
||||||
{
|
|
||||||
subscribeResult.RetainedMessages = new List<MqttRetainedMessageMatch>();
|
|
||||||
}
|
|
||||||
|
|
||||||
subscribeResult.RetainedMessages.Add(retainedMessageMatch);
|
|
||||||
|
|
||||||
// Clear the retained message from the list because the client should receive every message only
|
|
||||||
// one time even if multiple subscriptions affect them.
|
|
||||||
retainedMessages[index] = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task<InterceptingSubscriptionEventArgs> InterceptSubscribe(
|
|
||||||
MqttTopicFilter topicFilter,
|
|
||||||
List<MqttUserProperty> userProperties,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var eventArgs = new InterceptingSubscriptionEventArgs(cancellationToken, _session.Id, _session.UserName, new MqttSessionStatus(_session), topicFilter, userProperties);
|
|
||||||
|
|
||||||
if (topicFilter.QualityOfServiceLevel == MqttQualityOfServiceLevel.AtMostOnce)
|
|
||||||
{
|
|
||||||
eventArgs.Response.ReasonCode = MqttSubscribeReasonCode.GrantedQoS0;
|
|
||||||
}
|
|
||||||
else if (topicFilter.QualityOfServiceLevel == MqttQualityOfServiceLevel.AtLeastOnce)
|
|
||||||
{
|
|
||||||
eventArgs.Response.ReasonCode = MqttSubscribeReasonCode.GrantedQoS1;
|
|
||||||
}
|
|
||||||
else if (topicFilter.QualityOfServiceLevel == MqttQualityOfServiceLevel.ExactlyOnce)
|
|
||||||
{
|
|
||||||
eventArgs.Response.ReasonCode = MqttSubscribeReasonCode.GrantedQoS2;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (topicFilter.Topic.StartsWith("$share/"))
|
|
||||||
{
|
|
||||||
eventArgs.Response.ReasonCode = MqttSubscribeReasonCode.SharedSubscriptionsNotSupported;
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await _eventContainer.InterceptingSubscriptionEvent.InvokeAsync(eventArgs).ConfigureAwait(false);
|
if (!_noWildcardSubscriptionsByTopicHash.TryGetValue(topicHash, out var subscriptions))
|
||||||
|
{
|
||||||
|
subscriptions = new HashSet<MqttSubscription>();
|
||||||
|
_noWildcardSubscriptionsByTopicHash.Add(topicHash, subscriptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriptions.Add(subscription);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_subscriptionsLock.ExitWriteLock();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CreateSubscriptionResult
|
||||||
|
{
|
||||||
|
IsNewSubscription = isNewSubscription,
|
||||||
|
Subscription = subscription
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static void FilterRetainedApplicationMessages(
|
||||||
|
IList<MqttApplicationMessage> retainedMessages,
|
||||||
|
CreateSubscriptionResult createSubscriptionResult,
|
||||||
|
SubscribeResult subscribeResult)
|
||||||
|
{
|
||||||
|
if (createSubscriptionResult.Subscription.RetainHandling == MqttRetainHandling.DoNotSendOnSubscribe)
|
||||||
|
{
|
||||||
|
// This is a MQTT V5+ feature.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (createSubscriptionResult.Subscription.RetainHandling == MqttRetainHandling.SendAtSubscribeIfNewSubscriptionOnly && !createSubscriptionResult.IsNewSubscription)
|
||||||
|
{
|
||||||
|
// This is a MQTT V5+ feature.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var index = retainedMessages.Count - 1; index >= 0; index--)
|
||||||
|
{
|
||||||
|
var retainedMessage = retainedMessages[index];
|
||||||
|
if (retainedMessage == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
return eventArgs;
|
if (MqttTopicFilterComparer.Compare(retainedMessage.Topic, createSubscriptionResult.Subscription.Topic) != MqttTopicFilterCompareResult.IsMatch)
|
||||||
}
|
|
||||||
|
|
||||||
async Task<InterceptingUnsubscriptionEventArgs> InterceptUnsubscribe(
|
|
||||||
string topicFilter,
|
|
||||||
MqttSubscription mqttSubscription,
|
|
||||||
List<MqttUserProperty> userProperties,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var clientUnsubscribingTopicEventArgs = new InterceptingUnsubscriptionEventArgs(cancellationToken, _session.Id, _session.UserName, _session.Items, topicFilter, userProperties)
|
|
||||||
{
|
{
|
||||||
Response =
|
continue;
|
||||||
{
|
}
|
||||||
ReasonCode = mqttSubscription == null ? MqttUnsubscribeReasonCode.NoSubscriptionExisted : MqttUnsubscribeReasonCode.Success
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
await _eventContainer.InterceptingUnsubscriptionEvent.InvokeAsync(clientUnsubscribingTopicEventArgs).ConfigureAwait(false);
|
var retainedMessageMatch = new MqttRetainedMessageMatch(retainedMessage, createSubscriptionResult.Subscription.GrantedQualityOfServiceLevel);
|
||||||
|
if (retainedMessageMatch.SubscriptionQualityOfServiceLevel > retainedMessageMatch.ApplicationMessage.QualityOfServiceLevel)
|
||||||
|
{
|
||||||
|
// UPGRADING the QoS is not allowed!
|
||||||
|
// From MQTT spec: Subscribing to a Topic Filter at QoS 2 is equivalent to saying
|
||||||
|
// "I would like to receive Messages matching this filter at the QoS with which they were published".
|
||||||
|
// This means a publisher is responsible for determining the maximum QoS a Message can be delivered at,
|
||||||
|
// but a subscriber is able to require that the Server downgrades the QoS to one more suitable for its usage.
|
||||||
|
retainedMessageMatch.SubscriptionQualityOfServiceLevel = retainedMessageMatch.ApplicationMessage.QualityOfServiceLevel;
|
||||||
|
}
|
||||||
|
|
||||||
return clientUnsubscribingTopicEventArgs;
|
if (subscribeResult.RetainedMessages == null)
|
||||||
}
|
{
|
||||||
|
subscribeResult.RetainedMessages = new List<MqttRetainedMessageMatch>();
|
||||||
|
}
|
||||||
|
|
||||||
sealed class CreateSubscriptionResult
|
subscribeResult.RetainedMessages.Add(retainedMessageMatch);
|
||||||
{
|
|
||||||
public bool IsNewSubscription { get; set; }
|
|
||||||
|
|
||||||
public MqttSubscription Subscription { get; set; }
|
// Clear the retained message from the list because the client should receive every message only
|
||||||
|
// one time even if multiple subscriptions affect them.
|
||||||
|
retainedMessages[index] = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async Task<InterceptingSubscriptionEventArgs> InterceptSubscribe(
|
||||||
|
MqttTopicFilter topicFilter,
|
||||||
|
List<MqttUserProperty> userProperties,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var eventArgs = new InterceptingSubscriptionEventArgs(cancellationToken, _session.Id, _session.UserName, new MqttSessionStatus(_session), topicFilter, userProperties);
|
||||||
|
|
||||||
|
if (topicFilter.QualityOfServiceLevel == MqttQualityOfServiceLevel.AtMostOnce)
|
||||||
|
{
|
||||||
|
eventArgs.Response.ReasonCode = MqttSubscribeReasonCode.GrantedQoS0;
|
||||||
|
}
|
||||||
|
else if (topicFilter.QualityOfServiceLevel == MqttQualityOfServiceLevel.AtLeastOnce)
|
||||||
|
{
|
||||||
|
eventArgs.Response.ReasonCode = MqttSubscribeReasonCode.GrantedQoS1;
|
||||||
|
}
|
||||||
|
else if (topicFilter.QualityOfServiceLevel == MqttQualityOfServiceLevel.ExactlyOnce)
|
||||||
|
{
|
||||||
|
eventArgs.Response.ReasonCode = MqttSubscribeReasonCode.GrantedQoS2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (topicFilter.Topic.StartsWith("$share/"))
|
||||||
|
{
|
||||||
|
eventArgs.Response.ReasonCode = MqttSubscribeReasonCode.SharedSubscriptionsNotSupported;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await _eventContainer.InterceptingSubscriptionEvent.InvokeAsync(eventArgs).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return eventArgs;
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task<InterceptingUnsubscriptionEventArgs> InterceptUnsubscribe(
|
||||||
|
string topicFilter,
|
||||||
|
MqttSubscription mqttSubscription,
|
||||||
|
List<MqttUserProperty> userProperties,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var clientUnsubscribingTopicEventArgs = new InterceptingUnsubscriptionEventArgs(cancellationToken, _session.Id, _session.UserName, _session.Items, topicFilter, userProperties)
|
||||||
|
{
|
||||||
|
Response =
|
||||||
|
{
|
||||||
|
ReasonCode = mqttSubscription == null ? MqttUnsubscribeReasonCode.NoSubscriptionExisted : MqttUnsubscribeReasonCode.Success
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await _eventContainer.InterceptingUnsubscriptionEvent.InvokeAsync(clientUnsubscribingTopicEventArgs).ConfigureAwait(false);
|
||||||
|
|
||||||
|
return clientUnsubscribingTopicEventArgs;
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class CreateSubscriptionResult
|
||||||
|
{
|
||||||
|
public bool IsNewSubscription { get; set; }
|
||||||
|
|
||||||
|
public MqttSubscription Subscription { get; set; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -2,148 +2,146 @@
|
|||||||
// The .NET Foundation licenses this file to you under the MIT license.
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
using MQTTnet.Diagnostics.Logger;
|
using MQTTnet.Diagnostics.Logger;
|
||||||
using MQTTnet.Internal;
|
using MQTTnet.Internal;
|
||||||
|
|
||||||
namespace MQTTnet.Server.Internal
|
namespace MQTTnet.Server.Internal;
|
||||||
|
|
||||||
|
public sealed class MqttRetainedMessagesManager
|
||||||
{
|
{
|
||||||
public sealed class MqttRetainedMessagesManager
|
readonly Dictionary<string, MqttApplicationMessage> _messages = new Dictionary<string, MqttApplicationMessage>(4096);
|
||||||
|
readonly AsyncLock _storageAccessLock = new AsyncLock();
|
||||||
|
|
||||||
|
readonly MqttServerEventContainer _eventContainer;
|
||||||
|
readonly MqttNetSourceLogger _logger;
|
||||||
|
|
||||||
|
public MqttRetainedMessagesManager(MqttServerEventContainer eventContainer, IMqttNetLogger logger)
|
||||||
{
|
{
|
||||||
readonly Dictionary<string, MqttApplicationMessage> _messages = new Dictionary<string, MqttApplicationMessage>(4096);
|
_eventContainer = eventContainer ?? throw new ArgumentNullException(nameof(eventContainer));
|
||||||
readonly AsyncLock _storageAccessLock = new AsyncLock();
|
|
||||||
|
|
||||||
readonly MqttServerEventContainer _eventContainer;
|
ArgumentNullException.ThrowIfNull(logger);
|
||||||
readonly MqttNetSourceLogger _logger;
|
|
||||||
|
|
||||||
public MqttRetainedMessagesManager(MqttServerEventContainer eventContainer, IMqttNetLogger logger)
|
_logger = logger.WithSource(nameof(MqttRetainedMessagesManager));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Start()
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
_eventContainer = eventContainer ?? throw new ArgumentNullException(nameof(eventContainer));
|
var eventArgs = new LoadingRetainedMessagesEventArgs();
|
||||||
|
await _eventContainer.LoadingRetainedMessagesEvent.InvokeAsync(eventArgs).ConfigureAwait(false);
|
||||||
|
|
||||||
ArgumentNullException.ThrowIfNull(logger);
|
lock (_messages)
|
||||||
|
|
||||||
_logger = logger.WithSource(nameof(MqttRetainedMessagesManager));
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Start()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
var eventArgs = new LoadingRetainedMessagesEventArgs();
|
_messages.Clear();
|
||||||
await _eventContainer.LoadingRetainedMessagesEvent.InvokeAsync(eventArgs).ConfigureAwait(false);
|
|
||||||
|
|
||||||
lock (_messages)
|
if (eventArgs.LoadedRetainedMessages != null)
|
||||||
{
|
{
|
||||||
_messages.Clear();
|
foreach (var retainedMessage in eventArgs.LoadedRetainedMessages)
|
||||||
|
|
||||||
if (eventArgs.LoadedRetainedMessages != null)
|
|
||||||
{
|
{
|
||||||
foreach (var retainedMessage in eventArgs.LoadedRetainedMessages)
|
_messages[retainedMessage.Topic] = retainedMessage;
|
||||||
{
|
|
||||||
_messages[retainedMessage.Topic] = retainedMessage;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
|
||||||
{
|
|
||||||
_logger.Error(exception, "Unhandled exception while loading retained messages.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
catch (Exception exception)
|
||||||
public async Task UpdateMessage(string clientId, MqttApplicationMessage applicationMessage)
|
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(applicationMessage);
|
_logger.Error(exception, "Unhandled exception while loading retained messages.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try
|
public async Task UpdateMessage(string clientId, MqttApplicationMessage applicationMessage)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(applicationMessage);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
List<MqttApplicationMessage> messagesForSave = null;
|
||||||
|
var saveIsRequired = false;
|
||||||
|
|
||||||
|
lock (_messages)
|
||||||
{
|
{
|
||||||
List<MqttApplicationMessage> messagesForSave = null;
|
var payload = applicationMessage.Payload;
|
||||||
var saveIsRequired = false;
|
var hasPayload = payload.Length > 0;
|
||||||
|
|
||||||
lock (_messages)
|
if (!hasPayload)
|
||||||
{
|
{
|
||||||
var payload = applicationMessage.Payload;
|
saveIsRequired = _messages.Remove(applicationMessage.Topic);
|
||||||
var hasPayload = payload.Length > 0;
|
_logger.Verbose("Client '{0}' cleared retained message for topic '{1}'.", clientId, applicationMessage.Topic);
|
||||||
|
}
|
||||||
if (!hasPayload)
|
else
|
||||||
|
{
|
||||||
|
if (!_messages.TryGetValue(applicationMessage.Topic, out var existingMessage))
|
||||||
{
|
{
|
||||||
saveIsRequired = _messages.Remove(applicationMessage.Topic);
|
_messages[applicationMessage.Topic] = applicationMessage;
|
||||||
_logger.Verbose("Client '{0}' cleared retained message for topic '{1}'.", clientId, applicationMessage.Topic);
|
saveIsRequired = true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (!_messages.TryGetValue(applicationMessage.Topic, out var existingMessage))
|
if (existingMessage.QualityOfServiceLevel != applicationMessage.QualityOfServiceLevel ||
|
||||||
|
!MqttMemoryHelper.SequenceEqual(existingMessage.Payload, payload))
|
||||||
{
|
{
|
||||||
_messages[applicationMessage.Topic] = applicationMessage;
|
_messages[applicationMessage.Topic] = applicationMessage;
|
||||||
saveIsRequired = true;
|
saveIsRequired = true;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
if (existingMessage.QualityOfServiceLevel != applicationMessage.QualityOfServiceLevel ||
|
|
||||||
!MqttMemoryHelper.SequenceEqual(existingMessage.Payload, payload))
|
|
||||||
{
|
|
||||||
_messages[applicationMessage.Topic] = applicationMessage;
|
|
||||||
saveIsRequired = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.Verbose("Client '{0}' set retained message for topic '{1}'.", clientId, applicationMessage.Topic);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (saveIsRequired)
|
_logger.Verbose("Client '{0}' set retained message for topic '{1}'.", clientId, applicationMessage.Topic);
|
||||||
{
|
|
||||||
messagesForSave = new List<MqttApplicationMessage>(_messages.Values);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (saveIsRequired)
|
if (saveIsRequired)
|
||||||
{
|
{
|
||||||
using (await _storageAccessLock.EnterAsync().ConfigureAwait(false))
|
messagesForSave = new List<MqttApplicationMessage>(_messages.Values);
|
||||||
{
|
|
||||||
var eventArgs = new RetainedMessageChangedEventArgs(clientId, applicationMessage, messagesForSave);
|
|
||||||
await _eventContainer.RetainedMessageChangedEvent.InvokeAsync(eventArgs).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
|
||||||
{
|
|
||||||
_logger.Error(exception, "Unhandled exception while handling retained messages.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<IList<MqttApplicationMessage>> GetMessages()
|
if (saveIsRequired)
|
||||||
{
|
|
||||||
lock (_messages)
|
|
||||||
{
|
{
|
||||||
var result = new List<MqttApplicationMessage>(_messages.Values);
|
using (await _storageAccessLock.EnterAsync().ConfigureAwait(false))
|
||||||
return Task.FromResult((IList<MqttApplicationMessage>)result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<MqttApplicationMessage> GetMessage(string topic)
|
|
||||||
{
|
|
||||||
lock (_messages)
|
|
||||||
{
|
|
||||||
if (_messages.TryGetValue(topic, out var message))
|
|
||||||
{
|
{
|
||||||
return Task.FromResult(message);
|
var eventArgs = new RetainedMessageChangedEventArgs(clientId, applicationMessage, messagesForSave);
|
||||||
|
await _eventContainer.RetainedMessageChangedEvent.InvokeAsync(eventArgs).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
_logger.Error(exception, "Unhandled exception while handling retained messages.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return Task.FromResult<MqttApplicationMessage>(null);
|
public Task<IList<MqttApplicationMessage>> GetMessages()
|
||||||
|
{
|
||||||
|
lock (_messages)
|
||||||
|
{
|
||||||
|
var result = new List<MqttApplicationMessage>(_messages.Values);
|
||||||
|
return Task.FromResult((IList<MqttApplicationMessage>)result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<MqttApplicationMessage> GetMessage(string topic)
|
||||||
|
{
|
||||||
|
lock (_messages)
|
||||||
|
{
|
||||||
|
if (_messages.TryGetValue(topic, out var message))
|
||||||
|
{
|
||||||
|
return Task.FromResult(message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ClearMessages()
|
return Task.FromResult<MqttApplicationMessage>(null);
|
||||||
{
|
}
|
||||||
lock (_messages)
|
|
||||||
{
|
|
||||||
_messages.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
using (await _storageAccessLock.EnterAsync().ConfigureAwait(false))
|
public async Task ClearMessages()
|
||||||
{
|
{
|
||||||
await _eventContainer.RetainedMessagesClearedEvent.InvokeAsync(EventArgs.Empty).ConfigureAwait(false);
|
lock (_messages)
|
||||||
}
|
{
|
||||||
|
_messages.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
using (await _storageAccessLock.EnterAsync().ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
await _eventContainer.RetainedMessagesClearedEvent.InvokeAsync(EventArgs.Empty).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,52 +4,51 @@
|
|||||||
|
|
||||||
using MQTTnet.Internal;
|
using MQTTnet.Internal;
|
||||||
|
|
||||||
namespace MQTTnet.Server.Internal
|
namespace MQTTnet.Server.Internal;
|
||||||
|
|
||||||
|
public sealed class MqttServerEventContainer
|
||||||
{
|
{
|
||||||
public sealed class MqttServerEventContainer
|
public AsyncEvent<ApplicationMessageNotConsumedEventArgs> ApplicationMessageNotConsumedEvent { get; } = new AsyncEvent<ApplicationMessageNotConsumedEventArgs>();
|
||||||
{
|
|
||||||
public AsyncEvent<ApplicationMessageNotConsumedEventArgs> ApplicationMessageNotConsumedEvent { get; } = new AsyncEvent<ApplicationMessageNotConsumedEventArgs>();
|
|
||||||
|
|
||||||
public AsyncEvent<ClientAcknowledgedPublishPacketEventArgs> ClientAcknowledgedPublishPacketEvent { get; } = new AsyncEvent<ClientAcknowledgedPublishPacketEventArgs>();
|
public AsyncEvent<ClientAcknowledgedPublishPacketEventArgs> ClientAcknowledgedPublishPacketEvent { get; } = new AsyncEvent<ClientAcknowledgedPublishPacketEventArgs>();
|
||||||
|
|
||||||
public AsyncEvent<ClientConnectedEventArgs> ClientConnectedEvent { get; } = new AsyncEvent<ClientConnectedEventArgs>();
|
public AsyncEvent<ClientConnectedEventArgs> ClientConnectedEvent { get; } = new AsyncEvent<ClientConnectedEventArgs>();
|
||||||
|
|
||||||
public AsyncEvent<ClientDisconnectedEventArgs> ClientDisconnectedEvent { get; } = new AsyncEvent<ClientDisconnectedEventArgs>();
|
public AsyncEvent<ClientDisconnectedEventArgs> ClientDisconnectedEvent { get; } = new AsyncEvent<ClientDisconnectedEventArgs>();
|
||||||
|
|
||||||
public AsyncEvent<ClientSubscribedTopicEventArgs> ClientSubscribedTopicEvent { get; } = new AsyncEvent<ClientSubscribedTopicEventArgs>();
|
public AsyncEvent<ClientSubscribedTopicEventArgs> ClientSubscribedTopicEvent { get; } = new AsyncEvent<ClientSubscribedTopicEventArgs>();
|
||||||
|
|
||||||
public AsyncEvent<ClientUnsubscribedTopicEventArgs> ClientUnsubscribedTopicEvent { get; } = new AsyncEvent<ClientUnsubscribedTopicEventArgs>();
|
public AsyncEvent<ClientUnsubscribedTopicEventArgs> ClientUnsubscribedTopicEvent { get; } = new AsyncEvent<ClientUnsubscribedTopicEventArgs>();
|
||||||
|
|
||||||
public AsyncEvent<InterceptingClientApplicationMessageEnqueueEventArgs> InterceptingClientEnqueueEvent { get; } = new AsyncEvent<InterceptingClientApplicationMessageEnqueueEventArgs>();
|
public AsyncEvent<InterceptingClientApplicationMessageEnqueueEventArgs> InterceptingClientEnqueueEvent { get; } = new AsyncEvent<InterceptingClientApplicationMessageEnqueueEventArgs>();
|
||||||
|
|
||||||
public AsyncEvent<ApplicationMessageEnqueuedEventArgs> ApplicationMessageEnqueuedOrDroppedEvent { get; } = new AsyncEvent<ApplicationMessageEnqueuedEventArgs>();
|
public AsyncEvent<ApplicationMessageEnqueuedEventArgs> ApplicationMessageEnqueuedOrDroppedEvent { get; } = new AsyncEvent<ApplicationMessageEnqueuedEventArgs>();
|
||||||
|
|
||||||
public AsyncEvent<QueueMessageOverwrittenEventArgs> QueuedApplicationMessageOverwrittenEvent { get; } = new AsyncEvent<QueueMessageOverwrittenEventArgs>();
|
public AsyncEvent<QueueMessageOverwrittenEventArgs> QueuedApplicationMessageOverwrittenEvent { get; } = new AsyncEvent<QueueMessageOverwrittenEventArgs>();
|
||||||
|
|
||||||
public AsyncEvent<InterceptingPacketEventArgs> InterceptingInboundPacketEvent { get; } = new AsyncEvent<InterceptingPacketEventArgs>();
|
public AsyncEvent<InterceptingPacketEventArgs> InterceptingInboundPacketEvent { get; } = new AsyncEvent<InterceptingPacketEventArgs>();
|
||||||
|
|
||||||
public AsyncEvent<InterceptingPacketEventArgs> InterceptingOutboundPacketEvent { get; } = new AsyncEvent<InterceptingPacketEventArgs>();
|
public AsyncEvent<InterceptingPacketEventArgs> InterceptingOutboundPacketEvent { get; } = new AsyncEvent<InterceptingPacketEventArgs>();
|
||||||
|
|
||||||
public AsyncEvent<InterceptingPublishEventArgs> InterceptingPublishEvent { get; } = new AsyncEvent<InterceptingPublishEventArgs>();
|
public AsyncEvent<InterceptingPublishEventArgs> InterceptingPublishEvent { get; } = new AsyncEvent<InterceptingPublishEventArgs>();
|
||||||
|
|
||||||
public AsyncEvent<InterceptingSubscriptionEventArgs> InterceptingSubscriptionEvent { get; } = new AsyncEvent<InterceptingSubscriptionEventArgs>();
|
public AsyncEvent<InterceptingSubscriptionEventArgs> InterceptingSubscriptionEvent { get; } = new AsyncEvent<InterceptingSubscriptionEventArgs>();
|
||||||
|
|
||||||
public AsyncEvent<InterceptingUnsubscriptionEventArgs> InterceptingUnsubscriptionEvent { get; } = new AsyncEvent<InterceptingUnsubscriptionEventArgs>();
|
public AsyncEvent<InterceptingUnsubscriptionEventArgs> InterceptingUnsubscriptionEvent { get; } = new AsyncEvent<InterceptingUnsubscriptionEventArgs>();
|
||||||
|
|
||||||
public AsyncEvent<LoadingRetainedMessagesEventArgs> LoadingRetainedMessagesEvent { get; } = new AsyncEvent<LoadingRetainedMessagesEventArgs>();
|
public AsyncEvent<LoadingRetainedMessagesEventArgs> LoadingRetainedMessagesEvent { get; } = new AsyncEvent<LoadingRetainedMessagesEventArgs>();
|
||||||
|
|
||||||
public AsyncEvent<EventArgs> PreparingSessionEvent { get; } = new AsyncEvent<EventArgs>();
|
public AsyncEvent<EventArgs> PreparingSessionEvent { get; } = new AsyncEvent<EventArgs>();
|
||||||
|
|
||||||
public AsyncEvent<RetainedMessageChangedEventArgs> RetainedMessageChangedEvent { get; } = new AsyncEvent<RetainedMessageChangedEventArgs>();
|
public AsyncEvent<RetainedMessageChangedEventArgs> RetainedMessageChangedEvent { get; } = new AsyncEvent<RetainedMessageChangedEventArgs>();
|
||||||
|
|
||||||
public AsyncEvent<EventArgs> RetainedMessagesClearedEvent { get; } = new AsyncEvent<EventArgs>();
|
public AsyncEvent<EventArgs> RetainedMessagesClearedEvent { get; } = new AsyncEvent<EventArgs>();
|
||||||
|
|
||||||
public AsyncEvent<SessionDeletedEventArgs> SessionDeletedEvent { get; } = new AsyncEvent<SessionDeletedEventArgs>();
|
public AsyncEvent<SessionDeletedEventArgs> SessionDeletedEvent { get; } = new AsyncEvent<SessionDeletedEventArgs>();
|
||||||
|
|
||||||
public AsyncEvent<EventArgs> StartedEvent { get; } = new AsyncEvent<EventArgs>();
|
public AsyncEvent<EventArgs> StartedEvent { get; } = new AsyncEvent<EventArgs>();
|
||||||
|
|
||||||
public AsyncEvent<EventArgs> StoppedEvent { get; } = new AsyncEvent<EventArgs>();
|
public AsyncEvent<EventArgs> StoppedEvent { get; } = new AsyncEvent<EventArgs>();
|
||||||
|
|
||||||
public AsyncEvent<ValidatingConnectionEventArgs> ValidatingConnectionEvent { get; } = new AsyncEvent<ValidatingConnectionEventArgs>();
|
public AsyncEvent<ValidatingConnectionEventArgs> ValidatingConnectionEvent { get; } = new AsyncEvent<ValidatingConnectionEventArgs>();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -6,130 +6,129 @@ using MQTTnet.Diagnostics.Logger;
|
|||||||
using MQTTnet.Internal;
|
using MQTTnet.Internal;
|
||||||
using MQTTnet.Protocol;
|
using MQTTnet.Protocol;
|
||||||
|
|
||||||
namespace MQTTnet.Server.Internal
|
namespace MQTTnet.Server.Internal;
|
||||||
|
|
||||||
|
public sealed class MqttServerKeepAliveMonitor
|
||||||
{
|
{
|
||||||
public sealed class MqttServerKeepAliveMonitor
|
static readonly MqttServerClientDisconnectOptions DefaultDisconnectOptions = new MqttServerClientDisconnectOptions
|
||||||
{
|
{
|
||||||
static readonly MqttServerClientDisconnectOptions DefaultDisconnectOptions = new MqttServerClientDisconnectOptions
|
ReasonCode = MqttDisconnectReasonCode.KeepAliveTimeout,
|
||||||
|
UserProperties = null,
|
||||||
|
ReasonString = null,
|
||||||
|
ServerReference = null
|
||||||
|
};
|
||||||
|
|
||||||
|
readonly MqttNetSourceLogger _logger;
|
||||||
|
readonly MqttServerOptions _options;
|
||||||
|
readonly MqttClientSessionsManager _sessionsManager;
|
||||||
|
|
||||||
|
public MqttServerKeepAliveMonitor(MqttServerOptions options, MqttClientSessionsManager sessionsManager, IMqttNetLogger logger)
|
||||||
|
{
|
||||||
|
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||||
|
_sessionsManager = sessionsManager ?? throw new ArgumentNullException(nameof(sessionsManager));
|
||||||
|
|
||||||
|
ArgumentNullException.ThrowIfNull(logger);
|
||||||
|
|
||||||
|
_logger = logger.WithSource(nameof(MqttServerKeepAliveMonitor));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Start(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// The keep alive monitor spawns a real new thread (LongRunning) because it does not
|
||||||
|
// support async/await. Async etc. is avoided here because the thread will usually check
|
||||||
|
// the connections every few milliseconds and thus the context changes (due to async) are
|
||||||
|
// only consuming resources. Also there is just 1 thread for the entire server which is fine at all!
|
||||||
|
Task.Factory.StartNew(_ => DoWork(cancellationToken), cancellationToken, TaskCreationOptions.LongRunning).RunInBackground(_logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DoWork(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
ReasonCode = MqttDisconnectReasonCode.KeepAliveTimeout,
|
_logger.Info("Starting keep alive monitor");
|
||||||
UserProperties = null,
|
|
||||||
ReasonString = null,
|
|
||||||
ServerReference = null
|
|
||||||
};
|
|
||||||
|
|
||||||
readonly MqttNetSourceLogger _logger;
|
while (!cancellationToken.IsCancellationRequested)
|
||||||
readonly MqttServerOptions _options;
|
|
||||||
readonly MqttClientSessionsManager _sessionsManager;
|
|
||||||
|
|
||||||
public MqttServerKeepAliveMonitor(MqttServerOptions options, MqttClientSessionsManager sessionsManager, IMqttNetLogger logger)
|
|
||||||
{
|
|
||||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
|
||||||
_sessionsManager = sessionsManager ?? throw new ArgumentNullException(nameof(sessionsManager));
|
|
||||||
|
|
||||||
ArgumentNullException.ThrowIfNull(logger);
|
|
||||||
|
|
||||||
_logger = logger.WithSource(nameof(MqttServerKeepAliveMonitor));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Start(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
// The keep alive monitor spawns a real new thread (LongRunning) because it does not
|
|
||||||
// support async/await. Async etc. is avoided here because the thread will usually check
|
|
||||||
// the connections every few milliseconds and thus the context changes (due to async) are
|
|
||||||
// only consuming resources. Also there is just 1 thread for the entire server which is fine at all!
|
|
||||||
Task.Factory.StartNew(_ => DoWork(cancellationToken), cancellationToken, TaskCreationOptions.LongRunning).RunInBackground(_logger);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DoWork(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
_logger.Info("Starting keep alive monitor");
|
TryProcessClients();
|
||||||
|
Sleep(_options.KeepAliveOptions.MonitorInterval);
|
||||||
while (!cancellationToken.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
TryProcessClients();
|
|
||||||
Sleep(_options.KeepAliveOptions.MonitorInterval);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
catch (Exception exception)
|
|
||||||
{
|
|
||||||
_logger.Error(exception, "Unhandled exception while checking keep alive timeouts");
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_logger.Verbose("Stopped checking keep alive timeout");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
static void Sleep(TimeSpan timeout)
|
|
||||||
{
|
{
|
||||||
try
|
|
||||||
{
|
|
||||||
Thread.Sleep(timeout);
|
|
||||||
}
|
|
||||||
catch (ThreadAbortException)
|
|
||||||
{
|
|
||||||
// The ThreadAbortException is not actively catched in this project.
|
|
||||||
// So we use a one which is similar and will be catched properly.
|
|
||||||
throw new OperationCanceledException();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
catch (Exception exception)
|
||||||
void TryProcessClient(MqttConnectedClient connection, DateTime now)
|
|
||||||
{
|
{
|
||||||
try
|
_logger.Error(exception, "Unhandled exception while checking keep alive timeouts");
|
||||||
{
|
|
||||||
if (!connection.IsRunning)
|
|
||||||
{
|
|
||||||
// The connection is already dead or just created so there is no need to check it.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (connection.ConnectPacket.KeepAlivePeriod == 0)
|
|
||||||
{
|
|
||||||
// The keep alive feature is not used by the current connection.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Values described here: [MQTT-3.1.2-24].
|
|
||||||
// If the client sends 5 sec. the server will allow up to 7.5 seconds.
|
|
||||||
// If the client sends 1 sec. the server will allow up to 1.5 seconds.
|
|
||||||
var maxSecondsWithoutPacket = connection.ConnectPacket.KeepAlivePeriod * 1.5D;
|
|
||||||
|
|
||||||
var secondsWithoutPackage = (now - connection.Statistics.LastPacketSentTimestamp).TotalSeconds;
|
|
||||||
if (secondsWithoutPackage < maxSecondsWithoutPacket)
|
|
||||||
{
|
|
||||||
// A packet was received before the timeout is affected.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.Warning("Client '{0}': Did not receive any packet or keep alive signal", connection.Id);
|
|
||||||
|
|
||||||
// Execute the disconnection in background so that the keep alive monitor can continue
|
|
||||||
// with checking other connections.
|
|
||||||
// We do not need to wait for the task so no await is needed.
|
|
||||||
// Also the internal state of the connection must be swapped to "Finalizing" because the
|
|
||||||
// next iteration of the keep alive timer happens.
|
|
||||||
_ = connection.StopAsync(DefaultDisconnectOptions);
|
|
||||||
}
|
|
||||||
catch (Exception exception)
|
|
||||||
{
|
|
||||||
_logger.Error(exception, "Client {0}: Unhandled exception while checking keep alive timeouts", connection.Id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
void TryProcessClients()
|
|
||||||
{
|
{
|
||||||
var now = DateTime.UtcNow;
|
_logger.Verbose("Stopped checking keep alive timeout");
|
||||||
foreach (var client in _sessionsManager.GetClients())
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Sleep(TimeSpan timeout)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Thread.Sleep(timeout);
|
||||||
|
}
|
||||||
|
catch (ThreadAbortException)
|
||||||
|
{
|
||||||
|
// The ThreadAbortException is not actively catched in this project.
|
||||||
|
// So we use a one which is similar and will be catched properly.
|
||||||
|
throw new OperationCanceledException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TryProcessClient(MqttConnectedClient connection, DateTime now)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!connection.IsRunning)
|
||||||
{
|
{
|
||||||
TryProcessClient(client, now);
|
// The connection is already dead or just created so there is no need to check it.
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (connection.ConnectPacket.KeepAlivePeriod == 0)
|
||||||
|
{
|
||||||
|
// The keep alive feature is not used by the current connection.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Values described here: [MQTT-3.1.2-24].
|
||||||
|
// If the client sends 5 sec. the server will allow up to 7.5 seconds.
|
||||||
|
// If the client sends 1 sec. the server will allow up to 1.5 seconds.
|
||||||
|
var maxSecondsWithoutPacket = connection.ConnectPacket.KeepAlivePeriod * 1.5D;
|
||||||
|
|
||||||
|
var secondsWithoutPackage = (now - connection.Statistics.LastPacketSentTimestamp).TotalSeconds;
|
||||||
|
if (secondsWithoutPackage < maxSecondsWithoutPacket)
|
||||||
|
{
|
||||||
|
// A packet was received before the timeout is affected.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.Warning("Client '{0}': Did not receive any packet or keep alive signal", connection.Id);
|
||||||
|
|
||||||
|
// Execute the disconnection in background so that the keep alive monitor can continue
|
||||||
|
// with checking other connections.
|
||||||
|
// We do not need to wait for the task so no await is needed.
|
||||||
|
// Also the internal state of the connection must be swapped to "Finalizing" because the
|
||||||
|
// next iteration of the keep alive timer happens.
|
||||||
|
_ = connection.StopAsync(DefaultDisconnectOptions);
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
_logger.Error(exception, "Client {0}: Unhandled exception while checking keep alive timeouts", connection.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TryProcessClients()
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
foreach (var client in _sessionsManager.GetClients())
|
||||||
|
{
|
||||||
|
TryProcessClient(client, now);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,78 +4,77 @@
|
|||||||
|
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
|
||||||
namespace MQTTnet.Server.Internal
|
namespace MQTTnet.Server.Internal;
|
||||||
|
|
||||||
|
public sealed class MqttSessionsStorage
|
||||||
{
|
{
|
||||||
public sealed class MqttSessionsStorage
|
readonly Dictionary<string, MqttSession> _sessions = new Dictionary<string, MqttSession>(4096);
|
||||||
|
|
||||||
|
public void Clear()
|
||||||
{
|
{
|
||||||
readonly Dictionary<string, MqttSession> _sessions = new Dictionary<string, MqttSession>(4096);
|
// Make sure that the sessions are also getting disposed!
|
||||||
|
// This object can be reused after disposing.
|
||||||
|
Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
public void Clear()
|
public void Dispose()
|
||||||
|
{
|
||||||
|
foreach (var sessionItem in _sessions)
|
||||||
{
|
{
|
||||||
// Make sure that the sessions are also getting disposed!
|
sessionItem.Value.Dispose();
|
||||||
// This object can be reused after disposing.
|
|
||||||
Dispose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
_sessions.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyCollection<MqttSession> ReadAllSessions()
|
||||||
|
{
|
||||||
|
return new ReadOnlyCollection<MqttSession>(_sessions.Values.ToList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetSession(string id, out MqttSession session)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(id);
|
||||||
|
|
||||||
|
if (!_sessions.TryGetValue(id, out session))
|
||||||
{
|
{
|
||||||
foreach (var sessionItem in _sessions)
|
|
||||||
{
|
|
||||||
sessionItem.Value.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
_sessions.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
public IReadOnlyCollection<MqttSession> ReadAllSessions()
|
|
||||||
{
|
|
||||||
return new ReadOnlyCollection<MqttSession>(_sessions.Values.ToList());
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryGetSession(string id, out MqttSession session)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(id);
|
|
||||||
|
|
||||||
if (!_sessions.TryGetValue(id, out session))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the Session Expiry Interval is 0xFFFFFFFF (UINT_MAX), the Session does not expire.
|
|
||||||
if (session.ExpiryInterval > 0 && session.ExpiryInterval != uint.MaxValue)
|
|
||||||
{
|
|
||||||
if (session.DisconnectedTimestamp.HasValue)
|
|
||||||
{
|
|
||||||
if (DateTime.UtcNow > session.DisconnectedTimestamp.Value.AddSeconds(session.ExpiryInterval))
|
|
||||||
{
|
|
||||||
TryRemoveSession(id, out var oldSession);
|
|
||||||
oldSession?.Dispose();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryRemoveSession(string id, out MqttSession session)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(id);
|
|
||||||
|
|
||||||
if (_sessions.TryGetValue(id, out session))
|
|
||||||
{
|
|
||||||
_sessions.Remove(id);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateSession(string id, MqttSession session)
|
// If the Session Expiry Interval is 0xFFFFFFFF (UINT_MAX), the Session does not expire.
|
||||||
|
if (session.ExpiryInterval > 0 && session.ExpiryInterval != uint.MaxValue)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(id);
|
if (session.DisconnectedTimestamp.HasValue)
|
||||||
|
{
|
||||||
_sessions[id] = session;
|
if (DateTime.UtcNow > session.DisconnectedTimestamp.Value.AddSeconds(session.ExpiryInterval))
|
||||||
|
{
|
||||||
|
TryRemoveSession(id, out var oldSession);
|
||||||
|
oldSession?.Dispose();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryRemoveSession(string id, out MqttSession session)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(id);
|
||||||
|
|
||||||
|
if (_sessions.TryGetValue(id, out session))
|
||||||
|
{
|
||||||
|
_sessions.Remove(id);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateSession(string id, MqttSession session)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(id);
|
||||||
|
|
||||||
|
_sessions[id] = session;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,47 +4,46 @@
|
|||||||
|
|
||||||
using MQTTnet.Protocol;
|
using MQTTnet.Protocol;
|
||||||
|
|
||||||
namespace MQTTnet.Server.Internal
|
namespace MQTTnet.Server.Internal;
|
||||||
|
|
||||||
|
public sealed class MqttSubscription
|
||||||
{
|
{
|
||||||
public sealed class MqttSubscription
|
public MqttSubscription(
|
||||||
|
string topic,
|
||||||
|
bool noLocal,
|
||||||
|
MqttRetainHandling retainHandling,
|
||||||
|
bool retainAsPublished,
|
||||||
|
MqttQualityOfServiceLevel qualityOfServiceLevel,
|
||||||
|
uint identifier)
|
||||||
{
|
{
|
||||||
public MqttSubscription(
|
Topic = topic;
|
||||||
string topic,
|
NoLocal = noLocal;
|
||||||
bool noLocal,
|
RetainHandling = retainHandling;
|
||||||
MqttRetainHandling retainHandling,
|
RetainAsPublished = retainAsPublished;
|
||||||
bool retainAsPublished,
|
GrantedQualityOfServiceLevel = qualityOfServiceLevel;
|
||||||
MqttQualityOfServiceLevel qualityOfServiceLevel,
|
Identifier = identifier;
|
||||||
uint identifier)
|
|
||||||
{
|
|
||||||
Topic = topic;
|
|
||||||
NoLocal = noLocal;
|
|
||||||
RetainHandling = retainHandling;
|
|
||||||
RetainAsPublished = retainAsPublished;
|
|
||||||
GrantedQualityOfServiceLevel = qualityOfServiceLevel;
|
|
||||||
Identifier = identifier;
|
|
||||||
|
|
||||||
MqttTopicHash.Calculate(Topic, out var hash, out var hashMask, out var hasWildcard);
|
MqttTopicHash.Calculate(Topic, out var hash, out var hashMask, out var hasWildcard);
|
||||||
TopicHash = hash;
|
TopicHash = hash;
|
||||||
TopicHashMask = hashMask;
|
TopicHashMask = hashMask;
|
||||||
TopicHasWildcard = hasWildcard;
|
TopicHasWildcard = hasWildcard;
|
||||||
}
|
|
||||||
|
|
||||||
public MqttQualityOfServiceLevel GrantedQualityOfServiceLevel { get; }
|
|
||||||
|
|
||||||
public uint Identifier { get; }
|
|
||||||
|
|
||||||
public bool NoLocal { get; }
|
|
||||||
|
|
||||||
public bool RetainAsPublished { get; }
|
|
||||||
|
|
||||||
public MqttRetainHandling RetainHandling { get; }
|
|
||||||
|
|
||||||
public string Topic { get; }
|
|
||||||
|
|
||||||
public ulong TopicHash { get; }
|
|
||||||
|
|
||||||
public ulong TopicHashMask { get; }
|
|
||||||
|
|
||||||
public bool TopicHasWildcard { get; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public MqttQualityOfServiceLevel GrantedQualityOfServiceLevel { get; }
|
||||||
|
|
||||||
|
public uint Identifier { get; }
|
||||||
|
|
||||||
|
public bool NoLocal { get; }
|
||||||
|
|
||||||
|
public bool RetainAsPublished { get; }
|
||||||
|
|
||||||
|
public MqttRetainHandling RetainHandling { get; }
|
||||||
|
|
||||||
|
public string Topic { get; }
|
||||||
|
|
||||||
|
public ulong TopicHash { get; }
|
||||||
|
|
||||||
|
public ulong TopicHashMask { get; }
|
||||||
|
|
||||||
|
public bool TopicHasWildcard { get; }
|
||||||
}
|
}
|
||||||
@@ -2,267 +2,266 @@
|
|||||||
// The .NET Foundation licenses this file to you under the MIT license.
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
namespace MQTTnet.Server.Internal
|
namespace MQTTnet.Server.Internal;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The MqttSubscription object stores subscription parameters and calculates
|
||||||
|
* topic hashes.
|
||||||
|
*
|
||||||
|
* Use of Topic Hashes to improve message processing performance
|
||||||
|
* =============================================================
|
||||||
|
*
|
||||||
|
* Motivation
|
||||||
|
* -----------
|
||||||
|
* In a typical use case for MQTT there may be many publishers (sensors or
|
||||||
|
* other devices in the field) and few subscribers (monitoring all or many topics).
|
||||||
|
* Each publisher may have one or more topic(s) to publish and therefore both, the
|
||||||
|
* number of publishers and the number of topics may be large.
|
||||||
|
*
|
||||||
|
* Maintaining subscribers in a separate container
|
||||||
|
* -----------------------------------------------
|
||||||
|
* Instead of placing all sessions into a single _sessions container, subscribers
|
||||||
|
* are added into another _subscriberSessions container (if a client is a
|
||||||
|
* subscriber and a publisher then the client is present in both containers). The
|
||||||
|
* cost is some additional bookkeeping work upon subscription where each client
|
||||||
|
* session needs to maintain a list of subscribed topics.
|
||||||
|
*
|
||||||
|
* When the client subscribes to the first topic, then the session manager adds
|
||||||
|
* the client to the _subscriberSessions container, and when the client
|
||||||
|
* unsubscribes from the last topic then the session manager removes the client
|
||||||
|
* from the container. Now, when an application message arrives, only the list of
|
||||||
|
* subscribers need processing instead of looping through potentially thousands of
|
||||||
|
* publishers.
|
||||||
|
*
|
||||||
|
* Improving subscriber topic lookup
|
||||||
|
* ---------------------------------
|
||||||
|
* For each subscriber, it needs to be determined whether an application message
|
||||||
|
* matches any topic the subscriber has subscribed to. There may only be few
|
||||||
|
* subscribers but there may be many subscribed topics, including wildcard topics.
|
||||||
|
*
|
||||||
|
* The implemented approach uses a topic hash and a hash mask calculated on the
|
||||||
|
* subscribed topic and the published topic (the application message topic) to
|
||||||
|
* find candidates for a match, with the existing match logic evaluating a reduced
|
||||||
|
* number of candidates.
|
||||||
|
*
|
||||||
|
* For each subscription, the topic hash and a hash mask is stored with the
|
||||||
|
* subscription, and for each application message received, the hash is calculated
|
||||||
|
* for the published topic before attempting to find matching subscriptions. The
|
||||||
|
* hash calculation itself is simple and does not have a large performance impact.
|
||||||
|
*
|
||||||
|
* We'll first explain how topic hashes and hash masks are constructed and then how
|
||||||
|
* they are used.
|
||||||
|
*
|
||||||
|
* Topic hash
|
||||||
|
* ----------
|
||||||
|
* Topic hashes are stored as 64-bit numbers. Each byte within the 64-bit number
|
||||||
|
* relates to one MQTT topic level. A checksum is calculated for each topic level
|
||||||
|
* by iterating over the characters within the topic level (cast to byte) and the
|
||||||
|
* result is stored into the corresponding byte of the 64-bit number. If a topic
|
||||||
|
* level contains a wildcard character, then 0x00 is stored instead of the
|
||||||
|
* checksum.
|
||||||
|
*
|
||||||
|
* If there are less than 8 levels then the rest of the 64-bit number is filled
|
||||||
|
* with 0xff. If there are more than 8 levels then topics where the first 8 MQTT
|
||||||
|
* topic levels are identical will have the same hash value.
|
||||||
|
*
|
||||||
|
* This is the topic hash for the MQTT topic below: 0x655D4AF1FFFFFF
|
||||||
|
*
|
||||||
|
* client1/building1/level1/sensor1 (empty) (empty) (empty) (empty)
|
||||||
|
* \_____/ \_______/ \____/ \_____/ \_____/ \_____/ \_____/ \_____/
|
||||||
|
* | | | | | | | |
|
||||||
|
* 0x65 0x5D 0x4A 0xF1 0xFF 0xFF 0xFF 0xFF
|
||||||
|
*
|
||||||
|
* This is the topic hash for an MQTT topic containing a wildcard: 0x655D00F1FFFFFF
|
||||||
|
*
|
||||||
|
* client1/building1/ + /sensor1 (empty) (empty) (empty) (empty)
|
||||||
|
* \_____/ \_______/ \_/ \_____/ \_____/ \_____/ \_____/ \_____/
|
||||||
|
* | | | | | | | |
|
||||||
|
* 0x65 0x5D 0 0xF1 0xFF 0xFF 0xFF 0xFF
|
||||||
|
*
|
||||||
|
* For topics that contain the multi level wildcard # at the end, the topic hash
|
||||||
|
* is filled with 0x00: 0x65004A00000000
|
||||||
|
*
|
||||||
|
* client1/ + /level1/ # (empty) (empty) (empty) (empty)
|
||||||
|
* \_____/ \_/ \____/ \_/ \_____/ \_____/ \_____/ \_____/
|
||||||
|
* | | | | | | | |
|
||||||
|
* 0x65 0 0x4A 0 0 0 0 0
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* Topic hash mask
|
||||||
|
* ---------------
|
||||||
|
* The hash mask simply contains 0xFF for non-wildcard topic levels and 0x00 for
|
||||||
|
* wildcard topic levels. Here are the topic hash masks for the examples above.
|
||||||
|
*
|
||||||
|
* client1/building1/level1/sensor1 (empty) (empty) (empty) (empty)
|
||||||
|
* \_____/ \_______/ \____/ \_____/ \_____/ \_____/ \_____/ \_____/
|
||||||
|
* | | | | | | | |
|
||||||
|
* 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF
|
||||||
|
*
|
||||||
|
* client1/building1/ + /sensor1 (empty) (empty) (empty) (empty)
|
||||||
|
* \_____/ \_______/ \_/ \_____/ \_____/ \_____/ \_____/ \_____/
|
||||||
|
* | | | | | | | |
|
||||||
|
* 0xFF 0xFF 0 0xFF 0xFF 0xFF 0xFF 0xFF
|
||||||
|
*
|
||||||
|
* client1/ + /level1/ # (empty) (empty) (empty) (empty)
|
||||||
|
* \_____/ \_/ \____/ \_/ \_____/ \_____/ \_____/ \_____/
|
||||||
|
* | | | | | | | |
|
||||||
|
* 0xFF 0 0xFF 0 0 0 0 0
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* Topic hash and hash mask properties
|
||||||
|
* -----------------------------------
|
||||||
|
* The following properties of topic hashes and hash masks can be exploited to
|
||||||
|
* find potentially matching subscribed topics for a given published topic.
|
||||||
|
*
|
||||||
|
* (1) If a subscribed topic does not contain wildcards then the topic hash of the
|
||||||
|
* subscribed topic must be equal to the topic hash of the published topic,
|
||||||
|
* otherwise the subscribed topic cannot be a candidate for a match.
|
||||||
|
*
|
||||||
|
* (2) If a subscribed topic contains wildcards then the hash of the published
|
||||||
|
* topic masked with the subscribed topic's hash mask must be equal to the hash of
|
||||||
|
* the subscribed topic. I.e. a subscribed topic is a candidate for a match if:
|
||||||
|
* (publishedTopicHash & subscribedTopicHashMask) == subscribedTopicHash
|
||||||
|
*
|
||||||
|
* (3) If a subscribed topic contains wildcards then any potentially matching
|
||||||
|
* published topic must have a hash value that is greater than or equal to the
|
||||||
|
* hash value of the subscribed topic (because the subscribed topic contains
|
||||||
|
* zeroes in wildcard positions).
|
||||||
|
*
|
||||||
|
* Match finding
|
||||||
|
* -------------
|
||||||
|
* The subscription manager maintains two separate dictionaries to assist finding
|
||||||
|
* matches using topic hashes: a _noWildcardSubscriptionsByTopicHash dictionary
|
||||||
|
* containing all subscriptions that do not have wildcards, and a
|
||||||
|
* _wildcardSubscriptionsByTopicHash dictionary containing subscriptions with
|
||||||
|
* wildcards.
|
||||||
|
*
|
||||||
|
* For subscriptions without wildcards, all potential candidates for a match are
|
||||||
|
* obtained by a single look-up (exploiting point 1 above).
|
||||||
|
*
|
||||||
|
* For subscriptions with wildcards, the subscription manager loops through the
|
||||||
|
* wildcard subscriptions and selects candidates that satisfy condition
|
||||||
|
* (publishedTopicHash & subscribedTopicMask) == subscribedTopicHash (point 2).
|
||||||
|
* The loop could exit early if wildcard subscriptions were stored into a sorted
|
||||||
|
* dictionary (utilizing point 3), but, after testing, there does not seem to be
|
||||||
|
* any real benefit doing so.
|
||||||
|
*
|
||||||
|
* Other considerations
|
||||||
|
* --------------------
|
||||||
|
* Characters in the topic string are cast to byte and any additional bytes in a
|
||||||
|
* multi-byte character are disregarded. Best guess is that this does not impact
|
||||||
|
* performance in practice.
|
||||||
|
*
|
||||||
|
* Instead of one-byte checksums per topic level, one-word checksums per topic
|
||||||
|
* level could be used. If most topics contained four levels or less then hash
|
||||||
|
* buckets would be shallower.
|
||||||
|
*
|
||||||
|
* For very large numbers of topics, performing a parallel search may help further.
|
||||||
|
*
|
||||||
|
* To also handle a larger number of subscribers, it may be beneficial to maintain
|
||||||
|
* a subscribers-by-subscription-topic dictionary.
|
||||||
|
*/
|
||||||
|
public static class MqttTopicHash
|
||||||
{
|
{
|
||||||
/*
|
public static void Calculate(string topic, out ulong resultHash, out ulong resultHashMask, out bool resultHasWildcard)
|
||||||
* The MqttSubscription object stores subscription parameters and calculates
|
|
||||||
* topic hashes.
|
|
||||||
*
|
|
||||||
* Use of Topic Hashes to improve message processing performance
|
|
||||||
* =============================================================
|
|
||||||
*
|
|
||||||
* Motivation
|
|
||||||
* -----------
|
|
||||||
* In a typical use case for MQTT there may be many publishers (sensors or
|
|
||||||
* other devices in the field) and few subscribers (monitoring all or many topics).
|
|
||||||
* Each publisher may have one or more topic(s) to publish and therefore both, the
|
|
||||||
* number of publishers and the number of topics may be large.
|
|
||||||
*
|
|
||||||
* Maintaining subscribers in a separate container
|
|
||||||
* -----------------------------------------------
|
|
||||||
* Instead of placing all sessions into a single _sessions container, subscribers
|
|
||||||
* are added into another _subscriberSessions container (if a client is a
|
|
||||||
* subscriber and a publisher then the client is present in both containers). The
|
|
||||||
* cost is some additional bookkeeping work upon subscription where each client
|
|
||||||
* session needs to maintain a list of subscribed topics.
|
|
||||||
*
|
|
||||||
* When the client subscribes to the first topic, then the session manager adds
|
|
||||||
* the client to the _subscriberSessions container, and when the client
|
|
||||||
* unsubscribes from the last topic then the session manager removes the client
|
|
||||||
* from the container. Now, when an application message arrives, only the list of
|
|
||||||
* subscribers need processing instead of looping through potentially thousands of
|
|
||||||
* publishers.
|
|
||||||
*
|
|
||||||
* Improving subscriber topic lookup
|
|
||||||
* ---------------------------------
|
|
||||||
* For each subscriber, it needs to be determined whether an application message
|
|
||||||
* matches any topic the subscriber has subscribed to. There may only be few
|
|
||||||
* subscribers but there may be many subscribed topics, including wildcard topics.
|
|
||||||
*
|
|
||||||
* The implemented approach uses a topic hash and a hash mask calculated on the
|
|
||||||
* subscribed topic and the published topic (the application message topic) to
|
|
||||||
* find candidates for a match, with the existing match logic evaluating a reduced
|
|
||||||
* number of candidates.
|
|
||||||
*
|
|
||||||
* For each subscription, the topic hash and a hash mask is stored with the
|
|
||||||
* subscription, and for each application message received, the hash is calculated
|
|
||||||
* for the published topic before attempting to find matching subscriptions. The
|
|
||||||
* hash calculation itself is simple and does not have a large performance impact.
|
|
||||||
*
|
|
||||||
* We'll first explain how topic hashes and hash masks are constructed and then how
|
|
||||||
* they are used.
|
|
||||||
*
|
|
||||||
* Topic hash
|
|
||||||
* ----------
|
|
||||||
* Topic hashes are stored as 64-bit numbers. Each byte within the 64-bit number
|
|
||||||
* relates to one MQTT topic level. A checksum is calculated for each topic level
|
|
||||||
* by iterating over the characters within the topic level (cast to byte) and the
|
|
||||||
* result is stored into the corresponding byte of the 64-bit number. If a topic
|
|
||||||
* level contains a wildcard character, then 0x00 is stored instead of the
|
|
||||||
* checksum.
|
|
||||||
*
|
|
||||||
* If there are less than 8 levels then the rest of the 64-bit number is filled
|
|
||||||
* with 0xff. If there are more than 8 levels then topics where the first 8 MQTT
|
|
||||||
* topic levels are identical will have the same hash value.
|
|
||||||
*
|
|
||||||
* This is the topic hash for the MQTT topic below: 0x655D4AF1FFFFFF
|
|
||||||
*
|
|
||||||
* client1/building1/level1/sensor1 (empty) (empty) (empty) (empty)
|
|
||||||
* \_____/ \_______/ \____/ \_____/ \_____/ \_____/ \_____/ \_____/
|
|
||||||
* | | | | | | | |
|
|
||||||
* 0x65 0x5D 0x4A 0xF1 0xFF 0xFF 0xFF 0xFF
|
|
||||||
*
|
|
||||||
* This is the topic hash for an MQTT topic containing a wildcard: 0x655D00F1FFFFFF
|
|
||||||
*
|
|
||||||
* client1/building1/ + /sensor1 (empty) (empty) (empty) (empty)
|
|
||||||
* \_____/ \_______/ \_/ \_____/ \_____/ \_____/ \_____/ \_____/
|
|
||||||
* | | | | | | | |
|
|
||||||
* 0x65 0x5D 0 0xF1 0xFF 0xFF 0xFF 0xFF
|
|
||||||
*
|
|
||||||
* For topics that contain the multi level wildcard # at the end, the topic hash
|
|
||||||
* is filled with 0x00: 0x65004A00000000
|
|
||||||
*
|
|
||||||
* client1/ + /level1/ # (empty) (empty) (empty) (empty)
|
|
||||||
* \_____/ \_/ \____/ \_/ \_____/ \_____/ \_____/ \_____/
|
|
||||||
* | | | | | | | |
|
|
||||||
* 0x65 0 0x4A 0 0 0 0 0
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* Topic hash mask
|
|
||||||
* ---------------
|
|
||||||
* The hash mask simply contains 0xFF for non-wildcard topic levels and 0x00 for
|
|
||||||
* wildcard topic levels. Here are the topic hash masks for the examples above.
|
|
||||||
*
|
|
||||||
* client1/building1/level1/sensor1 (empty) (empty) (empty) (empty)
|
|
||||||
* \_____/ \_______/ \____/ \_____/ \_____/ \_____/ \_____/ \_____/
|
|
||||||
* | | | | | | | |
|
|
||||||
* 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF
|
|
||||||
*
|
|
||||||
* client1/building1/ + /sensor1 (empty) (empty) (empty) (empty)
|
|
||||||
* \_____/ \_______/ \_/ \_____/ \_____/ \_____/ \_____/ \_____/
|
|
||||||
* | | | | | | | |
|
|
||||||
* 0xFF 0xFF 0 0xFF 0xFF 0xFF 0xFF 0xFF
|
|
||||||
*
|
|
||||||
* client1/ + /level1/ # (empty) (empty) (empty) (empty)
|
|
||||||
* \_____/ \_/ \____/ \_/ \_____/ \_____/ \_____/ \_____/
|
|
||||||
* | | | | | | | |
|
|
||||||
* 0xFF 0 0xFF 0 0 0 0 0
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* Topic hash and hash mask properties
|
|
||||||
* -----------------------------------
|
|
||||||
* The following properties of topic hashes and hash masks can be exploited to
|
|
||||||
* find potentially matching subscribed topics for a given published topic.
|
|
||||||
*
|
|
||||||
* (1) If a subscribed topic does not contain wildcards then the topic hash of the
|
|
||||||
* subscribed topic must be equal to the topic hash of the published topic,
|
|
||||||
* otherwise the subscribed topic cannot be a candidate for a match.
|
|
||||||
*
|
|
||||||
* (2) If a subscribed topic contains wildcards then the hash of the published
|
|
||||||
* topic masked with the subscribed topic's hash mask must be equal to the hash of
|
|
||||||
* the subscribed topic. I.e. a subscribed topic is a candidate for a match if:
|
|
||||||
* (publishedTopicHash & subscribedTopicHashMask) == subscribedTopicHash
|
|
||||||
*
|
|
||||||
* (3) If a subscribed topic contains wildcards then any potentially matching
|
|
||||||
* published topic must have a hash value that is greater than or equal to the
|
|
||||||
* hash value of the subscribed topic (because the subscribed topic contains
|
|
||||||
* zeroes in wildcard positions).
|
|
||||||
*
|
|
||||||
* Match finding
|
|
||||||
* -------------
|
|
||||||
* The subscription manager maintains two separate dictionaries to assist finding
|
|
||||||
* matches using topic hashes: a _noWildcardSubscriptionsByTopicHash dictionary
|
|
||||||
* containing all subscriptions that do not have wildcards, and a
|
|
||||||
* _wildcardSubscriptionsByTopicHash dictionary containing subscriptions with
|
|
||||||
* wildcards.
|
|
||||||
*
|
|
||||||
* For subscriptions without wildcards, all potential candidates for a match are
|
|
||||||
* obtained by a single look-up (exploiting point 1 above).
|
|
||||||
*
|
|
||||||
* For subscriptions with wildcards, the subscription manager loops through the
|
|
||||||
* wildcard subscriptions and selects candidates that satisfy condition
|
|
||||||
* (publishedTopicHash & subscribedTopicMask) == subscribedTopicHash (point 2).
|
|
||||||
* The loop could exit early if wildcard subscriptions were stored into a sorted
|
|
||||||
* dictionary (utilizing point 3), but, after testing, there does not seem to be
|
|
||||||
* any real benefit doing so.
|
|
||||||
*
|
|
||||||
* Other considerations
|
|
||||||
* --------------------
|
|
||||||
* Characters in the topic string are cast to byte and any additional bytes in a
|
|
||||||
* multi-byte character are disregarded. Best guess is that this does not impact
|
|
||||||
* performance in practice.
|
|
||||||
*
|
|
||||||
* Instead of one-byte checksums per topic level, one-word checksums per topic
|
|
||||||
* level could be used. If most topics contained four levels or less then hash
|
|
||||||
* buckets would be shallower.
|
|
||||||
*
|
|
||||||
* For very large numbers of topics, performing a parallel search may help further.
|
|
||||||
*
|
|
||||||
* To also handle a larger number of subscribers, it may be beneficial to maintain
|
|
||||||
* a subscribers-by-subscription-topic dictionary.
|
|
||||||
*/
|
|
||||||
public static class MqttTopicHash
|
|
||||||
{
|
{
|
||||||
public static void Calculate(string topic, out ulong resultHash, out ulong resultHashMask, out bool resultHasWildcard)
|
// calculate topic hash
|
||||||
|
ulong hash = 0;
|
||||||
|
ulong hashMaskInverted = 0;
|
||||||
|
ulong levelBitMask = 0;
|
||||||
|
ulong fillLevelBitMask = 0;
|
||||||
|
var hasWildcard = false;
|
||||||
|
byte checkSum = 0;
|
||||||
|
var level = 0;
|
||||||
|
|
||||||
|
var i = 0;
|
||||||
|
while (i < topic.Length)
|
||||||
{
|
{
|
||||||
// calculate topic hash
|
var c = topic[i];
|
||||||
ulong hash = 0;
|
if (c == MqttTopicFilterComparer.LevelSeparator)
|
||||||
ulong hashMaskInverted = 0;
|
|
||||||
ulong levelBitMask = 0;
|
|
||||||
ulong fillLevelBitMask = 0;
|
|
||||||
var hasWildcard = false;
|
|
||||||
byte checkSum = 0;
|
|
||||||
var level = 0;
|
|
||||||
|
|
||||||
var i = 0;
|
|
||||||
while (i < topic.Length)
|
|
||||||
{
|
|
||||||
var c = topic[i];
|
|
||||||
if (c == MqttTopicFilterComparer.LevelSeparator)
|
|
||||||
{
|
|
||||||
// done with this level
|
|
||||||
hash <<= 8;
|
|
||||||
hash |= checkSum;
|
|
||||||
hashMaskInverted <<= 8;
|
|
||||||
hashMaskInverted |= levelBitMask;
|
|
||||||
checkSum = 0;
|
|
||||||
levelBitMask = 0;
|
|
||||||
++level;
|
|
||||||
if (level >= 8)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (c == MqttTopicFilterComparer.SingleLevelWildcard)
|
|
||||||
{
|
|
||||||
levelBitMask = 0xff;
|
|
||||||
hasWildcard = true;
|
|
||||||
}
|
|
||||||
else if (c == MqttTopicFilterComparer.MultiLevelWildcard)
|
|
||||||
{
|
|
||||||
// checksum is zero for a valid topic
|
|
||||||
levelBitMask = 0xff;
|
|
||||||
// fill rest with this fillLevelBitMask
|
|
||||||
fillLevelBitMask = 0xff;
|
|
||||||
hasWildcard = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// The checksum should be designed to reduce the hash bucket depth for the expected
|
|
||||||
// fairly regularly named MQTT topics that don't differ much,
|
|
||||||
// i.e. "room1/sensor1"
|
|
||||||
// "room1/sensor2"
|
|
||||||
// "room1/sensor3"
|
|
||||||
// etc.
|
|
||||||
if ((c & 1) == 0)
|
|
||||||
{
|
|
||||||
checkSum += (byte)c;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
checkSum ^= (byte)(c >> 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
++i;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shift hash left and leave zeroes to fill ulong
|
|
||||||
if (level < 8)
|
|
||||||
{
|
{
|
||||||
|
// done with this level
|
||||||
hash <<= 8;
|
hash <<= 8;
|
||||||
hash |= checkSum;
|
hash |= checkSum;
|
||||||
hashMaskInverted <<= 8;
|
hashMaskInverted <<= 8;
|
||||||
hashMaskInverted |= levelBitMask;
|
hashMaskInverted |= levelBitMask;
|
||||||
|
checkSum = 0;
|
||||||
|
levelBitMask = 0;
|
||||||
++level;
|
++level;
|
||||||
while (level < 8)
|
if (level >= 8)
|
||||||
{
|
{
|
||||||
hash <<= 8;
|
break;
|
||||||
hashMaskInverted <<= 8;
|
|
||||||
hashMaskInverted |= fillLevelBitMask;
|
|
||||||
++level;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (c == MqttTopicFilterComparer.SingleLevelWildcard)
|
||||||
if (!hasWildcard)
|
|
||||||
{
|
{
|
||||||
while (i < topic.Length)
|
levelBitMask = 0xff;
|
||||||
|
hasWildcard = true;
|
||||||
|
}
|
||||||
|
else if (c == MqttTopicFilterComparer.MultiLevelWildcard)
|
||||||
|
{
|
||||||
|
// checksum is zero for a valid topic
|
||||||
|
levelBitMask = 0xff;
|
||||||
|
// fill rest with this fillLevelBitMask
|
||||||
|
fillLevelBitMask = 0xff;
|
||||||
|
hasWildcard = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// The checksum should be designed to reduce the hash bucket depth for the expected
|
||||||
|
// fairly regularly named MQTT topics that don't differ much,
|
||||||
|
// i.e. "room1/sensor1"
|
||||||
|
// "room1/sensor2"
|
||||||
|
// "room1/sensor3"
|
||||||
|
// etc.
|
||||||
|
if ((c & 1) == 0)
|
||||||
{
|
{
|
||||||
var c = topic[i];
|
checkSum += (byte)c;
|
||||||
if (c == MqttTopicFilterComparer.SingleLevelWildcard || c == MqttTopicFilterComparer.MultiLevelWildcard)
|
}
|
||||||
{
|
else
|
||||||
hasWildcard = true;
|
{
|
||||||
break;
|
checkSum ^= (byte)(c >> 1);
|
||||||
}
|
|
||||||
|
|
||||||
++i;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resultHash = hash;
|
++i;
|
||||||
resultHashMask = ~hashMaskInverted;
|
|
||||||
resultHasWildcard = hasWildcard;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Shift hash left and leave zeroes to fill ulong
|
||||||
|
if (level < 8)
|
||||||
|
{
|
||||||
|
hash <<= 8;
|
||||||
|
hash |= checkSum;
|
||||||
|
hashMaskInverted <<= 8;
|
||||||
|
hashMaskInverted |= levelBitMask;
|
||||||
|
++level;
|
||||||
|
while (level < 8)
|
||||||
|
{
|
||||||
|
hash <<= 8;
|
||||||
|
hashMaskInverted <<= 8;
|
||||||
|
hashMaskInverted |= fillLevelBitMask;
|
||||||
|
++level;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasWildcard)
|
||||||
|
{
|
||||||
|
while (i < topic.Length)
|
||||||
|
{
|
||||||
|
var c = topic[i];
|
||||||
|
if (c == MqttTopicFilterComparer.SingleLevelWildcard || c == MqttTopicFilterComparer.MultiLevelWildcard)
|
||||||
|
{
|
||||||
|
hasWildcard = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resultHash = hash;
|
||||||
|
resultHashMask = ~hashMaskInverted;
|
||||||
|
resultHasWildcard = hasWildcard;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,23 +5,22 @@
|
|||||||
using MQTTnet.Packets;
|
using MQTTnet.Packets;
|
||||||
using MQTTnet.Protocol;
|
using MQTTnet.Protocol;
|
||||||
|
|
||||||
namespace MQTTnet.Server.Internal
|
namespace MQTTnet.Server.Internal;
|
||||||
|
|
||||||
|
public sealed class SubscribeResult
|
||||||
{
|
{
|
||||||
public sealed class SubscribeResult
|
public SubscribeResult(int topicsCount)
|
||||||
{
|
{
|
||||||
public SubscribeResult(int topicsCount)
|
ReasonCodes = new List<MqttSubscribeReasonCode>(topicsCount);
|
||||||
{
|
|
||||||
ReasonCodes = new List<MqttSubscribeReasonCode>(topicsCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool CloseConnection { get; set; }
|
|
||||||
|
|
||||||
public List<MqttSubscribeReasonCode> ReasonCodes { get; set; }
|
|
||||||
|
|
||||||
public string ReasonString { get; set; }
|
|
||||||
|
|
||||||
public List<MqttRetainedMessageMatch> RetainedMessages { get; set; }
|
|
||||||
|
|
||||||
public List<MqttUserProperty> UserProperties { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool CloseConnection { get; set; }
|
||||||
|
|
||||||
|
public List<MqttSubscribeReasonCode> ReasonCodes { get; set; }
|
||||||
|
|
||||||
|
public string ReasonString { get; set; }
|
||||||
|
|
||||||
|
public List<MqttRetainedMessageMatch> RetainedMessages { get; set; }
|
||||||
|
|
||||||
|
public List<MqttUserProperty> UserProperties { get; set; }
|
||||||
}
|
}
|
||||||
@@ -1,31 +1,30 @@
|
|||||||
namespace MQTTnet.Server.Internal
|
namespace MQTTnet.Server.Internal;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper class that stores subscriptions by their topic hash mask.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class TopicHashMaskSubscriptions
|
||||||
{
|
{
|
||||||
/// <summary>
|
public Dictionary<ulong, HashSet<MqttSubscription>> SubscriptionsByHashMask { get; } = new Dictionary<ulong, HashSet<MqttSubscription>>();
|
||||||
/// Helper class that stores subscriptions by their topic hash mask.
|
|
||||||
/// </summary>
|
public void AddSubscription(MqttSubscription subscription)
|
||||||
public sealed class TopicHashMaskSubscriptions
|
|
||||||
{
|
{
|
||||||
public Dictionary<ulong, HashSet<MqttSubscription>> SubscriptionsByHashMask { get; } = new Dictionary<ulong, HashSet<MqttSubscription>>();
|
if (!SubscriptionsByHashMask.TryGetValue(subscription.TopicHashMask, out var subscriptions))
|
||||||
|
|
||||||
public void AddSubscription(MqttSubscription subscription)
|
|
||||||
{
|
{
|
||||||
if (!SubscriptionsByHashMask.TryGetValue(subscription.TopicHashMask, out var subscriptions))
|
subscriptions = new HashSet<MqttSubscription>();
|
||||||
{
|
SubscriptionsByHashMask.Add(subscription.TopicHashMask, subscriptions);
|
||||||
subscriptions = new HashSet<MqttSubscription>();
|
|
||||||
SubscriptionsByHashMask.Add(subscription.TopicHashMask, subscriptions);
|
|
||||||
}
|
|
||||||
subscriptions.Add(subscription);
|
|
||||||
}
|
}
|
||||||
|
subscriptions.Add(subscription);
|
||||||
|
}
|
||||||
|
|
||||||
public void RemoveSubscription(MqttSubscription subscription)
|
public void RemoveSubscription(MqttSubscription subscription)
|
||||||
|
{
|
||||||
|
if (SubscriptionsByHashMask.TryGetValue(subscription.TopicHashMask, out var subscriptions))
|
||||||
{
|
{
|
||||||
if (SubscriptionsByHashMask.TryGetValue(subscription.TopicHashMask, out var subscriptions))
|
subscriptions.Remove(subscription);
|
||||||
|
if (subscriptions.Count == 0)
|
||||||
{
|
{
|
||||||
subscriptions.Remove(subscription);
|
SubscriptionsByHashMask.Remove(subscription.TopicHashMask);
|
||||||
if (subscriptions.Count == 0)
|
|
||||||
{
|
|
||||||
SubscriptionsByHashMask.Remove(subscription.TopicHashMask);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,14 +5,13 @@
|
|||||||
using MQTTnet.Packets;
|
using MQTTnet.Packets;
|
||||||
using MQTTnet.Protocol;
|
using MQTTnet.Protocol;
|
||||||
|
|
||||||
namespace MQTTnet.Server.Internal
|
namespace MQTTnet.Server.Internal;
|
||||||
|
|
||||||
|
public sealed class UnsubscribeResult
|
||||||
{
|
{
|
||||||
public sealed class UnsubscribeResult
|
public List<MqttUnsubscribeReasonCode> ReasonCodes { get; } = new List<MqttUnsubscribeReasonCode>(128);
|
||||||
{
|
|
||||||
public List<MqttUnsubscribeReasonCode> ReasonCodes { get; } = new List<MqttUnsubscribeReasonCode>(128);
|
|
||||||
|
|
||||||
public bool CloseConnection { get; set; }
|
public bool CloseConnection { get; set; }
|
||||||
|
|
||||||
public List<MqttUserProperty> UserProperties { get; set; }
|
public List<MqttUserProperty> UserProperties { get; set; }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -29,9 +29,9 @@ public static class MqttServerExtensions
|
|||||||
ArgumentNullException.ThrowIfNull(topic);
|
ArgumentNullException.ThrowIfNull(topic);
|
||||||
|
|
||||||
var payloadBuffer = EmptyBuffer.Array;
|
var payloadBuffer = EmptyBuffer.Array;
|
||||||
if (payload is string stringPayload)
|
if (payload != null)
|
||||||
{
|
{
|
||||||
payloadBuffer = Encoding.UTF8.GetBytes(stringPayload);
|
payloadBuffer = Encoding.UTF8.GetBytes(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
return server.InjectApplicationMessage(
|
return server.InjectApplicationMessage(
|
||||||
|
|||||||
@@ -21,10 +21,7 @@ public sealed class MqttServerFactory
|
|||||||
|
|
||||||
public IMqttNetLogger DefaultLogger { get; }
|
public IMqttNetLogger DefaultLogger { get; }
|
||||||
|
|
||||||
public IList<Func<MqttServerFactory, IMqttServerAdapter>> DefaultServerAdapters { get; } = new List<Func<MqttServerFactory, IMqttServerAdapter>>
|
public IList<Func<MqttServerFactory, IMqttServerAdapter>> DefaultServerAdapters { get; } = [_ => new MqttTcpServerAdapter()];
|
||||||
{
|
|
||||||
factory => new MqttTcpServerAdapter()
|
|
||||||
};
|
|
||||||
|
|
||||||
public IDictionary<object, object> Properties { get; } = new Dictionary<object, object>();
|
public IDictionary<object, object> Properties { get; } = new Dictionary<object, object>();
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,9 @@
|
|||||||
// The .NET Foundation licenses this file to you under the MIT license.
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
namespace MQTTnet.Server
|
namespace MQTTnet.Server;
|
||||||
|
|
||||||
|
public interface IMqttServerCertificateCredentials
|
||||||
{
|
{
|
||||||
public interface IMqttServerCertificateCredentials
|
string Password { get; }
|
||||||
{
|
|
||||||
string Password { get; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -2,12 +2,11 @@
|
|||||||
// The .NET Foundation licenses this file to you under the MIT license.
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
namespace MQTTnet.Server
|
namespace MQTTnet.Server;
|
||||||
{
|
|
||||||
public enum MqttPendingMessagesOverflowStrategy
|
|
||||||
{
|
|
||||||
DropOldestQueuedMessage,
|
|
||||||
|
|
||||||
DropNewMessage
|
public enum MqttPendingMessagesOverflowStrategy
|
||||||
}
|
{
|
||||||
|
DropOldestQueuedMessage,
|
||||||
|
|
||||||
|
DropNewMessage
|
||||||
}
|
}
|
||||||
@@ -2,10 +2,9 @@
|
|||||||
// The .NET Foundation licenses this file to you under the MIT license.
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
namespace MQTTnet.Server
|
namespace MQTTnet.Server;
|
||||||
|
|
||||||
|
public class MqttServerCertificateCredentials : IMqttServerCertificateCredentials
|
||||||
{
|
{
|
||||||
public class MqttServerCertificateCredentials : IMqttServerCertificateCredentials
|
public string Password { get; set; }
|
||||||
{
|
|
||||||
public string Password { get; set; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -2,20 +2,17 @@
|
|||||||
// The .NET Foundation licenses this file to you under the MIT license.
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System;
|
namespace MQTTnet.Server;
|
||||||
|
|
||||||
namespace MQTTnet.Server
|
public sealed class MqttServerKeepAliveOptions
|
||||||
{
|
{
|
||||||
public sealed class MqttServerKeepAliveOptions
|
/// <summary>
|
||||||
{
|
/// When this mode is enabled the MQTT server will not close a connection when the
|
||||||
/// <summary>
|
/// client is currently sending a (large) payload. This may lead to "dead" connections
|
||||||
/// When this mode is enabled the MQTT server will not close a connection when the
|
/// When this mode is disabled the MQTT server will disconnect a client when the keep
|
||||||
/// client is currently sending a (large) payload. This may lead to "dead" connections
|
/// alive timeout is reached even if the client is currently sending a (large) payload.
|
||||||
/// When this mode is disabled the MQTT server will disconnect a client when the keep
|
/// </summary>
|
||||||
/// alive timeout is reached even if the client is currently sending a (large) payload.
|
public bool DisconnectClientWhenReadingPayload { get; set; }
|
||||||
/// </summary>
|
|
||||||
public bool DisconnectClientWhenReadingPayload { get; set; }
|
|
||||||
|
|
||||||
public TimeSpan MonitorInterval { get; set; } = TimeSpan.FromMilliseconds(500);
|
public TimeSpan MonitorInterval { get; set; } = TimeSpan.FromMilliseconds(500);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
// The .NET Foundation licenses this file to you under the MIT license.
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Security;
|
using System.Net.Security;
|
||||||
using System.Security.Authentication;
|
using System.Security.Authentication;
|
||||||
@@ -10,207 +9,206 @@ using MQTTnet.Certificates;
|
|||||||
using System.Security.Cryptography.X509Certificates;
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
|
||||||
// ReSharper disable UnusedMember.Global
|
// ReSharper disable UnusedMember.Global
|
||||||
namespace MQTTnet.Server
|
namespace MQTTnet.Server;
|
||||||
|
|
||||||
|
public class MqttServerOptionsBuilder
|
||||||
{
|
{
|
||||||
public class MqttServerOptionsBuilder
|
readonly MqttServerOptions _options = new();
|
||||||
|
|
||||||
|
public MqttServerOptions Build()
|
||||||
{
|
{
|
||||||
readonly MqttServerOptions _options = new MqttServerOptions();
|
return _options;
|
||||||
|
}
|
||||||
|
|
||||||
public MqttServerOptions Build()
|
public MqttServerOptionsBuilder WithClientCertificate(RemoteCertificateValidationCallback validationCallback = null, bool checkCertificateRevocation = false)
|
||||||
|
{
|
||||||
|
_options.TlsEndpointOptions.ClientCertificateRequired = true;
|
||||||
|
_options.TlsEndpointOptions.CheckCertificateRevocation = checkCertificateRevocation;
|
||||||
|
_options.TlsEndpointOptions.RemoteCertificateValidationCallback = validationCallback;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MqttServerOptionsBuilder WithConnectionBacklog(int value)
|
||||||
|
{
|
||||||
|
_options.DefaultEndpointOptions.ConnectionBacklog = value;
|
||||||
|
_options.TlsEndpointOptions.ConnectionBacklog = value;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MqttServerOptionsBuilder WithDefaultCommunicationTimeout(TimeSpan value)
|
||||||
|
{
|
||||||
|
_options.DefaultCommunicationTimeout = value;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MqttServerOptionsBuilder WithDefaultEndpoint()
|
||||||
|
{
|
||||||
|
_options.DefaultEndpointOptions.IsEnabled = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MqttServerOptionsBuilder WithDefaultEndpointBoundIPAddress(IPAddress value)
|
||||||
|
{
|
||||||
|
_options.DefaultEndpointOptions.BoundInterNetworkAddress = value ?? IPAddress.Any;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MqttServerOptionsBuilder WithDefaultEndpointBoundIPV6Address(IPAddress value)
|
||||||
|
{
|
||||||
|
_options.DefaultEndpointOptions.BoundInterNetworkV6Address = value ?? IPAddress.Any;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MqttServerOptionsBuilder WithDefaultEndpointPort(int value)
|
||||||
|
{
|
||||||
|
_options.DefaultEndpointOptions.Port = value;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MqttServerOptionsBuilder WithDefaultEndpointReuseAddress()
|
||||||
|
{
|
||||||
|
_options.DefaultEndpointOptions.ReuseAddress = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MqttServerOptionsBuilder WithEncryptedEndpoint()
|
||||||
|
{
|
||||||
|
_options.TlsEndpointOptions.IsEnabled = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MqttServerOptionsBuilder WithEncryptedEndpointBoundIPAddress(IPAddress value)
|
||||||
|
{
|
||||||
|
_options.TlsEndpointOptions.BoundInterNetworkAddress = value;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MqttServerOptionsBuilder WithEncryptedEndpointBoundIPV6Address(IPAddress value)
|
||||||
|
{
|
||||||
|
_options.TlsEndpointOptions.BoundInterNetworkV6Address = value;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MqttServerOptionsBuilder WithEncryptedEndpointPort(int value)
|
||||||
|
{
|
||||||
|
_options.TlsEndpointOptions.Port = value;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MqttServerOptionsBuilder WithEncryptionSslProtocol(SslProtocols value)
|
||||||
|
{
|
||||||
|
_options.TlsEndpointOptions.SslProtocol = value;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MqttServerOptionsBuilder WithKeepAlive()
|
||||||
|
{
|
||||||
|
_options.DefaultEndpointOptions.KeepAlive = true;
|
||||||
|
_options.TlsEndpointOptions.KeepAlive = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MqttServerOptionsBuilder WithMaxPendingMessagesPerClient(int value)
|
||||||
|
{
|
||||||
|
_options.MaxPendingMessagesPerClient = value;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MqttServerOptionsBuilder WithPendingMessagesOverflowStrategy(MqttPendingMessagesOverflowStrategy value)
|
||||||
|
{
|
||||||
|
_options.PendingMessagesOverflowStrategy = value;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MqttServerOptionsBuilder WithoutDefaultEndpoint()
|
||||||
|
{
|
||||||
|
_options.DefaultEndpointOptions.IsEnabled = false;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MqttServerOptionsBuilder WithoutEncryptedEndpoint()
|
||||||
|
{
|
||||||
|
_options.TlsEndpointOptions.IsEnabled = false;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Usually the MQTT packets can be send partially to improve memory allocations.
|
||||||
|
/// This is done by using multiple TCP packets or WebSocket frames etc.
|
||||||
|
/// Unfortunately not all clients do support this and will close the connection when receiving partial packets.
|
||||||
|
/// </summary>
|
||||||
|
public MqttServerOptionsBuilder WithoutPacketFragmentation()
|
||||||
|
{
|
||||||
|
_options.DefaultEndpointOptions.AllowPacketFragmentation = false;
|
||||||
|
_options.TlsEndpointOptions.AllowPacketFragmentation = false;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MqttServerOptionsBuilder WithPersistentSessions(bool value = true)
|
||||||
|
{
|
||||||
|
_options.EnablePersistentSessions = value;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MqttServerOptionsBuilder WithRemoteCertificateValidationCallback(RemoteCertificateValidationCallback value)
|
||||||
|
{
|
||||||
|
_options.TlsEndpointOptions.RemoteCertificateValidationCallback = value;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MqttServerOptionsBuilder WithTcpKeepAliveInterval(int value)
|
||||||
|
{
|
||||||
|
_options.DefaultEndpointOptions.TcpKeepAliveInterval = value;
|
||||||
|
_options.TlsEndpointOptions.TcpKeepAliveInterval = value;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MqttServerOptionsBuilder WithTcpKeepAliveRetryCount(int value)
|
||||||
|
{
|
||||||
|
_options.DefaultEndpointOptions.TcpKeepAliveRetryCount = value;
|
||||||
|
_options.TlsEndpointOptions.TcpKeepAliveRetryCount = value;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MqttServerOptionsBuilder WithTcpKeepAliveTime(int value)
|
||||||
|
{
|
||||||
|
_options.DefaultEndpointOptions.TcpKeepAliveTime = value;
|
||||||
|
_options.TlsEndpointOptions.TcpKeepAliveTime = value;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MqttServerOptionsBuilder WithTlsEndpointReuseAddress()
|
||||||
|
{
|
||||||
|
_options.TlsEndpointOptions.ReuseAddress = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MqttServerOptionsBuilder WithEncryptionCertificate(byte[] value, IMqttServerCertificateCredentials credentials = null)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(value);
|
||||||
|
|
||||||
|
_options.TlsEndpointOptions.CertificateProvider = new BlobCertificateProvider(value)
|
||||||
{
|
{
|
||||||
return _options;
|
Password = credentials?.Password
|
||||||
}
|
};
|
||||||
|
|
||||||
public MqttServerOptionsBuilder WithClientCertificate(RemoteCertificateValidationCallback validationCallback = null, bool checkCertificateRevocation = false)
|
return this;
|
||||||
{
|
}
|
||||||
_options.TlsEndpointOptions.ClientCertificateRequired = true;
|
|
||||||
_options.TlsEndpointOptions.CheckCertificateRevocation = checkCertificateRevocation;
|
|
||||||
_options.TlsEndpointOptions.RemoteCertificateValidationCallback = validationCallback;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MqttServerOptionsBuilder WithConnectionBacklog(int value)
|
public MqttServerOptionsBuilder WithEncryptionCertificate(X509Certificate2 certificate)
|
||||||
{
|
{
|
||||||
_options.DefaultEndpointOptions.ConnectionBacklog = value;
|
ArgumentNullException.ThrowIfNull(certificate);
|
||||||
_options.TlsEndpointOptions.ConnectionBacklog = value;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MqttServerOptionsBuilder WithDefaultCommunicationTimeout(TimeSpan value)
|
_options.TlsEndpointOptions.CertificateProvider = new X509CertificateProvider(certificate);
|
||||||
{
|
return this;
|
||||||
_options.DefaultCommunicationTimeout = value;
|
}
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MqttServerOptionsBuilder WithDefaultEndpoint()
|
public MqttServerOptionsBuilder WithEncryptionCertificate(ICertificateProvider certificateProvider)
|
||||||
{
|
{
|
||||||
_options.DefaultEndpointOptions.IsEnabled = true;
|
ArgumentNullException.ThrowIfNull(certificateProvider);
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MqttServerOptionsBuilder WithDefaultEndpointBoundIPAddress(IPAddress value)
|
_options.TlsEndpointOptions.CertificateProvider = certificateProvider;
|
||||||
{
|
|
||||||
_options.DefaultEndpointOptions.BoundInterNetworkAddress = value ?? IPAddress.Any;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MqttServerOptionsBuilder WithDefaultEndpointBoundIPV6Address(IPAddress value)
|
return this;
|
||||||
{
|
|
||||||
_options.DefaultEndpointOptions.BoundInterNetworkV6Address = value ?? IPAddress.Any;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MqttServerOptionsBuilder WithDefaultEndpointPort(int value)
|
|
||||||
{
|
|
||||||
_options.DefaultEndpointOptions.Port = value;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MqttServerOptionsBuilder WithDefaultEndpointReuseAddress()
|
|
||||||
{
|
|
||||||
_options.DefaultEndpointOptions.ReuseAddress = true;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MqttServerOptionsBuilder WithEncryptedEndpoint()
|
|
||||||
{
|
|
||||||
_options.TlsEndpointOptions.IsEnabled = true;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MqttServerOptionsBuilder WithEncryptedEndpointBoundIPAddress(IPAddress value)
|
|
||||||
{
|
|
||||||
_options.TlsEndpointOptions.BoundInterNetworkAddress = value;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MqttServerOptionsBuilder WithEncryptedEndpointBoundIPV6Address(IPAddress value)
|
|
||||||
{
|
|
||||||
_options.TlsEndpointOptions.BoundInterNetworkV6Address = value;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MqttServerOptionsBuilder WithEncryptedEndpointPort(int value)
|
|
||||||
{
|
|
||||||
_options.TlsEndpointOptions.Port = value;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MqttServerOptionsBuilder WithEncryptionSslProtocol(SslProtocols value)
|
|
||||||
{
|
|
||||||
_options.TlsEndpointOptions.SslProtocol = value;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MqttServerOptionsBuilder WithKeepAlive()
|
|
||||||
{
|
|
||||||
_options.DefaultEndpointOptions.KeepAlive = true;
|
|
||||||
_options.TlsEndpointOptions.KeepAlive = true;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MqttServerOptionsBuilder WithMaxPendingMessagesPerClient(int value)
|
|
||||||
{
|
|
||||||
_options.MaxPendingMessagesPerClient = value;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MqttServerOptionsBuilder WithPendingMessagesOverflowStrategy(MqttPendingMessagesOverflowStrategy value)
|
|
||||||
{
|
|
||||||
_options.PendingMessagesOverflowStrategy = value;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MqttServerOptionsBuilder WithoutDefaultEndpoint()
|
|
||||||
{
|
|
||||||
_options.DefaultEndpointOptions.IsEnabled = false;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MqttServerOptionsBuilder WithoutEncryptedEndpoint()
|
|
||||||
{
|
|
||||||
_options.TlsEndpointOptions.IsEnabled = false;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Usually the MQTT packets can be send partially to improve memory allocations.
|
|
||||||
/// This is done by using multiple TCP packets or WebSocket frames etc.
|
|
||||||
/// Unfortunately not all clients do support this and will close the connection when receiving partial packets.
|
|
||||||
/// </summary>
|
|
||||||
public MqttServerOptionsBuilder WithoutPacketFragmentation()
|
|
||||||
{
|
|
||||||
_options.DefaultEndpointOptions.AllowPacketFragmentation = false;
|
|
||||||
_options.TlsEndpointOptions.AllowPacketFragmentation = false;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MqttServerOptionsBuilder WithPersistentSessions(bool value = true)
|
|
||||||
{
|
|
||||||
_options.EnablePersistentSessions = value;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MqttServerOptionsBuilder WithRemoteCertificateValidationCallback(RemoteCertificateValidationCallback value)
|
|
||||||
{
|
|
||||||
_options.TlsEndpointOptions.RemoteCertificateValidationCallback = value;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MqttServerOptionsBuilder WithTcpKeepAliveInterval(int value)
|
|
||||||
{
|
|
||||||
_options.DefaultEndpointOptions.TcpKeepAliveInterval = value;
|
|
||||||
_options.TlsEndpointOptions.TcpKeepAliveInterval = value;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MqttServerOptionsBuilder WithTcpKeepAliveRetryCount(int value)
|
|
||||||
{
|
|
||||||
_options.DefaultEndpointOptions.TcpKeepAliveRetryCount = value;
|
|
||||||
_options.TlsEndpointOptions.TcpKeepAliveRetryCount = value;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MqttServerOptionsBuilder WithTcpKeepAliveTime(int value)
|
|
||||||
{
|
|
||||||
_options.DefaultEndpointOptions.TcpKeepAliveTime = value;
|
|
||||||
_options.TlsEndpointOptions.TcpKeepAliveTime = value;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MqttServerOptionsBuilder WithTlsEndpointReuseAddress()
|
|
||||||
{
|
|
||||||
_options.TlsEndpointOptions.ReuseAddress = true;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MqttServerOptionsBuilder WithEncryptionCertificate(byte[] value, IMqttServerCertificateCredentials credentials = null)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(value);
|
|
||||||
|
|
||||||
_options.TlsEndpointOptions.CertificateProvider = new BlobCertificateProvider(value)
|
|
||||||
{
|
|
||||||
Password = credentials?.Password
|
|
||||||
};
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MqttServerOptionsBuilder WithEncryptionCertificate(X509Certificate2 certificate)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(certificate);
|
|
||||||
|
|
||||||
_options.TlsEndpointOptions.CertificateProvider = new X509CertificateProvider(certificate);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MqttServerOptionsBuilder WithEncryptionCertificate(ICertificateProvider certificateProvider)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(certificateProvider);
|
|
||||||
|
|
||||||
_options.TlsEndpointOptions.CertificateProvider = certificateProvider;
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user