mirror of
https://github.com/dotnet/MQTTnet.git
synced 2026-10-15 00:15:15 +08:00
Use latest language features (#2176)
* Make use of language features line AsyncDispose, init etc. * Fix build errors * Improve Unit Tests * Improve Unit Tests * Improve Unit Tests * Update samples * Remove obsolete benchmarks * Code cleanup * Improve unit tests * Improve Unit Tests * Fix Unit Tests * Improve Unit Tests * Improve Unit Tests * Restore unit tests * Exclude load tests from unit tests
This commit is contained in:
@@ -4,20 +4,19 @@
|
||||
|
||||
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)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public void OnGet()
|
||||
{
|
||||
|
||||
}
|
||||
public void OnGet()
|
||||
{
|
||||
_logger.LogDebug("OnGet");
|
||||
}
|
||||
}
|
||||
@@ -33,10 +33,8 @@ public static class ApplicationBuilderExtensions
|
||||
}
|
||||
|
||||
var adapter = app.ApplicationServices.GetRequiredService<MqttWebSocketServerAdapter>();
|
||||
using (var webSocket = await context.WebSockets.AcceptWebSocketAsync(subProtocol).ConfigureAwait(false))
|
||||
{
|
||||
await adapter.RunWebSocketConnectionAsync(webSocket, context);
|
||||
}
|
||||
using var webSocket = await context.WebSockets.AcceptWebSocketAsync(subProtocol).ConfigureAwait(false);
|
||||
await adapter.RunWebSocketConnectionAsync(webSocket, context);
|
||||
});
|
||||
|
||||
return app;
|
||||
|
||||
@@ -4,13 +4,12 @@
|
||||
|
||||
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.Routing;
|
||||
|
||||
namespace MQTTnet.AspNetCore
|
||||
namespace MQTTnet.AspNetCore;
|
||||
|
||||
public static class EndpointRouterExtensions
|
||||
{
|
||||
public static class EndpointRouterExtensions
|
||||
public static void MapMqtt(this IEndpointRouteBuilder endpoints, string pattern)
|
||||
{
|
||||
public static void MapMqtt(this IEndpointRouteBuilder endpoints, string pattern)
|
||||
ArgumentNullException.ThrowIfNull(endpoints);
|
||||
|
||||
endpoints.MapConnectionHandler<MqttConnectionHandler>(pattern, options =>
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(endpoints);
|
||||
|
||||
endpoints.MapConnectionHandler<MqttConnectionHandler>(pattern, options =>
|
||||
{
|
||||
options.WebSockets.SubProtocolSelector = MqttSubProtocolSelector.SelectSubProtocol;
|
||||
});
|
||||
}
|
||||
options.WebSockets.SubProtocolSelector = MqttSubProtocolSelector.SelectSubProtocol;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -7,28 +7,27 @@ using MQTTnet.Adapter;
|
||||
using MQTTnet.Diagnostics.Logger;
|
||||
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));
|
||||
|
||||
switch (options.ChannelOptions)
|
||||
case MqttClientTcpOptions tcpOptions:
|
||||
{
|
||||
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));
|
||||
return new MqttConnectionContext(formatter, tcpConnection);
|
||||
}
|
||||
default:
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
var formatter = new MqttPacketFormatterAdapter(options.ProtocolVersion, new MqttBufferWriter(4096, 65535));
|
||||
return new MqttConnectionContext(formatter, tcpConnection);
|
||||
}
|
||||
default:
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,13 +33,11 @@ public sealed class MqttConnectionHandler : ConnectionHandler, IMqttServerAdapte
|
||||
}
|
||||
|
||||
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;
|
||||
if (clientHandler != null)
|
||||
{
|
||||
await clientHandler(adapter).ConfigureAwait(false);
|
||||
}
|
||||
await clientHandler(adapter).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,10 +43,8 @@ public sealed class MqttWebSocketServerAdapter : IMqttServerAdapter
|
||||
var formatter = new MqttPacketFormatterAdapter(new MqttBufferWriter(4096, 65535));
|
||||
var channel = new MqttWebSocketChannel(webSocket, remoteEndPoint, isSecureConnection, clientCertificate);
|
||||
|
||||
using (var channelAdapter = new MqttChannelAdapter(channel, formatter, _logger))
|
||||
{
|
||||
await clientHandler(channelAdapter).ConfigureAwait(false);
|
||||
}
|
||||
using var channelAdapter = new MqttChannelAdapter(channel, formatter, _logger);
|
||||
await clientHandler(channelAdapter).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
finally
|
||||
|
||||
@@ -12,34 +12,28 @@ using System.Threading.Tasks;
|
||||
|
||||
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;
|
||||
|
||||
Action _callback;
|
||||
SocketError _error;
|
||||
|
||||
public SocketAwaitable(PipeScheduler ioScheduler)
|
||||
{
|
||||
_ioScheduler = ioScheduler;
|
||||
}
|
||||
|
||||
public bool IsCompleted => ReferenceEquals(_callback, _callbackCompleted);
|
||||
public bool IsCompleted => ReferenceEquals(_callback, CallbackCompleted);
|
||||
|
||||
public void Complete(int bytesTransferred, SocketError socketError)
|
||||
{
|
||||
_error = socketError;
|
||||
_bytesTransferred = bytesTransferred;
|
||||
var continuation = Interlocked.Exchange(ref _callback, _callbackCompleted);
|
||||
var continuation = Interlocked.Exchange(ref _callback, CallbackCompleted);
|
||||
|
||||
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()
|
||||
{
|
||||
Debug.Assert(ReferenceEquals(_callback, _callbackCompleted));
|
||||
Debug.Assert(ReferenceEquals(_callback, CallbackCompleted));
|
||||
|
||||
_callback = null;
|
||||
|
||||
@@ -64,7 +58,7 @@ public class SocketAwaitable : ICriticalNotifyCompletion
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -18,11 +18,11 @@ namespace MQTTnet.AspNetCore;
|
||||
public sealed class SocketConnection : ConnectionContext
|
||||
{
|
||||
readonly EndPoint _endPoint;
|
||||
|
||||
volatile bool _aborted;
|
||||
IDuplexPipe _application;
|
||||
SocketReceiver _receiver;
|
||||
SocketSender _sender;
|
||||
|
||||
Socket _socket;
|
||||
|
||||
public SocketConnection(EndPoint endPoint)
|
||||
@@ -40,10 +40,13 @@ public sealed class SocketConnection : ConnectionContext
|
||||
}
|
||||
|
||||
public override string ConnectionId { get; set; }
|
||||
|
||||
public override IFeatureCollection Features { get; }
|
||||
|
||||
public bool IsConnected { get; private set; }
|
||||
|
||||
public override IDictionary<object, object> Items { get; set; }
|
||||
|
||||
public override IDuplexPipe Transport { get; set; }
|
||||
|
||||
public override ValueTask DisposeAsync()
|
||||
@@ -78,7 +81,7 @@ public sealed class SocketConnection : ConnectionContext
|
||||
IsConnected = true;
|
||||
}
|
||||
|
||||
Exception ConnectionAborted()
|
||||
static Exception ConnectionAborted()
|
||||
{
|
||||
return new MqttCommunicationException("Connection Aborted");
|
||||
}
|
||||
@@ -95,8 +98,7 @@ public sealed class SocketConnection : ConnectionContext
|
||||
{
|
||||
error = new MqttCommunicationException(ex);
|
||||
}
|
||||
catch (SocketException ex) when (ex.SocketErrorCode == SocketError.OperationAborted || ex.SocketErrorCode == SocketError.ConnectionAborted ||
|
||||
ex.SocketErrorCode == SocketError.Interrupted || ex.SocketErrorCode == SocketError.InvalidArgument)
|
||||
catch (SocketException ex) when (ex.SocketErrorCode is SocketError.OperationAborted or SocketError.ConnectionAborted or SocketError.Interrupted or SocketError.InvalidArgument)
|
||||
{
|
||||
if (!_aborted)
|
||||
{
|
||||
@@ -123,10 +125,10 @@ public sealed class SocketConnection : ConnectionContext
|
||||
{
|
||||
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 MQTTnet.Internal;
|
||||
|
||||
namespace MQTTnet.Benchmarks
|
||||
namespace MQTTnet.Benchmarks;
|
||||
|
||||
[SimpleJob(RuntimeMoniker.Net60)]
|
||||
[MemoryDiagnoser]
|
||||
public class AsyncLockBenchmark : BaseBenchmark
|
||||
{
|
||||
[SimpleJob(RuntimeMoniker.Net60)]
|
||||
[MemoryDiagnoser]
|
||||
public class AsyncLockBenchmark : BaseBenchmark
|
||||
[Benchmark]
|
||||
public async Task Synchronize_100_Tasks()
|
||||
{
|
||||
[Benchmark]
|
||||
public async Task Synchronize_100_Tasks()
|
||||
const int tasksCount = 100;
|
||||
|
||||
var tasks = new Task[tasksCount];
|
||||
var asyncLock = new AsyncLock();
|
||||
var globalI = 0;
|
||||
|
||||
for (var i = 0; i < tasksCount; i++)
|
||||
{
|
||||
const int tasksCount = 100;
|
||||
|
||||
var tasks = new Task[tasksCount];
|
||||
var asyncLock = new AsyncLock();
|
||||
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.
|
||||
localI++;
|
||||
globalI = localI;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
|
||||
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++)
|
||||
tasks[i] = Task.Run(
|
||||
async () =>
|
||||
{
|
||||
using (await asyncLock.EnterAsync(cancellationToken.Token).ConfigureAwait(false))
|
||||
using (await asyncLock.EnterAsync().ConfigureAwait(false))
|
||||
{
|
||||
var localI = globalI;
|
||||
await Task.Delay(5).ConfigureAwait(false); // Increase the chance for wrong data.
|
||||
localI++;
|
||||
globalI = localI;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
|
||||
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))
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// 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.Tests.Mockups;
|
||||
|
||||
namespace MQTTnet.Benchmarks
|
||||
namespace MQTTnet.Benchmarks;
|
||||
|
||||
[MemoryDiagnoser]
|
||||
public sealed class ChannelAdapterBenchmark : BaseBenchmark
|
||||
{
|
||||
[MemoryDiagnoser]
|
||||
public sealed class ChannelAdapterBenchmark : BaseBenchmark
|
||||
MqttChannelAdapter _channelAdapter;
|
||||
int _iterations;
|
||||
MqttPublishPacket _packet;
|
||||
MemoryStream _stream;
|
||||
|
||||
[Benchmark]
|
||||
public void Receive_10000_Messages()
|
||||
{
|
||||
MqttChannelAdapter _channelAdapter;
|
||||
int _iterations;
|
||||
MqttPublishPacket _packet;
|
||||
MemoryStream _stream;
|
||||
_stream.Position = 0;
|
||||
|
||||
[Benchmark]
|
||||
public void Receive_10000_Messages()
|
||||
for (var i = 0; i < 10000; i++)
|
||||
{
|
||||
_stream.Position = 0;
|
||||
|
||||
for (var i = 0; i < 10000; i++)
|
||||
{
|
||||
_channelAdapter.ReceivePacketAsync(CancellationToken.None).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
_stream.Position = 0;
|
||||
_channelAdapter.ReceivePacketAsync(CancellationToken.None).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void Send_10000_Messages()
|
||||
_stream.Position = 0;
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void Send_10000_Messages()
|
||||
{
|
||||
_stream.Position = 0;
|
||||
|
||||
for (var i = 0; i < 10000; i++)
|
||||
{
|
||||
_stream.Position = 0;
|
||||
|
||||
for (var i = 0; i < 10000; i++)
|
||||
{
|
||||
_channelAdapter.SendPacketAsync(_packet, CancellationToken.None).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
_stream.Position = 0;
|
||||
_channelAdapter.SendPacketAsync(_packet, CancellationToken.None).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
_stream.Position = 0;
|
||||
}
|
||||
|
||||
[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++)
|
||||
{
|
||||
_stream.Write(serializedPacket, 0, serializedPacket.Length);
|
||||
}
|
||||
|
||||
_stream.Position = 0;
|
||||
|
||||
var channel = new MemoryMqttChannel(_stream);
|
||||
|
||||
_channelAdapter = new MqttChannelAdapter(channel, serializer, new MqttNetEventLogger());
|
||||
for (var i = 0; i < _iterations; i++)
|
||||
{
|
||||
_stream.Write(serializedPacket, 0, serializedPacket.Length);
|
||||
}
|
||||
|
||||
static byte[] Join(params ArraySegment<byte>[] chunks)
|
||||
{
|
||||
var buffer = new MemoryStream();
|
||||
foreach (var chunk in chunks)
|
||||
{
|
||||
buffer.Write(chunk.Array, chunk.Offset, chunk.Count);
|
||||
}
|
||||
_stream.Position = 0;
|
||||
|
||||
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.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Diagnostics;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using BenchmarkDotNet.Jobs;
|
||||
using MQTTnet.Diagnostics.Logger;
|
||||
|
||||
namespace MQTTnet.Benchmarks
|
||||
namespace MQTTnet.Benchmarks;
|
||||
|
||||
[SimpleJob(RuntimeMoniker.Net60)]
|
||||
[RPlotExporter]
|
||||
[MemoryDiagnoser]
|
||||
public class LoggerBenchmark : BaseBenchmark
|
||||
{
|
||||
[SimpleJob(RuntimeMoniker.Net60)]
|
||||
[RPlotExporter]
|
||||
[MemoryDiagnoser]
|
||||
public class LoggerBenchmark : BaseBenchmark
|
||||
MqttNetNullLogger _nullLogger;
|
||||
MqttNetSourceLogger _sourceNullLogger;
|
||||
|
||||
MqttNetEventLogger _eventLogger;
|
||||
MqttNetSourceLogger _sourceEventLogger;
|
||||
|
||||
MqttNetEventLogger _eventLoggerNoListener;
|
||||
MqttNetSourceLogger _sourceEventLoggerNoListener;
|
||||
|
||||
bool _useHandler;
|
||||
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
{
|
||||
MqttNetNullLogger _nullLogger;
|
||||
MqttNetSourceLogger _sourceNullLogger;
|
||||
_nullLogger = new MqttNetNullLogger();
|
||||
_sourceNullLogger = _nullLogger.WithSource("Source");
|
||||
|
||||
MqttNetEventLogger _eventLogger;
|
||||
MqttNetSourceLogger _sourceEventLogger;
|
||||
_eventLogger = new MqttNetEventLogger();
|
||||
_eventLogger.LogMessagePublished += OnLogMessagePublished;
|
||||
_sourceEventLogger = _eventLogger.WithSource("Source");
|
||||
|
||||
MqttNetEventLogger _eventLoggerNoListener;
|
||||
MqttNetSourceLogger _sourceEventLoggerNoListener;
|
||||
_eventLoggerNoListener = new MqttNetEventLogger();
|
||||
_sourceEventLoggerNoListener = _eventLoggerNoListener.WithSource("Source");
|
||||
}
|
||||
|
||||
bool _useHandler;
|
||||
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
void OnLogMessagePublished(object sender, MqttNetLogMessagePublishedEventArgs eventArgs)
|
||||
{
|
||||
if (_useHandler)
|
||||
{
|
||||
_nullLogger = new MqttNetNullLogger();
|
||||
_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)
|
||||
{
|
||||
if (_useHandler)
|
||||
{
|
||||
eventArgs.LogMessage.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void Log_10000_Messages_No_Listener()
|
||||
{
|
||||
_useHandler = false;
|
||||
|
||||
for (var i = 0; i < 10000; i++)
|
||||
{
|
||||
_sourceEventLoggerNoListener.Verbose("test log message {0}", "parameter");
|
||||
}
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void Log_10000_Messages_With_To_String()
|
||||
{
|
||||
_useHandler = true;
|
||||
|
||||
for (var i = 0; i < 10000; i++)
|
||||
{
|
||||
_sourceEventLogger.Verbose("test log message {0}", "parameter");
|
||||
}
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void Log_10000_Messages_Without_To_String()
|
||||
{
|
||||
_useHandler = false;
|
||||
|
||||
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");
|
||||
}
|
||||
Debug.WriteLine(eventArgs.LogMessage.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void Log_10000_Messages_No_Listener()
|
||||
{
|
||||
_useHandler = false;
|
||||
|
||||
for (var i = 0; i < 10000; i++)
|
||||
{
|
||||
_sourceEventLoggerNoListener.Verbose("test log message {0}", "parameter");
|
||||
}
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void Log_10000_Messages_With_To_String()
|
||||
{
|
||||
_useHandler = true;
|
||||
|
||||
for (var i = 0; i < 10000; i++)
|
||||
{
|
||||
_sourceEventLogger.Verbose("test log message {0}", "parameter");
|
||||
}
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void Log_10000_Messages_Without_To_String()
|
||||
{
|
||||
_useHandler = false;
|
||||
|
||||
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;
|
||||
|
||||
|
||||
namespace MQTTnet.Benchmarks
|
||||
namespace MQTTnet.Benchmarks;
|
||||
|
||||
[SimpleJob(RuntimeMoniker.Net60)]
|
||||
[RPlotExporter, RankColumn]
|
||||
[MemoryDiagnoser]
|
||||
public class MemoryCopyBenchmark
|
||||
{
|
||||
[SimpleJob(RuntimeMoniker.Net60)]
|
||||
[RPlotExporter, RankColumn]
|
||||
[MemoryDiagnoser]
|
||||
public class MemoryCopyBenchmark
|
||||
const int MaxLength = 1024 * 8;
|
||||
|
||||
byte[] _source;
|
||||
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;
|
||||
private byte[] source;
|
||||
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);
|
||||
}
|
||||
|
||||
_source = new byte[MaxLength];
|
||||
_target = new byte[MaxLength];
|
||||
}
|
||||
|
||||
[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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
@@ -23,22 +22,19 @@ public class MessageDeliveryBenchmark : BaseBenchmark
|
||||
int _messagesExpectedCount;
|
||||
int _messagesReceivedCount;
|
||||
Dictionary<string, IMqttClient> _mqttPublisherClientsByPublisherName;
|
||||
|
||||
MqttServer _mqttServer;
|
||||
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;
|
||||
List<MqttApplicationMessage> _topicPublishMessages;
|
||||
|
||||
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]
|
||||
public void Cleanup()
|
||||
{
|
||||
@@ -64,14 +60,11 @@ public class MessageDeliveryBenchmark : BaseBenchmark
|
||||
_mqttServer = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Publish messages and wait for messages sent to subscribers
|
||||
/// </summary>
|
||||
[Benchmark]
|
||||
public void DeliverMessages()
|
||||
{
|
||||
// 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
|
||||
|
||||
@@ -82,8 +75,6 @@ public class MessageDeliveryBenchmark : BaseBenchmark
|
||||
// same payload for all messages
|
||||
var payload = new byte[] { 1, 2, 3, 4 };
|
||||
|
||||
var tasks = new List<Task>();
|
||||
|
||||
// publish a message for each subscribed topic
|
||||
foreach (var topic in _allSubscribedTopics)
|
||||
{
|
||||
@@ -101,35 +92,23 @@ public class MessageDeliveryBenchmark : BaseBenchmark
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore all errors.
|
||||
}
|
||||
|
||||
_cancellationTokenSource.Dispose();
|
||||
|
||||
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]
|
||||
public void Setup()
|
||||
{
|
||||
_lockMsgCount = new object();
|
||||
|
||||
Dictionary<string, List<string>> singleWildcardTopicsByPublisher;
|
||||
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);
|
||||
}
|
||||
TopicGenerator.Generate(_numPublishers, _numTopicsPerPublisher, out _topicsByPublisher, out _, out _);
|
||||
|
||||
// Create server
|
||||
var serverFactory = new MqttServerFactory();
|
||||
@@ -139,7 +118,7 @@ public class MessageDeliveryBenchmark : BaseBenchmark
|
||||
_mqttServer.StartAsync().GetAwaiter().GetResult();
|
||||
|
||||
// Create publisher clients
|
||||
_mqttPublisherClientsByPublisherName = new Dictionary<string, IMqttClient>();
|
||||
_mqttPublisherClientsByPublisherName = [];
|
||||
foreach (var pt in _topicsByPublisher)
|
||||
{
|
||||
var publisherName = pt.Key;
|
||||
@@ -151,13 +130,13 @@ public class MessageDeliveryBenchmark : BaseBenchmark
|
||||
|
||||
// Create subscriber clients
|
||||
_mqttSubscriberClients = new List<IMqttClient>();
|
||||
for (var i = 0; i < NumSubscribers; i++)
|
||||
for (var i = 0; i < _numSubscribers; i++)
|
||||
{
|
||||
var mqttSubscriberClient = clientFactory.CreateMqttClient();
|
||||
_mqttSubscriberClients.Add(mqttSubscriberClient);
|
||||
|
||||
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
|
||||
lock (_lockMsgCount)
|
||||
@@ -171,12 +150,13 @@ public class MessageDeliveryBenchmark : BaseBenchmark
|
||||
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
||||
mqttSubscriberClient.ConnectAsync(subscriberOptions).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
|
||||
var allTopics = new List<string>();
|
||||
_publisherByTopic = new Dictionary<string, string>();
|
||||
_publisherByTopic = [];
|
||||
foreach (var t in _topicsByPublisher)
|
||||
{
|
||||
foreach (var topic in t.Value)
|
||||
@@ -189,21 +169,18 @@ public class MessageDeliveryBenchmark : BaseBenchmark
|
||||
// Subscribe to NumSubscribedTopics topics spread across all topics
|
||||
_allSubscribedTopics = new List<string>();
|
||||
|
||||
var totalNumTopics = NumPublishers * NumTopicsPerPublisher;
|
||||
var topicIndexStep = totalNumTopics / (NumSubscribedTopicsPerSubscriber * NumSubscribers);
|
||||
if (topicIndexStep * NumSubscribedTopicsPerSubscriber * NumSubscribers != totalNumTopics)
|
||||
var totalNumTopics = _numPublishers * _numTopicsPerPublisher;
|
||||
var topicIndexStep = totalNumTopics / (_numSubscribedTopicsPerSubscriber * _numSubscribers);
|
||||
if (topicIndexStep * _numSubscribedTopicsPerSubscriber * _numSubscribers != totalNumTopics)
|
||||
{
|
||||
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: {0}, topic step: {1}",
|
||||
totalNumTopics,
|
||||
topicIndexStep));
|
||||
$"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}");
|
||||
}
|
||||
|
||||
var topicIndex = 0;
|
||||
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];
|
||||
_allSubscribedTopics.Add(topic);
|
||||
|
||||
@@ -9,65 +9,64 @@ using Microsoft.AspNetCore.Hosting;
|
||||
using MQTTnet.AspNetCore;
|
||||
using MQTTnet.Diagnostics.Logger;
|
||||
|
||||
namespace MQTTnet.Benchmarks
|
||||
namespace MQTTnet.Benchmarks;
|
||||
|
||||
[SimpleJob(RuntimeMoniker.Net60)]
|
||||
[MemoryDiagnoser]
|
||||
public class MessageProcessingMqttConnectionContextBenchmark : BaseBenchmark
|
||||
{
|
||||
[SimpleJob(RuntimeMoniker.Net60)]
|
||||
[MemoryDiagnoser]
|
||||
public class MessageProcessingMqttConnectionContextBenchmark : BaseBenchmark
|
||||
IWebHost _host;
|
||||
IMqttClient _mqttClient;
|
||||
MqttApplicationMessage _message;
|
||||
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
{
|
||||
IWebHost _host;
|
||||
IMqttClient _mqttClient;
|
||||
MqttApplicationMessage _message;
|
||||
_host = WebHost.CreateDefaultBuilder()
|
||||
.UseKestrel(o => o.ListenAnyIP(1883, l => l.UseMqtt()))
|
||||
.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()
|
||||
.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();
|
||||
}
|
||||
_mqttClient.PublishAsync(_message).GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,36 +8,35 @@ using BenchmarkDotNet.Attributes;
|
||||
using BenchmarkDotNet.Jobs;
|
||||
using MQTTnet.Formatter;
|
||||
|
||||
namespace MQTTnet.Benchmarks
|
||||
namespace MQTTnet.Benchmarks;
|
||||
|
||||
[SimpleJob(RuntimeMoniker.Net60)]
|
||||
[MemoryDiagnoser]
|
||||
public class MqttBufferReaderBenchmark
|
||||
{
|
||||
[SimpleJob(RuntimeMoniker.Net60)]
|
||||
[MemoryDiagnoser]
|
||||
public class MqttBufferReaderBenchmark
|
||||
byte[] _buffer;
|
||||
int _bufferLength;
|
||||
|
||||
[GlobalSetup]
|
||||
public void GlobalSetup()
|
||||
{
|
||||
byte[] _buffer;
|
||||
int _bufferLength;
|
||||
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");
|
||||
|
||||
[GlobalSetup]
|
||||
public void GlobalSetup()
|
||||
{
|
||||
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();
|
||||
_bufferLength = writer.Length;
|
||||
}
|
||||
|
||||
_buffer = writer.GetBuffer();
|
||||
_bufferLength = writer.Length;
|
||||
}
|
||||
[Benchmark]
|
||||
public void Use_Span()
|
||||
{
|
||||
var span = _buffer.AsSpan(0, _bufferLength);
|
||||
Encoding.UTF8.GetString(span);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void Use_Span()
|
||||
{
|
||||
var span = _buffer.AsSpan(0, _bufferLength);
|
||||
Encoding.UTF8.GetString(span);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void Use_Encoding()
|
||||
{
|
||||
Encoding.UTF8.GetString(_buffer, 0, _bufferLength);
|
||||
}
|
||||
[Benchmark]
|
||||
public void Use_Encoding()
|
||||
{
|
||||
Encoding.UTF8.GetString(_buffer, 0, _bufferLength);
|
||||
}
|
||||
}
|
||||
@@ -8,89 +8,88 @@ using BenchmarkDotNet.Jobs;
|
||||
using MQTTnet.Formatter;
|
||||
using MQTTnet.Tests.Mockups;
|
||||
|
||||
namespace MQTTnet.Benchmarks
|
||||
namespace MQTTnet.Benchmarks;
|
||||
|
||||
[SimpleJob(RuntimeMoniker.Net60)]
|
||||
[MemoryDiagnoser]
|
||||
public class MqttPacketReaderWriterBenchmark : BaseBenchmark
|
||||
{
|
||||
[SimpleJob(RuntimeMoniker.Net60)]
|
||||
[MemoryDiagnoser]
|
||||
public class MqttPacketReaderWriterBenchmark : BaseBenchmark
|
||||
readonly byte[] _demoPayload = new byte[1024];
|
||||
|
||||
byte[] _readPayload;
|
||||
|
||||
[GlobalCleanup]
|
||||
public void GlobalCleanup()
|
||||
{
|
||||
readonly byte[] _demoPayload = new byte[1024];
|
||||
|
||||
byte[] _readPayload;
|
||||
}
|
||||
|
||||
[GlobalCleanup]
|
||||
public void GlobalCleanup()
|
||||
[GlobalSetup]
|
||||
public void GlobalSetup()
|
||||
{
|
||||
TestEnvironment.EnableLogger = false;
|
||||
|
||||
var writer = new MqttBufferWriter(4096, 65535);
|
||||
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]
|
||||
public void GlobalSetup()
|
||||
[Benchmark]
|
||||
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.WriteBinary(_demoPayload);
|
||||
writer.WriteByte(0x01);
|
||||
writer.WriteByte(0x02);
|
||||
writer.WriteVariableByteInteger(5647382);
|
||||
writer.WriteString("A relative short string.");
|
||||
writer.WriteVariableByteInteger(8574489);
|
||||
writer.WriteVariableByteInteger(8574589);
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
[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);
|
||||
}
|
||||
writer.Reset(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
|
||||
@@ -5,138 +5,135 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using BenchmarkDotNet.Loggers;
|
||||
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;
|
||||
static List<Type> _benchmarks;
|
||||
_benchmarks = CollectBenchmarks();
|
||||
HandleArguments(arguments);
|
||||
|
||||
public static void Main(string[] arguments)
|
||||
while (true)
|
||||
{
|
||||
_benchmarks = CollectBenchmarks();
|
||||
HandleArguments(arguments);
|
||||
RenderMenu();
|
||||
|
||||
while (true)
|
||||
var key = Console.ReadKey(true);
|
||||
|
||||
if (key.Key == ConsoleKey.DownArrow)
|
||||
{
|
||||
RenderMenu();
|
||||
|
||||
var key = Console.ReadKey(true);
|
||||
|
||||
if (key.Key == ConsoleKey.DownArrow)
|
||||
_selectedBenchmarkIndex++;
|
||||
if (_selectedBenchmarkIndex > _benchmarks.Count - 1)
|
||||
{
|
||||
_selectedBenchmarkIndex++;
|
||||
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;
|
||||
_selectedBenchmarkIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
else if (key.Key == ConsoleKey.UpArrow)
|
||||
{
|
||||
if (typeof(BaseBenchmark).IsAssignableFrom(type))
|
||||
_selectedBenchmarkIndex--;
|
||||
if (_selectedBenchmarkIndex < 0)
|
||||
{
|
||||
if (type != typeof(BaseBenchmark))
|
||||
{
|
||||
benchmarks.Add(type);
|
||||
}
|
||||
_selectedBenchmarkIndex = _benchmarks.Count - 1;
|
||||
}
|
||||
}
|
||||
|
||||
return benchmarks.OrderBy(b => b.Name).ToList();
|
||||
}
|
||||
|
||||
static void HandleArguments(string[] arguments)
|
||||
{
|
||||
if (arguments.Length == 0)
|
||||
else if (key.Key == ConsoleKey.Escape)
|
||||
{
|
||||
return;
|
||||
Environment.Exit(0);
|
||||
}
|
||||
|
||||
// Allow for preselection to avoid developer frustration.
|
||||
|
||||
if (int.TryParse(arguments[0], out var benchmarkIndex))
|
||||
else if (key.Key == ConsoleKey.Enter)
|
||||
{
|
||||
_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;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
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++)
|
||||
if (typeof(BaseBenchmark).IsAssignableFrom(type))
|
||||
{
|
||||
var benchmark = _benchmarks[index];
|
||||
|
||||
if (_selectedBenchmarkIndex == index)
|
||||
if (type != typeof(BaseBenchmark))
|
||||
{
|
||||
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.Jobs;
|
||||
using MQTTnet.Adapter;
|
||||
using MQTTnet.AspNetCore;
|
||||
using MQTTnet.Exceptions;
|
||||
using MQTTnet.Formatter;
|
||||
using MQTTnet.Packets;
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.IO;
|
||||
using System.IO.Pipelines;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MQTTnet.Benchmarks
|
||||
{
|
||||
[SimpleJob(RuntimeMoniker.Net60)]
|
||||
[RPlotExporter, RankColumn]
|
||||
[MemoryDiagnoser]
|
||||
public class ReaderExtensionsBenchmark
|
||||
{
|
||||
MqttPacketFormatterAdapter mqttPacketFormatter;
|
||||
MemoryStream stream;
|
||||
namespace MQTTnet.Benchmarks;
|
||||
|
||||
[GlobalSetup]
|
||||
public void GlobalSetup()
|
||||
{
|
||||
mqttPacketFormatter = new MqttPacketFormatterAdapter(MqttProtocolVersion.V311, new MqttBufferWriter(4096, 65535));
|
||||
var mqttMessage = new MqttApplicationMessageBuilder()
|
||||
[SimpleJob(RuntimeMoniker.Net60)]
|
||||
[RPlotExporter, RankColumn]
|
||||
[MemoryDiagnoser]
|
||||
public class ReaderExtensionsBenchmark
|
||||
{
|
||||
MqttPacketFormatterAdapter _mqttPacketFormatter;
|
||||
MemoryStream _stream;
|
||||
|
||||
[GlobalSetup]
|
||||
public void GlobalSetup()
|
||||
{
|
||||
_mqttPacketFormatter = new MqttPacketFormatterAdapter(MqttProtocolVersion.V311, new MqttBufferWriter(4096, 65535));
|
||||
var mqttMessage = new MqttApplicationMessageBuilder()
|
||||
.WithTopic("topic")
|
||||
.WithPayload(new byte[10 * 1024])
|
||||
.Build();
|
||||
|
||||
var packet = MqttPublishPacketFactory.Create(mqttMessage);
|
||||
|
||||
var buffer = mqttPacketFormatter.Encode(packet);
|
||||
stream = new MemoryStream();
|
||||
stream.Write(buffer.Packet);
|
||||
stream.Write(buffer.Payload.ToArray());
|
||||
mqttPacketFormatter.Cleanup();
|
||||
}
|
||||
|
||||
[Benchmark(Baseline = true)]
|
||||
public async Task Before()
|
||||
{
|
||||
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_Before.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[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;
|
||||
}
|
||||
}
|
||||
var packet = MqttPublishPacketFactory.Create(mqttMessage);
|
||||
|
||||
var buffer = _mqttPacketFormatter.Encode(packet);
|
||||
_stream = new MemoryStream();
|
||||
_stream.Write(buffer.Packet);
|
||||
_stream.Write(buffer.Payload.ToArray());
|
||||
_mqttPacketFormatter.Cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
[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 (_mqttPacketFormatter.TryDecode(buffer, out _, out consumed, out observed, out _))
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,28 +3,27 @@ using BenchmarkDotNet.Jobs;
|
||||
using MQTTnet.Tests.Mockups;
|
||||
using MQTTnet.Tests.Server;
|
||||
|
||||
namespace MQTTnet.Benchmarks
|
||||
namespace MQTTnet.Benchmarks;
|
||||
|
||||
[SimpleJob(RuntimeMoniker.Net60)]
|
||||
[RPlotExporter, RankColumn]
|
||||
[MemoryDiagnoser]
|
||||
public class RoundtripProcessingBenchmark : BaseBenchmark
|
||||
{
|
||||
[SimpleJob(RuntimeMoniker.Net60)]
|
||||
[RPlotExporter, RankColumn]
|
||||
[MemoryDiagnoser]
|
||||
public class RoundtripProcessingBenchmark : BaseBenchmark
|
||||
[GlobalSetup]
|
||||
public void GlobalSetup()
|
||||
{
|
||||
[GlobalSetup]
|
||||
public void GlobalSetup()
|
||||
{
|
||||
TestEnvironment.EnableLogger = false;
|
||||
}
|
||||
|
||||
[GlobalCleanup]
|
||||
public void GlobalCleanup()
|
||||
{
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void Handle_100_000_Messages_In_Receiving_Client()
|
||||
{
|
||||
new Load_Tests().Handle_100_000_Messages_In_Receiving_Client().GetAwaiter().GetResult();
|
||||
}
|
||||
TestEnvironment.EnableLogger = false;
|
||||
}
|
||||
|
||||
[GlobalCleanup]
|
||||
public void GlobalCleanup()
|
||||
{
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void Handle_100_000_Messages_In_Receiving_Client()
|
||||
{
|
||||
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.Threading.Tasks;
|
||||
|
||||
namespace MQTTnet.Benchmarks
|
||||
namespace MQTTnet.Benchmarks;
|
||||
|
||||
[SimpleJob(RuntimeMoniker.Net60)]
|
||||
[RPlotExporter, RankColumn]
|
||||
[MemoryDiagnoser]
|
||||
public class SendPacketAsyncBenchmark : BaseBenchmark
|
||||
{
|
||||
[SimpleJob(RuntimeMoniker.Net60)]
|
||||
[RPlotExporter, RankColumn]
|
||||
[MemoryDiagnoser]
|
||||
public class SendPacketAsyncBenchmark : BaseBenchmark
|
||||
MemoryStream _stream;
|
||||
MqttPacketBuffer _buffer;
|
||||
|
||||
[GlobalSetup]
|
||||
public void GlobalSetup()
|
||||
{
|
||||
MemoryStream stream;
|
||||
MqttPacketBuffer buffer;
|
||||
_stream = new MemoryStream(1024);
|
||||
var packet = new ArraySegment<byte>(new byte[10]);
|
||||
_buffer = new MqttPacketBuffer(packet);
|
||||
}
|
||||
|
||||
[GlobalSetup]
|
||||
public void GlobalSetup()
|
||||
[Benchmark(Baseline = true)]
|
||||
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);
|
||||
var packet = new ArraySegment<byte>(new byte[10]);
|
||||
buffer = new MqttPacketBuffer(packet);
|
||||
await output.WriteAsync(_buffer.Packet).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Benchmark(Baseline = true)]
|
||||
public async ValueTask Before()
|
||||
else
|
||||
{
|
||||
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)
|
||||
{
|
||||
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);
|
||||
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[buffer.Packet.Count..]);
|
||||
|
||||
output.Advance(buffer.Length);
|
||||
}
|
||||
}
|
||||
@@ -17,103 +17,102 @@ using MQTTnet.Diagnostics.Logger;
|
||||
using System.Buffers;
|
||||
using System.Net;
|
||||
|
||||
namespace MQTTnet.Benchmarks
|
||||
namespace MQTTnet.Benchmarks;
|
||||
|
||||
[SimpleJob(RuntimeMoniker.Net60)]
|
||||
[RPlotExporter]
|
||||
[MemoryDiagnoser]
|
||||
public class SerializerBenchmark : BaseBenchmark
|
||||
{
|
||||
[SimpleJob(RuntimeMoniker.Net60)]
|
||||
[RPlotExporter]
|
||||
[MemoryDiagnoser]
|
||||
public class SerializerBenchmark : BaseBenchmark
|
||||
MqttPacket _packet;
|
||||
ArraySegment<byte> _serializedPacket;
|
||||
IMqttPacketFormatter _serializer;
|
||||
MqttBufferWriter _bufferWriter;
|
||||
|
||||
[GlobalSetup]
|
||||
public void GlobalSetup()
|
||||
{
|
||||
MqttPacket _packet;
|
||||
ArraySegment<byte> _serializedPacket;
|
||||
IMqttPacketFormatter _serializer;
|
||||
MqttBufferWriter _bufferWriter;
|
||||
|
||||
[GlobalSetup]
|
||||
public void GlobalSetup()
|
||||
_packet = new MqttPublishPacket
|
||||
{
|
||||
_packet = new MqttPublishPacket
|
||||
{
|
||||
Topic = "A"
|
||||
};
|
||||
Topic = "A"
|
||||
};
|
||||
|
||||
_bufferWriter = new MqttBufferWriter(4096, 65535);
|
||||
_serializer = new MqttV3PacketFormatter(_bufferWriter, MqttProtocolVersion.V311);
|
||||
_serializedPacket = _serializer.Encode(_packet).Join();
|
||||
}
|
||||
_bufferWriter = new MqttBufferWriter(4096, 65535);
|
||||
_serializer = new MqttV3PacketFormatter(_bufferWriter, MqttProtocolVersion.V311);
|
||||
_serializedPacket = _serializer.Encode(_packet).Join();
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void Serialize_10000_Messages()
|
||||
[Benchmark]
|
||||
public void Serialize_10000_Messages()
|
||||
{
|
||||
for (var i = 0; i < 10000; i++)
|
||||
{
|
||||
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();
|
||||
|
||||
var header = 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;
|
||||
}
|
||||
|
||||
public EndPoint RemoteEndPoint { get; set; }
|
||||
|
||||
public bool IsSecureConnection { get; } = false;
|
||||
|
||||
public X509Certificate2 ClientCertificate { get; }
|
||||
|
||||
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()
|
||||
{
|
||||
}
|
||||
_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;
|
||||
}
|
||||
|
||||
public EndPoint RemoteEndPoint { get; set; }
|
||||
|
||||
public bool IsSecureConnection { get; } = false;
|
||||
|
||||
public X509Certificate2 ClientCertificate { get; set; }
|
||||
|
||||
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.Server;
|
||||
|
||||
namespace MQTTnet.Benchmarks
|
||||
namespace MQTTnet.Benchmarks;
|
||||
|
||||
[SimpleJob(RuntimeMoniker.Net60)]
|
||||
[RPlotExporter, RankColumn]
|
||||
[MemoryDiagnoser]
|
||||
public class ServerProcessingBenchmark : BaseBenchmark
|
||||
{
|
||||
[SimpleJob(RuntimeMoniker.Net60)]
|
||||
[RPlotExporter, RankColumn]
|
||||
[MemoryDiagnoser]
|
||||
public class ServerProcessingBenchmark : BaseBenchmark
|
||||
[GlobalSetup]
|
||||
public void GlobalSetup()
|
||||
{
|
||||
[GlobalSetup]
|
||||
public void GlobalSetup()
|
||||
{
|
||||
TestEnvironment.EnableLogger = false;
|
||||
}
|
||||
|
||||
[GlobalCleanup]
|
||||
public void GlobalCleanup()
|
||||
{
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void Handle_100_000_Messages_In_Server_MqttClient()
|
||||
{
|
||||
new Load_Tests().Handle_100_000_Messages_In_Server().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
//[Benchmark]
|
||||
public void Handle_100_000_Messages_In_Server_LowLevelMqttClient()
|
||||
{
|
||||
new Load_Tests().Handle_100_000_Messages_In_Low_Level_Client().GetAwaiter().GetResult();
|
||||
}
|
||||
TestEnvironment.EnableLogger = false;
|
||||
}
|
||||
|
||||
[GlobalCleanup]
|
||||
public void GlobalCleanup()
|
||||
{
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void Handle_100_000_Messages_In_Server_MqttClient()
|
||||
{
|
||||
new Load_Tests().Handle_100_000_Messages_In_Server().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
//[Benchmark]
|
||||
public void Handle_100_000_Messages_In_Server_LowLevelMqttClient()
|
||||
{
|
||||
new Load_Tests().Handle_100_000_Messages_In_Low_Level_Client().GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ public class SubscribeBenchmark : BaseBenchmark
|
||||
[GlobalSetup]
|
||||
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();
|
||||
|
||||
var serverOptions = new MqttServerOptionsBuilder().WithDefaultEndpoint().Build();
|
||||
|
||||
@@ -11,70 +11,69 @@ using BenchmarkDotNet.Attributes;
|
||||
using BenchmarkDotNet.Jobs;
|
||||
using MQTTnet.AspNetCore;
|
||||
|
||||
namespace MQTTnet.Benchmarks
|
||||
namespace MQTTnet.Benchmarks;
|
||||
|
||||
[SimpleJob(RuntimeMoniker.Net60)]
|
||||
[MemoryDiagnoser]
|
||||
public class TcpPipesBenchmark : BaseBenchmark
|
||||
{
|
||||
[SimpleJob(RuntimeMoniker.Net60)]
|
||||
[MemoryDiagnoser]
|
||||
public class TcpPipesBenchmark : BaseBenchmark
|
||||
IDuplexPipe _client;
|
||||
IDuplexPipe _server;
|
||||
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
{
|
||||
IDuplexPipe _client;
|
||||
IDuplexPipe _server;
|
||||
var server = new TcpListener(IPAddress.Any, 1883);
|
||||
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
server.Start(1);
|
||||
|
||||
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);
|
||||
|
||||
server.Start(1);
|
||||
|
||||
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 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);
|
||||
}
|
||||
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.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using BenchmarkDotNet.Jobs;
|
||||
|
||||
namespace MQTTnet.Benchmarks
|
||||
namespace MQTTnet.Benchmarks;
|
||||
|
||||
[SimpleJob(RuntimeMoniker.Net60)]
|
||||
[RPlotExporter]
|
||||
[MemoryDiagnoser]
|
||||
public class TopicFilterComparerBenchmark : BaseBenchmark
|
||||
{
|
||||
[SimpleJob(RuntimeMoniker.Net60)]
|
||||
[RPlotExporter]
|
||||
[MemoryDiagnoser]
|
||||
public class TopicFilterComparerBenchmark : BaseBenchmark
|
||||
readonly string _longTopic =
|
||||
"AAAAAAAAAAAAAssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssshhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC";
|
||||
|
||||
[Benchmark]
|
||||
public void MqttTopicFilterComparer_10000_LoopMethod()
|
||||
{
|
||||
static readonly char[] TopicLevelSeparator = { '/' };
|
||||
|
||||
readonly string _longTopic =
|
||||
"AAAAAAAAAAAAAssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssshhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC";
|
||||
|
||||
[Benchmark]
|
||||
public void MqttTopicFilterComparer_10000_LoopMethod()
|
||||
for (var i = 0; i < 100000; i++)
|
||||
{
|
||||
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/score/wimbledon", "sport/+/player1/#");
|
||||
MqttTopicFilterComparer.Compare("sport/tennis/player1", "sport/tennis/+");
|
||||
MqttTopicFilterComparer.Compare("/finance", "+/+");
|
||||
MqttTopicFilterComparer.Compare("/finance", "/+");
|
||||
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;
|
||||
}
|
||||
MqttTopicFilterComparer.Compare("sport/tennis/player1", "sport/#");
|
||||
MqttTopicFilterComparer.Compare("sport/tennis/player1/ranking", "sport/#/ranking");
|
||||
MqttTopicFilterComparer.Compare("sport/tennis/player1/score/wimbledon", "sport/+/player1/#");
|
||||
MqttTopicFilterComparer.Compare("sport/tennis/player1", "sport/tennis/+");
|
||||
MqttTopicFilterComparer.Compare("/finance", "+/+");
|
||||
MqttTopicFilterComparer.Compare("/finance", "/+");
|
||||
MqttTopicFilterComparer.Compare("/finance", "+");
|
||||
MqttTopicFilterComparer.Compare(_longTopic, _longTopic);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,84 +1,83 @@
|
||||
using System;
|
||||
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(
|
||||
int numPublishers,
|
||||
int numTopicsPerPublisher,
|
||||
out Dictionary<string, List<string>> topicsByPublisher,
|
||||
out Dictionary<string, List<string>> singleWildcardTopicsByPublisher,
|
||||
out Dictionary<string, List<string>> multiWildcardTopicsByPublisher)
|
||||
topicsByPublisher = new Dictionary<string, List<string>>();
|
||||
singleWildcardTopicsByPublisher = new Dictionary<string, List<string>>();
|
||||
multiWildcardTopicsByPublisher = new Dictionary<string, List<string>>();
|
||||
|
||||
// Find some reasonable distribution across three topic levels
|
||||
var topicsPerLevel = (int)Math.Pow(numTopicsPerPublisher, 1.0 / 3.0);
|
||||
if (topicsPerLevel <= 0)
|
||||
{
|
||||
topicsByPublisher = new Dictionary<string, List<string>>();
|
||||
singleWildcardTopicsByPublisher = new Dictionary<string, List<string>>();
|
||||
multiWildcardTopicsByPublisher = new Dictionary<string, List<string>>();
|
||||
topicsPerLevel = 1;
|
||||
}
|
||||
|
||||
// Find some reasonable distribution across three topic levels
|
||||
var topicsPerLevel = (int)Math.Pow(numTopicsPerPublisher, 1.0 / 3.0);
|
||||
if (topicsPerLevel <= 0)
|
||||
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)
|
||||
{
|
||||
topicsPerLevel = 1;
|
||||
}
|
||||
|
||||
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 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;
|
||||
}
|
||||
|
||||
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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
if (!topicsByPublisher.TryGetValue(publisherName, out topicList))
|
||||
{
|
||||
topicList = new List<string>();
|
||||
topicsByPublisher.Add(publisherName, topicList);
|
||||
}
|
||||
|
||||
topicList.Add(topic);
|
||||
topicList = new List<string>();
|
||||
topicsByPublisher.Add(publisherName, topicList);
|
||||
}
|
||||
|
||||
topicList.Add(topic);
|
||||
}
|
||||
}
|
||||
@@ -14,15 +14,15 @@ public class UnsubscribeBenchmark : BaseBenchmark
|
||||
{
|
||||
const int NumPublishers = 1;
|
||||
const int NumTopicsPerPublisher = 10000;
|
||||
|
||||
IMqttClient _mqttClient;
|
||||
MqttServer _mqttServer;
|
||||
|
||||
List<string> _topics;
|
||||
|
||||
[GlobalSetup]
|
||||
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();
|
||||
|
||||
var serverOptions = new MqttServerOptionsBuilder().WithDefaultEndpoint().Build();
|
||||
|
||||
@@ -4,27 +4,26 @@
|
||||
|
||||
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);
|
||||
|
||||
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
|
||||
};
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,12 +8,11 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MQTTnet.Protocol;
|
||||
|
||||
namespace MQTTnet.Extensions.Rpc
|
||||
namespace MQTTnet.Extensions.Rpc;
|
||||
|
||||
public interface IMqttRpcClient : IDisposable
|
||||
{
|
||||
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);
|
||||
}
|
||||
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.
|
||||
// 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;
|
||||
|
||||
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,
|
||||
new MqttRpcClientOptions
|
||||
{
|
||||
TopicGenerationStrategy = new DefaultMqttRpcClientTopicGenerationStrategy()
|
||||
});
|
||||
}
|
||||
return clientFactory.CreateMqttRpcClient(
|
||||
mqttClient,
|
||||
new MqttRpcClientOptions
|
||||
{
|
||||
TopicGenerationStrategy = new DefaultMqttRpcClientTopicGenerationStrategy()
|
||||
});
|
||||
}
|
||||
|
||||
public static IMqttRpcClient CreateMqttRpcClient(this MqttClientFactory _, IMqttClient mqttClient, MqttRpcClientOptions rpcClientOptions)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(mqttClient);
|
||||
ArgumentNullException.ThrowIfNull(rpcClientOptions);
|
||||
public static IMqttRpcClient CreateMqttRpcClient(this MqttClientFactory _, IMqttClient mqttClient, MqttRpcClientOptions rpcClientOptions)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(mqttClient);
|
||||
ArgumentNullException.ThrowIfNull(rpcClientOptions);
|
||||
|
||||
return new MqttRpcClient(mqttClient, rpcClientOptions);
|
||||
}
|
||||
return new MqttRpcClient(mqttClient, rpcClientOptions);
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MQTTnet.Exceptions;
|
||||
@@ -14,128 +13,125 @@ using MQTTnet.Formatter;
|
||||
using MQTTnet.Internal;
|
||||
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;
|
||||
readonly MqttRpcClientOptions _options;
|
||||
_mqttClient = mqttClient ?? throw new ArgumentNullException(nameof(mqttClient));
|
||||
_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));
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
|
||||
_mqttClient.ApplicationMessageReceivedAsync += HandleApplicationMessageReceivedAsync;
|
||||
tcs.Value.TrySetCanceled();
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
foreach (var tcs in _waitingCalls)
|
||||
{
|
||||
tcs.Value.TrySetCanceled();
|
||||
}
|
||||
|
||||
_waitingCalls.Clear();
|
||||
return await ExecuteAsync(methodName, payload, qualityOfServiceLevel, parameters, timeoutToken.Token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<byte[]> ExecuteAsync(TimeSpan timeout, string methodName, byte[] payload, MqttQualityOfServiceLevel qualityOfServiceLevel, IDictionary<string, object> parameters = null)
|
||||
catch (OperationCanceledException exception)
|
||||
{
|
||||
using (var timeoutToken = new CancellationTokenSource(timeout))
|
||||
if (timeoutToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
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.");
|
||||
throw new MqttCommunicationTimedOutException(exception);
|
||||
}
|
||||
|
||||
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;
|
||||
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 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 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.
|
||||
// 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;
|
||||
|
||||
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();
|
||||
|
||||
public MqttRpcClientOptions Build()
|
||||
return new MqttRpcClientOptions
|
||||
{
|
||||
return new MqttRpcClientOptions
|
||||
{
|
||||
TopicGenerationStrategy = _topicGenerationStrategy
|
||||
};
|
||||
}
|
||||
TopicGenerationStrategy = _topicGenerationStrategy
|
||||
};
|
||||
}
|
||||
|
||||
public MqttRpcClientOptionsBuilder WithTopicGenerationStrategy(IMqttRpcClientTopicGenerationStrategy value)
|
||||
{
|
||||
_topicGenerationStrategy = value ?? throw new ArgumentNullException(nameof(value));
|
||||
public MqttRpcClientOptionsBuilder WithTopicGenerationStrategy(IMqttRpcClientTopicGenerationStrategy 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.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace MQTTnet.Extensions.Rpc
|
||||
{
|
||||
public sealed class MqttRpcTopicPair
|
||||
{
|
||||
public string RequestTopic { get; set; }
|
||||
namespace MQTTnet.Extensions.Rpc;
|
||||
|
||||
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 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,
|
||||
IDictionary<string, object> parameters, MqttQualityOfServiceLevel qualityOfServiceLevel)
|
||||
{
|
||||
MethodName = methodName ?? throw new ArgumentNullException(nameof(methodName));
|
||||
Parameters = parameters;
|
||||
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; }
|
||||
MethodName = methodName ?? throw new ArgumentNullException(nameof(methodName));
|
||||
Parameters = parameters;
|
||||
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; }
|
||||
}
|
||||
@@ -47,7 +47,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<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>
|
||||
|
||||
@@ -230,7 +230,7 @@ public sealed class MqttTopicTemplate : IEquatable<MqttTopicTemplate>
|
||||
throw new ArgumentException("the topic has to match this template", nameof(topic));
|
||||
}
|
||||
|
||||
return parseParameterValuesInternal(topic);
|
||||
return ParseParameterValuesInternal(topic);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -330,7 +330,7 @@ public sealed class MqttTopicTemplate : IEquatable<MqttTopicTemplate>
|
||||
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
|
||||
var segments = topic.Split(MqttTopicFilterComparer.LevelSeparator);
|
||||
|
||||
@@ -5,28 +5,27 @@
|
||||
using MQTTnet.Packets;
|
||||
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>
|
||||
/// The reason string is sent to every client via a DISCONNECT packet.
|
||||
/// <remarks>MQTT 5.0.0+ feature.</remarks>
|
||||
/// </summary>
|
||||
public string ReasonString { get; set; }
|
||||
/// <summary>
|
||||
/// The reason string is sent to every client via a DISCONNECT packet.
|
||||
/// <remarks>MQTT 5.0.0+ feature.</remarks>
|
||||
/// </summary>
|
||||
public string ReasonString { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The server reference is sent to every client via a DISCONNECT packet.
|
||||
/// <remarks>MQTT 5.0.0+ feature.</remarks>
|
||||
/// </summary>
|
||||
public string ServerReference { get; set; }
|
||||
/// <summary>
|
||||
/// The server reference is sent to every client via a DISCONNECT packet.
|
||||
/// <remarks>MQTT 5.0.0+ feature.</remarks>
|
||||
/// </summary>
|
||||
public string ServerReference { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// These user properties are sent to every client via a DISCONNECT packet.
|
||||
/// <remarks>MQTT 5.0.0+ feature.</remarks>
|
||||
/// </summary>
|
||||
public List<MqttUserProperty> UserProperties { get; set; }
|
||||
}
|
||||
/// <summary>
|
||||
/// These user properties are sent to every client via a DISCONNECT packet.
|
||||
/// <remarks>MQTT 5.0.0+ feature.</remarks>
|
||||
/// </summary>
|
||||
public List<MqttUserProperty> UserProperties { get; set; }
|
||||
}
|
||||
@@ -5,50 +5,49 @@
|
||||
using MQTTnet.Packets;
|
||||
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.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;
|
||||
}
|
||||
_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.
|
||||
// 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));
|
||||
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; }
|
||||
SenderClientId = senderClientId ?? throw new ArgumentNullException( nameof(senderClientId));
|
||||
ReceiverClientId = receiverClientId ?? throw new ArgumentNullException(nameof(receiverClientId));
|
||||
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; }
|
||||
}
|
||||
@@ -2,26 +2,23 @@
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
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;
|
||||
}
|
||||
|
||||
/// <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; }
|
||||
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; }
|
||||
}
|
||||
@@ -2,51 +2,49 @@
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
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;
|
||||
SessionItems = sessionItems ?? throw new ArgumentNullException(nameof(sessionItems));
|
||||
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; }
|
||||
ClientId = clientId ?? throw new ArgumentNullException(nameof(clientId));
|
||||
UserName = userName;
|
||||
SessionItems = sessionItems ?? throw new ArgumentNullException(nameof(sessionItems));
|
||||
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; }
|
||||
}
|
||||
@@ -2,70 +2,67 @@
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using MQTTnet.Formatter;
|
||||
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;
|
||||
|
||||
public ClientConnectedEventArgs(MqttConnectPacket connectPacket, MqttProtocolVersion protocolVersion, EndPoint remoteEndPoint, IDictionary 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;
|
||||
_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;
|
||||
}
|
||||
@@ -2,91 +2,88 @@
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using MQTTnet.Packets;
|
||||
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;
|
||||
readonly MqttDisconnectPacket _disconnectPacket;
|
||||
DisconnectType = disconnectType;
|
||||
RemoteEndPoint = remoteEndPoint;
|
||||
SessionItems = sessionItems ?? throw new ArgumentNullException(nameof(sessionItems));
|
||||
|
||||
public ClientDisconnectedEventArgs(
|
||||
MqttConnectPacket connectPacket,
|
||||
MqttDisconnectPacket 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;
|
||||
_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;
|
||||
}
|
||||
@@ -2,42 +2,40 @@
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
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;
|
||||
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; }
|
||||
ClientId = clientId ?? throw new ArgumentNullException(nameof(clientId));
|
||||
UserName = userName;
|
||||
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; }
|
||||
}
|
||||
@@ -2,41 +2,39 @@
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
|
||||
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;
|
||||
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; }
|
||||
ClientId = clientId ?? throw new ArgumentNullException(nameof(clientId));
|
||||
UserName = userName;
|
||||
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; }
|
||||
}
|
||||
+23
-26
@@ -2,34 +2,31 @@
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
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));
|
||||
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; }
|
||||
SenderClientId = senderClientId ?? throw new ArgumentNullException(nameof(senderClientId));
|
||||
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; }
|
||||
}
|
||||
@@ -2,63 +2,60 @@
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
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));
|
||||
UserName = userName;
|
||||
RemoteEndPoint = remoteEndPoint;
|
||||
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; }
|
||||
CancellationToken = cancellationToken;
|
||||
ClientId = clientId ?? throw new ArgumentNullException(nameof(clientId));
|
||||
UserName = userName;
|
||||
RemoteEndPoint = remoteEndPoint;
|
||||
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; }
|
||||
}
|
||||
@@ -2,56 +2,53 @@
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
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;
|
||||
ClientId = clientId ?? throw new ArgumentNullException(nameof(clientId));
|
||||
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; }
|
||||
ApplicationMessage = applicationMessage ?? throw new ArgumentNullException(nameof(applicationMessage));
|
||||
CancellationToken = cancellationToken;
|
||||
ClientId = clientId ?? throw new ArgumentNullException(nameof(clientId));
|
||||
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; }
|
||||
}
|
||||
@@ -2,90 +2,86 @@
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
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,
|
||||
string clientId,
|
||||
string userName,
|
||||
MqttSessionStatus session,
|
||||
MqttTopicFilter topicFilter,
|
||||
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; }
|
||||
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; }
|
||||
}
|
||||
@@ -2,77 +2,73 @@
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
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;
|
||||
UserName = userName;
|
||||
SessionItems = sessionItems;
|
||||
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; }
|
||||
CancellationToken = cancellationToken;
|
||||
ClientId = clientId;
|
||||
UserName = userName;
|
||||
SessionItems = sessionItems;
|
||||
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; }
|
||||
}
|
||||
@@ -2,13 +2,9 @@
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
namespace MQTTnet.Server;
|
||||
|
||||
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.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using MQTTnet.Packets;
|
||||
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 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 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
|
||||
· 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>
|
||||
/// Gets the will delay interval.
|
||||
/// This is the time between the client disconnect and the time the will message will be sent.
|
||||
/// </summary>
|
||||
public uint? WillDelayInterval { get; set; }
|
||||
/// <summary>
|
||||
/// Gets the will delay interval.
|
||||
/// This is the time between the client disconnect and the time the will message will be sent.
|
||||
/// </summary>
|
||||
public uint? WillDelayInterval { get; set; }
|
||||
|
||||
// <summary>
|
||||
// Gets the last will message.
|
||||
// In MQTT, you use the last will message feature to notify other clients about an ungracefully disconnected client.
|
||||
// </summary>
|
||||
// TODO: Use single properties. No entire will message.
|
||||
//MqttApplicationMessage WillMessage { get; set; }
|
||||
}
|
||||
// <summary>
|
||||
// Gets the last will message.
|
||||
// In MQTT, you use the last will message feature to notify other clients about an ungracefully disconnected client.
|
||||
// </summary>
|
||||
// TODO: Use single properties. No entire will message.
|
||||
//MqttApplicationMessage WillMessage { get; set; }
|
||||
}
|
||||
@@ -2,21 +2,19 @@
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using 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));
|
||||
}
|
||||
|
||||
public MqttPacket Packet { get; }
|
||||
|
||||
public string ReceiverClientId { get; }
|
||||
ReceiverClientId = receiverClientId ?? throw new ArgumentNullException(nameof(receiverClientId));
|
||||
Packet = packet ?? throw new ArgumentNullException(nameof(packet));
|
||||
}
|
||||
|
||||
public MqttPacket Packet { get; }
|
||||
|
||||
public string ReceiverClientId { get; }
|
||||
}
|
||||
@@ -2,24 +2,20 @@
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
namespace MQTTnet.Server;
|
||||
|
||||
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));
|
||||
StoredRetainedMessages = storedRetainedMessages ?? throw new ArgumentNullException(nameof(storedRetainedMessages));
|
||||
}
|
||||
|
||||
public MqttApplicationMessage ChangedRetainedMessage { get; }
|
||||
|
||||
public string ClientId { get; }
|
||||
|
||||
public List<MqttApplicationMessage> StoredRetainedMessages { get; }
|
||||
ClientId = clientId ?? throw new ArgumentNullException(nameof(clientId));
|
||||
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; }
|
||||
}
|
||||
@@ -2,33 +2,31 @@
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
|
||||
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;
|
||||
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; }
|
||||
Id = id ?? throw new ArgumentNullException(nameof(id));
|
||||
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; }
|
||||
}
|
||||
@@ -8,118 +8,117 @@ using MQTTnet.Adapter;
|
||||
using MQTTnet.Diagnostics.Logger;
|
||||
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;
|
||||
|
||||
MqttServerOptions _serverOptions;
|
||||
|
||||
public Func<IMqttChannelAdapter, Task> ClientHandler { get; set; }
|
||||
|
||||
public bool TreatSocketOpeningErrorAsWarning { get; set; }
|
||||
|
||||
public void Dispose()
|
||||
public Task StartAsync(MqttServerOptions options, IMqttNetLogger logger)
|
||||
{
|
||||
if (_cancellationTokenSource != null)
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (options.TlsEndpointOptions.CertificateProvider == null)
|
||||
{
|
||||
throw new ArgumentException("TLS certificate is not set.");
|
||||
}
|
||||
|
||||
RegisterListeners(options.TlsEndpointOptions, logger, _cancellationTokenSource.Token);
|
||||
}
|
||||
_listeners.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
Task OnClientAcceptedAsync(IMqttChannelAdapter channelAdapter)
|
||||
{
|
||||
var clientHandler = ClientHandler;
|
||||
if (clientHandler == null)
|
||||
{
|
||||
return CompletedTask.Instance;
|
||||
}
|
||||
|
||||
public Task StopAsync()
|
||||
return clientHandler(channelAdapter);
|
||||
}
|
||||
|
||||
void RegisterListeners(MqttServerTcpEndpointBaseOptions tcpEndpointOptions, IMqttNetLogger logger, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!tcpEndpointOptions.BoundInterNetworkAddress.Equals(IPAddress.None))
|
||||
{
|
||||
Cleanup();
|
||||
return CompletedTask.Instance;
|
||||
}
|
||||
|
||||
void Cleanup()
|
||||
{
|
||||
try
|
||||
var listenerV4 = new MqttTcpServerListener(AddressFamily.InterNetwork, _serverOptions, tcpEndpointOptions, logger)
|
||||
{
|
||||
_cancellationTokenSource?.Cancel(false);
|
||||
}
|
||||
finally
|
||||
ClientHandler = OnClientAcceptedAsync
|
||||
};
|
||||
|
||||
if (listenerV4.Start(TreatSocketOpeningErrorAsWarning, cancellationToken))
|
||||
{
|
||||
_cancellationTokenSource?.Dispose();
|
||||
_cancellationTokenSource = null;
|
||||
|
||||
foreach (var listener in _listeners)
|
||||
{
|
||||
listener.Dispose();
|
||||
}
|
||||
|
||||
_listeners.Clear();
|
||||
_listeners.Add(listenerV4);
|
||||
}
|
||||
}
|
||||
|
||||
Task OnClientAcceptedAsync(IMqttChannelAdapter channelAdapter)
|
||||
if (!tcpEndpointOptions.BoundInterNetworkV6Address.Equals(IPAddress.None))
|
||||
{
|
||||
var clientHandler = ClientHandler;
|
||||
if (clientHandler == null)
|
||||
var listenerV6 = new MqttTcpServerListener(AddressFamily.InterNetworkV6, _serverOptions, tcpEndpointOptions, logger)
|
||||
{
|
||||
return CompletedTask.Instance;
|
||||
}
|
||||
ClientHandler = OnClientAcceptedAsync
|
||||
};
|
||||
|
||||
return clientHandler(channelAdapter);
|
||||
}
|
||||
|
||||
void RegisterListeners(MqttServerTcpEndpointBaseOptions tcpEndpointOptions, IMqttNetLogger logger, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!tcpEndpointOptions.BoundInterNetworkAddress.Equals(IPAddress.None))
|
||||
if (listenerV6.Start(TreatSocketOpeningErrorAsWarning, cancellationToken))
|
||||
{
|
||||
var listenerV4 = new MqttTcpServerListener(AddressFamily.InterNetwork, _serverOptions, tcpEndpointOptions, logger)
|
||||
{
|
||||
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);
|
||||
}
|
||||
_listeners.Add(listenerV6);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,248 +12,247 @@ using MQTTnet.Formatter;
|
||||
using MQTTnet.Implementations;
|
||||
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;
|
||||
readonly IMqttNetLogger _rootLogger;
|
||||
readonly AddressFamily _addressFamily;
|
||||
readonly MqttServerOptions _serverOptions;
|
||||
readonly MqttServerTcpEndpointBaseOptions _options;
|
||||
readonly MqttServerTlsTcpEndpointOptions _tlsOptions;
|
||||
_addressFamily = addressFamily;
|
||||
_serverOptions = serverOptions ?? throw new ArgumentNullException(nameof(serverOptions));
|
||||
_options = tcpEndpointOptions ?? throw new ArgumentNullException(nameof(tcpEndpointOptions));
|
||||
_rootLogger = logger;
|
||||
_logger = logger.WithSource(nameof(MqttTcpServerListener));
|
||||
|
||||
CrossPlatformSocket _socket;
|
||||
IPEndPoint _localEndPoint;
|
||||
|
||||
public MqttTcpServerListener(
|
||||
AddressFamily addressFamily,
|
||||
MqttServerOptions serverOptions,
|
||||
MqttServerTcpEndpointBaseOptions tcpEndpointOptions,
|
||||
IMqttNetLogger logger)
|
||||
if (_options is MqttServerTlsTcpEndpointOptions tlsOptions)
|
||||
{
|
||||
_addressFamily = addressFamily;
|
||||
_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;
|
||||
}
|
||||
_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
|
||||
{
|
||||
var boundIp = _options.BoundInterNetworkAddress;
|
||||
if (_addressFamily == AddressFamily.InterNetworkV6)
|
||||
var clientSocket = await _socket.AcceptAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (clientSocket == null)
|
||||
{
|
||||
boundIp = _options.BoundInterNetworkV6Address;
|
||||
continue;
|
||||
}
|
||||
|
||||
_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;
|
||||
_ = Task.Factory.StartNew(() => TryHandleClientConnectionAsync(clientSocket), cancellationToken, TaskCreationOptions.PreferFairness, TaskScheduler.Default).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
if (!treatErrorsAsWarning)
|
||||
if (exception is SocketException socketException)
|
||||
{
|
||||
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
|
||||
{
|
||||
var clientSocket = await _socket.AcceptAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (clientSocket == null)
|
||||
if (socketException.SocketErrorCode is SocketError.ConnectionAborted or SocketError.OperationAborted)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
_ = Task.Factory.StartNew(() => TryHandleClientConnectionAsync(clientSocket), cancellationToken, TaskCreationOptions.PreferFairness, TaskScheduler.Default).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);
|
||||
}
|
||||
_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)
|
||||
{
|
||||
Stream stream = null;
|
||||
EndPoint remoteEndPoint = null;
|
||||
|
||||
try
|
||||
{
|
||||
remoteEndPoint = clientSocket.RemoteEndPoint;
|
||||
|
||||
_logger.Verbose("TCP client '{0}' accepted (Local endpoint={1})", remoteEndPoint, _localEndPoint);
|
||||
|
||||
clientSocket.NoDelay = _options.NoDelay;
|
||||
stream = clientSocket.GetStream();
|
||||
var clientCertificate = _tlsOptions?.CertificateProvider?.GetCertificate();
|
||||
|
||||
if (clientCertificate != null)
|
||||
{
|
||||
if (!clientCertificate.HasPrivateKey)
|
||||
{
|
||||
throw new InvalidOperationException("The certificate for TLS encryption must contain the private key.");
|
||||
}
|
||||
|
||||
var sslStream = new SslStream(stream, false, _tlsOptions.RemoteCertificateValidationCallback);
|
||||
|
||||
await sslStream.AuthenticateAsServerAsync(
|
||||
new SslServerAuthenticationOptions
|
||||
{
|
||||
ServerCertificate = clientCertificate,
|
||||
ClientCertificateRequired = _tlsOptions.ClientCertificateRequired,
|
||||
EnabledSslProtocols = _tlsOptions.SslProtocol,
|
||||
CertificateRevocationCheckMode = _tlsOptions.CheckCertificateRevocation ? X509RevocationMode.Online : X509RevocationMode.NoCheck,
|
||||
EncryptionPolicy = EncryptionPolicy.RequireEncryption,
|
||||
CipherSuitesPolicy = _tlsOptions.CipherSuitesPolicy
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
stream = sslStream;
|
||||
|
||||
clientCertificate = sslStream.RemoteCertificate as X509Certificate2;
|
||||
|
||||
if (clientCertificate == null && sslStream.RemoteCertificate != null)
|
||||
{
|
||||
clientCertificate = new X509Certificate2(sslStream.RemoteCertificate.Export(X509ContentType.Cert));
|
||||
}
|
||||
}
|
||||
|
||||
var clientHandler = ClientHandler;
|
||||
if (clientHandler != null)
|
||||
{
|
||||
var tcpChannel = new MqttTcpChannel(stream, remoteEndPoint, clientCertificate);
|
||||
var bufferWriter = new MqttBufferWriter(_serverOptions.WriterBufferSize, _serverOptions.WriterBufferSizeMax);
|
||||
var packetFormatterAdapter = new MqttPacketFormatterAdapter(bufferWriter);
|
||||
|
||||
using (var clientAdapter = new MqttChannelAdapter(tcpChannel, packetFormatterAdapter, _rootLogger))
|
||||
{
|
||||
clientAdapter.AllowPacketFragmentation = _options.AllowPacketFragmentation;
|
||||
await clientHandler(clientAdapter).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
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.
|
||||
return;
|
||||
}
|
||||
|
||||
if (exception is SocketException socketException &&
|
||||
socketException.SocketErrorCode == SocketError.OperationAborted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.Error(exception, "Error while handling TCP client connection");
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
// ReSharper disable once MethodHasAsyncOverload
|
||||
stream?.Dispose();
|
||||
clientSocket?.Dispose();
|
||||
}
|
||||
catch (Exception disposeException)
|
||||
{
|
||||
_logger.Error(disposeException, "Error while cleaning up client connection");
|
||||
}
|
||||
}
|
||||
|
||||
_logger.Verbose("TCP client '{0}' disconnected (Local endpoint={1})", remoteEndPoint, _localEndPoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async Task TryHandleClientConnectionAsync(CrossPlatformSocket clientSocket)
|
||||
{
|
||||
Stream stream = null;
|
||||
EndPoint remoteEndPoint = null;
|
||||
|
||||
try
|
||||
{
|
||||
remoteEndPoint = clientSocket.RemoteEndPoint;
|
||||
|
||||
_logger.Verbose("TCP client '{0}' accepted (Local endpoint={1})", remoteEndPoint, _localEndPoint);
|
||||
|
||||
clientSocket.NoDelay = _options.NoDelay;
|
||||
stream = clientSocket.GetStream();
|
||||
var clientCertificate = _tlsOptions?.CertificateProvider?.GetCertificate();
|
||||
|
||||
if (clientCertificate != null)
|
||||
{
|
||||
if (!clientCertificate.HasPrivateKey)
|
||||
{
|
||||
throw new InvalidOperationException("The certificate for TLS encryption must contain the private key.");
|
||||
}
|
||||
|
||||
var sslStream = new SslStream(stream, false, _tlsOptions.RemoteCertificateValidationCallback);
|
||||
|
||||
await sslStream.AuthenticateAsServerAsync(
|
||||
new SslServerAuthenticationOptions
|
||||
{
|
||||
ServerCertificate = clientCertificate,
|
||||
ClientCertificateRequired = _tlsOptions.ClientCertificateRequired,
|
||||
EnabledSslProtocols = _tlsOptions.SslProtocol,
|
||||
CertificateRevocationCheckMode = _tlsOptions.CheckCertificateRevocation ? X509RevocationMode.Online : X509RevocationMode.NoCheck,
|
||||
EncryptionPolicy = EncryptionPolicy.RequireEncryption,
|
||||
CipherSuitesPolicy = _tlsOptions.CipherSuitesPolicy
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
stream = sslStream;
|
||||
|
||||
clientCertificate = sslStream.RemoteCertificate as X509Certificate2;
|
||||
|
||||
if (clientCertificate == null && sslStream.RemoteCertificate != null)
|
||||
{
|
||||
clientCertificate = new X509Certificate2(sslStream.RemoteCertificate.Export(X509ContentType.Cert));
|
||||
}
|
||||
}
|
||||
|
||||
var clientHandler = ClientHandler;
|
||||
if (clientHandler != null)
|
||||
{
|
||||
var tcpChannel = new MqttTcpChannel(stream, remoteEndPoint, clientCertificate);
|
||||
var bufferWriter = new MqttBufferWriter(_serverOptions.WriterBufferSize, _serverOptions.WriterBufferSizeMax);
|
||||
var packetFormatterAdapter = new MqttPacketFormatterAdapter(bufferWriter);
|
||||
|
||||
using var clientAdapter = new MqttChannelAdapter(tcpChannel, packetFormatterAdapter, _rootLogger);
|
||||
clientAdapter.AllowPacketFragmentation = _options.AllowPacketFragmentation;
|
||||
await clientHandler(clientAdapter).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
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.
|
||||
return;
|
||||
}
|
||||
|
||||
if (exception is SocketException socketException &&
|
||||
socketException.SocketErrorCode == SocketError.OperationAborted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.Error(exception, "Error while handling TCP client connection");
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
if (stream != null)
|
||||
{
|
||||
await stream.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
clientSocket?.Dispose();
|
||||
}
|
||||
catch (Exception disposeException)
|
||||
{
|
||||
_logger.Error(disposeException, "Error while cleaning up client connection");
|
||||
}
|
||||
}
|
||||
|
||||
_logger.Verbose("TCP client '{0}' disconnected (Local endpoint={1})", remoteEndPoint, _localEndPoint);
|
||||
}
|
||||
}
|
||||
@@ -4,18 +4,17 @@
|
||||
|
||||
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 List<uint> SubscriptionIdentifiers { get; set; }
|
||||
|
||||
public MqttQualityOfServiceLevel QualityOfServiceLevel { get; set; }
|
||||
}
|
||||
}
|
||||
public bool RetainAsPublished { get; set; }
|
||||
|
||||
public List<uint> SubscriptionIdentifiers { get; set; }
|
||||
|
||||
public MqttQualityOfServiceLevel QualityOfServiceLevel { get; set; }
|
||||
}
|
||||
@@ -4,24 +4,23 @@
|
||||
|
||||
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;
|
||||
ReasonString = reasonString;
|
||||
UserProperties = userProperties;
|
||||
}
|
||||
|
||||
public bool CloseConnection { get; }
|
||||
|
||||
public int ReasonCode { get; }
|
||||
|
||||
public string ReasonString { get; }
|
||||
|
||||
public List<MqttUserProperty> UserProperties { get; }
|
||||
ReasonCode = reasonCode;
|
||||
CloseConnection = closeConnection;
|
||||
ReasonString = reasonString;
|
||||
UserProperties = userProperties;
|
||||
}
|
||||
|
||||
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.
|
||||
// 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;
|
||||
|
||||
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);
|
||||
ArgumentNullException.ThrowIfNull(unsubscribeResult);
|
||||
PacketIdentifier = unsubscribePacket.PacketIdentifier
|
||||
};
|
||||
|
||||
var unsubAckPacket = new MqttUnsubAckPacket
|
||||
{
|
||||
PacketIdentifier = unsubscribePacket.PacketIdentifier
|
||||
};
|
||||
// MQTTv5.0.0 only.
|
||||
unsubAckPacket.ReasonCodes = unsubscribeResult.ReasonCodes;
|
||||
|
||||
// MQTTv5.0.0 only.
|
||||
unsubAckPacket.ReasonCodes = unsubscribeResult.ReasonCodes;
|
||||
|
||||
return unsubAckPacket;
|
||||
}
|
||||
return unsubAckPacket;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
namespace MQTTnet.Server.Internal
|
||||
{
|
||||
public interface ISubscriptionChangedNotification
|
||||
{
|
||||
void OnSubscriptionsAdded(MqttSession clientSession, List<string> subscriptionsTopics);
|
||||
namespace MQTTnet.Server.Internal;
|
||||
|
||||
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))
|
||||
{
|
||||
await channelAdapter.DisconnectAsync(timeout.Token).ConfigureAwait(false);
|
||||
}
|
||||
using var timeout = new CancellationTokenSource(_options.DefaultCommunicationTimeout);
|
||||
await channelAdapter.DisconnectAsync(timeout.Token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -673,14 +671,12 @@ public sealed class MqttClientSessionsManager : ISubscriptionChangedNotification
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var timeoutToken = new CancellationTokenSource(_options.DefaultCommunicationTimeout))
|
||||
using (var effectiveCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(timeoutToken.Token, cancellationToken))
|
||||
using var timeoutToken = new CancellationTokenSource(_options.DefaultCommunicationTimeout);
|
||||
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);
|
||||
if (firstPacket is MqttConnectPacket connectPacket)
|
||||
{
|
||||
return connectPacket;
|
||||
}
|
||||
return connectPacket;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
|
||||
@@ -4,101 +4,100 @@
|
||||
|
||||
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.
|
||||
_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;
|
||||
Interlocked.Increment(ref _statistics._sentApplicationMessagesCount);
|
||||
}
|
||||
|
||||
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)
|
||||
if (!(packet is MqttPingReqPacket || packet is MqttPingRespPacket))
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(packet);
|
||||
|
||||
// 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;
|
||||
}
|
||||
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);
|
||||
|
||||
// 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);
|
||||
}
|
||||
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;
|
||||
public long _sentPacketsCount;
|
||||
|
||||
public long _receivedApplicationMessagesCount;
|
||||
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);
|
||||
}
|
||||
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.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;
|
||||
readonly Dictionary<ulong, HashSet<MqttSubscription>> _noWildcardSubscriptionsByTopicHash = new Dictionary<ulong, HashSet<MqttSubscription>>();
|
||||
readonly MqttRetainedMessagesManager _retainedMessagesManager;
|
||||
public CheckSubscriptionsResult CheckSubscriptions(string topic, ulong topicHash, MqttQualityOfServiceLevel qualityOfServiceLevel, string senderId)
|
||||
{
|
||||
var possibleSubscriptions = new List<MqttSubscription>();
|
||||
|
||||
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)
|
||||
// Check for possible subscriptions. They might have collisions but this is fine.
|
||||
_subscriptionsLock.EnterReadLock();
|
||||
try
|
||||
{
|
||||
_session = session ?? throw new ArgumentNullException(nameof(session));
|
||||
_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))
|
||||
{
|
||||
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 subscriptionsByHashMask = wcs.Value.SubscriptionsByHashMask;
|
||||
foreach (var shm in subscriptionsByHashMask)
|
||||
var subscriptionHashMask = shm.Key;
|
||||
if ((topicHash & subscriptionHashMask) == subscriptionHash)
|
||||
{
|
||||
var subscriptionHashMask = shm.Key;
|
||||
if ((topicHash & subscriptionHashMask) == subscriptionHash)
|
||||
{
|
||||
var subscriptions = shm.Value;
|
||||
possibleSubscriptions.AddRange(subscriptions);
|
||||
}
|
||||
var subscriptions = shm.Value;
|
||||
possibleSubscriptions.AddRange(subscriptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_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;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_subscriptionsLock.ExitReadLock();
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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))
|
||||
if (subscription.NoLocal && senderIsReceiver)
|
||||
{
|
||||
var interceptorEventArgs = await InterceptSubscribe(topicFilterItem, subscribePacket.UserProperties, cancellationToken).ConfigureAwait(false);
|
||||
var topicFilter = interceptorEventArgs.TopicFilter;
|
||||
var processSubscription = interceptorEventArgs.ProcessSubscription && interceptorEventArgs.Response.ReasonCode <= MqttSubscribeReasonCode.GrantedQoS2;
|
||||
// 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()
|
||||
{
|
||||
_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.ReasonString = interceptorEventArgs.ReasonString;
|
||||
result.ReasonCodes.Add(interceptorEventArgs.Response.ReasonCode);
|
||||
|
||||
if (interceptorEventArgs.CloseConnection)
|
||||
@@ -188,329 +245,271 @@ namespace MQTTnet.Server.Internal
|
||||
result.CloseConnection = true;
|
||||
}
|
||||
|
||||
if (!processSubscription || string.IsNullOrEmpty(topicFilter.Topic))
|
||||
if (!acceptUnsubscription)
|
||||
{
|
||||
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)
|
||||
if (interceptorEventArgs.ProcessUnsubscription)
|
||||
{
|
||||
var eventArgs = new ClientSubscribedTopicEventArgs(_session.Id, _session.UserName, finalTopicFilter, _session.Items);
|
||||
await _eventContainer.ClientSubscribedTopicEvent.InvokeAsync(eventArgs).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
_subscriptions.Remove(topicFilter);
|
||||
|
||||
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.ReasonCodes.Add(interceptorEventArgs.Response.ReasonCode);
|
||||
|
||||
if (interceptorEventArgs.CloseConnection)
|
||||
// must remove subscription object from topic hash dictionary also
|
||||
if (existingSubscription != null)
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
var topicHash = existingSubscription.TopicHash;
|
||||
|
||||
if (!acceptUnsubscription)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (interceptorEventArgs.ProcessUnsubscription)
|
||||
{
|
||||
_subscriptions.Remove(topicFilter);
|
||||
|
||||
// must remove subscription object from topic hash dictionary also
|
||||
if (existingSubscription != null)
|
||||
if (existingSubscription.TopicHasWildcard)
|
||||
{
|
||||
var topicHash = existingSubscription.TopicHash;
|
||||
|
||||
if (existingSubscription.TopicHasWildcard)
|
||||
if (_wildcardSubscriptionsByTopicHash.TryGetValue(topicHash, out var subscriptions))
|
||||
{
|
||||
if (_wildcardSubscriptionsByTopicHash.TryGetValue(topicHash, out var subscriptions))
|
||||
subscriptions.RemoveSubscription(existingSubscription);
|
||||
if (subscriptions.SubscriptionsByHashMask.Count == 0)
|
||||
{
|
||||
subscriptions.RemoveSubscription(existingSubscription);
|
||||
if (subscriptions.SubscriptionsByHashMask.Count == 0)
|
||||
{
|
||||
_wildcardSubscriptionsByTopicHash.Remove(topicHash);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_noWildcardSubscriptionsByTopicHash.TryGetValue(topicHash, out var subscriptions))
|
||||
{
|
||||
subscriptions.Remove(existingSubscription);
|
||||
if (subscriptions.Count == 0)
|
||||
{
|
||||
_noWildcardSubscriptionsByTopicHash.Remove(topicHash);
|
||||
}
|
||||
_wildcardSubscriptionsByTopicHash.Remove(topicHash);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_noWildcardSubscriptionsByTopicHash.TryGetValue(topicHash, out var subscriptions))
|
||||
{
|
||||
subscriptions.Remove(existingSubscription);
|
||||
if (subscriptions.Count == 0)
|
||||
{
|
||||
_noWildcardSubscriptionsByTopicHash.Remove(topicHash);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
removedSubscriptions.Add(topicFilter);
|
||||
}
|
||||
|
||||
removedSubscriptions.Add(topicFilter);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_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;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_subscriptionsLock.ExitWriteLock();
|
||||
_subscriptionChangedNotification?.OnSubscriptionsRemoved(_session, removedSubscriptions);
|
||||
}
|
||||
|
||||
CreateSubscriptionResult CreateSubscription(MqttTopicFilter topicFilter, uint subscriptionIdentifier, MqttSubscribeReasonCode reasonCode)
|
||||
if (_eventContainer.ClientUnsubscribedTopicEvent.HasHandlers)
|
||||
{
|
||||
MqttQualityOfServiceLevel grantedQualityOfServiceLevel;
|
||||
|
||||
if (reasonCode == MqttSubscribeReasonCode.GrantedQoS0)
|
||||
foreach (var topicFilter in unsubscribePacket.TopicFilters)
|
||||
{
|
||||
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;
|
||||
}
|
||||
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
|
||||
// must remove object from topic hash dictionary first
|
||||
if (hasWildcard)
|
||||
{
|
||||
if (!_wildcardSubscriptionsByTopicHash.TryGetValue(topicHash, out var subscriptions))
|
||||
if (_wildcardSubscriptionsByTopicHash.TryGetValue(topicHash, out var subscriptions))
|
||||
{
|
||||
subscriptions = new TopicHashMaskSubscriptions();
|
||||
_wildcardSubscriptionsByTopicHash.Add(topicHash, subscriptions);
|
||||
subscriptions.RemoveSubscription(existingSubscription);
|
||||
// no need to remove empty entry because we'll be adding subscription again below
|
||||
}
|
||||
|
||||
subscriptions.AddSubscription(subscription);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!_noWildcardSubscriptionsByTopicHash.TryGetValue(topicHash, out var subscriptions))
|
||||
if (_noWildcardSubscriptionsByTopicHash.TryGetValue(topicHash, out var subscriptions))
|
||||
{
|
||||
subscriptions = new HashSet<MqttSubscription>();
|
||||
_noWildcardSubscriptionsByTopicHash.Add(topicHash, subscriptions);
|
||||
subscriptions.Remove(existingSubscription);
|
||||
// no need to remove empty entry because we'll be adding subscription again below
|
||||
}
|
||||
|
||||
subscriptions.Add(subscription);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_subscriptionsLock.ExitWriteLock();
|
||||
}
|
||||
|
||||
return new CreateSubscriptionResult
|
||||
{
|
||||
IsNewSubscription = isNewSubscription,
|
||||
Subscription = subscription
|
||||
};
|
||||
}
|
||||
isNewSubscription = existingSubscription == null;
|
||||
_subscriptions[topicFilter.Topic] = subscription;
|
||||
|
||||
static void FilterRetainedApplicationMessages(
|
||||
IList<MqttApplicationMessage> retainedMessages,
|
||||
CreateSubscriptionResult createSubscriptionResult,
|
||||
SubscribeResult subscribeResult)
|
||||
{
|
||||
if (createSubscriptionResult.Subscription.RetainHandling == MqttRetainHandling.DoNotSendOnSubscribe)
|
||||
// Add or re-add to topic hash dictionary
|
||||
if (hasWildcard)
|
||||
{
|
||||
// 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)
|
||||
if (!_wildcardSubscriptionsByTopicHash.TryGetValue(topicHash, out var subscriptions))
|
||||
{
|
||||
continue;
|
||||
subscriptions = new TopicHashMaskSubscriptions();
|
||||
_wildcardSubscriptionsByTopicHash.Add(topicHash, subscriptions);
|
||||
}
|
||||
|
||||
if (MqttTopicFilterComparer.Compare(retainedMessage.Topic, createSubscriptionResult.Subscription.Topic) != MqttTopicFilterCompareResult.IsMatch)
|
||||
{
|
||||
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;
|
||||
subscriptions.AddSubscription(subscription);
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
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)
|
||||
if (MqttTopicFilterComparer.Compare(retainedMessage.Topic, createSubscriptionResult.Subscription.Topic) != MqttTopicFilterCompareResult.IsMatch)
|
||||
{
|
||||
Response =
|
||||
{
|
||||
ReasonCode = mqttSubscription == null ? MqttUnsubscribeReasonCode.NoSubscriptionExisted : MqttUnsubscribeReasonCode.Success
|
||||
}
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
public bool IsNewSubscription { get; set; }
|
||||
subscribeResult.RetainedMessages.Add(retainedMessageMatch);
|
||||
|
||||
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.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
using MQTTnet.Diagnostics.Logger;
|
||||
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);
|
||||
readonly AsyncLock _storageAccessLock = new AsyncLock();
|
||||
_eventContainer = eventContainer ?? throw new ArgumentNullException(nameof(eventContainer));
|
||||
|
||||
readonly MqttServerEventContainer _eventContainer;
|
||||
readonly MqttNetSourceLogger _logger;
|
||||
ArgumentNullException.ThrowIfNull(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);
|
||||
|
||||
_logger = logger.WithSource(nameof(MqttRetainedMessagesManager));
|
||||
}
|
||||
|
||||
public async Task Start()
|
||||
{
|
||||
try
|
||||
lock (_messages)
|
||||
{
|
||||
var eventArgs = new LoadingRetainedMessagesEventArgs();
|
||||
await _eventContainer.LoadingRetainedMessagesEvent.InvokeAsync(eventArgs).ConfigureAwait(false);
|
||||
_messages.Clear();
|
||||
|
||||
lock (_messages)
|
||||
if (eventArgs.LoadedRetainedMessages != null)
|
||||
{
|
||||
_messages.Clear();
|
||||
|
||||
if (eventArgs.LoadedRetainedMessages != null)
|
||||
foreach (var retainedMessage in eventArgs.LoadedRetainedMessages)
|
||||
{
|
||||
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.");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpdateMessage(string clientId, MqttApplicationMessage applicationMessage)
|
||||
catch (Exception exception)
|
||||
{
|
||||
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 saveIsRequired = false;
|
||||
var payload = applicationMessage.Payload;
|
||||
var hasPayload = payload.Length > 0;
|
||||
|
||||
lock (_messages)
|
||||
if (!hasPayload)
|
||||
{
|
||||
var payload = applicationMessage.Payload;
|
||||
var hasPayload = payload.Length > 0;
|
||||
|
||||
if (!hasPayload)
|
||||
saveIsRequired = _messages.Remove(applicationMessage.Topic);
|
||||
_logger.Verbose("Client '{0}' cleared retained message for topic '{1}'.", clientId, applicationMessage.Topic);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!_messages.TryGetValue(applicationMessage.Topic, out var existingMessage))
|
||||
{
|
||||
saveIsRequired = _messages.Remove(applicationMessage.Topic);
|
||||
_logger.Verbose("Client '{0}' cleared retained message for topic '{1}'.", clientId, applicationMessage.Topic);
|
||||
_messages[applicationMessage.Topic] = applicationMessage;
|
||||
saveIsRequired = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!_messages.TryGetValue(applicationMessage.Topic, out var existingMessage))
|
||||
if (existingMessage.QualityOfServiceLevel != applicationMessage.QualityOfServiceLevel ||
|
||||
!MqttMemoryHelper.SequenceEqual(existingMessage.Payload, payload))
|
||||
{
|
||||
_messages[applicationMessage.Topic] = applicationMessage;
|
||||
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)
|
||||
{
|
||||
messagesForSave = new List<MqttApplicationMessage>(_messages.Values);
|
||||
}
|
||||
_logger.Verbose("Client '{0}' set retained message for topic '{1}'.", clientId, applicationMessage.Topic);
|
||||
}
|
||||
|
||||
if (saveIsRequired)
|
||||
{
|
||||
using (await _storageAccessLock.EnterAsync().ConfigureAwait(false))
|
||||
{
|
||||
var eventArgs = new RetainedMessageChangedEventArgs(clientId, applicationMessage, messagesForSave);
|
||||
await _eventContainer.RetainedMessageChangedEvent.InvokeAsync(eventArgs).ConfigureAwait(false);
|
||||
}
|
||||
messagesForSave = new List<MqttApplicationMessage>(_messages.Values);
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.Error(exception, "Unhandled exception while handling retained messages.");
|
||||
}
|
||||
}
|
||||
|
||||
public Task<IList<MqttApplicationMessage>> GetMessages()
|
||||
{
|
||||
lock (_messages)
|
||||
if (saveIsRequired)
|
||||
{
|
||||
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))
|
||||
using (await _storageAccessLock.EnterAsync().ConfigureAwait(false))
|
||||
{
|
||||
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()
|
||||
{
|
||||
lock (_messages)
|
||||
{
|
||||
_messages.Clear();
|
||||
}
|
||||
return Task.FromResult<MqttApplicationMessage>(null);
|
||||
}
|
||||
|
||||
using (await _storageAccessLock.EnterAsync().ConfigureAwait(false))
|
||||
{
|
||||
await _eventContainer.RetainedMessagesClearedEvent.InvokeAsync(EventArgs.Empty).ConfigureAwait(false);
|
||||
}
|
||||
public async Task ClearMessages()
|
||||
{
|
||||
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;
|
||||
|
||||
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<ApplicationMessageEnqueuedEventArgs> ApplicationMessageEnqueuedOrDroppedEvent { get; } = new AsyncEvent<ApplicationMessageEnqueuedEventArgs>();
|
||||
public AsyncEvent<InterceptingClientApplicationMessageEnqueueEventArgs> InterceptingClientEnqueueEvent { get; } = new AsyncEvent<InterceptingClientApplicationMessageEnqueueEventArgs>();
|
||||
|
||||
public AsyncEvent<QueueMessageOverwrittenEventArgs> QueuedApplicationMessageOverwrittenEvent { get; } = new AsyncEvent<QueueMessageOverwrittenEventArgs>();
|
||||
public AsyncEvent<ApplicationMessageEnqueuedEventArgs> ApplicationMessageEnqueuedOrDroppedEvent { get; } = new AsyncEvent<ApplicationMessageEnqueuedEventArgs>();
|
||||
|
||||
public AsyncEvent<InterceptingPacketEventArgs> InterceptingInboundPacketEvent { get; } = new AsyncEvent<InterceptingPacketEventArgs>();
|
||||
public AsyncEvent<QueueMessageOverwrittenEventArgs> QueuedApplicationMessageOverwrittenEvent { get; } = new AsyncEvent<QueueMessageOverwrittenEventArgs>();
|
||||
|
||||
public AsyncEvent<InterceptingPacketEventArgs> InterceptingOutboundPacketEvent { get; } = new AsyncEvent<InterceptingPacketEventArgs>();
|
||||
public AsyncEvent<InterceptingPacketEventArgs> InterceptingInboundPacketEvent { get; } = new AsyncEvent<InterceptingPacketEventArgs>();
|
||||
|
||||
public AsyncEvent<InterceptingPublishEventArgs> InterceptingPublishEvent { get; } = new AsyncEvent<InterceptingPublishEventArgs>();
|
||||
public AsyncEvent<InterceptingPacketEventArgs> InterceptingOutboundPacketEvent { get; } = new AsyncEvent<InterceptingPacketEventArgs>();
|
||||
|
||||
public AsyncEvent<InterceptingSubscriptionEventArgs> InterceptingSubscriptionEvent { get; } = new AsyncEvent<InterceptingSubscriptionEventArgs>();
|
||||
public AsyncEvent<InterceptingPublishEventArgs> InterceptingPublishEvent { get; } = new AsyncEvent<InterceptingPublishEventArgs>();
|
||||
|
||||
public AsyncEvent<InterceptingUnsubscriptionEventArgs> InterceptingUnsubscriptionEvent { get; } = new AsyncEvent<InterceptingUnsubscriptionEventArgs>();
|
||||
public AsyncEvent<InterceptingSubscriptionEventArgs> InterceptingSubscriptionEvent { get; } = new AsyncEvent<InterceptingSubscriptionEventArgs>();
|
||||
|
||||
public AsyncEvent<LoadingRetainedMessagesEventArgs> LoadingRetainedMessagesEvent { get; } = new AsyncEvent<LoadingRetainedMessagesEventArgs>();
|
||||
public AsyncEvent<InterceptingUnsubscriptionEventArgs> InterceptingUnsubscriptionEvent { get; } = new AsyncEvent<InterceptingUnsubscriptionEventArgs>();
|
||||
|
||||
public AsyncEvent<EventArgs> PreparingSessionEvent { get; } = new AsyncEvent<EventArgs>();
|
||||
public AsyncEvent<LoadingRetainedMessagesEventArgs> LoadingRetainedMessagesEvent { get; } = new AsyncEvent<LoadingRetainedMessagesEventArgs>();
|
||||
|
||||
public AsyncEvent<RetainedMessageChangedEventArgs> RetainedMessageChangedEvent { get; } = new AsyncEvent<RetainedMessageChangedEventArgs>();
|
||||
public AsyncEvent<EventArgs> PreparingSessionEvent { get; } = new AsyncEvent<EventArgs>();
|
||||
|
||||
public AsyncEvent<EventArgs> RetainedMessagesClearedEvent { get; } = new AsyncEvent<EventArgs>();
|
||||
public AsyncEvent<RetainedMessageChangedEventArgs> RetainedMessageChangedEvent { get; } = new AsyncEvent<RetainedMessageChangedEventArgs>();
|
||||
|
||||
public AsyncEvent<SessionDeletedEventArgs> SessionDeletedEvent { get; } = new AsyncEvent<SessionDeletedEventArgs>();
|
||||
public AsyncEvent<EventArgs> RetainedMessagesClearedEvent { get; } = new AsyncEvent<EventArgs>();
|
||||
|
||||
public AsyncEvent<EventArgs> StartedEvent { get; } = new AsyncEvent<EventArgs>();
|
||||
public AsyncEvent<SessionDeletedEventArgs> SessionDeletedEvent { get; } = new AsyncEvent<SessionDeletedEventArgs>();
|
||||
|
||||
public AsyncEvent<EventArgs> StoppedEvent { get; } = new AsyncEvent<EventArgs>();
|
||||
public AsyncEvent<EventArgs> StartedEvent { get; } = new AsyncEvent<EventArgs>();
|
||||
|
||||
public AsyncEvent<ValidatingConnectionEventArgs> ValidatingConnectionEvent { get; } = new AsyncEvent<ValidatingConnectionEventArgs>();
|
||||
}
|
||||
public AsyncEvent<EventArgs> StoppedEvent { get; } = new AsyncEvent<EventArgs>();
|
||||
|
||||
public AsyncEvent<ValidatingConnectionEventArgs> ValidatingConnectionEvent { get; } = new AsyncEvent<ValidatingConnectionEventArgs>();
|
||||
}
|
||||
@@ -6,130 +6,129 @@ using MQTTnet.Diagnostics.Logger;
|
||||
using MQTTnet.Internal;
|
||||
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,
|
||||
UserProperties = null,
|
||||
ReasonString = null,
|
||||
ServerReference = null
|
||||
};
|
||||
_logger.Info("Starting keep alive monitor");
|
||||
|
||||
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
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
_logger.Info("Starting keep alive monitor");
|
||||
|
||||
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");
|
||||
TryProcessClients();
|
||||
Sleep(_options.KeepAliveOptions.MonitorInterval);
|
||||
}
|
||||
}
|
||||
|
||||
static void Sleep(TimeSpan timeout)
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
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)
|
||||
catch (Exception exception)
|
||||
{
|
||||
try
|
||||
{
|
||||
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);
|
||||
}
|
||||
_logger.Error(exception, "Unhandled exception while checking keep alive timeouts");
|
||||
}
|
||||
|
||||
void TryProcessClients()
|
||||
finally
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
foreach (var client in _sessionsManager.GetClients())
|
||||
_logger.Verbose("Stopped checking keep alive timeout");
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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!
|
||||
// This object can be reused after disposing.
|
||||
Dispose();
|
||||
sessionItem.Value.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;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
_sessions[id] = session;
|
||||
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;
|
||||
}
|
||||
|
||||
public void UpdateSession(string id, MqttSession session)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(id);
|
||||
|
||||
_sessions[id] = session;
|
||||
}
|
||||
}
|
||||
@@ -4,47 +4,46 @@
|
||||
|
||||
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(
|
||||
string topic,
|
||||
bool noLocal,
|
||||
MqttRetainHandling retainHandling,
|
||||
bool retainAsPublished,
|
||||
MqttQualityOfServiceLevel qualityOfServiceLevel,
|
||||
uint identifier)
|
||||
{
|
||||
Topic = topic;
|
||||
NoLocal = noLocal;
|
||||
RetainHandling = retainHandling;
|
||||
RetainAsPublished = retainAsPublished;
|
||||
GrantedQualityOfServiceLevel = qualityOfServiceLevel;
|
||||
Identifier = 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);
|
||||
TopicHash = hash;
|
||||
TopicHashMask = hashMask;
|
||||
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; }
|
||||
MqttTopicHash.Calculate(Topic, out var hash, out var hashMask, out var hasWildcard);
|
||||
TopicHash = hash;
|
||||
TopicHashMask = hashMask;
|
||||
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; }
|
||||
}
|
||||
@@ -2,267 +2,266 @@
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// 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
|
||||
{
|
||||
/*
|
||||
* 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)
|
||||
{
|
||||
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
|
||||
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)
|
||||
{
|
||||
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)
|
||||
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;
|
||||
while (level < 8)
|
||||
if (level >= 8)
|
||||
{
|
||||
hash <<= 8;
|
||||
hashMaskInverted <<= 8;
|
||||
hashMaskInverted |= fillLevelBitMask;
|
||||
++level;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasWildcard)
|
||||
else if (c == MqttTopicFilterComparer.SingleLevelWildcard)
|
||||
{
|
||||
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];
|
||||
if (c == MqttTopicFilterComparer.SingleLevelWildcard || c == MqttTopicFilterComparer.MultiLevelWildcard)
|
||||
{
|
||||
hasWildcard = true;
|
||||
break;
|
||||
}
|
||||
|
||||
++i;
|
||||
checkSum += (byte)c;
|
||||
}
|
||||
else
|
||||
{
|
||||
checkSum ^= (byte)(c >> 1);
|
||||
}
|
||||
}
|
||||
|
||||
resultHash = hash;
|
||||
resultHashMask = ~hashMaskInverted;
|
||||
resultHasWildcard = hasWildcard;
|
||||
++i;
|
||||
}
|
||||
|
||||
// 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.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);
|
||||
}
|
||||
|
||||
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; }
|
||||
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; }
|
||||
}
|
||||
@@ -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>
|
||||
/// Helper class that stores subscriptions by their topic hash mask.
|
||||
/// </summary>
|
||||
public sealed class TopicHashMaskSubscriptions
|
||||
public Dictionary<ulong, HashSet<MqttSubscription>> SubscriptionsByHashMask { get; } = new Dictionary<ulong, HashSet<MqttSubscription>>();
|
||||
|
||||
public void AddSubscription(MqttSubscription subscription)
|
||||
{
|
||||
public Dictionary<ulong, HashSet<MqttSubscription>> SubscriptionsByHashMask { get; } = new Dictionary<ulong, HashSet<MqttSubscription>>();
|
||||
|
||||
public void AddSubscription(MqttSubscription subscription)
|
||||
if (!SubscriptionsByHashMask.TryGetValue(subscription.TopicHashMask, out var subscriptions))
|
||||
{
|
||||
if (!SubscriptionsByHashMask.TryGetValue(subscription.TopicHashMask, out var subscriptions))
|
||||
{
|
||||
subscriptions = new HashSet<MqttSubscription>();
|
||||
SubscriptionsByHashMask.Add(subscription.TopicHashMask, subscriptions);
|
||||
}
|
||||
subscriptions.Add(subscription);
|
||||
subscriptions = new HashSet<MqttSubscription>();
|
||||
SubscriptionsByHashMask.Add(subscription.TopicHashMask, subscriptions);
|
||||
}
|
||||
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);
|
||||
if (subscriptions.Count == 0)
|
||||
{
|
||||
SubscriptionsByHashMask.Remove(subscription.TopicHashMask);
|
||||
}
|
||||
SubscriptionsByHashMask.Remove(subscription.TopicHashMask);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,14 +5,13 @@
|
||||
using MQTTnet.Packets;
|
||||
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);
|
||||
|
||||
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(
|
||||
|
||||
@@ -21,10 +21,7 @@ public sealed class MqttServerFactory
|
||||
|
||||
public IMqttNetLogger DefaultLogger { get; }
|
||||
|
||||
public IList<Func<MqttServerFactory, IMqttServerAdapter>> DefaultServerAdapters { get; } = new List<Func<MqttServerFactory, IMqttServerAdapter>>
|
||||
{
|
||||
factory => new MqttTcpServerAdapter()
|
||||
};
|
||||
public IList<Func<MqttServerFactory, IMqttServerAdapter>> DefaultServerAdapters { get; } = [_ => new MqttTcpServerAdapter()];
|
||||
|
||||
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.
|
||||
// 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.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace MQTTnet.Server
|
||||
{
|
||||
public enum MqttPendingMessagesOverflowStrategy
|
||||
{
|
||||
DropOldestQueuedMessage,
|
||||
namespace MQTTnet.Server;
|
||||
|
||||
DropNewMessage
|
||||
}
|
||||
}
|
||||
public enum MqttPendingMessagesOverflowStrategy
|
||||
{
|
||||
DropOldestQueuedMessage,
|
||||
|
||||
DropNewMessage
|
||||
}
|
||||
@@ -2,10 +2,9 @@
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// 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.
|
||||
// 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
|
||||
/// client is currently sending a (large) payload. This may lead to "dead" connections
|
||||
/// When this mode is disabled the MQTT server will disconnect a client when the keep
|
||||
/// alive timeout is reached even if the client is currently sending a (large) payload.
|
||||
/// </summary>
|
||||
public bool DisconnectClientWhenReadingPayload { get; set; }
|
||||
/// <summary>
|
||||
/// When this mode is enabled the MQTT server will not close a connection when the
|
||||
/// client is currently sending a (large) payload. This may lead to "dead" connections
|
||||
/// When this mode is disabled the MQTT server will disconnect a client when the keep
|
||||
/// alive timeout is reached even if the client is currently sending a (large) payload.
|
||||
/// </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.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Security;
|
||||
using System.Security.Authentication;
|
||||
@@ -10,207 +9,206 @@ using MQTTnet.Certificates;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
// 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)
|
||||
{
|
||||
_options.TlsEndpointOptions.ClientCertificateRequired = true;
|
||||
_options.TlsEndpointOptions.CheckCertificateRevocation = checkCertificateRevocation;
|
||||
_options.TlsEndpointOptions.RemoteCertificateValidationCallback = validationCallback;
|
||||
return this;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public MqttServerOptionsBuilder WithConnectionBacklog(int value)
|
||||
{
|
||||
_options.DefaultEndpointOptions.ConnectionBacklog = value;
|
||||
_options.TlsEndpointOptions.ConnectionBacklog = value;
|
||||
return this;
|
||||
}
|
||||
public MqttServerOptionsBuilder WithEncryptionCertificate(X509Certificate2 certificate)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(certificate);
|
||||
|
||||
public MqttServerOptionsBuilder WithDefaultCommunicationTimeout(TimeSpan value)
|
||||
{
|
||||
_options.DefaultCommunicationTimeout = value;
|
||||
return this;
|
||||
}
|
||||
_options.TlsEndpointOptions.CertificateProvider = new X509CertificateProvider(certificate);
|
||||
return this;
|
||||
}
|
||||
|
||||
public MqttServerOptionsBuilder WithDefaultEndpoint()
|
||||
{
|
||||
_options.DefaultEndpointOptions.IsEnabled = true;
|
||||
return this;
|
||||
}
|
||||
public MqttServerOptionsBuilder WithEncryptionCertificate(ICertificateProvider certificateProvider)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(certificateProvider);
|
||||
|
||||
public MqttServerOptionsBuilder WithDefaultEndpointBoundIPAddress(IPAddress value)
|
||||
{
|
||||
_options.DefaultEndpointOptions.BoundInterNetworkAddress = value ?? IPAddress.Any;
|
||||
return this;
|
||||
}
|
||||
_options.TlsEndpointOptions.CertificateProvider = certificateProvider;
|
||||
|
||||
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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -5,59 +5,58 @@
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace MQTTnet.Server
|
||||
namespace MQTTnet.Server;
|
||||
|
||||
public abstract class MqttServerTcpEndpointBaseOptions
|
||||
{
|
||||
public abstract class MqttServerTcpEndpointBaseOptions
|
||||
{
|
||||
public bool IsEnabled { get; set; }
|
||||
public bool IsEnabled { get; set; }
|
||||
|
||||
public int Port { get; set; }
|
||||
public int Port { get; set; }
|
||||
|
||||
public int ConnectionBacklog { get; set; } = 100;
|
||||
public int ConnectionBacklog { get; set; } = 100;
|
||||
|
||||
public bool NoDelay { get; set; } = true;
|
||||
public bool NoDelay { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the sockets keep alive feature should be used.
|
||||
/// The value _null_ indicates that the OS and framework defaults should be used.
|
||||
/// </summary>
|
||||
public bool? KeepAlive { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets whether the sockets keep alive feature should be used.
|
||||
/// The value _null_ indicates that the OS and framework defaults should be used.
|
||||
/// </summary>
|
||||
public bool? KeepAlive { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Usually the MQTT packets can be send partially. This is done by using multiple TCP packets
|
||||
/// or WebSocket frames etc. Unfortunately not all clients do support this feature and
|
||||
/// will close the connection when receiving such packets. If such clients are connecting to this
|
||||
/// server the flag must be set to _false_.
|
||||
/// </summary>
|
||||
public bool AllowPacketFragmentation { get; set; } = true;
|
||||
/// <summary>
|
||||
/// Usually the MQTT packets can be send partially. This is done by using multiple TCP packets
|
||||
/// or WebSocket frames etc. Unfortunately not all clients do support this feature and
|
||||
/// will close the connection when receiving such packets. If such clients are connecting to this
|
||||
/// server the flag must be set to _false_.
|
||||
/// </summary>
|
||||
public bool AllowPacketFragmentation { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the TCP keep alive interval.
|
||||
/// The value _null_ indicates that the OS and framework defaults should be used.
|
||||
/// </summary>
|
||||
public int? TcpKeepAliveInterval { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the TCP keep alive interval.
|
||||
/// The value _null_ indicates that the OS and framework defaults should be used.
|
||||
/// </summary>
|
||||
public int? TcpKeepAliveInterval { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the TCP keep alive retry count.
|
||||
/// The value _null_ indicates that the OS and framework defaults should be used.
|
||||
/// </summary>
|
||||
public int? TcpKeepAliveRetryCount { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the TCP keep alive retry count.
|
||||
/// The value _null_ indicates that the OS and framework defaults should be used.
|
||||
/// </summary>
|
||||
public int? TcpKeepAliveRetryCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the TCP keep alive time.
|
||||
/// The value _null_ indicates that the OS and framework defaults should be used.
|
||||
/// </summary>
|
||||
public int? TcpKeepAliveTime { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the TCP keep alive time.
|
||||
/// The value _null_ indicates that the OS and framework defaults should be used.
|
||||
/// </summary>
|
||||
public int? TcpKeepAliveTime { get; set; }
|
||||
|
||||
public LingerOption LingerState { get; set; } = new LingerOption(true, 0);
|
||||
public LingerOption LingerState { get; set; } = new LingerOption(true, 0);
|
||||
|
||||
public IPAddress BoundInterNetworkAddress { get; set; } = IPAddress.Any;
|
||||
public IPAddress BoundInterNetworkAddress { get; set; } = IPAddress.Any;
|
||||
|
||||
public IPAddress BoundInterNetworkV6Address { get; set; } = IPAddress.IPv6Any;
|
||||
public IPAddress BoundInterNetworkV6Address { get; set; } = IPAddress.IPv6Any;
|
||||
|
||||
/// <summary>
|
||||
/// This requires admin permissions on Linux.
|
||||
/// </summary>
|
||||
public bool ReuseAddress { get; set; }
|
||||
}
|
||||
/// <summary>
|
||||
/// This requires admin permissions on Linux.
|
||||
/// </summary>
|
||||
public bool ReuseAddress { get; set; }
|
||||
}
|
||||
@@ -2,13 +2,12 @@
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace MQTTnet.Server
|
||||
namespace MQTTnet.Server;
|
||||
|
||||
public class MqttServerTcpEndpointOptions : MqttServerTcpEndpointBaseOptions
|
||||
{
|
||||
public class MqttServerTcpEndpointOptions : MqttServerTcpEndpointBaseOptions
|
||||
public MqttServerTcpEndpointOptions()
|
||||
{
|
||||
public MqttServerTcpEndpointOptions()
|
||||
{
|
||||
Port = 1883;
|
||||
}
|
||||
Port = 1883;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,29 +5,28 @@
|
||||
using System.Security.Authentication;
|
||||
using MQTTnet.Certificates;
|
||||
|
||||
namespace MQTTnet.Server
|
||||
namespace MQTTnet.Server;
|
||||
|
||||
public sealed class MqttServerTlsTcpEndpointOptions : MqttServerTcpEndpointBaseOptions
|
||||
{
|
||||
public sealed class MqttServerTlsTcpEndpointOptions : MqttServerTcpEndpointBaseOptions
|
||||
public MqttServerTlsTcpEndpointOptions()
|
||||
{
|
||||
public MqttServerTlsTcpEndpointOptions()
|
||||
{
|
||||
Port = 8883;
|
||||
}
|
||||
|
||||
public System.Net.Security.RemoteCertificateValidationCallback RemoteCertificateValidationCallback { get; set; }
|
||||
|
||||
public ICertificateProvider CertificateProvider { get; set; }
|
||||
|
||||
public bool ClientCertificateRequired { get; set; }
|
||||
|
||||
public bool CheckCertificateRevocation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The default value is SslProtocols.None, which allows the operating system to choose the best protocol to use, and to block protocols that are not secure.
|
||||
/// </summary>
|
||||
/// <seealso href="https://learn.microsoft.com/en-us/dotnet/api/system.security.authentication.sslprotocols">SslProtocols</seealso>
|
||||
public SslProtocols SslProtocol { get; set; } = SslProtocols.None;
|
||||
|
||||
public System.Net.Security.CipherSuitesPolicy CipherSuitesPolicy { get; set; }
|
||||
Port = 8883;
|
||||
}
|
||||
}
|
||||
|
||||
public System.Net.Security.RemoteCertificateValidationCallback RemoteCertificateValidationCallback { get; set; }
|
||||
|
||||
public ICertificateProvider CertificateProvider { get; set; }
|
||||
|
||||
public bool ClientCertificateRequired { get; set; }
|
||||
|
||||
public bool CheckCertificateRevocation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The default value is SslProtocols.None, which allows the operating system to choose the best protocol to use, and to block protocols that are not secure.
|
||||
/// </summary>
|
||||
/// <seealso href="https://learn.microsoft.com/en-us/dotnet/api/system.security.authentication.sslprotocols">SslProtocols</seealso>
|
||||
public SslProtocols SslProtocol { get; set; } = SslProtocols.None;
|
||||
|
||||
public System.Net.Security.CipherSuitesPolicy CipherSuitesPolicy { get; set; }
|
||||
}
|
||||
@@ -4,20 +4,19 @@
|
||||
|
||||
using MQTTnet.Protocol;
|
||||
|
||||
namespace MQTTnet.Server
|
||||
namespace MQTTnet.Server;
|
||||
|
||||
public sealed class MqttServerStopOptions
|
||||
{
|
||||
public sealed class MqttServerStopOptions
|
||||
/// <summary>
|
||||
/// These disconnect options are sent to every connected client via a DISCONNECT packet.
|
||||
/// <remarks>MQTT 5.0.0+ feature.</remarks>
|
||||
/// </summary>
|
||||
public MqttServerClientDisconnectOptions DefaultClientDisconnectOptions { get; set; } = new MqttServerClientDisconnectOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// These disconnect options are sent to every connected client via a DISCONNECT packet.
|
||||
/// <remarks>MQTT 5.0.0+ feature.</remarks>
|
||||
/// </summary>
|
||||
public MqttServerClientDisconnectOptions DefaultClientDisconnectOptions { get; set; } = new MqttServerClientDisconnectOptions
|
||||
{
|
||||
ReasonCode = MqttDisconnectReasonCode.ServerShuttingDown,
|
||||
UserProperties = null,
|
||||
ReasonString = null,
|
||||
ServerReference = null
|
||||
};
|
||||
}
|
||||
ReasonCode = MqttDisconnectReasonCode.ServerShuttingDown,
|
||||
UserProperties = null,
|
||||
ReasonString = null,
|
||||
ServerReference = null
|
||||
};
|
||||
}
|
||||
@@ -2,34 +2,31 @@
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
namespace MQTTnet.Server;
|
||||
|
||||
namespace MQTTnet.Server
|
||||
public sealed class MqttServerStopOptionsBuilder
|
||||
{
|
||||
public sealed class MqttServerStopOptionsBuilder
|
||||
readonly MqttServerStopOptions _options = new();
|
||||
|
||||
public MqttServerStopOptionsBuilder WithDefaultClientDisconnectOptions(MqttServerClientDisconnectOptions value)
|
||||
{
|
||||
readonly MqttServerStopOptions _options = new MqttServerStopOptions();
|
||||
_options.DefaultClientDisconnectOptions = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MqttServerStopOptionsBuilder WithDefaultClientDisconnectOptions(MqttServerClientDisconnectOptions value)
|
||||
{
|
||||
_options.DefaultClientDisconnectOptions = value;
|
||||
return this;
|
||||
}
|
||||
public MqttServerStopOptionsBuilder WithDefaultClientDisconnectOptions(Action<MqttServerClientDisconnectOptionsBuilder> builder)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(builder);
|
||||
|
||||
public MqttServerStopOptionsBuilder WithDefaultClientDisconnectOptions(Action<MqttServerClientDisconnectOptionsBuilder> builder)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(builder);
|
||||
var optionsBuilder = new MqttServerClientDisconnectOptionsBuilder();
|
||||
builder(optionsBuilder);
|
||||
|
||||
var optionsBuilder = new MqttServerClientDisconnectOptionsBuilder();
|
||||
builder.Invoke(optionsBuilder);
|
||||
_options.DefaultClientDisconnectOptions = optionsBuilder.Build();
|
||||
return this;
|
||||
}
|
||||
|
||||
_options.DefaultClientDisconnectOptions = optionsBuilder.Build();
|
||||
return this;
|
||||
}
|
||||
|
||||
public MqttServerStopOptions Build()
|
||||
{
|
||||
return _options;
|
||||
}
|
||||
public MqttServerStopOptions Build()
|
||||
{
|
||||
return _options;
|
||||
}
|
||||
}
|
||||
@@ -18,5 +18,5 @@ public sealed class SubscribeResponse
|
||||
|
||||
public string ReasonString { get; set; }
|
||||
|
||||
public List<MqttUserProperty> UserProperties { get; } = new();
|
||||
public List<MqttUserProperty> UserProperties { get; } = [];
|
||||
}
|
||||
@@ -17,5 +17,5 @@ public sealed class UnsubscribeResponse
|
||||
|
||||
public string ReasonString { get; set; }
|
||||
|
||||
public List<MqttUserProperty> UserProperties { get; } = new();
|
||||
public List<MqttUserProperty> UserProperties { get; } = [];
|
||||
}
|
||||
@@ -2,27 +2,58 @@
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MQTTnet.Internal;
|
||||
using MQTTnet.Exceptions;
|
||||
|
||||
namespace MQTTnet.TestApp
|
||||
namespace MQTTnet.TestApp;
|
||||
|
||||
public sealed class AsyncLockTest
|
||||
{
|
||||
public sealed class AsyncLockTest
|
||||
public async Task Run()
|
||||
{
|
||||
public async Task Run()
|
||||
try
|
||||
{
|
||||
var asyncLock = new AsyncLock();
|
||||
var semaphore = new SemaphoreSlim(1, 1);
|
||||
|
||||
using (var cancellationToken = new CancellationTokenSource())
|
||||
await semaphore.WaitAsync();
|
||||
try
|
||||
{
|
||||
for (var i = 0; i < 100000; i++)
|
||||
{
|
||||
using (await asyncLock.EnterAsync(cancellationToken.Token).ConfigureAwait(false))
|
||||
{
|
||||
}
|
||||
}
|
||||
// Wait for data from socket etc...
|
||||
// Then get an exception.
|
||||
semaphore.Dispose();
|
||||
throw new MqttCommunicationException("Connection closed");
|
||||
}
|
||||
finally
|
||||
{
|
||||
semaphore.Release();
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Console.WriteLine(exception.ToString());
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// var asyncLock = new AsyncLock();
|
||||
//
|
||||
// using var cancellationToken = new CancellationTokenSource();
|
||||
// for (var i = 0; i < 100000; i++)
|
||||
// {
|
||||
// using (await asyncLock.EnterAsync(cancellationToken.Token).ConfigureAwait(false))
|
||||
// {
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
@@ -6,49 +6,48 @@ using System;
|
||||
using System.Threading.Tasks;
|
||||
using MQTTnet.Diagnostics.Logger;
|
||||
|
||||
namespace MQTTnet.TestApp
|
||||
namespace MQTTnet.TestApp;
|
||||
|
||||
public static class ClientFlowTest
|
||||
{
|
||||
public static class ClientFlowTest
|
||||
public static async Task RunAsync()
|
||||
{
|
||||
public static async Task RunAsync()
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
var logger = new MqttNetEventLogger();
|
||||
MqttNetConsoleLogger.ForwardToConsole(logger);
|
||||
var logger = new MqttNetEventLogger();
|
||||
MqttNetConsoleLogger.ForwardToConsole(logger);
|
||||
|
||||
var factory = new MqttClientFactory(logger);
|
||||
var factory = new MqttClientFactory(logger);
|
||||
|
||||
var client = factory.CreateMqttClient();
|
||||
var client = factory.CreateMqttClient();
|
||||
|
||||
var options = new MqttClientOptionsBuilder()
|
||||
.WithTcpServer("localhost")
|
||||
.Build();
|
||||
var options = new MqttClientOptionsBuilder()
|
||||
.WithTcpServer("localhost")
|
||||
.Build();
|
||||
|
||||
Console.WriteLine("BEFORE CONNECT");
|
||||
await client.ConnectAsync(options);
|
||||
Console.WriteLine("AFTER CONNECT");
|
||||
Console.WriteLine("BEFORE CONNECT");
|
||||
await client.ConnectAsync(options);
|
||||
Console.WriteLine("AFTER CONNECT");
|
||||
|
||||
Console.WriteLine("BEFORE SUBSCRIBE");
|
||||
await client.SubscribeAsync("test/topic");
|
||||
Console.WriteLine("AFTER SUBSCRIBE");
|
||||
Console.WriteLine("BEFORE SUBSCRIBE");
|
||||
await client.SubscribeAsync("test/topic");
|
||||
Console.WriteLine("AFTER SUBSCRIBE");
|
||||
|
||||
Console.WriteLine("BEFORE PUBLISH");
|
||||
await client.PublishStringAsync("test/topic", "payload");
|
||||
Console.WriteLine("AFTER PUBLISH");
|
||||
Console.WriteLine("BEFORE PUBLISH");
|
||||
await client.PublishStringAsync("test/topic", "payload");
|
||||
Console.WriteLine("AFTER PUBLISH");
|
||||
|
||||
await Task.Delay(1000);
|
||||
await Task.Delay(1000);
|
||||
|
||||
Console.WriteLine("BEFORE DISCONNECT");
|
||||
await client.DisconnectAsync();
|
||||
Console.WriteLine("AFTER DISCONNECT");
|
||||
Console.WriteLine("BEFORE DISCONNECT");
|
||||
await client.DisconnectAsync();
|
||||
Console.WriteLine("AFTER DISCONNECT");
|
||||
|
||||
Console.WriteLine("FINISHED");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine(ex);
|
||||
}
|
||||
Console.WriteLine("FINISHED");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,99 +10,98 @@ using MQTTnet.Diagnostics.Logger;
|
||||
using MQTTnet.Internal;
|
||||
using MQTTnet.Protocol;
|
||||
|
||||
namespace MQTTnet.TestApp
|
||||
namespace MQTTnet.TestApp;
|
||||
|
||||
public static class ClientTest
|
||||
{
|
||||
public static class ClientTest
|
||||
public static async Task RunAsync()
|
||||
{
|
||||
public static async Task RunAsync()
|
||||
try
|
||||
{
|
||||
try
|
||||
var logger = new MqttNetEventLogger();
|
||||
MqttNetConsoleLogger.ForwardToConsole(logger);
|
||||
|
||||
var factory = new MqttClientFactory(logger);
|
||||
var client = factory.CreateMqttClient();
|
||||
var clientOptions = new MqttClientOptions
|
||||
{
|
||||
var logger = new MqttNetEventLogger();
|
||||
MqttNetConsoleLogger.ForwardToConsole(logger);
|
||||
|
||||
var factory = new MqttClientFactory(logger);
|
||||
var client = factory.CreateMqttClient();
|
||||
var clientOptions = new MqttClientOptions
|
||||
ChannelOptions = new MqttClientTcpOptions
|
||||
{
|
||||
ChannelOptions = new MqttClientTcpOptions
|
||||
{
|
||||
RemoteEndpoint = new DnsEndPoint("127.0.0.1", 0)
|
||||
}
|
||||
};
|
||||
RemoteEndpoint = new DnsEndPoint("127.0.0.1", 0)
|
||||
}
|
||||
};
|
||||
|
||||
client.ApplicationMessageReceivedAsync += e =>
|
||||
client.ApplicationMessageReceivedAsync += e =>
|
||||
{
|
||||
var payloadText = string.Empty;
|
||||
if (e.ApplicationMessage.Payload.Length > 0)
|
||||
{
|
||||
var payloadText = string.Empty;
|
||||
if (e.ApplicationMessage.Payload.Length > 0)
|
||||
{
|
||||
payloadText = Encoding.UTF8.GetString(e.ApplicationMessage.Payload);
|
||||
}
|
||||
payloadText = Encoding.UTF8.GetString(e.ApplicationMessage.Payload);
|
||||
}
|
||||
|
||||
Console.WriteLine("### RECEIVED APPLICATION MESSAGE ###");
|
||||
Console.WriteLine($"+ Topic = {e.ApplicationMessage.Topic}");
|
||||
Console.WriteLine($"+ Payload = {payloadText}");
|
||||
Console.WriteLine($"+ QoS = {e.ApplicationMessage.QualityOfServiceLevel}");
|
||||
Console.WriteLine($"+ Retain = {e.ApplicationMessage.Retain}");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("### RECEIVED APPLICATION MESSAGE ###");
|
||||
Console.WriteLine($"+ Topic = {e.ApplicationMessage.Topic}");
|
||||
Console.WriteLine($"+ Payload = {payloadText}");
|
||||
Console.WriteLine($"+ QoS = {e.ApplicationMessage.QualityOfServiceLevel}");
|
||||
Console.WriteLine($"+ Retain = {e.ApplicationMessage.Retain}");
|
||||
Console.WriteLine();
|
||||
|
||||
return CompletedTask.Instance;
|
||||
};
|
||||
return CompletedTask.Instance;
|
||||
};
|
||||
|
||||
client.ConnectedAsync += async e =>
|
||||
{
|
||||
Console.WriteLine("### CONNECTED WITH SERVER ###");
|
||||
client.ConnectedAsync += async _ =>
|
||||
{
|
||||
Console.WriteLine("### CONNECTED WITH SERVER ###");
|
||||
|
||||
await client.SubscribeAsync("#");
|
||||
await client.SubscribeAsync("#");
|
||||
|
||||
Console.WriteLine("### SUBSCRIBED ###");
|
||||
};
|
||||
Console.WriteLine("### SUBSCRIBED ###");
|
||||
};
|
||||
|
||||
client.DisconnectedAsync += async e =>
|
||||
{
|
||||
Console.WriteLine("### DISCONNECTED FROM SERVER ###");
|
||||
await Task.Delay(TimeSpan.FromSeconds(5));
|
||||
|
||||
try
|
||||
{
|
||||
await client.ConnectAsync(clientOptions);
|
||||
}
|
||||
catch
|
||||
{
|
||||
Console.WriteLine("### RECONNECTING FAILED ###");
|
||||
}
|
||||
};
|
||||
client.DisconnectedAsync += async _ =>
|
||||
{
|
||||
Console.WriteLine("### DISCONNECTED FROM SERVER ###");
|
||||
await Task.Delay(TimeSpan.FromSeconds(5));
|
||||
|
||||
try
|
||||
{
|
||||
await client.ConnectAsync(clientOptions);
|
||||
}
|
||||
catch (Exception exception)
|
||||
catch
|
||||
{
|
||||
Console.WriteLine("### CONNECTING FAILED ###" + Environment.NewLine + exception);
|
||||
Console.WriteLine("### RECONNECTING FAILED ###");
|
||||
}
|
||||
};
|
||||
|
||||
Console.WriteLine("### WAITING FOR APPLICATION MESSAGES ###");
|
||||
|
||||
while (true)
|
||||
{
|
||||
Console.ReadLine();
|
||||
|
||||
await client.SubscribeAsync("test");
|
||||
|
||||
var applicationMessage = new MqttApplicationMessageBuilder()
|
||||
.WithTopic("A/B/C")
|
||||
.WithPayload("Hello World")
|
||||
.WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce)
|
||||
.Build();
|
||||
|
||||
await client.PublishAsync(applicationMessage);
|
||||
}
|
||||
try
|
||||
{
|
||||
await client.ConnectAsync(clientOptions);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Console.WriteLine(exception);
|
||||
Console.WriteLine("### CONNECTING FAILED ###" + Environment.NewLine + exception);
|
||||
}
|
||||
|
||||
Console.WriteLine("### WAITING FOR APPLICATION MESSAGES ###");
|
||||
|
||||
while (true)
|
||||
{
|
||||
Console.ReadLine();
|
||||
|
||||
await client.SubscribeAsync("test");
|
||||
|
||||
var applicationMessage = new MqttApplicationMessageBuilder()
|
||||
.WithTopic("A/B/C")
|
||||
.WithPayload("Hello World")
|
||||
.WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce)
|
||||
.Build();
|
||||
|
||||
await client.PublishAsync(applicationMessage);
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Console.WriteLine(exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,342 +1,345 @@
|
||||
using MQTTnet.Server;
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Linq;
|
||||
using MQTTnet.Internal;
|
||||
|
||||
namespace MQTTnet.TestApp
|
||||
namespace MQTTnet.TestApp;
|
||||
|
||||
/// <summary>
|
||||
/// Connect a number of publisher clients and one subscriber client, then publish messages and measure
|
||||
/// the number of messages per second that can be exchanged between publishers and subscriber.
|
||||
/// Measurements are performed for subscriptions containing no wildcard, a single wildcard or multiple wildcards.
|
||||
/// </summary>
|
||||
public class MessageThroughputTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Connect a number of publisher clients and one subscriber client, then publish messages and measure
|
||||
/// the number of messages per second that can be exchanged between publishers and subscriber.
|
||||
/// Measurements are performed for subscriptions containing no wildcard, a single wildcard or multiple wildcards.
|
||||
/// </summary>
|
||||
public class MessageThroughputTest
|
||||
// Change these constants to suit
|
||||
const int NumPublishers = 5000;
|
||||
const int NumTopicsPerPublisher = 10;
|
||||
|
||||
// Note: Other code changes may be required when changing this constant:
|
||||
const int NumSubscribers = 1; // Fixed
|
||||
|
||||
// Number of publish calls before a response for all published messages is awaited.
|
||||
// This must be limited to a reasonable value that the server or TCP pipeline can handle.
|
||||
const int NumPublishCallsPerBatch = 250;
|
||||
|
||||
// Message counters are set/reset in the PublishAllAsync loop
|
||||
int _messagesReceivedCount;
|
||||
int _messagesExpectedCount;
|
||||
|
||||
CancellationTokenSource _cancellationTokenSource;
|
||||
|
||||
MqttServer _mqttServer;
|
||||
Dictionary<string, IMqttClient> _mqttPublisherClientsByPublisherName;
|
||||
List<IMqttClient> _mqttSubscriberClients;
|
||||
|
||||
Dictionary<string, List<string>> _topicsByPublisher;
|
||||
Dictionary<string, List<string>> _singleWildcardTopicsByPublisher;
|
||||
Dictionary<string, List<string>> _multiWildcardTopicsByPublisher;
|
||||
|
||||
public async Task Run()
|
||||
{
|
||||
// Change these constants to suit
|
||||
const int NumPublishers = 5000;
|
||||
const int NumTopicsPerPublisher = 10;
|
||||
|
||||
// Note: Other code changes may be required when changing this constant:
|
||||
const int NumSubscribers = 1; // Fixed
|
||||
|
||||
// Number of publish calls before a response for all published messages is awaited.
|
||||
// This must be limited to a reasonable value that the server or TCP pipeline can handle.
|
||||
const int NumPublishCallsPerBatch = 250;
|
||||
|
||||
// Message counters are set/reset in the PublishAllAsync loop
|
||||
int _messagesReceivedCount;
|
||||
int _messagesExpectedCount;
|
||||
|
||||
CancellationTokenSource _cancellationTokenSource;
|
||||
|
||||
MqttServer _mqttServer;
|
||||
Dictionary<string, IMqttClient> _mqttPublisherClientsByPublisherName;
|
||||
List<IMqttClient> _mqttSubscriberClients;
|
||||
|
||||
Dictionary<string, List<string>> _topicsByPublisher;
|
||||
Dictionary<string, List<string>> _singleWildcardTopicsByPublisher;
|
||||
Dictionary<string, List<string>> _multiWildcardTopicsByPublisher;
|
||||
|
||||
public async Task Run()
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Begin message throughput test");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Begin message throughput test");
|
||||
Console.WriteLine();
|
||||
|
||||
Console.WriteLine("Number of publishers: " + NumPublishers);
|
||||
Console.WriteLine("Number of published topics (total): " + NumPublishers * NumTopicsPerPublisher);
|
||||
Console.WriteLine("Number of subscribers: " + NumSubscribers);
|
||||
Console.WriteLine("Number of publishers: " + NumPublishers);
|
||||
Console.WriteLine("Number of published topics (total): " + NumPublishers * NumTopicsPerPublisher);
|
||||
Console.WriteLine("Number of subscribers: " + NumSubscribers);
|
||||
|
||||
await Setup();
|
||||
await Setup();
|
||||
|
||||
await Subscribe_to_No_Wildcard_Topics();
|
||||
await Subscribe_to_Single_Wildcard_Topics();
|
||||
await Subscribe_to_Multi_Wildcard_Topics();
|
||||
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("End message throughput test");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ConsoleWriteLineError(ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await Cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Setup()
|
||||
{
|
||||
new TopicGenerator().Generate(NumPublishers, NumTopicsPerPublisher, out _topicsByPublisher, out _singleWildcardTopicsByPublisher, out _multiWildcardTopicsByPublisher);
|
||||
|
||||
var serverOptions = new MqttServerOptionsBuilder().WithDefaultEndpoint().Build();
|
||||
var mqttClientFactory = new MqttClientFactory();
|
||||
var mqttServerFactory = new MqttServerFactory();
|
||||
_mqttServer = mqttServerFactory.CreateMqttServer(serverOptions);
|
||||
await _mqttServer.StartAsync();
|
||||
await Subscribe_to_No_Wildcard_Topics();
|
||||
await Subscribe_to_Single_Wildcard_Topics();
|
||||
await Subscribe_to_Multi_Wildcard_Topics();
|
||||
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Begin connect " + NumPublishers + " publisher(s)...");
|
||||
Console.WriteLine("End message throughput test");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ConsoleWriteLineError(ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await Cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
var stopWatch = new System.Diagnostics.Stopwatch();
|
||||
stopWatch.Start();
|
||||
_mqttPublisherClientsByPublisherName = new Dictionary<string, IMqttClient>();
|
||||
foreach (var pt in _topicsByPublisher)
|
||||
{
|
||||
var publisherName = pt.Key;
|
||||
var mqttClient = mqttClientFactory.CreateMqttClient();
|
||||
var publisherOptions = new MqttClientOptionsBuilder()
|
||||
.WithTcpServer("localhost")
|
||||
.WithClientId(publisherName)
|
||||
.Build();
|
||||
await mqttClient.ConnectAsync(publisherOptions);
|
||||
_mqttPublisherClientsByPublisherName.Add(publisherName, mqttClient);
|
||||
}
|
||||
stopWatch.Stop();
|
||||
public async Task Setup()
|
||||
{
|
||||
new TopicGenerator().Generate(NumPublishers, NumTopicsPerPublisher, out _topicsByPublisher, out _singleWildcardTopicsByPublisher, out _multiWildcardTopicsByPublisher);
|
||||
|
||||
Console.Write($"{NumPublishers} publisher(s) connected in {stopWatch.ElapsedMilliseconds / 1000.0:0.000} seconds, ");
|
||||
Console.WriteLine($"connections per second: {NumPublishers / (stopWatch.ElapsedMilliseconds / 1000.0):0.000}");
|
||||
var serverOptions = new MqttServerOptionsBuilder().WithDefaultEndpoint().Build();
|
||||
var mqttClientFactory = new MqttClientFactory();
|
||||
var mqttServerFactory = new MqttServerFactory();
|
||||
_mqttServer = mqttServerFactory.CreateMqttServer(serverOptions);
|
||||
await _mqttServer.StartAsync();
|
||||
|
||||
_mqttSubscriberClients = new List<IMqttClient>();
|
||||
for (var i = 0; i < NumSubscribers; ++i)
|
||||
{
|
||||
var mqttClient = mqttClientFactory.CreateMqttClient();
|
||||
var subsriberOptions = new MqttClientOptionsBuilder()
|
||||
.WithTcpServer("localhost")
|
||||
.WithClientId("sub" + i)
|
||||
.Build();
|
||||
await mqttClient.ConnectAsync(subsriberOptions);
|
||||
mqttClient.ApplicationMessageReceivedAsync += HandleApplicationMessageReceivedAsync;
|
||||
_mqttSubscriberClients.Add(mqttClient);
|
||||
}
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Begin connect " + NumPublishers + " publisher(s)...");
|
||||
|
||||
var stopWatch = new Stopwatch();
|
||||
stopWatch.Start();
|
||||
_mqttPublisherClientsByPublisherName = [];
|
||||
foreach (var pt in _topicsByPublisher)
|
||||
{
|
||||
var publisherName = pt.Key;
|
||||
var mqttClient = mqttClientFactory.CreateMqttClient();
|
||||
var publisherOptions = new MqttClientOptionsBuilder()
|
||||
.WithTcpServer("localhost")
|
||||
.WithClientId(publisherName)
|
||||
.Build();
|
||||
await mqttClient.ConnectAsync(publisherOptions);
|
||||
_mqttPublisherClientsByPublisherName.Add(publisherName, mqttClient);
|
||||
}
|
||||
stopWatch.Stop();
|
||||
|
||||
Console.Write($"{NumPublishers} publisher(s) connected in {stopWatch.ElapsedMilliseconds / 1000.0:0.000} seconds, ");
|
||||
Console.WriteLine($"connections per second: {NumPublishers / (stopWatch.ElapsedMilliseconds / 1000.0):0.000}");
|
||||
|
||||
_mqttSubscriberClients = new List<IMqttClient>();
|
||||
for (var i = 0; i < NumSubscribers; ++i)
|
||||
{
|
||||
var mqttClient = mqttClientFactory.CreateMqttClient();
|
||||
var subsriberOptions = new MqttClientOptionsBuilder()
|
||||
.WithTcpServer("localhost")
|
||||
.WithClientId("sub" + i)
|
||||
.Build();
|
||||
await mqttClient.ConnectAsync(subsriberOptions);
|
||||
mqttClient.ApplicationMessageReceivedAsync += HandleApplicationMessageReceivedAsync;
|
||||
_mqttSubscriberClients.Add(mqttClient);
|
||||
}
|
||||
}
|
||||
|
||||
Task HandleApplicationMessageReceivedAsync(MqttApplicationMessageReceivedEventArgs eventArgs)
|
||||
{
|
||||
// count messages and signal cancellation when expected message count is reached
|
||||
++_messagesReceivedCount;
|
||||
if (_messagesReceivedCount == _messagesExpectedCount)
|
||||
{
|
||||
_cancellationTokenSource.Cancel();
|
||||
}
|
||||
return CompletedTask.Instance;
|
||||
}
|
||||
|
||||
public async Task Cleanup()
|
||||
{
|
||||
foreach (var mqttClient in _mqttSubscriberClients)
|
||||
{
|
||||
await mqttClient.DisconnectAsync();
|
||||
mqttClient.ApplicationMessageReceivedAsync -= HandleApplicationMessageReceivedAsync;
|
||||
mqttClient.Dispose();
|
||||
}
|
||||
|
||||
Task HandleApplicationMessageReceivedAsync(MqttApplicationMessageReceivedEventArgs eventArgs)
|
||||
foreach (var pub in _mqttPublisherClientsByPublisherName)
|
||||
{
|
||||
// count messages and signal cancellation when expected message count is reached
|
||||
++_messagesReceivedCount;
|
||||
if (_messagesReceivedCount == _messagesExpectedCount)
|
||||
{
|
||||
_cancellationTokenSource.Cancel();
|
||||
}
|
||||
return CompletedTask.Instance;
|
||||
var mqttClient = pub.Value;
|
||||
await mqttClient.DisconnectAsync();
|
||||
mqttClient.Dispose();
|
||||
}
|
||||
|
||||
public async Task Cleanup()
|
||||
await _mqttServer.StopAsync();
|
||||
_mqttServer.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Measure no-wildcard topic subscription message exchange performance
|
||||
/// </summary>
|
||||
public Task Subscribe_to_No_Wildcard_Topics()
|
||||
{
|
||||
return ProcessMessages(_topicsByPublisher, "no wildcards");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Measure single-wildcard topic subscription message exchange performance
|
||||
/// </summary>
|
||||
public Task Subscribe_to_Single_Wildcard_Topics()
|
||||
{
|
||||
return ProcessMessages(_singleWildcardTopicsByPublisher, "single wildcard");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Measure multi-wildcard topic subscription message exchange performance
|
||||
/// </summary>
|
||||
public Task Subscribe_to_Multi_Wildcard_Topics()
|
||||
{
|
||||
return ProcessMessages(_multiWildcardTopicsByPublisher, "multi wildcard");
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to all topics, then run message exchange
|
||||
/// </summary>
|
||||
public async Task ProcessMessages(Dictionary<string, List<string>> topicsByPublisher, string topicTypeDescription)
|
||||
{
|
||||
var numTopics = CountTopics(topicsByPublisher);
|
||||
|
||||
Console.WriteLine();
|
||||
Console.Write($"Subscribing to {numTopics} topics ");
|
||||
ConsoleWriteInfo($"({topicTypeDescription})");
|
||||
Console.WriteLine($" for {NumSubscribers} subscriber(s)...");
|
||||
|
||||
var stopWatch = new Stopwatch();
|
||||
stopWatch.Start();
|
||||
|
||||
// each subscriber subscribes to all topics, one call per topic
|
||||
foreach (var subscriber in _mqttSubscriberClients)
|
||||
{
|
||||
foreach (var mqttClient in _mqttSubscriberClients)
|
||||
{
|
||||
await mqttClient.DisconnectAsync();
|
||||
mqttClient.ApplicationMessageReceivedAsync -= HandleApplicationMessageReceivedAsync;
|
||||
mqttClient.Dispose();
|
||||
}
|
||||
|
||||
foreach (var pub in _mqttPublisherClientsByPublisherName)
|
||||
{
|
||||
var mqttClient = pub.Value;
|
||||
await mqttClient.DisconnectAsync();
|
||||
mqttClient.Dispose();
|
||||
}
|
||||
|
||||
await _mqttServer.StopAsync();
|
||||
_mqttServer.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Measure no-wildcard topic subscription message exchange performance
|
||||
/// </summary>
|
||||
public Task Subscribe_to_No_Wildcard_Topics()
|
||||
{
|
||||
return ProcessMessages(_topicsByPublisher, "no wildcards");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Measure single-wildcard topic subscription message exchange performance
|
||||
/// </summary>
|
||||
public Task Subscribe_to_Single_Wildcard_Topics()
|
||||
{
|
||||
return ProcessMessages(_singleWildcardTopicsByPublisher, "single wildcard");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Measure multi-wildcard topic subscription message exchange performance
|
||||
/// </summary>
|
||||
public Task Subscribe_to_Multi_Wildcard_Topics()
|
||||
{
|
||||
return ProcessMessages(_multiWildcardTopicsByPublisher, "multi wildcard");
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to all topics, then run message exchange
|
||||
/// </summary>
|
||||
public async Task ProcessMessages(Dictionary<string, List<string>> topicsByPublisher, string topicTypeDescription)
|
||||
{
|
||||
var numTopics = CountTopics(topicsByPublisher);
|
||||
|
||||
Console.WriteLine();
|
||||
Console.Write($"Subscribing to {numTopics} topics ");
|
||||
ConsoleWriteInfo($"({topicTypeDescription})");
|
||||
Console.WriteLine($" for {NumSubscribers} subscriber(s)...");
|
||||
|
||||
var stopWatch = new System.Diagnostics.Stopwatch();
|
||||
stopWatch.Start();
|
||||
|
||||
// each subscriber subscribes to all topics, one call per topic
|
||||
foreach (var subscriber in _mqttSubscriberClients)
|
||||
{
|
||||
foreach (var tp in topicsByPublisher)
|
||||
{
|
||||
var topics = tp.Value;
|
||||
foreach (var topic in topics)
|
||||
{
|
||||
var topicFilter = new Packets.MqttTopicFilter() { Topic = topic };
|
||||
await subscriber.SubscribeAsync(topicFilter).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stopWatch.Stop();
|
||||
|
||||
Console.Write($"{NumSubscribers} subscriber(s) subscribed in {stopWatch.ElapsedMilliseconds / 1000.0:0.000} seconds, ");
|
||||
Console.WriteLine($"subscribe calls per second: {numTopics / (stopWatch.ElapsedMilliseconds / 1000.0):0.000}");
|
||||
|
||||
await PublishAllAsync();
|
||||
|
||||
Console.WriteLine($"Unsubscribing {numTopics} topics ({topicTypeDescription}) for {NumSubscribers} subscriber(s)...");
|
||||
|
||||
stopWatch.Restart();
|
||||
|
||||
// unsubscribe to all topics, one call per topic
|
||||
foreach (var subscriber in _mqttSubscriberClients)
|
||||
{
|
||||
foreach (var tp in topicsByPublisher)
|
||||
{
|
||||
var topics = tp.Value;
|
||||
foreach (var topic in topics)
|
||||
{
|
||||
var topicFilter = new Packets.MqttTopicFilter() { Topic = topic };
|
||||
|
||||
MqttClientUnsubscribeOptions options =
|
||||
new MqttClientUnsubscribeOptionsBuilder()
|
||||
.WithTopicFilter(topicFilter)
|
||||
.Build();
|
||||
await subscriber.UnsubscribeAsync(options).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stopWatch.Stop();
|
||||
|
||||
Console.Write($"{NumSubscribers} subscriber(s) unsubscribed in {stopWatch.ElapsedMilliseconds / 1000.0:0.000} seconds, ");
|
||||
Console.WriteLine($"unsubscribe calls per second: {numTopics / (stopWatch.ElapsedMilliseconds / 1000.0):0.000}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Publish messages in batches of NumPublishCallsPerBatch, wait for messages sent to subscriber
|
||||
/// </summary>
|
||||
async Task PublishAllAsync()
|
||||
{
|
||||
Console.WriteLine("Begin message exchange...");
|
||||
|
||||
int publisherIndexCounter = 0; // index to loop around all publishers to publish
|
||||
int topicIndexCounter = 0; // index to loop around all topics to publish
|
||||
int totalNumMessagesReceived = 0;
|
||||
|
||||
var publisherNames = _topicsByPublisher.Keys.ToList();
|
||||
|
||||
// There should be one message received per publish
|
||||
_messagesExpectedCount = NumPublishCallsPerBatch;
|
||||
|
||||
var stopWatch = new System.Diagnostics.Stopwatch();
|
||||
stopWatch.Start();
|
||||
|
||||
// Loop for a while and exchange messages
|
||||
|
||||
while (stopWatch.ElapsedMilliseconds < 10000)
|
||||
{
|
||||
_messagesReceivedCount = 0;
|
||||
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
for (var publishCallCount = 0; publishCallCount < NumPublishCallsPerBatch; ++publishCallCount)
|
||||
{
|
||||
// pick a publisher
|
||||
var publisherName = publisherNames[publisherIndexCounter % publisherNames.Count];
|
||||
var publisherTopics = _topicsByPublisher[publisherName];
|
||||
// pick a publisher topic
|
||||
var topic = publisherTopics[topicIndexCounter % publisherTopics.Count];
|
||||
var message = new MqttApplicationMessageBuilder()
|
||||
.WithTopic(topic)
|
||||
.Build();
|
||||
await _mqttPublisherClientsByPublisherName[publisherName].PublishAsync(message).ConfigureAwait(false);
|
||||
++topicIndexCounter;
|
||||
++publisherIndexCounter;
|
||||
}
|
||||
|
||||
// Wait for at least one message per publish to be received by subscriber (in the subscriber's application message handler),
|
||||
// then loop around to send another batch
|
||||
try
|
||||
{
|
||||
await Task.Delay(30000, _cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
_cancellationTokenSource.Dispose();
|
||||
|
||||
if (_messagesReceivedCount < _messagesExpectedCount)
|
||||
{
|
||||
ConsoleWriteLineError(string.Format("Messages Received Count mismatch, expected {0}, received {1}", _messagesExpectedCount, _messagesReceivedCount));
|
||||
return;
|
||||
}
|
||||
|
||||
totalNumMessagesReceived += _messagesReceivedCount;
|
||||
}
|
||||
|
||||
stopWatch.Stop();
|
||||
|
||||
Console.Write($"{totalNumMessagesReceived} messages published and received in {stopWatch.ElapsedMilliseconds / 1000.0:0.000} seconds, ");
|
||||
ConsoleWriteLineSuccess($"messages per second: {(int)(totalNumMessagesReceived / (stopWatch.ElapsedMilliseconds / 1000.0))}");
|
||||
}
|
||||
|
||||
int CountTopics(Dictionary<string, List<string>> topicsByPublisher)
|
||||
{
|
||||
var count = 0;
|
||||
foreach (var tp in topicsByPublisher)
|
||||
{
|
||||
count += tp.Value.Count;
|
||||
var topics = tp.Value;
|
||||
foreach (var topic in topics)
|
||||
{
|
||||
var topicFilter = new Packets.MqttTopicFilter() { Topic = topic };
|
||||
await subscriber.SubscribeAsync(topicFilter).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
void ConsoleWriteLineError(string message)
|
||||
stopWatch.Stop();
|
||||
|
||||
Console.Write($"{NumSubscribers} subscriber(s) subscribed in {stopWatch.ElapsedMilliseconds / 1000.0:0.000} seconds, ");
|
||||
Console.WriteLine($"subscribe calls per second: {numTopics / (stopWatch.ElapsedMilliseconds / 1000.0):0.000}");
|
||||
|
||||
await PublishAllAsync();
|
||||
|
||||
Console.WriteLine($"Unsubscribing {numTopics} topics ({topicTypeDescription}) for {NumSubscribers} subscriber(s)...");
|
||||
|
||||
stopWatch.Restart();
|
||||
|
||||
// unsubscribe to all topics, one call per topic
|
||||
foreach (var subscriber in _mqttSubscriberClients)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Red;
|
||||
Console.WriteLine(message);
|
||||
Console.ResetColor();
|
||||
foreach (var tp in topicsByPublisher)
|
||||
{
|
||||
var topics = tp.Value;
|
||||
foreach (var topic in topics)
|
||||
{
|
||||
var topicFilter = new Packets.MqttTopicFilter { Topic = topic };
|
||||
|
||||
var options =
|
||||
new MqttClientUnsubscribeOptionsBuilder()
|
||||
.WithTopicFilter(topicFilter)
|
||||
.Build();
|
||||
await subscriber.UnsubscribeAsync(options).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ConsoleWriteLineSuccess(string message)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Green;
|
||||
Console.WriteLine(message);
|
||||
Console.ResetColor();
|
||||
}
|
||||
|
||||
void ConsoleWriteInfo(string message)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.White;
|
||||
Console.Write(message);
|
||||
Console.ResetColor();
|
||||
}
|
||||
stopWatch.Stop();
|
||||
|
||||
Console.Write($"{NumSubscribers} subscriber(s) unsubscribed in {stopWatch.ElapsedMilliseconds / 1000.0:0.000} seconds, ");
|
||||
Console.WriteLine($"unsubscribe calls per second: {numTopics / (stopWatch.ElapsedMilliseconds / 1000.0):0.000}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Publish messages in batches of NumPublishCallsPerBatch, wait for messages sent to subscriber
|
||||
/// </summary>
|
||||
async Task PublishAllAsync()
|
||||
{
|
||||
Console.WriteLine("Begin message exchange...");
|
||||
|
||||
var publisherIndexCounter = 0; // index to loop around all publishers to publish
|
||||
var topicIndexCounter = 0; // index to loop around all topics to publish
|
||||
var totalNumMessagesReceived = 0;
|
||||
|
||||
var publisherNames = _topicsByPublisher.Keys.ToList();
|
||||
|
||||
// There should be one message received per publish
|
||||
_messagesExpectedCount = NumPublishCallsPerBatch;
|
||||
|
||||
var stopWatch = new Stopwatch();
|
||||
stopWatch.Start();
|
||||
|
||||
// Loop for a while and exchange messages
|
||||
|
||||
while (stopWatch.ElapsedMilliseconds < 10000)
|
||||
{
|
||||
_messagesReceivedCount = 0;
|
||||
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
for (var publishCallCount = 0; publishCallCount < NumPublishCallsPerBatch; ++publishCallCount)
|
||||
{
|
||||
// pick a publisher
|
||||
var publisherName = publisherNames[publisherIndexCounter % publisherNames.Count];
|
||||
var publisherTopics = _topicsByPublisher[publisherName];
|
||||
// pick a publisher topic
|
||||
var topic = publisherTopics[topicIndexCounter % publisherTopics.Count];
|
||||
var message = new MqttApplicationMessageBuilder()
|
||||
.WithTopic(topic)
|
||||
.Build();
|
||||
|
||||
await _mqttPublisherClientsByPublisherName[publisherName].PublishAsync(message).ConfigureAwait(false);
|
||||
++topicIndexCounter;
|
||||
++publisherIndexCounter;
|
||||
}
|
||||
|
||||
// Wait for at least one message per publish to be received by subscriber (in the subscriber's application message handler),
|
||||
// then loop around to send another batch
|
||||
try
|
||||
{
|
||||
await Task.Delay(30000, _cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.Write(exception.ToString());
|
||||
}
|
||||
|
||||
_cancellationTokenSource.Dispose();
|
||||
|
||||
if (_messagesReceivedCount < _messagesExpectedCount)
|
||||
{
|
||||
ConsoleWriteLineError($"Messages Received Count mismatch, expected {_messagesExpectedCount}, received {_messagesReceivedCount}");
|
||||
return;
|
||||
}
|
||||
|
||||
totalNumMessagesReceived += _messagesReceivedCount;
|
||||
}
|
||||
|
||||
stopWatch.Stop();
|
||||
|
||||
Console.Write($"{totalNumMessagesReceived} messages published and received in {stopWatch.ElapsedMilliseconds / 1000.0:0.000} seconds, ");
|
||||
ConsoleWriteLineSuccess($"messages per second: {(int)(totalNumMessagesReceived / (stopWatch.ElapsedMilliseconds / 1000.0))}");
|
||||
}
|
||||
|
||||
static int CountTopics(Dictionary<string, List<string>> topicsByPublisher)
|
||||
{
|
||||
var count = 0;
|
||||
foreach (var tp in topicsByPublisher)
|
||||
{
|
||||
count += tp.Value.Count;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
static void ConsoleWriteLineError(string message)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Red;
|
||||
Console.WriteLine(message);
|
||||
Console.ResetColor();
|
||||
}
|
||||
|
||||
static void ConsoleWriteLineSuccess(string message)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Green;
|
||||
Console.WriteLine(message);
|
||||
Console.ResetColor();
|
||||
}
|
||||
|
||||
static void ConsoleWriteInfo(string message)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.White;
|
||||
Console.Write(message);
|
||||
Console.ResetColor();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,58 +6,57 @@ using System;
|
||||
using System.Text;
|
||||
using MQTTnet.Diagnostics.Logger;
|
||||
|
||||
namespace MQTTnet.TestApp
|
||||
namespace MQTTnet.TestApp;
|
||||
|
||||
public static class MqttNetConsoleLogger
|
||||
{
|
||||
public static class MqttNetConsoleLogger
|
||||
static readonly object Lock = new();
|
||||
|
||||
public static void ForwardToConsole(MqttNetEventLogger logger)
|
||||
{
|
||||
static readonly object _lock = new object();
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
public static void ForwardToConsole(MqttNetEventLogger logger)
|
||||
logger.LogMessagePublished -= PrintToConsole;
|
||||
logger.LogMessagePublished += PrintToConsole;
|
||||
}
|
||||
|
||||
public static void PrintToConsole(string message, ConsoleColor color)
|
||||
{
|
||||
lock (Lock)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
logger.LogMessagePublished -= PrintToConsole;
|
||||
logger.LogMessagePublished += PrintToConsole;
|
||||
}
|
||||
|
||||
public static void PrintToConsole(string message, ConsoleColor color)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var backupColor = Console.ForegroundColor;
|
||||
Console.ForegroundColor = color;
|
||||
Console.Write(message);
|
||||
Console.ForegroundColor = backupColor;
|
||||
}
|
||||
}
|
||||
|
||||
static void PrintToConsole(object sender, MqttNetLogMessagePublishedEventArgs e)
|
||||
{
|
||||
var output = new StringBuilder();
|
||||
output.AppendLine($">> [{e.LogMessage.Timestamp:O}] [{e.LogMessage.ThreadId}] [{e.LogMessage.Source}] [{e.LogMessage.Level}]: {e.LogMessage.Message}");
|
||||
if (e.LogMessage.Exception != null)
|
||||
{
|
||||
output.AppendLine(e.LogMessage.Exception.ToString());
|
||||
}
|
||||
|
||||
var color = ConsoleColor.Red;
|
||||
switch (e.LogMessage.Level)
|
||||
{
|
||||
case MqttNetLogLevel.Error:
|
||||
color = ConsoleColor.Red;
|
||||
break;
|
||||
case MqttNetLogLevel.Warning:
|
||||
color = ConsoleColor.Yellow;
|
||||
break;
|
||||
case MqttNetLogLevel.Info:
|
||||
color = ConsoleColor.Green;
|
||||
break;
|
||||
case MqttNetLogLevel.Verbose:
|
||||
color = ConsoleColor.Gray;
|
||||
break;
|
||||
}
|
||||
|
||||
PrintToConsole(output.ToString(), color);
|
||||
var backupColor = Console.ForegroundColor;
|
||||
Console.ForegroundColor = color;
|
||||
Console.Write(message);
|
||||
Console.ForegroundColor = backupColor;
|
||||
}
|
||||
}
|
||||
|
||||
static void PrintToConsole(object sender, MqttNetLogMessagePublishedEventArgs e)
|
||||
{
|
||||
var output = new StringBuilder();
|
||||
output.AppendLine($">> [{e.LogMessage.Timestamp:O}] [{e.LogMessage.ThreadId}] [{e.LogMessage.Source}] [{e.LogMessage.Level}]: {e.LogMessage.Message}");
|
||||
if (e.LogMessage.Exception != null)
|
||||
{
|
||||
output.AppendLine(e.LogMessage.Exception.ToString());
|
||||
}
|
||||
|
||||
var color = ConsoleColor.Red;
|
||||
switch (e.LogMessage.Level)
|
||||
{
|
||||
case MqttNetLogLevel.Error:
|
||||
color = ConsoleColor.Red;
|
||||
break;
|
||||
case MqttNetLogLevel.Warning:
|
||||
color = ConsoleColor.Yellow;
|
||||
break;
|
||||
case MqttNetLogLevel.Info:
|
||||
color = ConsoleColor.Green;
|
||||
break;
|
||||
case MqttNetLogLevel.Verbose:
|
||||
color = ConsoleColor.Gray;
|
||||
break;
|
||||
}
|
||||
|
||||
PrintToConsole(output.ToString(), color);
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user