@@ -22,6 +22,7 @@ | |||
* [Server] Added packet statistics for the connected clients. | |||
* [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] The keep alive timeouts are now checked for every client (Thanks to @RainerMueller82). | |||
</releaseNotes> | |||
<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> | |||
@@ -8,7 +8,7 @@ using MQTTnet.Server; | |||
namespace MQTTnet.AspNetCore | |||
{ | |||
public class MqttWebSocketServerAdapter : IMqttServerAdapter, IDisposable | |||
public sealed class MqttWebSocketServerAdapter : IMqttServerAdapter, IDisposable | |||
{ | |||
public event EventHandler<MqttServerAdapterClientAcceptedEventArgs> ClientAccepted; | |||
@@ -347,13 +347,8 @@ namespace MQTTnet.Client | |||
{ | |||
while (!cancellationToken.IsCancellationRequested) | |||
{ | |||
await Task.Delay(_options.KeepAlivePeriod, cancellationToken).ConfigureAwait(false); | |||
if (cancellationToken.IsCancellationRequested) | |||
{ | |||
return; | |||
} | |||
await SendAndReceiveAsync<MqttPingRespPacket>(new MqttPingReqPacket()).ConfigureAwait(false); | |||
await Task.Delay(_options.KeepAlivePeriod, cancellationToken).ConfigureAwait(false); | |||
} | |||
} | |||
catch (OperationCanceledException) | |||
@@ -10,9 +10,9 @@ using MQTTnet.Protocol; | |||
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 MqttClientSession _session; | |||
private readonly IMqttNetLogger _logger; | |||
@@ -33,24 +33,24 @@ namespace MQTTnet.Server | |||
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); | |||
} | |||
private async Task SendPendingPublishPacketsAsync(IMqttChannelAdapter adapter, CancellationToken cancellationToken) | |||
private async Task SendQueuedPacketsAsync(IMqttChannelAdapter adapter, CancellationToken cancellationToken) | |||
{ | |||
try | |||
{ | |||
while (!cancellationToken.IsCancellationRequested) | |||
{ | |||
await SendPendingPublishPacketAsync(adapter, cancellationToken); | |||
await SendQueuedPacketAsync(adapter, cancellationToken); | |||
} | |||
} | |||
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 | |||
{ | |||
packet = _pendingPublishPackets.Take(cancellationToken); | |||
packet = _queue.Take(cancellationToken); | |||
await adapter.SendPacketsAsync(_options.DefaultCommunicationTimeout, cancellationToken, packet).ConfigureAwait(false); | |||
_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); | |||
} | |||
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(); | |||
} | |||
} | |||
public void Dispose() | |||
{ | |||
_queue?.Dispose(); | |||
} | |||
} | |||
} |
@@ -12,7 +12,7 @@ using MQTTnet.Serializer; | |||
namespace MQTTnet.Server | |||
{ | |||
public sealed class MqttClientSession | |||
public sealed class MqttClientSession : IDisposable | |||
{ | |||
private readonly Stopwatch _lastPacketReceivedTracker = Stopwatch.StartNew(); | |||
private readonly Stopwatch _lastNonKeepAlivePacketReceivedTracker = Stopwatch.StartNew(); | |||
@@ -57,22 +57,29 @@ namespace MQTTnet.Server | |||
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)); | |||
try | |||
{ | |||
_lastPacketReceivedTracker.Restart(); | |||
_lastNonKeepAlivePacketReceivedTracker.Restart(); | |||
var cancellationTokenSource = new CancellationTokenSource(); | |||
_willMessage = willMessage; | |||
_willMessage = connectPacket.WillMessage; | |||
_adapter = adapter; | |||
_cancellationTokenSource = cancellationTokenSource; | |||
_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); | |||
} | |||
catch (OperationCanceledException) | |||
@@ -136,6 +143,12 @@ namespace MQTTnet.Server | |||
_pendingMessagesQueue.Enqueue(publishPacket); | |||
} | |||
public void Dispose() | |||
{ | |||
_pendingMessagesQueue?.Dispose(); | |||
_cancellationTokenSource?.Dispose(); | |||
} | |||
private async Task ReceivePacketsAsync(IMqttChannelAdapter adapter, CancellationToken cancellationToken) | |||
{ | |||
try | |||
@@ -143,6 +156,14 @@ namespace MQTTnet.Server | |||
while (!cancellationToken.IsCancellationRequested) | |||
{ | |||
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); | |||
} | |||
} | |||
@@ -163,13 +184,6 @@ namespace MQTTnet.Server | |||
private Task ProcessReceivedPacketAsync(IMqttChannelAdapter adapter, MqttBasePacket packet, CancellationToken cancellationToken) | |||
{ | |||
_lastPacketReceivedTracker.Restart(); | |||
if (!(packet is MqttPingReqPacket)) | |||
{ | |||
_lastNonKeepAlivePacketReceivedTracker.Restart(); | |||
} | |||
if (packet is MqttPublishPacket publishPacket) | |||
{ | |||
return HandleIncomingPublishPacketAsync(adapter, publishPacket, cancellationToken); | |||
@@ -292,5 +306,42 @@ namespace MQTTnet.Server | |||
var response = new MqttPubCompPacket { PacketIdentifier = pubRelPacket.PacketIdentifier }; | |||
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 | |||
}); | |||
await clientSession.Session.RunAsync(connectPacket.WillMessage, clientAdapter).ConfigureAwait(false); | |||
await clientSession.Session.RunAsync(connectPacket, clientAdapter).ConfigureAwait(false); | |||
} | |||
catch (Exception exception) | |||
{ | |||
@@ -210,9 +210,11 @@ namespace MQTTnet.Server | |||
if (isSessionPresent) | |||
{ | |||
if (connectPacket.CleanSession) | |||
{ | |||
{ | |||
_sessions.Remove(connectPacket.ClientId); | |||
await clientSession.StopAsync().ConfigureAwait(false); | |||
clientSession.Dispose(); | |||
clientSession = null; | |||
_logger.Trace<MqttClientSessionsManager>("Stopped existing session of client '{0}'.", connectPacket.ClientId); | |||
@@ -71,7 +71,6 @@ namespace MQTTnet.Server | |||
} | |||
_logger.Info<MqttServer>("Started."); | |||
Started?.Invoke(this, new MqttServerStartedEventArgs()); | |||
} | |||
@@ -27,7 +27,9 @@ | |||
<TextBox x:Name="ClientId"></TextBox> | |||
<TextBlock>Clean session:</TextBlock> | |||
<CheckBox x:Name="CleanSession" IsChecked="True"></CheckBox> | |||
<TextBlock>Keep alive interval:</TextBlock> | |||
<TextBox x:Name="KeepAliveInterval" Text="5"></TextBox> | |||
<StackPanel Orientation="Horizontal"> | |||
<RadioButton x:Name="UseTcp" IsChecked="True" GroupName="connection">TCP</RadioButton> | |||
<RadioButton x:Name="UseWs" GroupName="connection">WS</RadioButton> | |||
@@ -103,7 +103,8 @@ namespace MQTTnet.TestApp.UniversalWindows | |||
}; | |||
options.CleanSession = CleanSession.IsChecked == true; | |||
options.KeepAlivePeriod = TimeSpan.FromSeconds(double.Parse(KeepAliveInterval.Text)); | |||
try | |||
{ | |||
if (_mqttClient != null) | |||