@@ -22,6 +22,7 @@ | |||||
* [Server] Added packet statistics for the connected clients. | * [Server] Added packet statistics for the connected clients. | ||||
* [Server] Fixed a security issue which sends retained packages to a failed subscription. | * [Server] Fixed a security issue which sends retained packages to a failed subscription. | ||||
* [Server] Fixed the response (MaximumQoS) of a subscription (Thanks to @redbeans2017). | * [Server] Fixed the response (MaximumQoS) of a subscription (Thanks to @redbeans2017). | ||||
* [Server] The keep alive timeouts are now checked for every client (Thanks to @RainerMueller82). | |||||
</releaseNotes> | </releaseNotes> | ||||
<copyright>Copyright Christian Kratky 2016-2017</copyright> | <copyright>Copyright Christian Kratky 2016-2017</copyright> | ||||
<tags>MQTT Message Queue Telemetry Transport MQTTClient MQTTServer Server MQTTBroker Broker NETStandard IoT InternetOfThings Messaging Hardware Arduino Sensor Actuator M2M ESP Smart Home Cities Automation Xamarin</tags> | <tags>MQTT Message Queue Telemetry Transport MQTTClient MQTTServer Server MQTTBroker Broker NETStandard IoT InternetOfThings Messaging Hardware Arduino Sensor Actuator M2M ESP Smart Home Cities Automation Xamarin</tags> | ||||
@@ -8,7 +8,7 @@ using MQTTnet.Server; | |||||
namespace MQTTnet.AspNetCore | namespace MQTTnet.AspNetCore | ||||
{ | { | ||||
public class MqttWebSocketServerAdapter : IMqttServerAdapter, IDisposable | |||||
public sealed class MqttWebSocketServerAdapter : IMqttServerAdapter, IDisposable | |||||
{ | { | ||||
public event EventHandler<MqttServerAdapterClientAcceptedEventArgs> ClientAccepted; | public event EventHandler<MqttServerAdapterClientAcceptedEventArgs> ClientAccepted; | ||||
@@ -347,13 +347,8 @@ namespace MQTTnet.Client | |||||
{ | { | ||||
while (!cancellationToken.IsCancellationRequested) | while (!cancellationToken.IsCancellationRequested) | ||||
{ | { | ||||
await Task.Delay(_options.KeepAlivePeriod, cancellationToken).ConfigureAwait(false); | |||||
if (cancellationToken.IsCancellationRequested) | |||||
{ | |||||
return; | |||||
} | |||||
await SendAndReceiveAsync<MqttPingRespPacket>(new MqttPingReqPacket()).ConfigureAwait(false); | await SendAndReceiveAsync<MqttPingRespPacket>(new MqttPingReqPacket()).ConfigureAwait(false); | ||||
await Task.Delay(_options.KeepAlivePeriod, cancellationToken).ConfigureAwait(false); | |||||
} | } | ||||
} | } | ||||
catch (OperationCanceledException) | catch (OperationCanceledException) | ||||
@@ -10,9 +10,9 @@ using MQTTnet.Protocol; | |||||
namespace MQTTnet.Server | namespace MQTTnet.Server | ||||
{ | { | ||||
public sealed class MqttClientPendingMessagesQueue | |||||
public sealed class MqttClientPendingMessagesQueue : IDisposable | |||||
{ | { | ||||
private readonly BlockingCollection<MqttPublishPacket> _pendingPublishPackets = new BlockingCollection<MqttPublishPacket>(); | |||||
private readonly BlockingCollection<MqttBasePacket> _queue = new BlockingCollection<MqttBasePacket>(); | |||||
private readonly IMqttServerOptions _options; | private readonly IMqttServerOptions _options; | ||||
private readonly MqttClientSession _session; | private readonly MqttClientSession _session; | ||||
private readonly IMqttNetLogger _logger; | private readonly IMqttNetLogger _logger; | ||||
@@ -33,24 +33,24 @@ namespace MQTTnet.Server | |||||
return; | return; | ||||
} | } | ||||
Task.Run(async () => await SendPendingPublishPacketsAsync(adapter, cancellationToken), cancellationToken).ConfigureAwait(false); | |||||
Task.Run(async () => await SendQueuedPacketsAsync(adapter, cancellationToken), cancellationToken).ConfigureAwait(false); | |||||
} | } | ||||
public void Enqueue(MqttPublishPacket publishPacket) | |||||
public void Enqueue(MqttBasePacket packet) | |||||
{ | { | ||||
if (publishPacket == null) throw new ArgumentNullException(nameof(publishPacket)); | |||||
if (packet == null) throw new ArgumentNullException(nameof(packet)); | |||||
_pendingPublishPackets.Add(publishPacket); | |||||
_queue.Add(packet); | |||||
_logger.Trace<MqttClientPendingMessagesQueue>("Enqueued packet (ClientId: {0}).", _session.ClientId); | _logger.Trace<MqttClientPendingMessagesQueue>("Enqueued packet (ClientId: {0}).", _session.ClientId); | ||||
} | } | ||||
private async Task SendPendingPublishPacketsAsync(IMqttChannelAdapter adapter, CancellationToken cancellationToken) | |||||
private async Task SendQueuedPacketsAsync(IMqttChannelAdapter adapter, CancellationToken cancellationToken) | |||||
{ | { | ||||
try | try | ||||
{ | { | ||||
while (!cancellationToken.IsCancellationRequested) | while (!cancellationToken.IsCancellationRequested) | ||||
{ | { | ||||
await SendPendingPublishPacketAsync(adapter, cancellationToken); | |||||
await SendQueuedPacketAsync(adapter, cancellationToken); | |||||
} | } | ||||
} | } | ||||
catch (OperationCanceledException) | catch (OperationCanceledException) | ||||
@@ -62,12 +62,12 @@ namespace MQTTnet.Server | |||||
} | } | ||||
} | } | ||||
private async Task SendPendingPublishPacketAsync(IMqttChannelAdapter adapter, CancellationToken cancellationToken) | |||||
private async Task SendQueuedPacketAsync(IMqttChannelAdapter adapter, CancellationToken cancellationToken) | |||||
{ | { | ||||
MqttPublishPacket packet = null; | |||||
MqttBasePacket packet = null; | |||||
try | try | ||||
{ | { | ||||
packet = _pendingPublishPackets.Take(cancellationToken); | |||||
packet = _queue.Take(cancellationToken); | |||||
await adapter.SendPacketsAsync(_options.DefaultCommunicationTimeout, cancellationToken, packet).ConfigureAwait(false); | await adapter.SendPacketsAsync(_options.DefaultCommunicationTimeout, cancellationToken, packet).ConfigureAwait(false); | ||||
_logger.Trace<MqttClientPendingMessagesQueue>("Enqueued packet sent (ClientId: {0}).", _session.ClientId); | _logger.Trace<MqttClientPendingMessagesQueue>("Enqueued packet sent (ClientId: {0}).", _session.ClientId); | ||||
@@ -90,14 +90,22 @@ namespace MQTTnet.Server | |||||
_logger.Error<MqttClientPendingMessagesQueue>(exception, "Sending publish packet failed (ClientId: {0}).", _session.ClientId); | _logger.Error<MqttClientPendingMessagesQueue>(exception, "Sending publish packet failed (ClientId: {0}).", _session.ClientId); | ||||
} | } | ||||
if (packet != null && packet.QualityOfServiceLevel > MqttQualityOfServiceLevel.AtMostOnce) | |||||
if (packet is MqttPublishPacket publishPacket) | |||||
{ | { | ||||
packet.Dup = true; | |||||
_pendingPublishPackets.Add(packet, CancellationToken.None); | |||||
if (publishPacket.QualityOfServiceLevel > MqttQualityOfServiceLevel.AtMostOnce) | |||||
{ | |||||
publishPacket.Dup = true; | |||||
_queue.Add(packet, CancellationToken.None); | |||||
} | |||||
} | } | ||||
await _session.StopAsync(); | await _session.StopAsync(); | ||||
} | } | ||||
} | } | ||||
public void Dispose() | |||||
{ | |||||
_queue?.Dispose(); | |||||
} | |||||
} | } | ||||
} | } |
@@ -12,7 +12,7 @@ using MQTTnet.Serializer; | |||||
namespace MQTTnet.Server | namespace MQTTnet.Server | ||||
{ | { | ||||
public sealed class MqttClientSession | |||||
public sealed class MqttClientSession : IDisposable | |||||
{ | { | ||||
private readonly Stopwatch _lastPacketReceivedTracker = Stopwatch.StartNew(); | private readonly Stopwatch _lastPacketReceivedTracker = Stopwatch.StartNew(); | ||||
private readonly Stopwatch _lastNonKeepAlivePacketReceivedTracker = Stopwatch.StartNew(); | private readonly Stopwatch _lastNonKeepAlivePacketReceivedTracker = Stopwatch.StartNew(); | ||||
@@ -57,22 +57,29 @@ namespace MQTTnet.Server | |||||
public bool IsConnected => _adapter != null; | public bool IsConnected => _adapter != null; | ||||
public async Task RunAsync(MqttApplicationMessage willMessage, IMqttChannelAdapter adapter) | |||||
public async Task RunAsync(MqttConnectPacket connectPacket, IMqttChannelAdapter adapter) | |||||
{ | { | ||||
if (connectPacket == null) throw new ArgumentNullException(nameof(connectPacket)); | |||||
if (adapter == null) throw new ArgumentNullException(nameof(adapter)); | if (adapter == null) throw new ArgumentNullException(nameof(adapter)); | ||||
try | try | ||||
{ | { | ||||
_lastPacketReceivedTracker.Restart(); | |||||
_lastNonKeepAlivePacketReceivedTracker.Restart(); | |||||
var cancellationTokenSource = new CancellationTokenSource(); | var cancellationTokenSource = new CancellationTokenSource(); | ||||
_willMessage = willMessage; | |||||
_willMessage = connectPacket.WillMessage; | |||||
_adapter = adapter; | _adapter = adapter; | ||||
_cancellationTokenSource = cancellationTokenSource; | _cancellationTokenSource = cancellationTokenSource; | ||||
_pendingMessagesQueue.Start(adapter, cancellationTokenSource.Token); | _pendingMessagesQueue.Start(adapter, cancellationTokenSource.Token); | ||||
_lastPacketReceivedTracker.Restart(); | |||||
_lastNonKeepAlivePacketReceivedTracker.Restart(); | |||||
if (connectPacket.KeepAlivePeriod > 0) | |||||
{ | |||||
StartCheckingKeepAliveTimeout(TimeSpan.FromSeconds(connectPacket.KeepAlivePeriod), cancellationTokenSource.Token); | |||||
} | |||||
await ReceivePacketsAsync(adapter, cancellationTokenSource.Token).ConfigureAwait(false); | await ReceivePacketsAsync(adapter, cancellationTokenSource.Token).ConfigureAwait(false); | ||||
} | } | ||||
catch (OperationCanceledException) | catch (OperationCanceledException) | ||||
@@ -136,6 +143,12 @@ namespace MQTTnet.Server | |||||
_pendingMessagesQueue.Enqueue(publishPacket); | _pendingMessagesQueue.Enqueue(publishPacket); | ||||
} | } | ||||
public void Dispose() | |||||
{ | |||||
_pendingMessagesQueue?.Dispose(); | |||||
_cancellationTokenSource?.Dispose(); | |||||
} | |||||
private async Task ReceivePacketsAsync(IMqttChannelAdapter adapter, CancellationToken cancellationToken) | private async Task ReceivePacketsAsync(IMqttChannelAdapter adapter, CancellationToken cancellationToken) | ||||
{ | { | ||||
try | try | ||||
@@ -143,6 +156,14 @@ namespace MQTTnet.Server | |||||
while (!cancellationToken.IsCancellationRequested) | while (!cancellationToken.IsCancellationRequested) | ||||
{ | { | ||||
var packet = await adapter.ReceivePacketAsync(TimeSpan.Zero, cancellationToken).ConfigureAwait(false); | var packet = await adapter.ReceivePacketAsync(TimeSpan.Zero, cancellationToken).ConfigureAwait(false); | ||||
_lastPacketReceivedTracker.Restart(); | |||||
if (!(packet is MqttPingReqPacket)) | |||||
{ | |||||
_lastNonKeepAlivePacketReceivedTracker.Restart(); | |||||
} | |||||
await ProcessReceivedPacketAsync(adapter, packet, cancellationToken).ConfigureAwait(false); | await ProcessReceivedPacketAsync(adapter, packet, cancellationToken).ConfigureAwait(false); | ||||
} | } | ||||
} | } | ||||
@@ -163,13 +184,6 @@ namespace MQTTnet.Server | |||||
private Task ProcessReceivedPacketAsync(IMqttChannelAdapter adapter, MqttBasePacket packet, CancellationToken cancellationToken) | private Task ProcessReceivedPacketAsync(IMqttChannelAdapter adapter, MqttBasePacket packet, CancellationToken cancellationToken) | ||||
{ | { | ||||
_lastPacketReceivedTracker.Restart(); | |||||
if (!(packet is MqttPingReqPacket)) | |||||
{ | |||||
_lastNonKeepAlivePacketReceivedTracker.Restart(); | |||||
} | |||||
if (packet is MqttPublishPacket publishPacket) | if (packet is MqttPublishPacket publishPacket) | ||||
{ | { | ||||
return HandleIncomingPublishPacketAsync(adapter, publishPacket, cancellationToken); | return HandleIncomingPublishPacketAsync(adapter, publishPacket, cancellationToken); | ||||
@@ -292,5 +306,42 @@ namespace MQTTnet.Server | |||||
var response = new MqttPubCompPacket { PacketIdentifier = pubRelPacket.PacketIdentifier }; | var response = new MqttPubCompPacket { PacketIdentifier = pubRelPacket.PacketIdentifier }; | ||||
return adapter.SendPacketsAsync(_options.DefaultCommunicationTimeout, cancellationToken, response); | return adapter.SendPacketsAsync(_options.DefaultCommunicationTimeout, cancellationToken, response); | ||||
} | } | ||||
private void StartCheckingKeepAliveTimeout(TimeSpan keepAlivePeriod, CancellationToken cancellationToken) | |||||
{ | |||||
Task.Run( | |||||
async () => await CheckKeepAliveTimeoutAsync(keepAlivePeriod, cancellationToken).ConfigureAwait(false) | |||||
, cancellationToken); | |||||
} | |||||
private async Task CheckKeepAliveTimeoutAsync(TimeSpan keepAlivePeriod, CancellationToken cancellationToken) | |||||
{ | |||||
try | |||||
{ | |||||
while (!cancellationToken.IsCancellationRequested) | |||||
{ | |||||
// Values described here: [MQTT-3.1.2-24]. | |||||
if (_lastPacketReceivedTracker.Elapsed.TotalSeconds > keepAlivePeriod.TotalSeconds * 1.5D) | |||||
{ | |||||
_logger.Warning<MqttClientSession>("Client '{0}': Did not receive any packet or keep alive signal.", ClientId); | |||||
await StopAsync(); | |||||
return; | |||||
} | |||||
await Task.Delay(keepAlivePeriod, cancellationToken); | |||||
} | |||||
} | |||||
catch (OperationCanceledException) | |||||
{ | |||||
} | |||||
catch (Exception exception) | |||||
{ | |||||
_logger.Error<MqttClientSession>(exception, "Client '{0}': Unhandled exception while checking keep alive timeouts.", ClientId); | |||||
} | |||||
finally | |||||
{ | |||||
_logger.Trace<MqttClientSession>("Client {0}: Stopped checking keep alive timeout.", ClientId); | |||||
} | |||||
} | |||||
} | } | ||||
} | } |
@@ -70,7 +70,7 @@ namespace MQTTnet.Server | |||||
ProtocolVersion = clientAdapter.PacketSerializer.ProtocolVersion | ProtocolVersion = clientAdapter.PacketSerializer.ProtocolVersion | ||||
}); | }); | ||||
await clientSession.Session.RunAsync(connectPacket.WillMessage, clientAdapter).ConfigureAwait(false); | |||||
await clientSession.Session.RunAsync(connectPacket, clientAdapter).ConfigureAwait(false); | |||||
} | } | ||||
catch (Exception exception) | catch (Exception exception) | ||||
{ | { | ||||
@@ -210,9 +210,11 @@ namespace MQTTnet.Server | |||||
if (isSessionPresent) | if (isSessionPresent) | ||||
{ | { | ||||
if (connectPacket.CleanSession) | if (connectPacket.CleanSession) | ||||
{ | |||||
{ | |||||
_sessions.Remove(connectPacket.ClientId); | _sessions.Remove(connectPacket.ClientId); | ||||
await clientSession.StopAsync().ConfigureAwait(false); | await clientSession.StopAsync().ConfigureAwait(false); | ||||
clientSession.Dispose(); | |||||
clientSession = null; | clientSession = null; | ||||
_logger.Trace<MqttClientSessionsManager>("Stopped existing session of client '{0}'.", connectPacket.ClientId); | _logger.Trace<MqttClientSessionsManager>("Stopped existing session of client '{0}'.", connectPacket.ClientId); | ||||
@@ -71,7 +71,6 @@ namespace MQTTnet.Server | |||||
} | } | ||||
_logger.Info<MqttServer>("Started."); | _logger.Info<MqttServer>("Started."); | ||||
Started?.Invoke(this, new MqttServerStartedEventArgs()); | Started?.Invoke(this, new MqttServerStartedEventArgs()); | ||||
} | } | ||||
@@ -27,7 +27,9 @@ | |||||
<TextBox x:Name="ClientId"></TextBox> | <TextBox x:Name="ClientId"></TextBox> | ||||
<TextBlock>Clean session:</TextBlock> | <TextBlock>Clean session:</TextBlock> | ||||
<CheckBox x:Name="CleanSession" IsChecked="True"></CheckBox> | <CheckBox x:Name="CleanSession" IsChecked="True"></CheckBox> | ||||
<TextBlock>Keep alive interval:</TextBlock> | |||||
<TextBox x:Name="KeepAliveInterval" Text="5"></TextBox> | |||||
<StackPanel Orientation="Horizontal"> | <StackPanel Orientation="Horizontal"> | ||||
<RadioButton x:Name="UseTcp" IsChecked="True" GroupName="connection">TCP</RadioButton> | <RadioButton x:Name="UseTcp" IsChecked="True" GroupName="connection">TCP</RadioButton> | ||||
<RadioButton x:Name="UseWs" GroupName="connection">WS</RadioButton> | <RadioButton x:Name="UseWs" GroupName="connection">WS</RadioButton> | ||||
@@ -103,7 +103,8 @@ namespace MQTTnet.TestApp.UniversalWindows | |||||
}; | }; | ||||
options.CleanSession = CleanSession.IsChecked == true; | options.CleanSession = CleanSession.IsChecked == true; | ||||
options.KeepAlivePeriod = TimeSpan.FromSeconds(double.Parse(KeepAliveInterval.Text)); | |||||
try | try | ||||
{ | { | ||||
if (_mqttClient != null) | if (_mqttClient != null) | ||||