mirror of
https://github.com/dotnet/MQTTnet.git
synced 2026-10-20 11:45:43 +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:
@@ -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; }
|
||||
}
|
||||
Reference in New Issue
Block a user