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:
Christian
2025-08-30 10:52:59 +02:00
committed by GitHub
parent 247988e633
commit e43e7f8e36
330 changed files with 21417 additions and 22919 deletions
@@ -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);
}
}