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