You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
366 lines
14 KiB
C#
366 lines
14 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
|
|
namespace uPLibrary.Networking.M2Mqtt.Managers
|
|
{
|
|
/// <summary>
|
|
/// Manager for topics and subscribers
|
|
/// </summary>
|
|
public class MqttSubscriberManager
|
|
{
|
|
#region Constants ...
|
|
|
|
// topic wildcards '+' and '#'
|
|
private const string PLUS_WILDCARD = "+";
|
|
private const string SHARP_WILDCARD = "#";
|
|
|
|
// replace for wildcards '+' and '#' for using regular expression on topic match
|
|
private const string PLUS_WILDCARD_REPLACE = @"[^/]+";
|
|
private const string SHARP_WILDCARD_REPLACE = @".*";
|
|
|
|
#endregion
|
|
|
|
// MQTT subscription comparer
|
|
private MqttSubscriptionComparer comparer;
|
|
|
|
// subscribers list for each topic
|
|
private Dictionary<string, List<MqttSubscription>> subscribers;
|
|
|
|
/// <summary>
|
|
/// Constructor
|
|
/// </summary>
|
|
public MqttSubscriberManager()
|
|
{
|
|
this.subscribers = new Dictionary<string, List<MqttSubscription>>();
|
|
this.comparer = new MqttSubscriptionComparer(MqttSubscriptionComparer.MqttSubscriptionComparerType.OnClientId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add a subscriber for a topic
|
|
/// </summary>
|
|
/// <param name="topic">Topic for subscription</param>
|
|
/// <param name="qosLevel">QoS level for the topic subscription</param>
|
|
/// <param name="client">Client to subscribe</param>
|
|
public void Subscribe(string topic, byte qosLevel, MqttClient client)
|
|
{
|
|
string topicReplaced = topic.Replace(PLUS_WILDCARD, PLUS_WILDCARD_REPLACE).Replace(SHARP_WILDCARD, SHARP_WILDCARD_REPLACE);
|
|
|
|
lock (this.subscribers)
|
|
{
|
|
// if the topic doesn't exist
|
|
if (!this.subscribers.ContainsKey(topicReplaced))
|
|
{
|
|
// create a new empty subscription list for the topic
|
|
List<MqttSubscription> list = new List<MqttSubscription>();
|
|
this.subscribers.Add(topicReplaced, list);
|
|
}
|
|
|
|
// query for check client already subscribed
|
|
var query = from s in this.subscribers[topicReplaced]
|
|
where s.ClientId == client.ClientId
|
|
select s;
|
|
|
|
// if the client isn't already subscribed to the topic
|
|
if (query.Count() == 0)
|
|
{
|
|
MqttSubscription subscription = new MqttSubscription()
|
|
{
|
|
ClientId = client.ClientId,
|
|
Topic = topicReplaced,
|
|
QosLevel = qosLevel,
|
|
Client = client
|
|
};
|
|
// add subscription to the list for the topic
|
|
this.subscribers[topicReplaced].Add(subscription);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Remove a subscriber for a topic
|
|
/// </summary>
|
|
/// <param name="topic">Topic for unsubscription</param>
|
|
/// <param name="client">Client to unsubscribe</param>
|
|
public void Unsubscribe(string topic, MqttClient client)
|
|
{
|
|
string topicReplaced = topic.Replace(PLUS_WILDCARD, PLUS_WILDCARD_REPLACE).Replace(SHARP_WILDCARD, SHARP_WILDCARD_REPLACE);
|
|
|
|
lock (this.subscribers)
|
|
{
|
|
// if the topic exists
|
|
if (this.subscribers.ContainsKey(topicReplaced))
|
|
{
|
|
// query for check client subscribed
|
|
var query = from s in this.subscribers[topicReplaced]
|
|
where s.ClientId == client.ClientId
|
|
select s;
|
|
|
|
// if the client is subscribed for the topic
|
|
if (query.Count() > 0)
|
|
{
|
|
MqttSubscription subscription = query.First();
|
|
|
|
// remove subscription from the list for the topic
|
|
this.subscribers[topicReplaced].Remove(subscription);
|
|
// dispose subscription
|
|
subscription.Dispose();
|
|
|
|
// remove topic if there aren't subscribers
|
|
if (this.subscribers[topicReplaced].Count == 0)
|
|
this.subscribers.Remove(topicReplaced);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Remove a subscriber for all topics
|
|
/// </summary>
|
|
/// <param name="client">Client to unsubscribe</param>
|
|
public void Unsubscribe(MqttClient client)
|
|
{
|
|
lock (this.subscribers)
|
|
{
|
|
List<string> topicToRemove = new List<string>();
|
|
|
|
foreach (string topic in this.subscribers.Keys)
|
|
{
|
|
// query for check client subscribed
|
|
var query = from s in this.subscribers[topic]
|
|
where s.ClientId == client.ClientId
|
|
select s;
|
|
|
|
// if the client is subscribed for the topic
|
|
if (query.Count() > 0)
|
|
{
|
|
MqttSubscription subscription = query.First();
|
|
|
|
// remove subscription from the list for the topic
|
|
this.subscribers[topic].Remove(subscription);
|
|
// dispose subscription
|
|
subscription.Dispose();
|
|
|
|
// add topic to remove list if there aren't subscribers
|
|
if (this.subscribers[topic].Count == 0)
|
|
topicToRemove.Add(topic);
|
|
}
|
|
}
|
|
|
|
// remove topic without subscribers
|
|
// loop needed to avoid exception on modify collection inside previous loop
|
|
foreach (string topic in topicToRemove)
|
|
this.subscribers.Remove(topic);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get subscription list for a specified topic and QoS Level
|
|
/// </summary>
|
|
/// <param name="topic">Topic to get subscription list</param>
|
|
/// <param name="qosLevel">QoS level requested</param>
|
|
/// <returns>Subscription list</returns>
|
|
public List<MqttSubscription> GetSubscriptions(string topic, byte qosLevel)
|
|
{
|
|
lock (this.subscribers)
|
|
{
|
|
var query = from ss in this.subscribers
|
|
where (new Regex(ss.Key)).IsMatch(topic) // check for topics based also on wildcard with regex
|
|
from s in this.subscribers[ss.Key]
|
|
where s.QosLevel == qosLevel // check for subscriber only with a specified QoS level granted
|
|
select s;
|
|
|
|
// use comparer for multiple subscriptions that overlap (e.g. /test/# and /test/+/foo)
|
|
// If a client is subscribed to multiple subscriptions with topics that overlap
|
|
// it has more entries into subscriptions list but broker sends only one message
|
|
this.comparer.Type = MqttSubscriptionComparer.MqttSubscriptionComparerType.OnClientId;
|
|
return query.Distinct(comparer).ToList();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get a subscription for a specified topic and client
|
|
/// </summary>
|
|
/// <param name="topic">Topic to get subscription</param>
|
|
/// <param name="clientId">Client Id to get subscription</param>
|
|
/// <returns>Subscription list</returns>
|
|
public MqttSubscription GetSubscription(string topic, string clientId)
|
|
{
|
|
lock (this.subscribers)
|
|
{
|
|
var query = from ss in this.subscribers
|
|
where (new Regex(ss.Key)).IsMatch(topic) // check for topics based also on wildcard with regex
|
|
from s in this.subscribers[ss.Key]
|
|
where s.ClientId == clientId // check for subscriber only with a specified Client Id
|
|
select s;
|
|
|
|
// use comparer for multiple subscriptions that overlap (e.g. /test/# and /test/+/foo)
|
|
// If a client is subscribed to multiple subscriptions with topics that overlap
|
|
// it has more entries into subscriptions list but broker sends only one message
|
|
this.comparer.Type = MqttSubscriptionComparer.MqttSubscriptionComparerType.OnClientId;
|
|
return query.Distinct(comparer).FirstOrDefault();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get subscription list for a specified topic
|
|
/// </summary>
|
|
/// <param name="topic">Topic to get subscription list</param>
|
|
/// <returns>Subscription list</returns>
|
|
public List<MqttSubscription> GetSubscriptionsByTopic(string topic)
|
|
{
|
|
lock (this.subscribers)
|
|
{
|
|
var query = from ss in this.subscribers
|
|
where (new Regex(ss.Key)).IsMatch(topic) // check for topics based also on wildcard with regex
|
|
from s in this.subscribers[ss.Key]
|
|
select s;
|
|
|
|
// use comparer for multiple subscriptions that overlap (e.g. /test/# and /test/+/foo)
|
|
// If a client is subscribed to multiple subscriptions with topics that overlap
|
|
// it has more entries into subscriptions list but broker sends only one message
|
|
this.comparer.Type = MqttSubscriptionComparer.MqttSubscriptionComparerType.OnClientId;
|
|
return query.Distinct(comparer).ToList();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get subscription list for a specified client
|
|
/// </summary>
|
|
/// <param name="clientId">Client Id to get subscription list</param>
|
|
/// <returns>Subscription lis</returns>
|
|
public List<MqttSubscription> GetSubscriptionsByClient(string clientId)
|
|
{
|
|
lock (this.subscribers)
|
|
{
|
|
var query = from ss in this.subscribers
|
|
from s in this.subscribers[ss.Key]
|
|
where s.ClientId == clientId
|
|
select s;
|
|
|
|
// use comparer for multiple subscriptions that overlap (e.g. /test/# and /test/+/foo)
|
|
// If a client is subscribed to multiple subscriptions with topics that overlap
|
|
// it has more entries into subscriptions list but broker sends only one message
|
|
//this.comparer.Type = MqttSubscriptionComparer.MqttSubscriptionComparerType.OnTopic;
|
|
//return query.Distinct(comparer).ToList();
|
|
|
|
// I need all subscriptions, also overlapped (used to save session)
|
|
return query.ToList();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// MQTT subscription comparer
|
|
/// </summary>
|
|
public class MqttSubscriptionComparer : IEqualityComparer<MqttSubscription>
|
|
{
|
|
/// <summary>
|
|
/// MQTT subscription comparer type
|
|
/// </summary>
|
|
public MqttSubscriptionComparerType Type { get; set; }
|
|
|
|
/// <summary>
|
|
/// Constructor
|
|
/// </summary>
|
|
/// <param name="type">MQTT subscription comparer type</param>
|
|
public MqttSubscriptionComparer(MqttSubscriptionComparerType type)
|
|
{
|
|
this.Type = type;
|
|
}
|
|
|
|
public bool Equals(MqttSubscription x, MqttSubscription y)
|
|
{
|
|
if (this.Type == MqttSubscriptionComparerType.OnClientId)
|
|
return x.ClientId.Equals(y.ClientId);
|
|
else if (this.Type == MqttSubscriptionComparerType.OnTopic)
|
|
return (new Regex(x.Topic)).IsMatch(y.Topic);
|
|
else
|
|
return false;
|
|
}
|
|
public int GetHashCode(MqttSubscription obj)
|
|
{
|
|
if (this.Type == MqttSubscriptionComparerType.OnClientId)
|
|
return obj.ClientId.GetHashCode();
|
|
else if (this.Type == MqttSubscriptionComparerType.OnTopic)
|
|
return obj.Topic.GetHashCode();
|
|
else
|
|
return 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// MQTT subscription comparer type
|
|
/// </summary>
|
|
public enum MqttSubscriptionComparerType
|
|
{
|
|
OnClientId,
|
|
OnTopic
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// MQTT subscription
|
|
/// </summary>
|
|
public class MqttSubscription
|
|
{
|
|
/// <summary>
|
|
/// Client Id
|
|
/// </summary>
|
|
public string ClientId { get; set; }
|
|
|
|
/// <summary>
|
|
/// Topic of subscription
|
|
/// </summary>
|
|
public string Topic { get; set; }
|
|
|
|
/// <summary>
|
|
/// QoS level granted for the subscription
|
|
/// </summary>
|
|
public byte QosLevel { get; set; }
|
|
|
|
/// <summary>
|
|
/// Client related to the subscription
|
|
/// </summary>
|
|
public MqttClient Client { get; set; }
|
|
|
|
/// <summary>
|
|
/// Constructor
|
|
/// </summary>
|
|
public MqttSubscription()
|
|
{
|
|
this.ClientId = null;
|
|
this.Topic = null;
|
|
this.QosLevel = 0;
|
|
this.Client = null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Constructor
|
|
/// </summary>
|
|
/// <param name="clientId">Client Id of the subscription</param>
|
|
/// <param name="topic">Topic of subscription</param>
|
|
/// <param name="qosLevel">QoS level of subscription</param>
|
|
/// <param name="client">Client related to the subscription</param>
|
|
public MqttSubscription(string clientId, string topic, byte qosLevel, MqttClient client = null)
|
|
{
|
|
this.ClientId = clientId;
|
|
this.Topic = topic;
|
|
this.QosLevel = qosLevel;
|
|
this.Client = client;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Dispose subscription
|
|
/// </summary>
|
|
public void Dispose()
|
|
{
|
|
this.ClientId = null;
|
|
this.Topic = null;
|
|
this.QosLevel = 0;
|
|
this.Client = null;
|
|
}
|
|
}
|
|
}
|