mirror of
https://github.com/dotnet/MQTTnet.git
synced 2026-10-20 11:45:43 +08:00
1546 possible bug in server repeatedly sending the same message to client subscribed with cleansession=false (#1569)
This commit is contained in:
@@ -86,7 +86,7 @@ namespace MQTTnet.Adapter
|
||||
await Task.WhenAny(connectTask, timeout.Task).ConfigureAwait(false);
|
||||
if (timeout.Task.IsCompleted && !connectTask.IsCompleted)
|
||||
{
|
||||
throw new OperationCanceledException("MQTT connect cancelled.", cancellationToken);
|
||||
throw new OperationCanceledException("MQTT connect canceled.", cancellationToken);
|
||||
}
|
||||
|
||||
// Make sure that the exception from the connect task gets thrown.
|
||||
@@ -317,7 +317,7 @@ namespace MQTTnet.Adapter
|
||||
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return ReadFixedHeaderResult.Cancelled;
|
||||
return ReadFixedHeaderResult.Canceled;
|
||||
}
|
||||
|
||||
if (bytesRead == 0)
|
||||
|
||||
@@ -14,26 +14,6 @@ namespace MQTTnet.Client
|
||||
{
|
||||
public static class MqttClientExtensions
|
||||
{
|
||||
public static async Task<bool> TryPingAsync(this IMqttClient client, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (client == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(client));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await client.PingAsync(cancellationToken).ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore errors.
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static Task DisconnectAsync(this IMqttClient client, MqttClientDisconnectReason reason = MqttClientDisconnectReason.NormalDisconnection, string reasonString = null)
|
||||
{
|
||||
if (client == null)
|
||||
@@ -89,6 +69,17 @@ namespace MQTTnet.Client
|
||||
return mqttClient.PublishBinaryAsync(topic, payloadBuffer, qualityOfServiceLevel, retain, cancellationToken);
|
||||
}
|
||||
|
||||
public static Task ReconnectAsync(this IMqttClient client, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (client.Options == null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The MQTT client was not connected before. A reconnect is only permitted when the client was already connected or at least tried to.");
|
||||
}
|
||||
|
||||
return client.ConnectAsync(client.Options, cancellationToken);
|
||||
}
|
||||
|
||||
public static Task SendExtendedAuthenticationExchangeDataAsync(this IMqttClient client, MqttExtendedAuthenticationExchangeData data)
|
||||
{
|
||||
if (client == null)
|
||||
@@ -137,6 +128,26 @@ namespace MQTTnet.Client
|
||||
return mqttClient.SubscribeAsync(subscribeOptions, cancellationToken);
|
||||
}
|
||||
|
||||
public static async Task<bool> TryPingAsync(this IMqttClient client, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (client == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(client));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await client.PingAsync(cancellationToken).ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore errors.
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static Task<MqttClientUnsubscribeResult> UnsubscribeAsync(this IMqttClient mqttClient, string topic, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (mqttClient == null)
|
||||
|
||||
@@ -6,9 +6,9 @@ namespace MQTTnet.Formatter
|
||||
{
|
||||
public struct ReadFixedHeaderResult
|
||||
{
|
||||
public static ReadFixedHeaderResult Cancelled { get; } = new ReadFixedHeaderResult
|
||||
public static ReadFixedHeaderResult Canceled { get; } = new ReadFixedHeaderResult
|
||||
{
|
||||
IsCancelled = true
|
||||
IsCanceled = true
|
||||
};
|
||||
|
||||
public static ReadFixedHeaderResult ConnectionClosed { get; } = new ReadFixedHeaderResult
|
||||
@@ -16,7 +16,7 @@ namespace MQTTnet.Formatter
|
||||
IsConnectionClosed = true
|
||||
};
|
||||
|
||||
public bool IsCancelled { get; set; }
|
||||
public bool IsCanceled { get; set; }
|
||||
|
||||
public bool IsConnectionClosed { get; set; }
|
||||
|
||||
|
||||
@@ -10,40 +10,36 @@ namespace MQTTnet.Internal
|
||||
{
|
||||
public sealed class MqttPacketBusItem
|
||||
{
|
||||
readonly AsyncTaskCompletionSource<int> _promise = new AsyncTaskCompletionSource<int>();
|
||||
|
||||
readonly AsyncTaskCompletionSource<bool> _promise = new AsyncTaskCompletionSource<bool>();
|
||||
|
||||
public MqttPacketBusItem(MqttPacket packet)
|
||||
{
|
||||
Packet = packet ?? throw new ArgumentNullException(nameof(packet));
|
||||
}
|
||||
|
||||
|
||||
public event EventHandler Completed;
|
||||
|
||||
public MqttPacket Packet { get; }
|
||||
|
||||
public event EventHandler Delivered;
|
||||
|
||||
public Task WaitForDeliveryAsync()
|
||||
{
|
||||
return _promise.Task;
|
||||
}
|
||||
|
||||
public void MarkAsDelivered()
|
||||
{
|
||||
if (_promise.TrySetResult(0))
|
||||
{
|
||||
Delivered?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
public void MarkAsFailed(Exception exception)
|
||||
{
|
||||
if (exception == null) throw new ArgumentNullException(nameof(exception));
|
||||
|
||||
_promise.TrySetException(exception);
|
||||
}
|
||||
|
||||
public void MarkAsCancelled()
|
||||
public void Cancel()
|
||||
{
|
||||
_promise.TrySetCanceled();
|
||||
}
|
||||
|
||||
public void Complete()
|
||||
{
|
||||
_promise.TrySetResult(true);
|
||||
Completed?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
public void Fail(Exception exception)
|
||||
{
|
||||
_promise.TrySetException(exception);
|
||||
}
|
||||
|
||||
public Task WaitAsync()
|
||||
{
|
||||
return _promise.Task;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -507,15 +507,15 @@ namespace MQTTnet.Server
|
||||
try
|
||||
{
|
||||
await SendPacketAsync(packetBusItem.Packet, cancellationToken).ConfigureAwait(false);
|
||||
packetBusItem.MarkAsDelivered();
|
||||
packetBusItem.Complete();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
packetBusItem.MarkAsCancelled();
|
||||
packetBusItem.Cancel();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
packetBusItem.MarkAsFailed(exception);
|
||||
packetBusItem.Fail(exception);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -563,7 +563,7 @@ namespace MQTTnet.Server
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// This can happen when connections are created and dropped very quickly.
|
||||
// It is not an issue if the cancellation token cannot be cancelled multiple times.
|
||||
// It is not an issue if the cancellation token cannot be canceled multiple times.
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
@@ -25,7 +24,8 @@ namespace MQTTnet.Server
|
||||
readonly MqttServerOptions _serverOptions;
|
||||
readonly MqttClientSubscriptionsManager _subscriptionsManager;
|
||||
|
||||
readonly ConcurrentDictionary<ushort, MqttPublishPacket> _unacknowledgedPublishPackets = new ConcurrentDictionary<ushort, MqttPublishPacket>();
|
||||
// Do not use a dictionary in order to keep the ordering of the messages.
|
||||
readonly List<MqttPublishPacket> _unacknowledgedPublishPackets = new List<MqttPublishPacket>();
|
||||
|
||||
// Bookkeeping to know if this is a subscribing client; lazy initialize later.
|
||||
HashSet<string> _subscribedTopics;
|
||||
@@ -72,7 +72,14 @@ namespace MQTTnet.Server
|
||||
|
||||
public MqttPublishPacket AcknowledgePublishPacket(ushort packetIdentifier)
|
||||
{
|
||||
_unacknowledgedPublishPackets.TryRemove(packetIdentifier, out var publishPacket);
|
||||
MqttPublishPacket publishPacket;
|
||||
|
||||
lock (_unacknowledgedPublishPackets)
|
||||
{
|
||||
publishPacket = _unacknowledgedPublishPackets.FirstOrDefault(p => p.PacketIdentifier.Equals(packetIdentifier));
|
||||
_unacknowledgedPublishPackets.Remove(publishPacket);
|
||||
}
|
||||
|
||||
return publishPacket;
|
||||
}
|
||||
|
||||
@@ -130,7 +137,10 @@ namespace MQTTnet.Server
|
||||
{
|
||||
publishPacket.PacketIdentifier = _packetIdentifierProvider.GetNextPacketIdentifier();
|
||||
|
||||
_unacknowledgedPublishPackets[publishPacket.PacketIdentifier] = publishPacket;
|
||||
lock (_unacknowledgedPublishPackets)
|
||||
{
|
||||
_unacknowledgedPublishPackets.Add(publishPacket);
|
||||
}
|
||||
}
|
||||
|
||||
_packetBus.EnqueueItem(packetBusItem, MqttPacketBusPartition.Data);
|
||||
@@ -145,8 +155,10 @@ namespace MQTTnet.Server
|
||||
{
|
||||
// This will only return the matching PUBLISH packet but does not remove it.
|
||||
// This is required for QoS 2.
|
||||
_unacknowledgedPublishPackets.TryGetValue(packetIdentifier, out var publishPacket);
|
||||
return publishPacket;
|
||||
lock (_unacknowledgedPublishPackets)
|
||||
{
|
||||
return _unacknowledgedPublishPackets.FirstOrDefault(p => p.PacketIdentifier.Equals(packetIdentifier));
|
||||
}
|
||||
}
|
||||
|
||||
public void Recover()
|
||||
@@ -154,9 +166,33 @@ namespace MQTTnet.Server
|
||||
// TODO: Keep the bus and only insert pending items again.
|
||||
// TODO: Check if packet identifier must be restarted or not.
|
||||
// TODO: Recover package identifier.
|
||||
|
||||
/*
|
||||
The Session state in the Client consists of:
|
||||
· QoS 1 and QoS 2 messages which have been sent to the Server, but have not been completely acknowledged.
|
||||
· QoS 2 messages which have been received from the Server, but have not been completely acknowledged.
|
||||
|
||||
The Session state in the Server consists of:
|
||||
· The existence of a Session, even if the rest of the Session state is empty.
|
||||
· The Client’s subscriptions.
|
||||
· QoS 1 and QoS 2 messages which have been sent to the Client, but have not been completely acknowledged.
|
||||
· QoS 1 and QoS 2 messages pending transmission to the Client.
|
||||
· QoS 2 messages which have been received from the Client, but have not been completely acknowledged.
|
||||
· Optionally, QoS 0 messages pending transmission to the Client.
|
||||
*/
|
||||
|
||||
// Create a copy of all currently unacknowledged publish packets and clear the storage.
|
||||
// We must re-enqueue them in order to trigger other code.
|
||||
List<MqttPublishPacket> unacknowledgedPublishPackets;
|
||||
lock (_unacknowledgedPublishPackets)
|
||||
{
|
||||
unacknowledgedPublishPackets = _unacknowledgedPublishPackets.ToList();
|
||||
_unacknowledgedPublishPackets.Clear();
|
||||
}
|
||||
|
||||
_packetBus.Clear();
|
||||
|
||||
foreach (var publishPacket in _unacknowledgedPublishPackets.Values.ToList())
|
||||
foreach (var publishPacket in unacknowledgedPublishPackets)
|
||||
{
|
||||
EnqueueDataPacket(new MqttPacketBusItem(publishPacket));
|
||||
}
|
||||
|
||||
@@ -3,29 +3,89 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using MQTTnet.Internal;
|
||||
using MQTTnet.Packets;
|
||||
using MQTTnet.Protocol;
|
||||
|
||||
namespace MQTTnet.Server
|
||||
{
|
||||
public static class MqttServerExtensions
|
||||
{
|
||||
public static Task InjectApplicationMessage(
|
||||
this MqttServer server,
|
||||
string topic,
|
||||
string payload = null,
|
||||
MqttQualityOfServiceLevel qualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce,
|
||||
bool retain = false)
|
||||
{
|
||||
if (server == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(server));
|
||||
}
|
||||
|
||||
if (topic == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(topic));
|
||||
}
|
||||
|
||||
var payloadBuffer = EmptyBuffer.Array;
|
||||
if (payload is string stringPayload)
|
||||
{
|
||||
payloadBuffer = Encoding.UTF8.GetBytes(stringPayload);
|
||||
}
|
||||
|
||||
return server.InjectApplicationMessage(
|
||||
new InjectedMqttApplicationMessage(
|
||||
new MqttApplicationMessage
|
||||
{
|
||||
Topic = topic,
|
||||
Payload = payloadBuffer,
|
||||
QualityOfServiceLevel = qualityOfServiceLevel,
|
||||
Retain = retain
|
||||
}));
|
||||
}
|
||||
|
||||
public static Task SubscribeAsync(this MqttServer server, string clientId, params MqttTopicFilter[] topicFilters)
|
||||
{
|
||||
if (server == null) throw new ArgumentNullException(nameof(server));
|
||||
if (clientId == null) throw new ArgumentNullException(nameof(clientId));
|
||||
if (topicFilters == null) throw new ArgumentNullException(nameof(topicFilters));
|
||||
if (server == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(server));
|
||||
}
|
||||
|
||||
if (clientId == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(clientId));
|
||||
}
|
||||
|
||||
if (topicFilters == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(topicFilters));
|
||||
}
|
||||
|
||||
return server.SubscribeAsync(clientId, topicFilters);
|
||||
}
|
||||
|
||||
|
||||
public static Task SubscribeAsync(this MqttServer server, string clientId, string topic)
|
||||
{
|
||||
if (server == null) throw new ArgumentNullException(nameof(server));
|
||||
if (clientId == null) throw new ArgumentNullException(nameof(clientId));
|
||||
if (topic == null) throw new ArgumentNullException(nameof(topic));
|
||||
if (server == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(server));
|
||||
}
|
||||
|
||||
return server.SubscribeAsync(clientId, new MqttTopicFilterBuilder().WithTopic(topic).Build());
|
||||
if (clientId == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(clientId));
|
||||
}
|
||||
|
||||
if (topic == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(topic));
|
||||
}
|
||||
|
||||
var topicFilters = new MqttTopicFilterBuilder().WithTopic(topic).Build();
|
||||
return server.SubscribeAsync(clientId, topicFilters);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,7 @@ namespace MQTTnet.Server
|
||||
var packetBusItem = new MqttPacketBusItem(publishPacketFactory.Create(applicationMessage));
|
||||
_session.EnqueueDataPacket(packetBusItem);
|
||||
|
||||
return packetBusItem.WaitForDeliveryAsync();
|
||||
return packetBusItem.WaitAsync();
|
||||
}
|
||||
|
||||
public Task DeleteAsync()
|
||||
|
||||
Reference in New Issue
Block a user