mirror of
https://github.com/dotnet/MQTTnet.git
synced 2026-10-19 02:53:06 +08:00
v4.0.0 (#1357)
This commit is contained in:
@@ -1,4 +1,8 @@
|
||||
using System;
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
@@ -6,135 +10,86 @@ using System.Threading.Tasks;
|
||||
using MQTTnet.Packets;
|
||||
using MQTTnet.Protocol;
|
||||
|
||||
namespace MQTTnet.Server.Internal
|
||||
namespace MQTTnet.Server
|
||||
{
|
||||
public sealed class MqttClientSubscriptionsManager
|
||||
public sealed class MqttClientSubscriptionsManager : IDisposable
|
||||
{
|
||||
// We use a reader writer lock because the subscriptions are only read most of the time.
|
||||
// Since writing is done by multiple threads (server API or connection thread), we cannot avoid locking
|
||||
// completely by swapping references etc.
|
||||
readonly ReaderWriterLockSlim _subscriptionsLock = new ReaderWriterLockSlim();
|
||||
readonly Dictionary<string, Subscription> _subscriptions = new Dictionary<string, Subscription>(4096);
|
||||
readonly MqttClientSession _clientSession;
|
||||
readonly IMqttServerOptions _options;
|
||||
readonly MqttServerEventDispatcher _eventDispatcher;
|
||||
readonly IMqttRetainedMessagesManager _retainedMessagesManager;
|
||||
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 SemaphoreSlim _subscriptionsLock = new SemaphoreSlim(1);
|
||||
readonly Dictionary<ulong, TopicHashMaskSubscriptions> _wildcardSubscriptionsByTopicHash = new Dictionary<ulong, TopicHashMaskSubscriptions>();
|
||||
|
||||
public MqttClientSubscriptionsManager(
|
||||
MqttClientSession clientSession,
|
||||
IMqttServerOptions serverOptions,
|
||||
MqttServerEventDispatcher eventDispatcher,
|
||||
IMqttRetainedMessagesManager retainedMessagesManager)
|
||||
MqttSession session,
|
||||
MqttServerEventContainer eventContainer,
|
||||
MqttRetainedMessagesManager retainedMessagesManager,
|
||||
ISubscriptionChangedNotification subscriptionChangedNotification)
|
||||
{
|
||||
_clientSession = clientSession ?? throw new ArgumentNullException(nameof(clientSession));
|
||||
_options = serverOptions ?? throw new ArgumentNullException(nameof(serverOptions));
|
||||
_eventDispatcher = eventDispatcher ?? throw new ArgumentNullException(nameof(eventDispatcher));
|
||||
_session = session ?? throw new ArgumentNullException(nameof(session));
|
||||
_eventContainer = eventContainer ?? throw new ArgumentNullException(nameof(eventContainer));
|
||||
_retainedMessagesManager = retainedMessagesManager ?? throw new ArgumentNullException(nameof(retainedMessagesManager));
|
||||
_subscriptionChangedNotification = subscriptionChangedNotification;
|
||||
}
|
||||
|
||||
public async Task<SubscribeResult> Subscribe(MqttSubscribePacket subscribePacket)
|
||||
public CheckSubscriptionsResult CheckSubscriptions(string topic, ulong topicHash, MqttQualityOfServiceLevel applicationMessageQoSLevel, string senderClientId)
|
||||
{
|
||||
if (subscribePacket == null) throw new ArgumentNullException(nameof(subscribePacket));
|
||||
var possibleSubscriptions = new List<MqttSubscription>();
|
||||
|
||||
var retainedApplicationMessages = await _retainedMessagesManager.GetMessagesAsync().ConfigureAwait(false);
|
||||
var result = new SubscribeResult();
|
||||
|
||||
// The topic filters are order by its QoS so that the higher QoS will win over a
|
||||
// lower one.
|
||||
foreach (var originalTopicFilter in subscribePacket.TopicFilters.OrderByDescending(f => f.QualityOfServiceLevel))
|
||||
{
|
||||
var interceptorContext = await InterceptSubscribe(originalTopicFilter).ConfigureAwait(false);
|
||||
var finalTopicFilter = interceptorContext?.TopicFilter ?? originalTopicFilter;
|
||||
var acceptSubscription = interceptorContext?.AcceptSubscription ?? true;
|
||||
var closeConnection = interceptorContext?.CloseConnection ?? false;
|
||||
|
||||
if (string.IsNullOrEmpty(finalTopicFilter.Topic) || !acceptSubscription)
|
||||
{
|
||||
result.ReturnCodes.Add(MqttSubscribeReturnCode.Failure);
|
||||
result.ReasonCodes.Add(MqttSubscribeReasonCode.UnspecifiedError);
|
||||
}
|
||||
else
|
||||
{
|
||||
result.ReturnCodes.Add(ConvertToSubscribeReturnCode(finalTopicFilter.QualityOfServiceLevel));
|
||||
result.ReasonCodes.Add(ConvertToSubscribeReasonCode(finalTopicFilter.QualityOfServiceLevel));
|
||||
}
|
||||
|
||||
if (closeConnection)
|
||||
{
|
||||
result.CloseConnection = true;
|
||||
}
|
||||
|
||||
if (!acceptSubscription || string.IsNullOrEmpty(finalTopicFilter.Topic))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var subscription = CreateSubscription(finalTopicFilter, subscribePacket);
|
||||
|
||||
await _eventDispatcher.SafeNotifyClientSubscribedTopicAsync(_clientSession.ClientId, finalTopicFilter).ConfigureAwait(false);
|
||||
|
||||
FilterRetainedApplicationMessages(retainedApplicationMessages, subscription, result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<List<MqttUnsubscribeReasonCode>> Unsubscribe(MqttUnsubscribePacket unsubscribePacket)
|
||||
{
|
||||
if (unsubscribePacket == null) throw new ArgumentNullException(nameof(unsubscribePacket));
|
||||
|
||||
var reasonCodes = new List<MqttUnsubscribeReasonCode>();
|
||||
|
||||
foreach (var topicFilter in unsubscribePacket.TopicFilters)
|
||||
{
|
||||
var interceptorContext = await InterceptUnsubscribe(topicFilter).ConfigureAwait(false);
|
||||
if (interceptorContext != null && !interceptorContext.AcceptUnsubscription)
|
||||
{
|
||||
reasonCodes.Add(MqttUnsubscribeReasonCode.ImplementationSpecificError);
|
||||
continue;
|
||||
}
|
||||
|
||||
_subscriptionsLock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
reasonCodes.Add(_subscriptions.Remove(topicFilter)
|
||||
? MqttUnsubscribeReasonCode.Success
|
||||
: MqttUnsubscribeReasonCode.NoSubscriptionExisted);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_subscriptionsLock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var topicFilter in unsubscribePacket.TopicFilters)
|
||||
{
|
||||
await _eventDispatcher.SafeNotifyClientUnsubscribedTopicAsync(_clientSession.ClientId, topicFilter).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return reasonCodes;
|
||||
}
|
||||
|
||||
public CheckSubscriptionsResult CheckSubscriptions(string topic, MqttQualityOfServiceLevel qosLevel, string senderClientId)
|
||||
{
|
||||
List<Subscription> subscriptions;
|
||||
_subscriptionsLock.EnterReadLock();
|
||||
// Check for possible subscriptions. They might have collisions but this is fine.
|
||||
_subscriptionsLock.Wait();
|
||||
try
|
||||
{
|
||||
subscriptions = _subscriptions.Values.ToList();
|
||||
if (_noWildcardSubscriptionsByTopicHash.TryGetValue(topicHash, out var noWildcardSubscriptions))
|
||||
{
|
||||
possibleSubscriptions.AddRange(noWildcardSubscriptions.ToList());
|
||||
}
|
||||
|
||||
foreach (var wcs in _wildcardSubscriptionsByTopicHash)
|
||||
{
|
||||
var wildcardSubscriptions = wcs.Value;
|
||||
var subscriptionHash = wcs.Key;
|
||||
var subscriptionHashMask = wildcardSubscriptions.HashMask;
|
||||
|
||||
if ((topicHash & subscriptionHashMask) == subscriptionHash)
|
||||
{
|
||||
possibleSubscriptions.AddRange(wildcardSubscriptions.Subscriptions.ToList());
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_subscriptionsLock.ExitReadLock();
|
||||
_subscriptionsLock.Release();
|
||||
}
|
||||
|
||||
// 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(senderClientId, _clientSession.ClientId);
|
||||
var senderIsReceiver = string.Equals(senderClientId, _session.Id);
|
||||
var maxQoSLevel = -1; // Not subscribed.
|
||||
|
||||
var qosLevels = new HashSet<MqttQualityOfServiceLevel>();
|
||||
var subscriptionIdentifiers = new HashSet<uint>();
|
||||
HashSet<uint> subscriptionIdentifiers = null;
|
||||
var retainAsPublished = false;
|
||||
|
||||
foreach (var subscription in subscriptions)
|
||||
foreach (var subscription in possibleSubscriptions)
|
||||
{
|
||||
if (subscription.NoLocal && senderIsReceiver)
|
||||
{
|
||||
@@ -142,177 +97,437 @@ namespace MQTTnet.Server.Internal
|
||||
continue;
|
||||
}
|
||||
|
||||
if (MqttTopicFilterComparer.Compare(topic, subscription.Topic) != MqttTopicFilterCompareResult.IsMatch)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (subscription.RetainAsPublished)
|
||||
{
|
||||
// This is a MQTTv5 feature!
|
||||
retainAsPublished = true;
|
||||
}
|
||||
|
||||
if (!MqttTopicFilterComparer.IsMatch(topic, subscription.Topic))
|
||||
if ((int)subscription.GrantedQualityOfServiceLevel > maxQoSLevel)
|
||||
{
|
||||
continue;
|
||||
maxQoSLevel = (int)subscription.GrantedQualityOfServiceLevel;
|
||||
}
|
||||
|
||||
qosLevels.Add(subscription.QualityOfServiceLevel);
|
||||
|
||||
if (subscription.Identifier > 0)
|
||||
{
|
||||
if (subscriptionIdentifiers == null)
|
||||
{
|
||||
subscriptionIdentifiers = new HashSet<uint>();
|
||||
}
|
||||
|
||||
subscriptionIdentifiers.Add(subscription.Identifier);
|
||||
}
|
||||
}
|
||||
|
||||
if (qosLevels.Count == 0)
|
||||
if (maxQoSLevel == -1)
|
||||
{
|
||||
return CheckSubscriptionsResult.NotSubscribed;
|
||||
}
|
||||
|
||||
return new CheckSubscriptionsResult
|
||||
var result = new CheckSubscriptionsResult
|
||||
{
|
||||
IsSubscribed = true,
|
||||
RetainAsPublished = retainAsPublished,
|
||||
SubscriptionIdentifiers = subscriptionIdentifiers.ToList(),
|
||||
QualityOfServiceLevel = GetEffectiveQoS(qosLevel, qosLevels)
|
||||
SubscriptionIdentifiers = subscriptionIdentifiers?.ToList() ?? EmptySubscriptionIdentifiers,
|
||||
|
||||
// Start with the same QoS as the publisher.
|
||||
QualityOfServiceLevel = applicationMessageQoSLevel
|
||||
};
|
||||
|
||||
// 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)applicationMessageQoSLevel)
|
||||
{
|
||||
result.QualityOfServiceLevel = (MqttQualityOfServiceLevel)maxQoSLevel;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Subscription CreateSubscription(MqttTopicFilter topicFilter, MqttSubscribePacket subscribePacket)
|
||||
public void Dispose()
|
||||
{
|
||||
var subscription = new Subscription
|
||||
_subscriptionsLock.Dispose();
|
||||
}
|
||||
|
||||
public async Task<SubscribeResult> Subscribe(MqttSubscribePacket subscribePacket, CancellationToken cancellationToken)
|
||||
{
|
||||
if (subscribePacket == null)
|
||||
{
|
||||
Topic = topicFilter.Topic,
|
||||
NoLocal = topicFilter.NoLocal,
|
||||
RetainHandling = topicFilter.RetainHandling,
|
||||
RetainAsPublished = topicFilter.RetainAsPublished,
|
||||
QualityOfServiceLevel = topicFilter.QualityOfServiceLevel,
|
||||
Identifier = subscribePacket.Properties?.SubscriptionIdentifier ?? 0
|
||||
throw new ArgumentNullException(nameof(subscribePacket));
|
||||
}
|
||||
|
||||
var retainedApplicationMessages = await _retainedMessagesManager.GetMessages().ConfigureAwait(false);
|
||||
var result = new SubscribeResult
|
||||
{
|
||||
ReasonCodes = new List<MqttSubscribeReasonCode>(subscribePacket.TopicFilters.Count)
|
||||
};
|
||||
|
||||
_subscriptionsLock.EnterWriteLock();
|
||||
var addedSubscriptions = new List<string>();
|
||||
|
||||
// The topic filters are order by its QoS so that the higher QoS will win over a
|
||||
// lower one.
|
||||
foreach (var originalTopicFilter in subscribePacket.TopicFilters.OrderByDescending(f => f.QualityOfServiceLevel))
|
||||
{
|
||||
var subscriptionEventArgs = await InterceptSubscribe(originalTopicFilter, cancellationToken).ConfigureAwait(false);
|
||||
var finalTopicFilter = subscriptionEventArgs.TopicFilter;
|
||||
var processSubscription = subscriptionEventArgs.ProcessSubscription && subscriptionEventArgs.Response.ReasonCode <= MqttSubscribeReasonCode.GrantedQoS2;
|
||||
|
||||
result.UserProperties = subscriptionEventArgs.UserProperties;
|
||||
result.ReasonString = subscriptionEventArgs.ReasonString;
|
||||
result.ReasonCodes.Add(subscriptionEventArgs.Response.ReasonCode);
|
||||
|
||||
if (subscriptionEventArgs.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(finalTopicFilter.Topic))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var createSubscriptionResult = CreateSubscription(finalTopicFilter, subscribePacket.SubscriptionIdentifier, subscriptionEventArgs.Response.ReasonCode);
|
||||
|
||||
addedSubscriptions.Add(finalTopicFilter.Topic);
|
||||
|
||||
if (_eventContainer.ClientSubscribedTopicEvent.HasHandlers)
|
||||
{
|
||||
var eventArgs = new ClientSubscribedTopicEventArgs
|
||||
{
|
||||
ClientId = _session.Id,
|
||||
TopicFilter = finalTopicFilter
|
||||
};
|
||||
|
||||
await _eventContainer.ClientSubscribedTopicEvent.InvokeAsync(eventArgs).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
FilterRetainedApplicationMessages(retainedApplicationMessages, createSubscriptionResult, result);
|
||||
}
|
||||
|
||||
_subscriptionChangedNotification?.OnSubscriptionsAdded(_session, addedSubscriptions);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<MqttUnsubscribeResult> Unsubscribe(MqttUnsubscribePacket unsubscribePacket, CancellationToken cancellationToken)
|
||||
{
|
||||
if (unsubscribePacket == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(unsubscribePacket));
|
||||
}
|
||||
|
||||
var result = new MqttUnsubscribeResult();
|
||||
|
||||
var removedSubscriptions = new List<string>();
|
||||
|
||||
await _subscriptionsLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
subscription.IsNewSubscription = !_subscriptions.ContainsKey(topicFilter.Topic);
|
||||
_subscriptions[topicFilter.Topic] = subscription;
|
||||
foreach (var topicFilter in unsubscribePacket.TopicFilters)
|
||||
{
|
||||
_subscriptions.TryGetValue(topicFilter, out var existingSubscription);
|
||||
|
||||
var interceptorContext = await InterceptUnsubscribe(topicFilter, existingSubscription, cancellationToken).ConfigureAwait(false);
|
||||
var acceptUnsubscription = interceptorContext.Response.ReasonCode == MqttUnsubscribeReasonCode.Success;
|
||||
|
||||
result.ReasonCodes.Add(interceptorContext.Response.ReasonCode);
|
||||
|
||||
if (interceptorContext.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 (!acceptUnsubscription)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (interceptorContext.ProcessUnsubscription)
|
||||
{
|
||||
_subscriptions.Remove(topicFilter);
|
||||
|
||||
// must remove subscription object from topic hash dictionary also
|
||||
|
||||
if (existingSubscription.TopicHasWildcard)
|
||||
{
|
||||
if (_wildcardSubscriptionsByTopicHash.TryGetValue(existingSubscription.TopicHash, out var subs))
|
||||
{
|
||||
subs.Subscriptions.Remove(existingSubscription);
|
||||
if (subs.Subscriptions.Count == 0)
|
||||
{
|
||||
_wildcardSubscriptionsByTopicHash.Remove(existingSubscription.TopicHash);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_noWildcardSubscriptionsByTopicHash.TryGetValue(existingSubscription.TopicHash, out var subs))
|
||||
{
|
||||
subs.Remove(existingSubscription);
|
||||
if (subs.Count == 0)
|
||||
{
|
||||
_noWildcardSubscriptionsByTopicHash.Remove(existingSubscription.TopicHash);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
removedSubscriptions.Add(topicFilter);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_subscriptionsLock.ExitWriteLock();
|
||||
_subscriptionsLock.Release();
|
||||
|
||||
if (_subscriptionChangedNotification != null)
|
||||
{
|
||||
_subscriptionChangedNotification.OnSubscriptionsRemoved(_session, removedSubscriptions);
|
||||
}
|
||||
}
|
||||
|
||||
return subscription;
|
||||
if (_eventContainer.ClientUnsubscribedTopicEvent.HasHandlers)
|
||||
{
|
||||
foreach (var topicFilter in unsubscribePacket.TopicFilters)
|
||||
{
|
||||
var eventArgs = new ClientUnsubscribedTopicEventArgs
|
||||
{
|
||||
ClientId = _session.Id,
|
||||
TopicFilter = topicFilter
|
||||
};
|
||||
|
||||
await _eventContainer.ClientUnsubscribedTopicEvent.InvokeAsync(eventArgs).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static void FilterRetainedApplicationMessages(IList<MqttApplicationMessage> retainedApplicationMessages, Subscription subscription, SubscribeResult subscribeResult)
|
||||
CreateSubscriptionResult CreateSubscription(MqttTopicFilter topicFilter, uint subscriptionIdentifier, MqttSubscribeReasonCode reasonCode)
|
||||
{
|
||||
for (var i = retainedApplicationMessages.Count - 1; i >= 0; i--)
|
||||
MqttQualityOfServiceLevel grantedQualityOfServiceLevel;
|
||||
|
||||
if (reasonCode == MqttSubscribeReasonCode.GrantedQoS0)
|
||||
{
|
||||
var retainedApplicationMessage = retainedApplicationMessages[i];
|
||||
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.Wait();
|
||||
try
|
||||
{
|
||||
MqttSubscription.CalculateTopicHash(topicFilter.Topic, out var topicHash, out var topicHashMask, 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 subs))
|
||||
{
|
||||
subs.Subscriptions.Remove(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
|
||||
if (hasWildcard)
|
||||
{
|
||||
if (!_wildcardSubscriptionsByTopicHash.TryGetValue(topicHash, out var subscriptions))
|
||||
{
|
||||
subscriptions = new TopicHashMaskSubscriptions(topicHashMask);
|
||||
_wildcardSubscriptionsByTopicHash.Add(topicHash, subscriptions);
|
||||
}
|
||||
|
||||
subscriptions.Subscriptions.Add(subscription);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!_noWildcardSubscriptionsByTopicHash.TryGetValue(topicHash, out var subscriptions))
|
||||
{
|
||||
subscriptions = new HashSet<MqttSubscription>();
|
||||
_noWildcardSubscriptionsByTopicHash.Add(topicHash, subscriptions);
|
||||
}
|
||||
|
||||
subscriptions.Add(subscription);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_subscriptionsLock.Release();
|
||||
}
|
||||
|
||||
return new CreateSubscriptionResult
|
||||
{
|
||||
IsNewSubscription = isNewSubscription,
|
||||
Subscription = subscription
|
||||
};
|
||||
}
|
||||
|
||||
static void FilterRetainedApplicationMessages(
|
||||
IList<MqttApplicationMessage> retainedApplicationMessages,
|
||||
CreateSubscriptionResult createSubscriptionResult,
|
||||
SubscribeResult subscribeResult)
|
||||
{
|
||||
for (var index = retainedApplicationMessages.Count - 1; index >= 0; index--)
|
||||
{
|
||||
var retainedApplicationMessage = retainedApplicationMessages[index];
|
||||
if (retainedApplicationMessage == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (subscription.RetainHandling == MqttRetainHandling.DoNotSendOnSubscribe)
|
||||
if (createSubscriptionResult.Subscription.RetainHandling == MqttRetainHandling.DoNotSendOnSubscribe)
|
||||
{
|
||||
// This is a MQTT V5+ feature.
|
||||
continue;
|
||||
}
|
||||
|
||||
if (subscription.RetainHandling == MqttRetainHandling.SendAtSubscribeIfNewSubscriptionOnly && !subscription.IsNewSubscription)
|
||||
if (createSubscriptionResult.Subscription.RetainHandling == MqttRetainHandling.SendAtSubscribeIfNewSubscriptionOnly && !createSubscriptionResult.IsNewSubscription)
|
||||
{
|
||||
// This is a MQTT V5+ feature.
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!MqttTopicFilterComparer.IsMatch(retainedApplicationMessage.Topic, subscription.Topic))
|
||||
if (MqttTopicFilterComparer.Compare(retainedApplicationMessage.Topic, createSubscriptionResult.Subscription.Topic) != MqttTopicFilterCompareResult.IsMatch)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var queuedApplicationMessage = new MqttQueuedApplicationMessage
|
||||
var retainedMessageMatch = new MqttRetainedMessageMatch
|
||||
{
|
||||
ApplicationMessage = retainedApplicationMessage,
|
||||
IsRetainedMessage = true,
|
||||
SubscriptionQualityOfServiceLevel = subscription.QualityOfServiceLevel
|
||||
SubscriptionQualityOfServiceLevel = createSubscriptionResult.Subscription.GrantedQualityOfServiceLevel
|
||||
};
|
||||
|
||||
if (subscription.Identifier > 0)
|
||||
if (subscribeResult.RetainedMessages == null)
|
||||
{
|
||||
queuedApplicationMessage.SubscriptionIdentifiers = new List<uint> { subscription.Identifier };
|
||||
subscribeResult.RetainedMessages = new List<MqttRetainedMessageMatch>();
|
||||
}
|
||||
|
||||
subscribeResult.RetainedApplicationMessages.Add(queuedApplicationMessage);
|
||||
subscribeResult.RetainedMessages.Add(retainedMessageMatch);
|
||||
|
||||
retainedApplicationMessages[i] = null;
|
||||
// Clear the retained message from the list because the client should receive every message only
|
||||
// one time even if multiple subscriptions affect them.
|
||||
retainedApplicationMessages[index] = null;
|
||||
}
|
||||
}
|
||||
|
||||
static MqttSubscribeReturnCode ConvertToSubscribeReturnCode(MqttQualityOfServiceLevel qualityOfServiceLevel)
|
||||
async Task<InterceptingSubscriptionEventArgs> InterceptSubscribe(MqttTopicFilter topicFilter, CancellationToken cancellationToken)
|
||||
{
|
||||
return (MqttSubscribeReturnCode)(int)qualityOfServiceLevel;
|
||||
}
|
||||
|
||||
static MqttSubscribeReasonCode ConvertToSubscribeReasonCode(MqttQualityOfServiceLevel qualityOfServiceLevel)
|
||||
{
|
||||
return (MqttSubscribeReasonCode)(int)qualityOfServiceLevel;
|
||||
}
|
||||
|
||||
async Task<MqttSubscriptionInterceptorContext> InterceptSubscribe(MqttTopicFilter topicFilter)
|
||||
{
|
||||
var interceptor = _options.SubscriptionInterceptor;
|
||||
if (interceptor == null)
|
||||
var eventArgs = new InterceptingSubscriptionEventArgs
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var context = new MqttSubscriptionInterceptorContext
|
||||
{
|
||||
ClientId = _clientSession.ClientId,
|
||||
ClientId = _session.Id,
|
||||
TopicFilter = topicFilter,
|
||||
SessionItems = _clientSession.Items
|
||||
SessionItems = _session.Items,
|
||||
Session = new MqttSessionStatus(_session),
|
||||
CancellationToken = cancellationToken
|
||||
};
|
||||
|
||||
await interceptor.InterceptSubscriptionAsync(context).ConfigureAwait(false);
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
async Task<MqttUnsubscriptionInterceptorContext> InterceptUnsubscribe(string topicFilter)
|
||||
{
|
||||
var interceptor = _options.UnsubscriptionInterceptor;
|
||||
if (interceptor == null)
|
||||
if (topicFilter.QualityOfServiceLevel == MqttQualityOfServiceLevel.AtMostOnce)
|
||||
{
|
||||
return null;
|
||||
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;
|
||||
}
|
||||
|
||||
var context = new MqttUnsubscriptionInterceptorContext
|
||||
if (topicFilter.Topic.StartsWith("$share/"))
|
||||
{
|
||||
ClientId = _clientSession.ClientId,
|
||||
Topic = topicFilter,
|
||||
SessionItems = _clientSession.Items
|
||||
};
|
||||
|
||||
await interceptor.InterceptUnsubscriptionAsync(context).ConfigureAwait(false);
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
static MqttQualityOfServiceLevel GetEffectiveQoS(MqttQualityOfServiceLevel qosLevel, HashSet<MqttQualityOfServiceLevel> subscribedQoSLevels)
|
||||
{
|
||||
MqttQualityOfServiceLevel effectiveQoS;
|
||||
if (subscribedQoSLevels.Contains(qosLevel))
|
||||
{
|
||||
effectiveQoS = qosLevel;
|
||||
}
|
||||
else if (subscribedQoSLevels.Count == 1)
|
||||
{
|
||||
effectiveQoS = subscribedQoSLevels.First();
|
||||
eventArgs.Response.ReasonCode = MqttSubscribeReasonCode.SharedSubscriptionsNotSupported;
|
||||
}
|
||||
else
|
||||
{
|
||||
effectiveQoS = subscribedQoSLevels.Max();
|
||||
await _eventContainer.InterceptingSubscriptionEvent.InvokeAsync(eventArgs).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return effectiveQoS;
|
||||
return eventArgs;
|
||||
}
|
||||
|
||||
async Task<InterceptingUnsubscriptionEventArgs> InterceptUnsubscribe(string topicFilter, MqttSubscription mqttSubscription, CancellationToken cancellationToken)
|
||||
{
|
||||
var clientUnsubscribingTopicEventArgs = new InterceptingUnsubscriptionEventArgs
|
||||
{
|
||||
ClientId = _session.Id,
|
||||
Topic = topicFilter,
|
||||
SessionItems = _session.Items,
|
||||
CancellationToken = cancellationToken
|
||||
};
|
||||
|
||||
if (mqttSubscription == null)
|
||||
{
|
||||
clientUnsubscribingTopicEventArgs.Response.ReasonCode = MqttUnsubscribeReasonCode.NoSubscriptionExisted;
|
||||
}
|
||||
else
|
||||
{
|
||||
clientUnsubscribingTopicEventArgs.Response.ReasonCode = MqttUnsubscribeReasonCode.Success;
|
||||
}
|
||||
|
||||
await _eventContainer.InterceptingUnsubscriptionEvent.InvokeAsync(clientUnsubscribingTopicEventArgs).ConfigureAwait(false);
|
||||
|
||||
return clientUnsubscribingTopicEventArgs;
|
||||
}
|
||||
|
||||
public sealed class CreateSubscriptionResult
|
||||
{
|
||||
public bool IsNewSubscription { get; set; }
|
||||
|
||||
public MqttSubscription Subscription { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user