Compare commits

..
Author SHA1 Message Date
Christian cb3c44f48d Rename InspectPackage method (#1713) 2023-04-23 13:57:59 +02:00
Christian 8b3b9d33de Add overloads for options builder (#1711)
* Add new overload for options builder

* Refactor code

* Update ReleaseNotes.md
2023-04-11 22:11:16 +02:00
scottbrogden-iheartmediaandChristian 59d99509c8 Notify what subscription topics failed as part of the SynchronizingSu… (#1678)
* Notify what subscription topics failed as part of the SynchronizingSubscriptionsFailedAsync callback

* Update ReleaseNotes.md

---------

Co-authored-by: Christian <6939810+chkr1011@users.noreply.github.com>
2023-04-08 14:03:58 +02:00
RossHNPCandChristian 2933b76237 ClientMessageQueueInterceptor continues with subscription processing. (#1685)
* ClientMessageQueueInterceptor continues with subscription processing.

* Update readme

---------

Co-authored-by: Christian <6939810+chkr1011@users.noreply.github.com>
2023-04-06 17:55:33 +02:00
ChristianandMichi 36c6d8974e Allow disabling of packet fragmentation (#1709)
* Add compatibility option for AWS

* Fix UWP build

* Fix comment in MqttClientOptions (#1706)

* Refactor naming

* Fix wrong option value

---------

Co-authored-by: Michi <michael@volz.dev>
2023-04-06 17:54:36 +02:00
Christian d6cab76728 Add user properties to event args to interceptors for subscribing and unsubscribing (#1707)
Add user properties to event args
2023-04-06 17:31:42 +02:00
Christian b4f740f338 Update readme 2023-04-05 22:12:58 +02:00
Christian a004de25ac Fix unit test 2023-02-22 21:46:33 +01:00
老九 d0584413ca Add PayloadSegmentable feature (#1585) 2023-02-22 20:53:55 +01:00
Christian ba57537108 Fix running state handling in server (#1680)
Fix running state handling
2023-02-17 21:17:31 +01:00
Christian 6dcb110737 Add InterceptingClientEnqueueAsync event (#1679) 2023-02-17 19:13:06 +01:00
Christian b599a832da Add new method for reading a single retained message (#1676) 2023-02-15 19:46:37 +01:00
69 changed files with 1118 additions and 654 deletions
+11 -1
View File
@@ -1,10 +1,20 @@
* [Core] Added net7.0 builds (#1635, thanks to @YAJeff).
* [Core] Added support for payload segments (#1585, thanks to @xljiulang)
* [Client] Added support for passing MQTT v5 options (User properties etc.) for disconnects.
* [Client] An internal exception is no longer caught silently when calling _DisconnectAsync_ to indicate that the disconnect is not clean (BREAKING CHANGE).
* [Client] MQTTv5 features are now checked and an exception is thrown if they are used when using protocol version 3.1.1 and lower. These checks can be disabled in client options. (BREAKING CHANGE!).
* [Client] Added support for disabling packet fragmentation (required for i.e. AWS, #1690, thanks to @logicaloud).
* [Client] Added new overloads for options builder.
* [Client] Renamed _InspectPackage_ to _InspectPacketAsync_ (BREAKING CHANGE).
* [ManagedClient] Exposed added and removed topics in _ManagedProcessFailedEventArgs_ (#1678, thanks to @scottbrogden-iheartmedia).
* [Server] Exposed MQTT v5 sent properties from the affected client in _ClientDisconnectedAsync_ event.
* [Server] Fixed wrong client ID passed to _InterceptingUnsubscriptionEventArgs_ (#1631, thanks to @ghord).
* [Server] Fixed wrong client ID passed to _InterceptingUnsubscriptionEventArgs_ (#1631, thanks to @ghord).
* [Server] Exposed socket settings for TCP keep alive in TCP options (#1544).
* [Server] Exposed server session items at server level and allow custom session items for injected application messages (#1638).
* [Server] Improved performance for retained message handling when subscribing using "DoNotSendOnSubscribe" or "SendAtSubscribeIfNewSubscriptionOnly" (#1661, thanks to @Int32Overflow).
* [Server] Added support for changing the used TLS certificate while the server is running (#1652, thanks to @YAJeff). The certificate provider will now be invoked for every new connection!
* [Server] Added a new API method which allows reading a single retained message without the need to processing the entire set of retained messages (#1659).
* [Server] Added a new event (InterceptingClientEnqueueAsync) which allows intercepting when an application message is enqueued for a client (#1648).
* [Server] Fixed race condition when handling connections which leads to stopped message transfers (#1677, thanks to @RazvanEmilR).
* [Server] Fixed not delivered packets to other sessions if the _InterceptingClientEnqueueEvent_ is used and accepting the packet is denied (#1685, thanks to @RossHNPC).
* [Server] Added missing user properties to subscribe and unsubscribe interceptor (#1702).
+16
View File
@@ -50,6 +50,22 @@ MQTTnet is a high performance .NET library for MQTT based communication. It prov
* A custom message interceptor can be added which allows transforming or extending every received application message
* Validate subscriptions and deny subscribing of certain topics depending on requesting clients
## Getting Started
MQTTnet is delivered via <b>NuGet</b> package manager. You can find the packages here: https://www.nuget.org/packages/MQTTnet/
Use these command in the Package Manager console to install MQTTnet manually:
```
Install-Package MQTTnet
```
Samples for using MQTTnet are part of this repository. For starters these samples are recommended:
- [Connect with a broker](https://github.com/dotnet/MQTTnet/blob/master/Samples/Client/Client_Connection_Samples.cs)
- [Subscribing to data](https://github.com/dotnet/MQTTnet/blob/master/Samples/Client/Client_Subscribe_Samples.cs)
- [Publishing data](https://github.com/dotnet/MQTTnet/blob/master/Samples/Client/Client_Publish_Samples.cs)
- [Host own broker](https://github.com/dotnet/MQTTnet/blob/master/Samples/Server/Server_Simple_Samples.cs)
## Code of Conduct
This project has adopted the code of conduct defined by the Contributor Covenant to clarify expected behavior in our community.
@@ -70,6 +70,32 @@ public static class Client_Connection_Samples
await mqttClient.DisconnectAsync(mqttClientDisconnectOptions, CancellationToken.None);
}
}
public static async Task Connect_With_Amazon_AWS()
{
/*
* This sample creates a simple MQTT client and connects to an Amazon Web Services broker.
*
* The broker requires special settings which are set here.
*/
var mqttFactory = new MqttFactory();
using (var mqttClient = mqttFactory.CreateMqttClient())
{
var mqttClientOptions = new MqttClientOptionsBuilder()
.WithTcpServer("amazon.web.services.broker")
// Disabling packet fragmentation is very important!
.WithoutPacketFragmentation()
.Build();
await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None);
Console.WriteLine("The MQTT client is connected.");
await mqttClient.DisconnectAsync();
}
}
public static async Task Connect_Client_Timeout()
{
@@ -26,7 +26,7 @@ public static class PackageInspection_Samples
.WithTcpServer("broker.hivemq.com")
.Build();
mqttClient.InspectPackage += OnInspectPackage;
mqttClient.InspectPacketAsync += OnInspectPacket;
await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None);
@@ -39,7 +39,7 @@ public static class PackageInspection_Samples
}
}
static Task OnInspectPackage(InspectMqttPacketEventArgs eventArgs)
static Task OnInspectPacket(InspectMqttPacketEventArgs eventArgs)
{
if (eventArgs.Direction == MqttPacketFlowDirection.Inbound)
{
@@ -103,7 +103,7 @@ namespace MQTTnet.AspNetCore.Tests
connection.Transport = pipe;
var ctx = new MqttConnectionContext(serializer, connection);
await ctx.SendPacketAsync(new MqttPublishPacket { Payload = new byte[20_000] }, CancellationToken.None).ConfigureAwait(false);
await ctx.SendPacketAsync(new MqttPublishPacket { PayloadSegment = new ArraySegment<byte>(new byte[20_000]) }, CancellationToken.None).ConfigureAwait(false);
var readResult = await pipe.Send.Reader.ReadAsync();
Assert.IsTrue(readResult.Buffer.Length > 20000);
@@ -19,7 +19,7 @@ namespace MQTTnet.AspNetCore.Tests
{
var serializer = new MqttPacketFormatterAdapter(MqttProtocolVersion.V311, new MqttBufferWriter(4096, 65535));
var buffer = serializer.Encode(new MqttPublishPacket {Topic = "a", Payload = new byte[5]}).Join();
var buffer = serializer.Encode(new MqttPublishPacket {Topic = "a", PayloadSegment = new byte[5]}).Join();
var sequence = new ReadOnlySequence<byte>(buffer.Array, buffer.Offset, buffer.Count);
@@ -2,38 +2,34 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.AspNetCore.Http;
using MQTTnet.Adapter;
using MQTTnet.Formatter;
using MQTTnet.Implementations;
using MQTTnet.Server;
using System;
using System.Net.WebSockets;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using MQTTnet.Adapter;
using MQTTnet.Diagnostics;
using MQTTnet.Formatter;
using MQTTnet.Implementations;
using MQTTnet.Server;
namespace MQTTnet.AspNetCore
{
public sealed class MqttWebSocketServerAdapter : IMqttServerAdapter
{
IMqttNetLogger _logger = new MqttNetNullLogger();
IMqttNetLogger _logger = MqttNetNullLogger.Instance;
public Func<IMqttChannelAdapter, Task> ClientHandler { get; set; }
public Task StartAsync(MqttServerOptions options, IMqttNetLogger logger)
public void Dispose()
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
return Task.CompletedTask;
}
public Task StopAsync()
{
return Task.CompletedTask;
}
public async Task RunWebSocketConnectionAsync(WebSocket webSocket, HttpContext httpContext)
{
if (webSocket == null) throw new ArgumentNullException(nameof(webSocket));
if (webSocket == null)
{
throw new ArgumentNullException(nameof(webSocket));
}
var endpoint = $"{httpContext.Connection.RemoteIpAddress}:{httpContext.Connection.RemotePort}";
@@ -48,7 +44,7 @@ namespace MQTTnet.AspNetCore
var formatter = new MqttPacketFormatterAdapter(new MqttBufferWriter(4096, 65535));
var channel = new MqttWebSocketChannel(webSocket, endpoint, isSecureConnection, clientCertificate);
using (var channelAdapter = new MqttChannelAdapter(channel, formatter, null, _logger))
using (var channelAdapter = new MqttChannelAdapter(channel, formatter, _logger))
{
await clientHandler(channelAdapter).ConfigureAwait(false);
}
@@ -60,8 +56,15 @@ namespace MQTTnet.AspNetCore
}
}
public void Dispose()
public Task StartAsync(MqttServerOptions options, IMqttNetLogger logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
return Task.CompletedTask;
}
public Task StopAsync()
{
return Task.CompletedTask;
}
}
}
@@ -73,7 +73,7 @@ namespace MQTTnet.Benchmarks
var channel = new MemoryMqttChannel(_stream);
_channelAdapter = new MqttChannelAdapter(channel, serializer, null, new MqttNetEventLogger());
_channelAdapter = new MqttChannelAdapter(channel, serializer, new MqttNetEventLogger());
}
static byte[] Join(params ArraySegment<byte>[] chunks)
@@ -54,7 +54,7 @@ namespace MQTTnet.Benchmarks
public void Deserialize_10000_Messages()
{
var channel = new BenchmarkMqttChannel(_serializedPacket);
var reader = new MqttChannelAdapter(channel, new MqttPacketFormatterAdapter(new MqttBufferWriter(4096, 65535)), null, new MqttNetEventLogger());
var reader = new MqttChannelAdapter(channel, new MqttPacketFormatterAdapter(new MqttBufferWriter(4096, 65535)), new MqttNetEventLogger());
for (var i = 0; i < 10000; i++)
{
@@ -353,13 +353,13 @@ namespace MQTTnet.Extensions.ManagedClient
return remainingTime < TimeSpan.Zero ? TimeSpan.Zero : remainingTime;
}
async Task HandleSubscriptionExceptionAsync(Exception exception)
async Task HandleSubscriptionExceptionAsync(Exception exception, List<MqttTopicFilter> addedSubscriptions, List<string> removedSubscriptions)
{
_logger.Warning(exception, "Synchronizing subscriptions failed.");
if (_synchronizingSubscriptionsFailedEvent.HasHandlers)
{
await _synchronizingSubscriptionsFailedEvent.InvokeAsync(new ManagedProcessFailedEventArgs(exception)).ConfigureAwait(false);
await _synchronizingSubscriptionsFailedEvent.InvokeAsync(new ManagedProcessFailedEventArgs(exception, addedSubscriptions, removedSubscriptions)).ConfigureAwait(false);
}
}
@@ -457,11 +457,13 @@ namespace MQTTnet.Extensions.ManagedClient
{
_logger.Info("Publishing subscriptions at reconnect");
List<MqttTopicFilter> topicFilters = null;
try
{
if (_reconnectSubscriptions.Any())
{
var topicFilters = new List<MqttTopicFilter>();
topicFilters = new List<MqttTopicFilter>();
foreach (var sub in _reconnectSubscriptions)
{
@@ -479,7 +481,7 @@ namespace MQTTnet.Extensions.ManagedClient
}
catch (Exception exception)
{
await HandleSubscriptionExceptionAsync(exception).ConfigureAwait(false);
await HandleSubscriptionExceptionAsync(exception, topicFilters, null).ConfigureAwait(false);
}
}
@@ -591,6 +593,9 @@ namespace MQTTnet.Extensions.ManagedClient
}
await InternalClient.UnsubscribeAsync(unsubscribeOptionsBuilder.Build()).ConfigureAwait(false);
//clear because these worked, maybe the subscribe below will fail, only report those
removedSubscriptions.Clear();
}
if (addedSubscriptions != null && addedSubscriptions.Any())
@@ -607,7 +612,7 @@ namespace MQTTnet.Extensions.ManagedClient
}
catch (Exception exception)
{
await HandleSubscriptionExceptionAsync(exception).ConfigureAwait(false);
await HandleSubscriptionExceptionAsync(exception, addedSubscriptions, removedSubscriptions).ConfigureAwait(false);
}
}
@@ -24,7 +24,7 @@ namespace MQTTnet.Extensions.ManagedClient
/// <summary>
/// Defines the maximum amount of topic filters which will be sent in a SUBSCRIBE/UNSUBSCRIBE packet.
/// Amazon AWS limits this number to 8. The default is int.MaxValue.
/// Amazon Web Services (AWS) limits this number to 8. The default is int.MaxValue.
/// </summary>
public int MaxTopicFiltersInSubscribeUnsubscribePackets { get; set; } = int.MaxValue;
}
@@ -2,17 +2,33 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using MQTTnet.Packets;
using System;
using System.Collections.Generic;
using System.Linq;
namespace MQTTnet.Extensions.ManagedClient
{
public class ManagedProcessFailedEventArgs : EventArgs
{
public ManagedProcessFailedEventArgs(Exception exception)
public ManagedProcessFailedEventArgs(Exception exception, List<MqttTopicFilter> addedSubscriptions, List<string> removedSubscriptions)
{
Exception = exception ?? throw new ArgumentNullException(nameof(exception));
if (addedSubscriptions != null)
{
AddedSubscriptions = new List<string>(addedSubscriptions.Select(item => item.Topic));
}
if (removedSubscriptions != null)
{
RemovedSubscriptions = new List<string>(removedSubscriptions);
}
}
public Exception Exception { get; }
public List<string> AddedSubscriptions { get; }
public List<string> RemovedSubscriptions { get; }
}
}
@@ -4,6 +4,7 @@
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MQTTnet.Client;
@@ -119,7 +120,6 @@ namespace MQTTnet.Extensions.Rpc
finally
{
_waitingCalls.TryRemove(responseTopic, out _);
await _mqttClient.UnsubscribeAsync(responseTopic, CancellationToken.None).ConfigureAwait(false);
}
}
@@ -131,7 +131,8 @@ namespace MQTTnet.Extensions.Rpc
return CompletedTask.Instance;
}
awaitable.TrySetResult(eventArgs.ApplicationMessage.Payload);
var payloadBuffer = eventArgs.ApplicationMessage.PayloadSegment.ToArray();
awaitable.TrySetResult(payloadBuffer);
// Set this message to handled to that other code can avoid execution etc.
eventArgs.IsHandled = true;
@@ -27,8 +27,10 @@ namespace MQTTnet.Extensions.WebSocket4Net
return new MqttChannelAdapter(
new MqttTcpChannel(options),
new MqttPacketFormatterAdapter(options.ProtocolVersion, new MqttBufferWriter(options.WriterBufferSize, options.WriterBufferSizeMax)),
packetInspector,
logger);
logger)
{
PacketInspector = packetInspector
};
}
case MqttClientWebSocketOptions webSocketOptions:
@@ -36,8 +38,10 @@ namespace MQTTnet.Extensions.WebSocket4Net
return new MqttChannelAdapter(
new WebSocket4NetMqttChannel(options, webSocketOptions),
new MqttPacketFormatterAdapter(options.ProtocolVersion, new MqttBufferWriter(options.WriterBufferSize, options.WriterBufferSizeMax)),
packetInspector,
logger);
logger)
{
PacketInspector = packetInspector
};
}
default:
+10 -1
View File
@@ -33,9 +33,18 @@ namespace MQTTnet.TestApp
client.ApplicationMessageReceivedAsync += e =>
{
var payloadText = string.Empty;
if (e.ApplicationMessage.PayloadSegment.Count > 0)
{
payloadText = Encoding.UTF8.GetString(
e.ApplicationMessage.PayloadSegment.Array,
e.ApplicationMessage.PayloadSegment.Offset,
e.ApplicationMessage.PayloadSegment.Count);
}
Console.WriteLine("### RECEIVED APPLICATION MESSAGE ###");
Console.WriteLine($"+ Topic = {e.ApplicationMessage.Topic}");
Console.WriteLine($"+ Payload = {Encoding.UTF8.GetString(e.ApplicationMessage.Payload)}");
Console.WriteLine($"+ Payload = {payloadText}");
Console.WriteLine($"+ QoS = {e.ApplicationMessage.QualityOfServiceLevel}");
Console.WriteLine($"+ Retain = {e.ApplicationMessage.Retain}");
Console.WriteLine();
+4 -4
View File
@@ -196,7 +196,7 @@ namespace MQTTnet.TestApp
return new MqttApplicationMessage
{
Topic = "A/B/C",
Payload = Encoding.UTF8.GetBytes(Payload),
PayloadSegment = new ArraySegment<byte>(Encoding.UTF8.GetBytes(Payload)),
QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce
};
}
@@ -229,7 +229,7 @@ namespace MQTTnet.TestApp
var message = new MqttApplicationMessage
{
Topic = "A/B/C",
Payload = Encoding.UTF8.GetBytes("Hello World"),
PayloadSegment = new ArraySegment<byte>(Encoding.UTF8.GetBytes("Hello World")),
QualityOfServiceLevel = MqttQualityOfServiceLevel.ExactlyOnce
};
@@ -280,7 +280,7 @@ namespace MQTTnet.TestApp
var message = new MqttApplicationMessage
{
Topic = "A/B/C",
Payload = Encoding.UTF8.GetBytes("Hello World"),
PayloadSegment = new ArraySegment<byte>(Encoding.UTF8.GetBytes("Hello World")),
QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce
};
@@ -331,7 +331,7 @@ namespace MQTTnet.TestApp
var message = new MqttApplicationMessage
{
Topic = "A/B/C",
Payload = Encoding.UTF8.GetBytes("Hello World"),
PayloadSegment = new ArraySegment<byte>(Encoding.UTF8.GetBytes("Hello World")),
QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce
};
+11 -4
View File
@@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using MQTTnet.Diagnostics;
@@ -100,7 +101,7 @@ namespace MQTTnet.TestApp
{
// Replace the payload with the timestamp. But also extending a JSON
// based payload with the timestamp is a suitable use case.
e.ApplicationMessage.Payload = Encoding.UTF8.GetBytes(DateTime.Now.ToString("O"));
e.ApplicationMessage.PayloadSegment = new ArraySegment<byte>(Encoding.UTF8.GetBytes(DateTime.Now.ToString("O")));
}
if (e.ApplicationMessage.Topic == "not_allowed_topic")
@@ -143,10 +144,16 @@ namespace MQTTnet.TestApp
mqttServer.InterceptingPublishAsync += e =>
{
MqttNetConsoleLogger.PrintToConsole(
$"'{e.ClientId}' reported '{e.ApplicationMessage.Topic}' > '{Encoding.UTF8.GetString(e.ApplicationMessage.Payload ?? new byte[0])}'",
ConsoleColor.Magenta);
var payloadText = string.Empty;
if (e.ApplicationMessage.PayloadSegment.Count > 0)
{
payloadText = Encoding.UTF8.GetString(
e.ApplicationMessage.PayloadSegment.Array,
e.ApplicationMessage.PayloadSegment.Offset,
e.ApplicationMessage.PayloadSegment.Count);
}
MqttNetConsoleLogger.PrintToConsole($"'{e.ClientId}' reported '{e.ApplicationMessage.Topic}' > '{payloadText}'", ConsoleColor.Magenta);
return CompletedTask.Instance;
};
@@ -99,6 +99,8 @@ namespace MQTTnet.Tests.Clients.MqttClient
// Perform a clean disconnect.
await client.DisconnectAsync(disconnectOptions);
await LongTestDelay();
Assert.IsNotNull(eventArgs);
Assert.AreEqual(MqttClientDisconnectType.Clean, eventArgs.DisconnectType);
}
@@ -150,6 +152,8 @@ namespace MQTTnet.Tests.Clients.MqttClient
// Perform a clean disconnect.
await client.DisconnectAsync(disconnectOptions);
await LongTestDelay();
Assert.IsNotNull(eventArgs);
Assert.IsNotNull(eventArgs.UserProperties);
@@ -30,7 +30,7 @@ namespace MQTTnet.Tests.Diagnostics
var packets = new List<string>();
mqttClient.InspectPackage += eventArgs =>
mqttClient.InspectPacketAsync += eventArgs =>
{
packets.Add(eventArgs.Direction + ":" + Convert.ToBase64String(eventArgs.Buffer));
return CompletedTask.Instance;
@@ -111,7 +111,7 @@ namespace MQTTnet.Tests.Formatter
PacketIdentifier = 123,
Dup = true,
Retain = true,
Payload = Encoding.ASCII.GetBytes("HELLO"),
PayloadSegment = new ArraySegment<byte>(Encoding.ASCII.GetBytes("HELLO")),
QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce,
Topic = "A/B/C"
};
@@ -318,10 +318,9 @@ namespace MQTTnet.Tests.Formatter
var publishPacket = new MqttPublishPacket
{
Topic = "abcdefghijklmnopqrstuvwxyz0123456789",
Payload = payload
PayloadSegment = new ArraySegment<byte>(payload)
};
var serializationHelper = new MqttPacketSerializationHelper();
var buffer = serializationHelper.Encode(publishPacket);
@@ -329,17 +328,17 @@ namespace MQTTnet.Tests.Formatter
Assert.IsNotNull(publishPacketCopy);
Assert.AreEqual(publishPacket.Topic, publishPacketCopy.Topic);
CollectionAssert.AreEqual(publishPacket.Payload, publishPacketCopy.Payload);
CollectionAssert.AreEqual(publishPacket.PayloadSegment.ToArray(), publishPacketCopy.PayloadSegment.ToArray());
// Now modify the payload and test again.
publishPacket.Payload = Encoding.UTF8.GetBytes("MQTT");
publishPacket.PayloadSegment = new ArraySegment<byte>(Encoding.UTF8.GetBytes("MQTT"));
buffer = serializationHelper.Encode(publishPacket);
var publishPacketCopy2 = serializationHelper.Decode(buffer) as MqttPublishPacket;
Assert.IsNotNull(publishPacketCopy2);
Assert.AreEqual(publishPacket.Topic, publishPacketCopy2.Topic);
CollectionAssert.AreEqual(publishPacket.Payload, publishPacketCopy2.Payload);
CollectionAssert.AreEqual(publishPacket.PayloadSegment.ToArray(), publishPacketCopy2.PayloadSegment.ToArray());
}
[TestMethod]
@@ -463,7 +462,7 @@ namespace MQTTnet.Tests.Formatter
PacketIdentifier = 123,
Dup = true,
Retain = true,
Payload = Encoding.ASCII.GetBytes("HELLO"),
PayloadSegment = new ArraySegment<byte>(Encoding.ASCII.GetBytes("HELLO")),
QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce,
Topic = "A/B/C"
};
@@ -567,7 +566,6 @@ namespace MQTTnet.Tests.Formatter
using (var adapter = new MqttChannelAdapter(
channel,
new MqttPacketFormatterAdapter(protocolVersion, new MqttBufferWriter(4096, 65535)),
null,
new MqttNetEventLogger()))
{
var receivedPacket = adapter.ReceivePacketAsync(CancellationToken.None).GetAwaiter().GetResult();
@@ -583,19 +581,12 @@ namespace MQTTnet.Tests.Formatter
MqttProtocolVersion DeserializeAndDetectVersion(MqttPacketFormatterAdapter packetFormatterAdapter, byte[] buffer)
{
var channel = new MemoryMqttChannel(buffer);
var adapter = new MqttChannelAdapter(channel, packetFormatterAdapter, null, new MqttNetEventLogger());
var adapter = new MqttChannelAdapter(channel, packetFormatterAdapter, new MqttNetEventLogger());
adapter.ReceivePacketAsync(CancellationToken.None).GetAwaiter().GetResult();
return packetFormatterAdapter.ProtocolVersion;
}
MqttBufferReader ReaderFactory(byte[] data)
{
var reader = new MqttBufferReader();
reader.SetBuffer(data, 0, data.Length);
return reader;
}
TPacket Roundtrip<TPacket>(TPacket packet, MqttProtocolVersion protocolVersion = MqttProtocolVersion.V311, MqttBufferWriter bufferWriter = null) where TPacket : MqttPacket
{
var writer = bufferWriter ?? WriterFactory();
@@ -604,7 +595,7 @@ namespace MQTTnet.Tests.Formatter
using (var channel = new MemoryMqttChannel(buffer.Join().ToArray()))
{
var adapter = new MqttChannelAdapter(channel, new MqttPacketFormatterAdapter(protocolVersion, new MqttBufferWriter(4096, 65535)), null, new MqttNetEventLogger());
var adapter = new MqttChannelAdapter(channel, new MqttPacketFormatterAdapter(protocolVersion, new MqttBufferWriter(4096, 65535)), new MqttNetEventLogger());
return (TPacket)adapter.ReceivePacketAsync(CancellationToken.None).GetAwaiter().GetResult();
}
}
@@ -2,7 +2,9 @@
// 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.Text;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MQTTnet.Exceptions;
@@ -296,7 +298,7 @@ namespace MQTTnet.Tests.Formatter
PacketIdentifier = 123,
Dup = true,
Retain = true,
Payload = Encoding.ASCII.GetBytes("Payload"),
PayloadSegment = new ArraySegment<byte>(Encoding.ASCII.GetBytes("Payload")),
QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce,
Topic = "Topic",
ResponseTopic = "/Response",
@@ -320,7 +322,7 @@ namespace MQTTnet.Tests.Formatter
Assert.AreEqual(publishPacket.PacketIdentifier, deserialized.PacketIdentifier);
Assert.AreEqual(publishPacket.Dup, deserialized.Dup);
Assert.AreEqual(publishPacket.Retain, deserialized.Retain);
CollectionAssert.AreEqual(publishPacket.Payload, deserialized.Payload);
CollectionAssert.AreEqual(publishPacket.PayloadSegment.ToArray(), deserialized.PayloadSegment.ToArray());
Assert.AreEqual(publishPacket.QualityOfServiceLevel, deserialized.QualityOfServiceLevel);
Assert.AreEqual(publishPacket.Topic, deserialized.Topic);
Assert.AreEqual(null, deserialized.ResponseTopic); // Not supported in v3.1.1.
@@ -2,7 +2,9 @@
// 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.Text;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MQTTnet.Formatter;
@@ -257,7 +259,7 @@ namespace MQTTnet.Tests.Formatter
PacketIdentifier = 123,
Dup = true,
Retain = true,
Payload = Encoding.ASCII.GetBytes("Payload"),
PayloadSegment = new ArraySegment<byte>(Encoding.ASCII.GetBytes("Payload")),
QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce,
Topic = "Topic",
ResponseTopic = "/Response",
@@ -281,7 +283,7 @@ namespace MQTTnet.Tests.Formatter
Assert.AreEqual(publishPacket.PacketIdentifier, deserialized.PacketIdentifier);
Assert.AreEqual(publishPacket.Dup, deserialized.Dup);
Assert.AreEqual(publishPacket.Retain, deserialized.Retain);
CollectionAssert.AreEqual(publishPacket.Payload, deserialized.Payload);
CollectionAssert.AreEqual(publishPacket.PayloadSegment.ToArray(), deserialized.PayloadSegment.ToArray());
Assert.AreEqual(publishPacket.QualityOfServiceLevel, deserialized.QualityOfServiceLevel);
Assert.AreEqual(publishPacket.Topic, deserialized.Topic);
Assert.AreEqual(publishPacket.ResponseTopic, deserialized.ResponseTopic);
@@ -51,10 +51,7 @@ namespace MQTTnet.Tests.Mockups
public void AssertReceivedCountEquals(int expectedCount)
{
lock (_receivedEventArgs)
{
Assert.AreEqual(expectedCount, _receivedEventArgs.Count);
}
Assert.AreEqual(expectedCount, Count);
}
public string GeneratePayloadSequence()
@@ -0,0 +1,48 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MQTTnet.Tests
{
[TestClass]
public class MqttApplicationMessageTest
{
[TestMethod]
public void PayloadSegment()
{
var message = new MqttApplicationMessage();
Assert.AreEqual(0, message.Payload.Length);
Assert.AreEqual(0, message.PayloadSegment.Count);
Assert.IsTrue(object.ReferenceEquals(message.Payload, message.Payload));
Assert.IsTrue(object.ReferenceEquals(message.Payload, message.PayloadSegment.Array));
message.Payload = new byte[] { 1, 2 };
Assert.AreEqual(2, message.Payload.Length);
Assert.AreEqual(2, message.PayloadSegment.Count);
Assert.IsTrue(object.ReferenceEquals(message.Payload, message.Payload));
Assert.IsTrue(object.ReferenceEquals(message.Payload, message.PayloadSegment.Array));
message.Payload = new byte[] { 1, 2, 3 };
Assert.AreEqual(3, message.Payload.Length);
Assert.AreEqual(3, message.PayloadSegment.Count);
Assert.IsTrue(object.ReferenceEquals(message.Payload, message.Payload));
Assert.IsTrue(object.ReferenceEquals(message.Payload, message.PayloadSegment.Array));
message.PayloadSegment = new ArraySegment<byte>(new byte[] { 1, 2, 3 });
Assert.AreEqual(3, message.Payload.Length);
Assert.AreEqual(3, message.PayloadSegment.Count);
Assert.IsTrue(object.ReferenceEquals(message.Payload, message.Payload));
Assert.IsTrue(object.ReferenceEquals(message.Payload, message.PayloadSegment.Array));
message.PayloadSegment = new ArraySegment<byte>(new byte[] { 1, 2, 3 }, 1, 1);
Assert.AreEqual(1, message.Payload.Length);
Assert.AreEqual(1, message.PayloadSegment.Count);
Assert.IsTrue(object.ReferenceEquals(message.Payload, message.Payload));
Assert.IsFalse(object.ReferenceEquals(message.Payload, message.PayloadSegment.Array));
}
}
}
@@ -31,7 +31,7 @@ namespace MQTTnet.Tests
{
var formatterAdapter = new MqttPacketFormatterAdapter(_protocolVersion, new MqttBufferWriter(4096, 65535));
var adapter = new MqttChannelAdapter(channel, formatterAdapter, null, MqttNetNullLogger.Instance);
var adapter = new MqttChannelAdapter(channel, formatterAdapter, MqttNetNullLogger.Instance);
return adapter.ReceivePacketAsync(CancellationToken.None).GetAwaiter().GetResult();
}
}
+1 -1
View File
@@ -954,7 +954,7 @@ namespace MQTTnet.Tests.Server
var client = testEnvironment.CreateClient();
client.InspectPackage += e =>
client.InspectPacketAsync += e =>
{
if (e.Buffer.Length > 0)
{
@@ -228,7 +228,7 @@ namespace MQTTnet.Tests.Server
CheckSubscriptionsResult CheckSubscriptions(string topic, MqttQualityOfServiceLevel applicationMessageQoSLevel, string senderClientId)
{
MqttSubscription.CalculateTopicHash(topic, out var topicHash, out _, out _);
MqttTopicHash.Calculate(topic, out var topicHash, out _, out _);
return _subscriptionsManager.CheckSubscriptions(topic, topicHash, applicationMessageQoSLevel, senderClientId);
}
}
@@ -1,58 +0,0 @@
// 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.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MQTTnet.Client;
using MQTTnet.Formatter;
using MQTTnet.Protocol;
namespace MQTTnet.Tests.Server
{
[TestClass]
public sealed class Publish_Tests : BaseTestClass
{
[TestMethod]
public async Task Return_Success_When_Subscribed()
{
using (var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500))
{
await testEnvironment.StartServer();
var sender = await testEnvironment.ConnectClient();
var receiver = await testEnvironment.ConnectClient();
await receiver.SubscribeAsync("A");
// AtLeastOnce is required to get an ACK packet from the server.
var publishResult = await sender.PublishStringAsync("A", "Payload", MqttQualityOfServiceLevel.AtLeastOnce);
Assert.AreEqual(MqttClientPublishReasonCode.Success, publishResult.ReasonCode);
Assert.AreEqual(true, publishResult.IsSuccess);
}
}
[TestMethod]
public async Task Return_NoMatchingSubscribers_When_Not_Subscribed()
{
using (var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500))
{
await testEnvironment.StartServer();
var sender = await testEnvironment.ConnectClient();
var receiver = await testEnvironment.ConnectClient();
await receiver.SubscribeAsync("A");
// AtLeastOnce is required to get an ACK packet from the server.
var publishResult = await sender.PublishStringAsync("B", "Payload", MqttQualityOfServiceLevel.AtLeastOnce);
Assert.AreEqual(MqttClientPublishReasonCode.NoMatchingSubscribers, publishResult.ReasonCode);
Assert.AreEqual(true, publishResult.IsSuccess);
}
}
}
}
@@ -5,6 +5,8 @@
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MQTTnet.Client;
using MQTTnet.Formatter;
using MQTTnet.Internal;
using MQTTnet.Protocol;
namespace MQTTnet.Tests.Server
@@ -27,5 +29,118 @@ namespace MQTTnet.Tests.Server
await client.PublishStringAsync("test", qualityOfServiceLevel: MqttQualityOfServiceLevel.AtLeastOnce);
}
}
[TestMethod]
public async Task Return_NoMatchingSubscribers_When_Not_Subscribed()
{
using (var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500))
{
await testEnvironment.StartServer();
var sender = await testEnvironment.ConnectClient();
var receiver = await testEnvironment.ConnectClient();
await receiver.SubscribeAsync("A");
// AtLeastOnce is required to get an ACK packet from the server.
var publishResult = await sender.PublishStringAsync("B", "Payload", MqttQualityOfServiceLevel.AtLeastOnce);
Assert.AreEqual(MqttClientPublishReasonCode.NoMatchingSubscribers, publishResult.ReasonCode);
Assert.AreEqual(true, publishResult.IsSuccess);
}
}
[TestMethod]
public async Task Return_Success_When_Subscribed()
{
using (var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500))
{
await testEnvironment.StartServer();
var sender = await testEnvironment.ConnectClient();
var receiver = await testEnvironment.ConnectClient();
await receiver.SubscribeAsync("A");
// AtLeastOnce is required to get an ACK packet from the server.
var publishResult = await sender.PublishStringAsync("A", "Payload", MqttQualityOfServiceLevel.AtLeastOnce);
Assert.AreEqual(MqttClientPublishReasonCode.Success, publishResult.ReasonCode);
Assert.AreEqual(true, publishResult.IsSuccess);
}
}
[TestMethod]
public async Task Intercept_Client_Enqueue()
{
using (var testEnvironment = CreateTestEnvironment())
{
var server = await testEnvironment.StartServer();
var sender = await testEnvironment.ConnectClient();
var receiver = await testEnvironment.ConnectClient();
await receiver.SubscribeAsync("A");
var receivedMessages = testEnvironment.CreateApplicationMessageHandler(receiver);
await sender.PublishStringAsync("A", "Payload", MqttQualityOfServiceLevel.AtLeastOnce);
await LongTestDelay();
receivedMessages.AssertReceivedCountEquals(1);
server.InterceptingClientEnqueueAsync += e =>
{
e.AcceptEnqueue = false;
return CompletedTask.Instance;
};
await sender.PublishStringAsync("A", "Payload", MqttQualityOfServiceLevel.AtLeastOnce);
await LongTestDelay();
// Do not increase because the internal enqueue to the target client is not accepted!
receivedMessages.AssertReceivedCountEquals(1);
}
}
[TestMethod]
public async Task Intercept_Client_Enqueue_Multiple_Clients_Subscribed_Messages_Are_Filtered()
{
using (var testEnvironment = CreateTestEnvironment())
{
var server = await testEnvironment.StartServer();
var sender = await testEnvironment.ConnectClient();
var receiverOne = await testEnvironment.ConnectClient(o => o.WithClientId("One"));
await receiverOne.SubscribeAsync("A");
var receivedMessagesOne = testEnvironment.CreateApplicationMessageHandler(receiverOne);
var receiverTwo = await testEnvironment.ConnectClient(o => o.WithClientId("Two"));
await receiverTwo.SubscribeAsync("A");
var receivedMessagesTwo = testEnvironment.CreateApplicationMessageHandler(receiverTwo);
var receiverThree = await testEnvironment.ConnectClient(o => o.WithClientId("Three"));
await receiverThree.SubscribeAsync("A");
var receivedMessagesThree = testEnvironment.CreateApplicationMessageHandler(receiverThree);
server.InterceptingClientEnqueueAsync += e =>
{
if (e.ReceiverClientId.Contains("Two")) e.AcceptEnqueue = false;
return CompletedTask.Instance;
};
await sender.PublishStringAsync("A", "Payload", MqttQualityOfServiceLevel.AtLeastOnce);
await LongTestDelay();
receivedMessagesOne.AssertReceivedCountEquals(1);
receivedMessagesTwo.AssertReceivedCountEquals(0);
receivedMessagesThree.AssertReceivedCountEquals(1);
}
}
}
}
@@ -164,7 +164,7 @@ namespace MQTTnet.Tests.Server
var clientOptions = new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", testEnvironment.ServerPort).WithCredentials(userName, password).Build();
var ex = await Assert.ThrowsExceptionAsync<MqttConnectingFailedException>(async () => await client.ConnectAsync(clientOptions));
var ex = await Assert.ThrowsExceptionAsync<MqttConnectingFailedException>(() => client.ConnectAsync(clientOptions));
Assert.AreEqual(MqttClientConnectResultCode.BadUserNameOrPassword, ex.Result.ResultCode);
}
}
+25 -2
View File
@@ -62,7 +62,7 @@ namespace MQTTnet.Tests.Server
}
}
}
[TestMethod]
public async Task Deny_Invalid_Topic()
{
@@ -90,6 +90,29 @@ namespace MQTTnet.Tests.Server
}
}
[TestMethod]
public async Task Intercept_Subscribe_With_User_Properties()
{
using (var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500))
{
var server = await testEnvironment.StartServer();
InterceptingSubscriptionEventArgs eventArgs = null;
server.InterceptingSubscriptionAsync += e =>
{
eventArgs = e;
return CompletedTask.Instance;
};
var client = await testEnvironment.ConnectClient();
var subscribeOptions = testEnvironment.Factory.CreateSubscribeOptionsBuilder().WithTopicFilter("X").WithUserProperty("A", "1").Build();
await client.SubscribeAsync(subscribeOptions);
CollectionAssert.AreEqual(subscribeOptions.UserProperties.ToList(), eventArgs.UserProperties);
}
}
[TestMethod]
[ExpectedException(typeof(MqttClientDisconnectedException))]
public async Task Disconnect_While_Subscribing()
@@ -105,7 +128,7 @@ namespace MQTTnet.Tests.Server
await client.SubscribeAsync("#");
}
}
[TestMethod]
public async Task Enqueue_Message_After_Subscription()
{
@@ -47,7 +47,7 @@ namespace MQTTnet.Tests.Server
var topics = t.Value;
foreach (var topic in topics)
{
MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var hashMask, out var hasWildcard);
MqttTopicHash.Calculate(topic, out var topicHash, out var hashMask, out var hasWildcard);
bucketDepths.TryGetValue(topicHash, out var currentValue);
++currentValue;
@@ -96,7 +96,7 @@ namespace MQTTnet.Tests.Server
{
var topic = "asdfasdf/asdfasdf/asdfasdf/asdfasdf/asdfas/dfaf/assfdgsdfgdf/#";
MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard);
MqttTopicHash.Calculate(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard);
Assert.IsTrue(topicHasWildcard, "Wildcard not detected");
}
@@ -110,7 +110,7 @@ namespace MQTTnet.Tests.Server
{
var topic = "asdfasdf/asdfasdf/asdfasdf/asdfasdf/asdfas/dfaf/assfdgsdfgdf/+";
MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard);
MqttTopicHash.Calculate(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard);
Assert.IsTrue(topicHasWildcard, "Wildcard not detected");
}
@@ -150,7 +150,7 @@ namespace MQTTnet.Tests.Server
// UInt64 is limited to 8 levels
MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard);
MqttTopicHash.Calculate(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard);
Assert.IsTrue(topicHasWildcard, "Wildcard not detected");
@@ -212,7 +212,7 @@ namespace MQTTnet.Tests.Server
var topic = sb.ToString();
MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard);
MqttTopicHash.Calculate(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard);
Assert.IsFalse(topicHasWildcard, "Wildcard detected when not present");
@@ -269,7 +269,7 @@ namespace MQTTnet.Tests.Server
var topic = sb.ToString();
MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard);
MqttTopicHash.Calculate(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard);
Assert.IsTrue(topicHasWildcard, "Wildcard not detected");
@@ -319,7 +319,7 @@ namespace MQTTnet.Tests.Server
var l3 = "#";
var topic = $"{l0}/{l1}/{l2}/{l3}";
MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard);
MqttTopicHash.Calculate(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard);
Assert.IsTrue(topicHasWildcard, "Wildcard not detected");
@@ -354,7 +354,7 @@ namespace MQTTnet.Tests.Server
var l3 = "prop1";
var topic = $"{l0}/{l1}/{l2}/{l3}";
MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard);
MqttTopicHash.Calculate(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard);
Assert.IsFalse(topicHasWildcard, "Wildcard detected when not wildcard present");
@@ -447,7 +447,7 @@ namespace MQTTnet.Tests.Server
var l3 = "prop1";
var topic = $"{l0}/{l1}/{l2}/{l3}";
MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard);
MqttTopicHash.Calculate(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard);
Assert.IsTrue(topicHasWildcard, "Wildcard not detected");
@@ -475,7 +475,7 @@ namespace MQTTnet.Tests.Server
void CheckTopicHash(string topic, ulong expectedHash, ulong expectedHashMask)
{
MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var hashMask, out var hasWildcard);
MqttTopicHash.Calculate(topic, out var topicHash, out var hashMask, out var hasWildcard);
Console.WriteLine();
Console.WriteLine("Topic: " + topic);
@@ -501,7 +501,7 @@ namespace MQTTnet.Tests.Server
{
var topicToFind = topicsToFind[countDown];
MqttSubscription.CalculateTopicHash(topicToFind, out var topicHash, out _, out _);
MqttTopicHash.Calculate(topicToFind, out var topicHash, out _, out _);
clientSession.TryCheckSubscriptions(topicToFind, topicHash, MqttQualityOfServiceLevel.AtMostOnce, "OtherClient", out var result);
if (result.IsSubscribed)
@@ -2,10 +2,14 @@
// 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.Linq;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MQTTnet.Client;
using MQTTnet.Formatter;
using MQTTnet.Internal;
using MQTTnet.Protocol;
using MQTTnet.Server;
namespace MQTTnet.Tests.Server
{
@@ -28,5 +32,28 @@ namespace MQTTnet.Tests.Server
await client.UnsubscribeAsync("#");
}
}
[TestMethod]
public async Task Intercept_Unsubscribe_With_User_Properties()
{
using (var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500))
{
var server = await testEnvironment.StartServer();
InterceptingUnsubscriptionEventArgs eventArgs = null;
server.InterceptingUnsubscriptionAsync += e =>
{
eventArgs = e;
return CompletedTask.Instance;
};
var client = await testEnvironment.ConnectClient();
var unsubscribeOptions = testEnvironment.Factory.CreateUnsubscribeOptionsBuilder().WithTopicFilter("X").WithUserProperty("A", "1").Build();
await client.UnsubscribeAsync(unsubscribeOptions);
CollectionAssert.AreEqual(unsubscribeOptions.UserProperties.ToList(), eventArgs.UserProperties);
}
}
}
}
@@ -150,18 +150,8 @@ namespace MQTTnet.Tests
{
Assert.AreEqual(expectedResult, MqttTopicFilterComparer.Compare(topic, filter));
ulong topicHash;
ulong topicHashMask;
bool topicHasWildcard;
MqttSubscription.CalculateTopicHash(topic, out topicHash, out topicHashMask, out topicHasWildcard);
ulong filterTopicHash;
ulong filterTopicHashMask;
bool filterTopicHasWildcard;
MqttSubscription.CalculateTopicHash(filter, out filterTopicHash, out filterTopicHashMask, out filterTopicHasWildcard);
MqttTopicHash.Calculate(topic, out var topicHash, out _, out _);
MqttTopicHash.Calculate(filter, out var filterTopicHash, out var filterTopicHashMask, out _);
if (expectedResult == MqttTopicFilterCompareResult.IsMatch)
{
+23 -19
View File
@@ -27,18 +27,15 @@ namespace MQTTnet.Adapter
readonly byte[] _fixedHeaderBuffer = new byte[2];
readonly MqttNetSourceLogger _logger;
readonly MqttPacketInspector _packetInspector;
readonly byte[] _singleByteBuffer = new byte[1];
readonly AsyncLock _syncRoot = new AsyncLock();
Statistics _statistics; // mutable struct, don't make readonly!
Statistics _statistics; // mutable struct, don't make readonly!
public MqttChannelAdapter(IMqttChannel channel, MqttPacketFormatterAdapter packetFormatterAdapter, MqttPacketInspector packetInspector, IMqttNetLogger logger)
public MqttChannelAdapter(IMqttChannel channel, MqttPacketFormatterAdapter packetFormatterAdapter, IMqttNetLogger logger)
{
_channel = channel ?? throw new ArgumentNullException(nameof(channel));
_packetInspector = packetInspector;
PacketFormatterAdapter = packetFormatterAdapter ?? throw new ArgumentNullException(nameof(packetFormatterAdapter));
@@ -64,6 +61,10 @@ namespace MQTTnet.Adapter
public MqttPacketFormatterAdapter PacketFormatterAdapter { get; }
public MqttPacketInspector PacketInspector { get; set; }
public bool AllowPacketFragmentation { get; set; } = true;
public async Task ConnectAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
@@ -126,7 +127,7 @@ namespace MQTTnet.Adapter
try
{
_packetInspector?.BeginReceivePacket();
PacketInspector?.BeginReceivePacket();
ReceivedMqttPacket receivedPacket;
var receivedPacketTask = ReceiveAsync(cancellationToken);
@@ -144,7 +145,7 @@ namespace MQTTnet.Adapter
return null;
}
_packetInspector?.EndReceivePacket();
PacketInspector?.EndReceivePacket();
Interlocked.Add(ref _statistics._bytesSent, receivedPacket.TotalLength);
@@ -180,7 +181,10 @@ namespace MQTTnet.Adapter
return null;
}
public void ResetStatistics() => _statistics.Reset();
public void ResetStatistics()
{
_statistics.Reset();
}
public async Task SendPacketAsync(MqttPacket packet, CancellationToken cancellationToken)
{
@@ -197,18 +201,18 @@ namespace MQTTnet.Adapter
try
{
var packetBuffer = PacketFormatterAdapter.Encode(packet);
_packetInspector?.BeginSendPacket(packetBuffer);
PacketInspector?.BeginSendPacket(packetBuffer);
_logger.Verbose("TX ({0} bytes) >>> {1}", packetBuffer.Length, packet);
if (packetBuffer.Payload.Count > 0)
if (packetBuffer.Payload.Count == 0 || !AllowPacketFragmentation)
{
await _channel.WriteAsync(packetBuffer.Packet, false, cancellationToken).ConfigureAwait(false);
await _channel.WriteAsync(packetBuffer.Payload, true, cancellationToken).ConfigureAwait(false);
await _channel.WriteAsync(packetBuffer.Join(), true, cancellationToken).ConfigureAwait(false);
}
else
{
await _channel.WriteAsync(packetBuffer.Packet, true, cancellationToken).ConfigureAwait(false);
await _channel.WriteAsync(packetBuffer.Packet, false, cancellationToken).ConfigureAwait(false);
await _channel.WriteAsync(packetBuffer.Payload, true, cancellationToken).ConfigureAwait(false);
}
Interlocked.Add(ref _statistics._bytesReceived, packetBuffer.Length);
@@ -270,7 +274,7 @@ namespace MQTTnet.Adapter
return 0;
}
_packetInspector?.FillReceiveBuffer(_singleByteBuffer);
PacketInspector?.FillReceiveBuffer(_singleByteBuffer);
encodedByte = _singleByteBuffer[0];
@@ -309,7 +313,7 @@ namespace MQTTnet.Adapter
{
return ReadFixedHeaderResult.ConnectionClosed;
}
if (cancellationToken.IsCancellationRequested)
{
return ReadFixedHeaderResult.Canceled;
@@ -323,7 +327,7 @@ namespace MQTTnet.Adapter
totalBytesRead += bytesRead;
}
_packetInspector?.FillReceiveBuffer(buffer);
PacketInspector?.FillReceiveBuffer(buffer);
var hasRemainingLength = buffer[1] != 0;
if (!hasRemainingLength)
@@ -407,7 +411,7 @@ namespace MQTTnet.Adapter
bodyOffset += readBytes;
} while (bodyOffset < bodyLength);
_packetInspector?.FillReceiveBuffer(body);
PacketInspector?.FillReceiveBuffer(body);
var bodySegment = new ArraySegment<byte>(body, 0, bodyLength);
return new ReceivedMqttPacket(fixedHeader.Flags, bodySegment, fixedHeader.TotalLength);
@@ -455,7 +459,7 @@ namespace MQTTnet.Adapter
throw new MqttCommunicationException(exception);
}
private struct Statistics
struct Statistics
{
public long _bytesReceived;
public long _bytesSent;
+1 -1
View File
@@ -15,7 +15,7 @@ namespace MQTTnet.Client
event Func<MqttClientDisconnectedEventArgs, Task> DisconnectedAsync;
event Func<InspectMqttPacketEventArgs, Task> InspectPackage;
event Func<InspectMqttPacketEventArgs, Task> InspectPacketAsync;
bool IsConnected { get; }
+3 -3
View File
@@ -87,7 +87,7 @@ namespace MQTTnet.Client
remove => _disconnectedEvent.RemoveHandler(value);
}
public event Func<InspectMqttPacketEventArgs, Task> InspectPackage
public event Func<InspectMqttPacketEventArgs, Task> InspectPacketAsync
{
add => _inspectPacketEvent.AddHandler(value);
remove => _inspectPacketEvent.RemoveHandler(value);
@@ -488,9 +488,9 @@ namespace MQTTnet.Client
using (var effectiveCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(backgroundCancellationToken, cancellationToken))
{
_logger.Verbose("Trying to connect with server '{0}'.", Options.ChannelOptions);
_logger.Verbose("Trying to connect with server '{0}'", Options.ChannelOptions);
await _adapter.ConnectAsync(effectiveCancellationToken.Token).ConfigureAwait(false);
_logger.Verbose("Connection with server established.");
_logger.Verbose("Connection with server established");
_publishPacketReceiverQueue?.Dispose();
_publishPacketReceiverQueue = new AsyncQueue<MqttPublishPacket>();
@@ -24,6 +24,14 @@ namespace MQTTnet.Client
/// </summary>
public string AuthenticationMethod { get; set; }
/// <summary>
/// Usually the MQTT packets can be send partially. This is done by using multiple TCP packets
/// or WebSocket frames etc. Unfortunately not all brokers (like Amazon Web Services (AWS)) do support this feature and
/// will close the connection when receiving such packets. If such a service is used this flag must
/// be set to _false_.
/// </summary>
public bool AllowPacketFragmentation { get; set; } = true;
public IMqttClientChannelOptions ChannelOptions { get; set; }
/// <summary>
@@ -214,4 +222,4 @@ namespace MQTTnet.Client
/// </summary>
public int WriterBufferSizeMax { get; set; } = 65535;
}
}
}
@@ -75,7 +75,7 @@ namespace MQTTnet.Client
_options.ChannelOptions = (IMqttClientChannelOptions)_tcpOptions ?? _webSocketOptions;
MqttClientOptionsValidator.ThrowIfNotSupported(_options);
return _options;
}
@@ -86,12 +86,24 @@ namespace MQTTnet.Client
return this;
}
/// <summary>
/// Clean session is used in MQTT versions below 5.0.0. It is the same as setting "CleanStart".
/// </summary>
public MqttClientOptionsBuilder WithCleanSession(bool value = true)
{
_options.CleanSession = value;
return this;
}
/// <summary>
/// Clean start is used in MQTT versions 5.0.0 and higher. It is the same as setting "CleanSession".
/// </summary>
public MqttClientOptionsBuilder WithCleanStart(bool value = true)
{
_options.CleanSession = value;
return this;
}
public MqttClientOptionsBuilder WithClientId(string value)
{
_options.ClientId = value;
@@ -188,6 +200,18 @@ namespace MQTTnet.Client
return WithKeepAlivePeriod(TimeSpan.Zero);
}
/// <summary>
/// Usually the MQTT packets can be send partially. This is done by using multiple TCP packets
/// or WebSocket frames etc. Unfortunately not all brokers (like Amazon Web Services (AWS)) do support this feature and
/// will close the connection when receiving such packets. If such a service is used this flag must
/// be set to _true_.
/// </summary>
public MqttClientOptionsBuilder WithoutPacketFragmentation()
{
_options.AllowPacketFragmentation = false;
return this;
}
public MqttClientOptionsBuilder WithProtocolVersion(MqttProtocolVersion value)
{
if (value == MqttProtocolVersion.Unknown)
@@ -266,7 +290,7 @@ namespace MQTTnet.Client
return this;
}
public MqttClientOptionsBuilder WithTcpServer(Action<MqttClientTcpOptions> optionsBuilder)
{
if (optionsBuilder == null)
@@ -373,6 +397,18 @@ namespace MQTTnet.Client
return this;
}
public MqttClientOptionsBuilder WithWillContentType(string willContentType)
{
_options.WillContentType = willContentType;
return this;
}
public MqttClientOptionsBuilder WithWillCorrelationData(byte[] willCorrelationData)
{
_options.WillCorrelationData = willCorrelationData;
return this;
}
public MqttClientOptionsBuilder WithWillDelayInterval(uint willDelayInterval)
{
_options.WillDelayInterval = willDelayInterval;
@@ -384,27 +420,33 @@ namespace MQTTnet.Client
_options.WillPayload = willPayload;
return this;
}
public MqttClientOptionsBuilder WithWillPayload(string willPayload)
{
if (string.IsNullOrEmpty(willPayload))
{
return WithWillPayload((byte[])null);
}
_options.WillPayload = Encoding.UTF8.GetBytes(willPayload);
return this;
}
public MqttClientOptionsBuilder WithWillPayloadFormatIndicator(MqttPayloadFormatIndicator willPayloadFormatIndicator)
{
_options.WillPayloadFormatIndicator = willPayloadFormatIndicator;
return this;
}
public MqttClientOptionsBuilder WithWillQualityOfServiceLevel(MqttQualityOfServiceLevel willQualityOfServiceLevel)
{
_options.WillQualityOfServiceLevel = willQualityOfServiceLevel;
return this;
}
public MqttClientOptionsBuilder WithWillTopic(string willTopic)
public MqttClientOptionsBuilder WithWillResponseTopic(string willResponseTopic)
{
_options.WillTopic = willTopic;
_options.WillResponseTopic = willResponseTopic;
return this;
}
@@ -413,38 +455,20 @@ namespace MQTTnet.Client
_options.WillRetain = willRetain;
return this;
}
public MqttClientOptionsBuilder WithWillContentType(string willContentType)
public MqttClientOptionsBuilder WithWillTopic(string willTopic)
{
_options.WillContentType = willContentType;
_options.WillTopic = willTopic;
return this;
}
public MqttClientOptionsBuilder WithWillCorrelationData(byte[] willCorrelationData)
{
_options.WillCorrelationData = willCorrelationData;
return this;
}
public MqttClientOptionsBuilder WithWillResponseTopic(string willResponseTopic)
{
_options.WillResponseTopic = willResponseTopic;
return this;
}
public MqttClientOptionsBuilder WithWillPayloadFormatIndicator(MqttPayloadFormatIndicator willPayloadFormatIndicator)
{
_options.WillPayloadFormatIndicator = willPayloadFormatIndicator;
return this;
}
public MqttClientOptionsBuilder WithWillUserProperty(string name, string value)
{
if (_options.WillUserProperties == null)
{
_options.WillUserProperties = new List<MqttUserProperty>();
}
_options.WillUserProperties.Add(new MqttUserProperty(name, value));
return this;
}
@@ -42,6 +42,6 @@ namespace MQTTnet.Client
/// Gets the user properties which were part of the UNSUBACK packet.
/// <remarks>MQTT 5.0.0+ feature.</remarks>
/// </summary>
public IReadOnlyCollection<MqttUserProperty> UserProperties { get; }
public IReadOnlyCollection<MqttUserProperty> UserProperties { get; set; }
}
}
@@ -19,7 +19,7 @@ namespace MQTTnet.Formatter
return new MqttApplicationMessage
{
Topic = publishPacket.Topic,
Payload = publishPacket.Payload,
PayloadSegment = publishPacket.PayloadSegment,
QualityOfServiceLevel = publishPacket.QualityOfServiceLevel,
Retain = publishPacket.Retain,
Dup = publishPacket.Dup,
@@ -16,7 +16,7 @@ namespace MQTTnet.Formatter
return new MqttPublishPacket
{
Topic = publishPacket.Topic,
Payload = publishPacket.Payload,
PayloadSegment = publishPacket.PayloadSegment,
Retain = publishPacket.Retain,
QualityOfServiceLevel = publishPacket.QualityOfServiceLevel,
Dup = publishPacket.Dup,
@@ -36,7 +36,7 @@ namespace MQTTnet.Formatter
var packet = new MqttPublishPacket
{
Topic = applicationMessage.Topic,
Payload = applicationMessage.Payload,
PayloadSegment = applicationMessage.PayloadSegment,
QualityOfServiceLevel = applicationMessage.QualityOfServiceLevel,
Retain = applicationMessage.Retain,
Dup = applicationMessage.Dup,
@@ -65,10 +65,16 @@ namespace MQTTnet.Formatter
throw new MqttProtocolViolationException("The CONNECT packet contains no will message (WillFlag).");
}
ArraySegment<byte> willMessageBuffer = default;
if (connectPacket.WillMessage?.Length > 0)
{
willMessageBuffer = new ArraySegment<byte>(connectPacket.WillMessage);
}
var packet = new MqttPublishPacket
{
Topic = connectPacket.WillTopic,
Payload = connectPacket.WillMessage,
PayloadSegment = willMessageBuffer,
QualityOfServiceLevel = connectPacket.WillQoS,
Retain = connectPacket.WillRetain,
ContentType = connectPacket.WillContentType,
@@ -54,12 +54,12 @@ namespace MQTTnet.Formatter.V3
return DecodePubRelPacket(receivedMqttPacket.Body);
case MqttControlPacketType.PubComp:
return DecodePubCompPacket(receivedMqttPacket.Body);
case MqttControlPacketType.PingReq:
return MqttPingReqPacket.Instance;
case MqttControlPacketType.PingResp:
return MqttPingRespPacket.Instance;
case MqttControlPacketType.Connect:
return DecodeConnectPacket(receivedMqttPacket.Body);
case MqttControlPacketType.ConnAck:
@@ -73,7 +73,7 @@ namespace MQTTnet.Formatter.V3
}
case MqttControlPacketType.Disconnect:
return DisconnectPacket;
case MqttControlPacketType.Subscribe:
return DecodeSubscribePacket(receivedMqttPacket.Body);
case MqttControlPacketType.SubAck:
@@ -103,9 +103,10 @@ namespace MQTTnet.Formatter.V3
var remainingLength = (uint)(_bufferWriter.Length - 5);
var publishPacket = packet as MqttPublishPacket;
if (publishPacket?.Payload != null)
var payloadSegment = publishPacket?.PayloadSegment;
if (payloadSegment != null)
{
remainingLength += (uint)publishPacket.Payload.Length;
remainingLength += (uint)payloadSegment.Value.Count;
}
var remainingLengthSize = MqttBufferWriter.GetVariableByteIntegerSize(remainingLength);
@@ -119,18 +120,13 @@ namespace MQTTnet.Formatter.V3
_bufferWriter.WriteVariableByteInteger(remainingLength);
var buffer = _bufferWriter.GetBuffer();
var firstSegment = new ArraySegment<byte>(buffer, headerOffset, _bufferWriter.Length - headerOffset);
if (publishPacket?.Payload != null)
{
var payloadSegment = new ArraySegment<byte>(publishPacket.Payload, 0, publishPacket.Payload.Length);
return new MqttPacketBuffer(firstSegment, payloadSegment);
}
return new MqttPacketBuffer(firstSegment);
return payloadSegment == null
? new MqttPacketBuffer(firstSegment)
: new MqttPacketBuffer(firstSegment, payloadSegment.Value);
}
MqttPacket DecodeConnAckPacket(ArraySegment<byte> body)
{
ThrowIfBodyIsEmpty(body);
@@ -177,7 +173,7 @@ namespace MQTTnet.Formatter.V3
var tryPrivate = (protocolVersion & 0x80) > 0;
protocolVersion &= 0x7F;
if (protocolVersion != 3 && protocolVersion != 4)
{
throw new MqttProtocolViolationException("MQTT protocol version do not match MQTT v3.");
@@ -282,7 +278,7 @@ namespace MQTTnet.Formatter.V3
if (!_bufferReader.EndOfStream)
{
packet.Payload = _bufferReader.ReadRemainingData();
packet.PayloadSegment = new ArraySegment<byte>(_bufferReader.ReadRemainingData());
}
return packet;
@@ -421,7 +417,7 @@ namespace MQTTnet.Formatter.V3
{
protocolVersion |= 0x80;
}
bufferWriter.WriteByte((byte)protocolVersion);
byte connectFlags = 0x0;
@@ -528,7 +528,7 @@ namespace MQTTnet.Formatter.V5
if (!_bufferReader.EndOfStream)
{
packet.Payload = _bufferReader.ReadRemainingData();
packet.PayloadSegment = new ArraySegment<byte>(_bufferReader.ReadRemainingData());
}
return packet;
@@ -37,9 +37,11 @@ namespace MQTTnet.Formatter.V5
var remainingLength = (uint)_bufferWriter.Length - 5;
var publishPacket = packet as MqttPublishPacket;
if (publishPacket?.Payload != null)
var payloadSegment = publishPacket?.PayloadSegment;
if (payloadSegment != null)
{
remainingLength += (uint)publishPacket.Payload.Length;
remainingLength += (uint)payloadSegment.Value.Count;
}
var remainingLengthSize = MqttBufferWriter.GetVariableByteIntegerSize(remainingLength);
@@ -53,18 +55,13 @@ namespace MQTTnet.Formatter.V5
_bufferWriter.WriteVariableByteInteger(remainingLength);
var buffer = _bufferWriter.GetBuffer();
var firstSegment = new ArraySegment<byte>(buffer, headerOffset, _bufferWriter.Length - headerOffset);
if (publishPacket?.Payload != null)
{
var payloadSegment = new ArraySegment<byte>(publishPacket.Payload, 0, publishPacket.Payload.Length);
return new MqttPacketBuffer(firstSegment, payloadSegment);
}
return new MqttPacketBuffer(firstSegment);
return payloadSegment == null
? new MqttPacketBuffer(firstSegment)
: new MqttPacketBuffer(firstSegment, payloadSegment.Value);
}
byte EncodeAuthPacket(MqttAuthPacket packet)
{
_bufferWriter.WriteByte((byte)packet.ReasonCode);
@@ -40,7 +40,12 @@ namespace MQTTnet.Implementations
var bufferWriter = new MqttBufferWriter(options.WriterBufferSize, options.WriterBufferSizeMax);
var packetFormatterAdapter = new MqttPacketFormatterAdapter(options.ProtocolVersion, bufferWriter);
return new MqttChannelAdapter(channel, packetFormatterAdapter, packetInspector, logger);
return new MqttChannelAdapter(channel, packetFormatterAdapter, logger)
{
AllowPacketFragmentation = options.AllowPacketFragmentation,
PacketInspector = packetInspector
};
}
}
}
@@ -95,7 +95,7 @@ namespace MQTTnet.Implementations
var packetFormatterAdapter = new MqttPacketFormatterAdapter(bufferWriter);
var tcpChannel = new MqttTcpChannel(args.Socket, clientCertificate, _options);
using (var clientAdapter = new MqttChannelAdapter(tcpChannel, packetFormatterAdapter, null, _rootLogger))
using (var clientAdapter = new MqttChannelAdapter(tcpChannel, packetFormatterAdapter, _rootLogger))
{
await clientHandler(clientAdapter).ConfigureAwait(false);
}
@@ -229,7 +229,7 @@ namespace MQTTnet.Implementations
var bufferWriter = new MqttBufferWriter(_serverOptions.WriterBufferSize, _serverOptions.WriterBufferSizeMax);
var packetFormatterAdapter = new MqttPacketFormatterAdapter(bufferWriter);
using (var clientAdapter = new MqttChannelAdapter(tcpChannel, packetFormatterAdapter, null, _rootLogger))
using (var clientAdapter = new MqttChannelAdapter(tcpChannel, packetFormatterAdapter, _rootLogger))
{
await clientHandler(clientAdapter).ConfigureAwait(false);
}
@@ -63,7 +63,7 @@ namespace MQTTnet.Implementations
await clientWebSocket.ConnectAsync(new Uri(uri), cancellationToken).ConfigureAwait(false);
}
catch (Exception)
catch
{
// Prevent a memory leak when always creating new instance which will fail while connecting.
clientWebSocket.Dispose();
@@ -103,7 +103,7 @@ namespace MQTTnet.Implementations
public async Task WriteAsync(ArraySegment<byte> buffer, bool isEndOfPacket, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
#if NET5_0_OR_GREATER
// MQTT Control Packets MUST be sent in WebSocket binary data frames. If any other type of data frame is received the recipient MUST close the Network Connection [MQTT-6.0.0-1].
// A single WebSocket data frame can contain multiple or partial MQTT Control Packets. The receiver MUST NOT assume that MQTT Control Packets are aligned on WebSocket frame boundaries [MQTT-6.0.0-2].
@@ -9,7 +9,7 @@ namespace MQTTnet.LowLevelClient
{
public interface ILowLevelMqttClient : IDisposable
{
event Func<InspectMqttPacketEventArgs, Task> InspectPackage;
event Func<InspectMqttPacketEventArgs, Task> InspectPacketAsync;
bool IsConnected { get; }
@@ -32,7 +32,7 @@ namespace MQTTnet.LowLevelClient
_logger = logger.WithSource(nameof(LowLevelMqttClient));
}
public event Func<InspectMqttPacketEventArgs, Task> InspectPackage
public event Func<InspectMqttPacketEventArgs, Task> InspectPacketAsync
{
add => _inspectPacketEvent.AddHandler(value);
remove => _inspectPacketEvent.RemoveHandler(value);
@@ -52,13 +52,19 @@ namespace MQTTnet.LowLevelClient
throw new InvalidOperationException("Low level MQTT client is already connected. Disconnect first before connecting again.");
}
var newAdapter = _clientAdapterFactory.CreateClientAdapter(options, new MqttPacketInspector(_inspectPacketEvent, _rootLogger), _rootLogger);
MqttPacketInspector packetInspector = null;
if (_inspectPacketEvent.HasHandlers)
{
packetInspector = new MqttPacketInspector(_inspectPacketEvent, _rootLogger);
}
var newAdapter = _clientAdapterFactory.CreateClientAdapter(options, packetInspector, _rootLogger);
try
{
_logger.Verbose("Trying to connect with server '{0}'.", options.ChannelOptions);
_logger.Verbose("Trying to connect with server '{0}'", options.ChannelOptions);
await newAdapter.ConnectAsync(cancellationToken).ConfigureAwait(false);
_logger.Verbose("Connection with server established.");
_logger.Verbose("Connection with server established");
}
catch (Exception)
{
+52 -4
View File
@@ -2,14 +2,19 @@
// 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.Collections.Generic;
using MQTTnet.Internal;
using MQTTnet.Packets;
using MQTTnet.Protocol;
using System;
using System.Collections.Generic;
namespace MQTTnet
{
public sealed class MqttApplicationMessage
{
byte[] _payloadCache;
ArraySegment<byte> _payloadSegment = EmptyBuffer.ArraySegment;
/// <summary>
/// Gets or sets the content type.
/// The content type must be a UTF-8 encoded string. The content type value identifies the kind of UTF-8 encoded
@@ -48,10 +53,53 @@ namespace MQTTnet
public uint MessageExpiryInterval { get; set; }
/// <summary>
/// Gets or sets the payload.
/// The payload is the data bytes sent via the MQTT protocol.
/// Gets or sets the payload.
/// The payload is the data bytes sent via the MQTT protocol.
/// </summary>
public byte[] Payload { get; set; }
[Obsolete("Use PayloadSegment instead. This property will be removed in a future release.")]
public byte[] Payload
{
get
{
if (_payloadSegment.Array == null)
{
return null;
}
// just reference from _payloadSegment.Array
if (_payloadSegment.Count == _payloadSegment.Array.Length)
{
return _payloadSegment.Array;
}
// copy from _payloadSegment
if (_payloadCache == null)
{
_payloadCache = new byte[_payloadSegment.Count];
MqttMemoryHelper.Copy(_payloadSegment.Array, _payloadSegment.Offset, _payloadCache, 0, _payloadCache.Length);
}
return _payloadCache;
}
set
{
_payloadCache = null;
_payloadSegment = value == null || value.Length == 0 ? EmptyBuffer.ArraySegment : new ArraySegment<byte>(value);
}
}
/// <summary>
/// Get or set ArraySegment style of Payload.
/// </summary>
public ArraySegment<byte> PayloadSegment
{
get => _payloadSegment;
set
{
_payloadCache = null;
_payloadSegment = value;
}
}
/// <summary>
/// Gets or sets the payload format indicator.
+58 -43
View File
@@ -2,13 +2,16 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using MQTTnet.Exceptions;
using MQTTnet.Internal;
using MQTTnet.Packets;
using MQTTnet.Protocol;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using MQTTnet.Exceptions;
using MQTTnet.Packets;
using MQTTnet.Protocol;
namespace MQTTnet
{
@@ -17,7 +20,8 @@ namespace MQTTnet
string _contentType;
byte[] _correlationData;
uint _messageExpiryInterval;
byte[] _payload;
ArraySegment<byte> _payloadSegment;
MqttPayloadFormatIndicator _payloadFormatIndicator;
MqttQualityOfServiceLevel _qualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce;
string _responseTopic;
@@ -37,7 +41,7 @@ namespace MQTTnet
var applicationMessage = new MqttApplicationMessage
{
Topic = _topic,
Payload = _payload,
PayloadSegment = _payloadSegment,
QualityOfServiceLevel = _qualityOfServiceLevel,
Retain = _retain,
ContentType = _contentType,
@@ -85,7 +89,15 @@ namespace MQTTnet
public MqttApplicationMessageBuilder WithPayload(byte[] payload)
{
_payload = payload;
_payloadSegment = payload == null || payload.Length == 0
? EmptyBuffer.ArraySegment
: new ArraySegment<byte>(payload);
return this;
}
public MqttApplicationMessageBuilder WithPayloadSegment(ArraySegment<byte> payloadSegment)
{
_payloadSegment = payloadSegment;
return this;
}
@@ -93,70 +105,73 @@ namespace MQTTnet
{
if (payload == null)
{
_payload = null;
return this;
return WithPayload(default(byte[]));
}
_payload = payload as byte[] ?? payload.ToArray();
if (payload is byte[] byteArray)
{
return WithPayload(byteArray);
}
return this;
if (payload is ArraySegment<byte> arraySegment)
{
return WithPayloadSegment(arraySegment);
}
return WithPayload(payload.ToArray());
}
public MqttApplicationMessageBuilder WithPayload(Stream payload)
{
if (payload == null)
{
_payload = null;
return this;
}
return WithPayload(payload, payload.Length - payload.Position);
return payload == null
? WithPayload(default(byte[]))
: WithPayload(payload, payload.Length - payload.Position);
}
public MqttApplicationMessageBuilder WithPayload(Stream payload, long length)
{
if (payload == null)
if (payload == null || length == 0)
{
_payload = null;
return this;
return WithPayload(default(byte[]));
}
if (payload.Length == 0)
var payloadBuffer = new byte[length];
var totalRead = 0;
do
{
_payload = null;
}
else
{
_payload = new byte[length];
var totalRead = 0;
do
var bytesRead = payload.Read(payloadBuffer, totalRead, payloadBuffer.Length - totalRead);
if (bytesRead == 0)
{
var bytesRead = payload.Read(_payload, totalRead, _payload.Length - totalRead);
if (bytesRead == 0)
{
break;
}
break;
}
totalRead += bytesRead;
} while (totalRead < length);
}
totalRead += bytesRead;
} while (totalRead < length);
return this;
return WithPayload(payloadBuffer);
}
public MqttApplicationMessageBuilder WithPayload(string payload)
{
if (payload == null)
if (string.IsNullOrEmpty(payload))
{
_payload = null;
return this;
return WithPayload(default(byte[]));
}
_payload = string.IsNullOrEmpty(payload) ? null : Encoding.UTF8.GetBytes(payload);
return this;
var payloadBuffer = Encoding.UTF8.GetBytes(payload);
return WithPayload(payloadBuffer);
}
#if NETCOREAPP3_1_OR_GREATER || NETSTANDARD2_1
public MqttApplicationMessageBuilder WithPayloadSegment(ReadOnlyMemory<byte> payloadSegment)
{
return MemoryMarshal.TryGetArray(payloadSegment, out var segment)
? WithPayloadSegment(segment)
: WithPayload(payloadSegment.ToArray());
}
#endif
/// <summary>
/// Adds the payload format indicator to the message.
/// <remarks>MQTT 5.0.0+ feature.</remarks>
@@ -11,19 +11,13 @@ namespace MQTTnet
{
public static string ConvertPayloadToString(this MqttApplicationMessage applicationMessage)
{
if (applicationMessage == null) throw new ArgumentNullException(nameof(applicationMessage));
if (applicationMessage.Payload == null)
if (applicationMessage == null)
{
return null;
throw new ArgumentNullException(nameof(applicationMessage));
}
if (applicationMessage.Payload.Length == 0)
{
return string.Empty;
}
return Encoding.UTF8.GetString(applicationMessage.Payload, 0, applicationMessage.Payload.Length);
var payloadSegment = applicationMessage.PayloadSegment;
return Encoding.UTF8.GetString(payloadSegment.Array, payloadSegment.Offset, payloadSegment.Count);
}
}
}
+4 -3
View File
@@ -2,6 +2,7 @@
// 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 MQTTnet.Protocol;
@@ -17,10 +18,10 @@ namespace MQTTnet.Packets
public uint MessageExpiryInterval { get; set; }
public byte[] Payload { get; set; }
public MqttPayloadFormatIndicator PayloadFormatIndicator { get; set; } = MqttPayloadFormatIndicator.Unspecified;
public ArraySegment<byte> PayloadSegment { get; set; }
public MqttQualityOfServiceLevel QualityOfServiceLevel { get; set; } = MqttQualityOfServiceLevel.AtMostOnce;
public string ResponseTopic { get; set; }
@@ -38,7 +39,7 @@ namespace MQTTnet.Packets
public override string ToString()
{
return
$"Publish: [Topic={Topic}] [Payload.Length={Payload?.Length}] [QoSLevel={QualityOfServiceLevel}] [Dup={Dup}] [Retain={Retain}] [PacketIdentifier={PacketIdentifier}]";
$"Publish: [Topic={Topic}] [PayloadLength={PayloadSegment.Count}] [QoSLevel={QualityOfServiceLevel}] [Dup={Dup}] [Retain={Retain}] [PacketIdentifier={PacketIdentifier}]";
}
}
}
@@ -0,0 +1,35 @@
// 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;
namespace MQTTnet.Server
{
public sealed class InterceptingClientApplicationMessageEnqueueEventArgs : EventArgs
{
public InterceptingClientApplicationMessageEnqueueEventArgs(string senderClientId, string receiverClientId, MqttApplicationMessage applicationMessage)
{
SenderClientId = senderClientId ?? throw new ArgumentNullException(nameof(senderClientId));
ReceiverClientId = receiverClientId ?? throw new ArgumentNullException(nameof(receiverClientId));
ApplicationMessage = applicationMessage ?? throw new ArgumentNullException(nameof(applicationMessage));
}
/// <summary>
/// Gets or sets whether the enqueue of the application message should be performed or not.
/// If set to _False_ the client will not receive the application message.
/// </summary>
public bool AcceptEnqueue { get; set; } = true;
public MqttApplicationMessage ApplicationMessage { get; }
/// <summary>
/// Indicates if the connection with the sender should be closed.
/// </summary>
public bool CloseSenderConnection { get; set; }
public string ReceiverClientId { get; }
public string SenderClientId { get; }
}
}
@@ -16,12 +16,14 @@ namespace MQTTnet.Server
CancellationToken cancellationToken,
string clientId,
MqttSessionStatus session,
MqttTopicFilter topicFilter)
MqttTopicFilter topicFilter,
List<MqttUserProperty> userProperties)
{
CancellationToken = cancellationToken;
ClientId = clientId;
Session = session;
TopicFilter = topicFilter;
UserProperties = userProperties;
}
/// <summary>
@@ -75,6 +77,7 @@ namespace MQTTnet.Server
/// <summary>
/// Gets or sets the user properties.
/// <remarks>MQTT 5.0.0+ feature.</remarks>
/// </summary>
public List<MqttUserProperty> UserProperties { get; set; }
}
@@ -4,18 +4,21 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using MQTTnet.Packets;
namespace MQTTnet.Server
{
public sealed class InterceptingUnsubscriptionEventArgs : EventArgs
{
public InterceptingUnsubscriptionEventArgs(CancellationToken cancellationToken, string clientId, IDictionary sessionItems, string topic)
public InterceptingUnsubscriptionEventArgs(CancellationToken cancellationToken, string clientId, IDictionary sessionItems, string topic, List<MqttUserProperty> userProperties)
{
CancellationToken = cancellationToken;
ClientId = clientId;
SessionItems = sessionItems;
Topic = topic;
UserProperties = userProperties;
}
/// <summary>
@@ -59,5 +62,11 @@ namespace MQTTnet.Server
/// separator).
/// </summary>
public string Topic { get; }
/// <summary>
/// Gets or sets the user properties.
/// <remarks>MQTT 5.0.0+ feature.</remarks>
/// </summary>
public List<MqttUserProperty> UserProperties { get; set; }
}
}
+2 -3
View File
@@ -94,10 +94,9 @@ namespace MQTTnet.Server
try
{
var cancellationToken = _cancellationToken.Token;
_ = Task.Factory.StartNew(() => SendPacketsLoop(cancellationToken), cancellationToken, TaskCreationOptions.PreferFairness, TaskScheduler.Default).ConfigureAwait(false);
IsRunning = true;
_ = Task.Factory.StartNew(() => SendPacketsLoop(cancellationToken), cancellationToken, TaskCreationOptions.PreferFairness, TaskScheduler.Default).ConfigureAwait(false);
await ReceivePackagesLoop(cancellationToken).ConfigureAwait(false);
}
@@ -179,7 +179,7 @@ namespace MQTTnet.Server
}
// Calculate application message topic hash once for subscription checks
MqttSubscription.CalculateTopicHash(applicationMessage.Topic, out var topicHash, out _, out _);
MqttTopicHash.Calculate(applicationMessage.Topic, out var topicHash, out _, out _);
foreach (var session in subscriberSessions)
{
@@ -199,6 +199,18 @@ namespace MQTTnet.Server
{
continue;
}
if (_eventContainer.InterceptingClientEnqueueEvent.HasHandlers)
{
var eventArgs = new InterceptingClientApplicationMessageEnqueueEventArgs(senderId, session.Id, applicationMessage);
await _eventContainer.InterceptingClientEnqueueEvent.InvokeAsync(eventArgs).ConfigureAwait(false);
if (!eventArgs.AcceptEnqueue)
{
// Continue checking the other subscriptions
continue;
}
}
var publishPacketCopy = MqttPacketFactories.Publish.Create(applicationMessage);
publishPacketCopy.QualityOfServiceLevel = checkSubscriptionsResult.QualityOfServiceLevel;
@@ -219,10 +231,10 @@ namespace MQTTnet.Server
publishPacketCopy.Retain = false;
}
session.EnqueueDataPacket(new MqttPacketBusItem(publishPacketCopy));
matchingSubscribersCount++;
_logger.Verbose("Client '{0}': Queued PUBLISH packet with topic '{1}'.", session.Id, applicationMessage.Topic);
session.EnqueueDataPacket(new MqttPacketBusItem(publishPacketCopy));
_logger.Verbose("Client '{0}': Queued PUBLISH packet with topic '{1}'", session.Id, applicationMessage.Topic);
}
if (matchingSubscribersCount == 0)
@@ -233,7 +245,7 @@ namespace MQTTnet.Server
}
catch (Exception exception)
{
_logger.Error(exception, "Unhandled exception while processing next queued application message.");
_logger.Error(exception, "Error while processing next queued application message");
}
}
@@ -592,13 +604,8 @@ namespace MQTTnet.Server
if (_eventContainer.ClientDisconnectedEvent.HasHandlers)
{
var eventArgs = new ClientDisconnectedEventArgs(
oldClient.Id,
null,
MqttClientDisconnectType.Takeover,
oldClient.Endpoint,
oldClient.Session.Items);
var eventArgs = new ClientDisconnectedEventArgs(oldClient.Id, null, MqttClientDisconnectType.Takeover, oldClient.Endpoint, oldClient.Session.Items);
await _eventContainer.ClientDisconnectedEvent.TryInvokeAsync(eventArgs, _logger).ConfigureAwait(false);
}
}
@@ -177,15 +177,15 @@ namespace MQTTnet.Server
// lower one.
foreach (var topicFilterItem in subscribePacket.TopicFilters.OrderByDescending(f => f.QualityOfServiceLevel))
{
var subscriptionEventArgs = await InterceptSubscribe(topicFilterItem, cancellationToken).ConfigureAwait(false);
var topicFilter = subscriptionEventArgs.TopicFilter;
var processSubscription = subscriptionEventArgs.ProcessSubscription && subscriptionEventArgs.Response.ReasonCode <= MqttSubscribeReasonCode.GrantedQoS2;
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 = subscriptionEventArgs.UserProperties;
result.ReasonString = subscriptionEventArgs.ReasonString;
result.ReasonCodes.Add(subscriptionEventArgs.Response.ReasonCode);
result.UserProperties = interceptorEventArgs.UserProperties;
result.ReasonString = interceptorEventArgs.ReasonString;
result.ReasonCodes.Add(interceptorEventArgs.Response.ReasonCode);
if (subscriptionEventArgs.CloseConnection)
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!
@@ -197,7 +197,7 @@ namespace MQTTnet.Server
continue;
}
var createSubscriptionResult = CreateSubscription(topicFilter, subscribePacket.SubscriptionIdentifier, subscriptionEventArgs.Response.ReasonCode);
var createSubscriptionResult = CreateSubscription(topicFilter, subscribePacket.SubscriptionIdentifier, interceptorEventArgs.Response.ReasonCode);
addedSubscriptions.Add(topicFilter.Topic);
finalTopicFilters.Add(topicFilter);
@@ -238,12 +238,13 @@ namespace MQTTnet.Server
{
_subscriptions.TryGetValue(topicFilter, out var existingSubscription);
var interceptorContext = await InterceptUnsubscribe(topicFilter, existingSubscription, cancellationToken).ConfigureAwait(false);
var acceptUnsubscription = interceptorContext.Response.ReasonCode == MqttUnsubscribeReasonCode.Success;
var interceptorEventArgs = await InterceptUnsubscribe(topicFilter, existingSubscription, unsubscribePacket.UserProperties, cancellationToken).ConfigureAwait(false);
var acceptUnsubscription = interceptorEventArgs.Response.ReasonCode == MqttUnsubscribeReasonCode.Success;
result.ReasonCodes.Add(interceptorContext.Response.ReasonCode);
result.UserProperties = interceptorEventArgs.UserProperties;
result.ReasonCodes.Add(interceptorEventArgs.Response.ReasonCode);
if (interceptorContext.CloseConnection)
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!
@@ -255,7 +256,7 @@ namespace MQTTnet.Server
continue;
}
if (interceptorContext.ProcessUnsubscription)
if (interceptorEventArgs.ProcessUnsubscription)
{
_subscriptions.Remove(topicFilter);
@@ -342,7 +343,7 @@ namespace MQTTnet.Server
using (_subscriptionsLock.EnterAsync(CancellationToken.None).GetAwaiter().GetResult())
{
MqttSubscription.CalculateTopicHash(topicFilter.Topic, out var topicHash, out var topicHashMask, out var hasWildcard);
MqttTopicHash.Calculate(topicFilter.Topic, out var topicHash, out _, out var hasWildcard);
if (_subscriptions.TryGetValue(topicFilter.Topic, out var existingSubscription))
{
@@ -452,9 +453,12 @@ namespace MQTTnet.Server
}
}
async Task<InterceptingSubscriptionEventArgs> InterceptSubscribe(MqttTopicFilter topicFilter, CancellationToken cancellationToken)
async Task<InterceptingSubscriptionEventArgs> InterceptSubscribe(
MqttTopicFilter topicFilter,
List<MqttUserProperty> userProperties,
CancellationToken cancellationToken)
{
var eventArgs = new InterceptingSubscriptionEventArgs(cancellationToken, _session.Id, new MqttSessionStatus(_session), topicFilter);
var eventArgs = new InterceptingSubscriptionEventArgs(cancellationToken, _session.Id, new MqttSessionStatus(_session), topicFilter, userProperties);
if (topicFilter.QualityOfServiceLevel == MqttQualityOfServiceLevel.AtMostOnce)
{
@@ -481,9 +485,13 @@ namespace MQTTnet.Server
return eventArgs;
}
async Task<InterceptingUnsubscriptionEventArgs> InterceptUnsubscribe(string topicFilter, MqttSubscription mqttSubscription, CancellationToken cancellationToken)
async Task<InterceptingUnsubscriptionEventArgs> InterceptUnsubscribe(
string topicFilter,
MqttSubscription mqttSubscription,
List<MqttUserProperty> userProperties,
CancellationToken cancellationToken)
{
var clientUnsubscribingTopicEventArgs = new InterceptingUnsubscriptionEventArgs(cancellationToken, _session.Id, _session.Items, topicFilter)
var clientUnsubscribingTopicEventArgs = new InterceptingUnsubscriptionEventArgs(cancellationToken, _session.Id, _session.Items, topicFilter, userProperties)
{
Response =
{
@@ -22,18 +22,20 @@ namespace MQTTnet.Server
public MqttRetainedMessagesManager(MqttServerEventContainer eventContainer, IMqttNetLogger logger)
{
_eventContainer = eventContainer ?? throw new ArgumentNullException(nameof(eventContainer));
if (logger == null) throw new ArgumentNullException(nameof(logger));
if (logger == null)
throw new ArgumentNullException(nameof(logger));
_logger = logger.WithSource(nameof(MqttRetainedMessagesManager));
}
public async Task Start()
{
try
{
var eventArgs = new LoadingRetainedMessagesEventArgs();
await _eventContainer.LoadingRetainedMessagesEvent.InvokeAsync(eventArgs).ConfigureAwait(false);
lock (_messages)
{
_messages.Clear();
@@ -64,10 +66,11 @@ namespace MQTTnet.Server
{
List<MqttApplicationMessage> messagesForSave = null;
var saveIsRequired = false;
lock (_messages)
{
var hasPayload = applicationMessage.Payload != null && applicationMessage.Payload.Length > 0;
var payloadSegment = applicationMessage.PayloadSegment;
var hasPayload = payloadSegment.Count > 0;
if (!hasPayload)
{
@@ -83,7 +86,7 @@ namespace MQTTnet.Server
}
else
{
if (existingMessage.QualityOfServiceLevel != applicationMessage.QualityOfServiceLevel || !existingMessage.Payload.SequenceEqual(applicationMessage.Payload ?? EmptyBuffer.Array))
if (existingMessage.QualityOfServiceLevel != applicationMessage.QualityOfServiceLevel || !SequenceEqual(existingMessage.PayloadSegment, payloadSegment))
{
_messages[applicationMessage.Topic] = applicationMessage;
saveIsRequired = true;
@@ -113,7 +116,7 @@ namespace MQTTnet.Server
_logger.Error(exception, "Unhandled exception while handling retained messages.");
}
}
public Task<IList<MqttApplicationMessage>> GetMessages()
{
lock (_messages)
@@ -123,6 +126,19 @@ namespace MQTTnet.Server
}
}
public Task<MqttApplicationMessage> GetMessage(string topic)
{
lock (_messages)
{
if (_messages.TryGetValue(topic, out var message))
{
return Task.FromResult(message);
}
return null;
}
}
public async Task ClearMessages()
{
lock (_messages)
@@ -135,5 +151,14 @@ namespace MQTTnet.Server
await _eventContainer.RetainedMessagesClearedEvent.InvokeAsync(EventArgs.Empty).ConfigureAwait(false);
}
}
private static bool SequenceEqual(ArraySegment<byte> source, ArraySegment<byte> target)
{
#if NETCOREAPP3_1_OR_GREATER || NETSTANDARD2_1
return source.AsSpan().SequenceEqual(target);
#else
return source.Count == target.Count && Enumerable.SequenceEqual(source, target);
#endif
}
}
}
}
@@ -21,12 +21,14 @@ namespace MQTTnet.Server
public AsyncEvent<ClientUnsubscribedTopicEventArgs> ClientUnsubscribedTopicEvent { get; } = new AsyncEvent<ClientUnsubscribedTopicEventArgs>();
public AsyncEvent<InterceptingClientApplicationMessageEnqueueEventArgs> InterceptingClientEnqueueEvent { get; } = new AsyncEvent<InterceptingClientApplicationMessageEnqueueEventArgs>();
public AsyncEvent<InterceptingPacketEventArgs> InterceptingInboundPacketEvent { get; } = new AsyncEvent<InterceptingPacketEventArgs>();
public AsyncEvent<InterceptingPacketEventArgs> InterceptingOutboundPacketEvent { get; } = new AsyncEvent<InterceptingPacketEventArgs>();
public AsyncEvent<InterceptingPublishEventArgs> InterceptingPublishEvent { get; } = new AsyncEvent<InterceptingPublishEventArgs>();
public AsyncEvent<InterceptingSubscriptionEventArgs> InterceptingSubscriptionEvent { get; } = new AsyncEvent<InterceptingSubscriptionEventArgs>();
public AsyncEvent<InterceptingUnsubscriptionEventArgs> InterceptingUnsubscriptionEvent { get; } = new AsyncEvent<InterceptingUnsubscriptionEventArgs>();
@@ -6,163 +6,6 @@ using MQTTnet.Protocol;
namespace MQTTnet.Server
{
/*
* The MqttSubscription object stores subscription parameters and calculates
* topic hashes.
*
* Use of Topic Hashes to improve message processing performance
* =============================================================
*
* Motivation
* -----------
* In a typical use case for MQTT there may be many publishers (sensors or
* other devices in the field) and few subscribers (monitoring all or many topics).
* Each publisher may have one or more topic(s) to publish and therefore both, the
* number of publishers and the number of topics may be large.
*
* Maintaining subscribers in a separate container
* -----------------------------------------------
* Instead of placing all sessions into a single _sessions container, subscribers
* are added into another _subscriberSessions container (if a client is a
* subscriber and a publisher then the client is present in both containers). The
* cost is some additional bookkeeping work upon subscription where each client
* session needs to maintain a list of subscribed topics.
*
* When the client subscribes to the first topic, then the session manager adds
* the client to the _subscriberSessions container, and when the client
* unsubscribes from the last topic then the session manager removes the client
* from the container. Now, when an application message arrives, only the list of
* subscribers need processing instead of looping through potentially thousands of
* publishers.
*
* Improving subscriber topic lookup
* ---------------------------------
* For each subscriber, it needs to be determined whether an application message
* matches any topic the subscriber has subscribed to. There may only be few
* subscribers but there may be many subscribed topics, including wildcard topics.
*
* The implemented approach uses a topic hash and a hash mask calculated on the
* subscribed topic and the published topic (the application message topic) to
* find candidates for a match, with the existing match logic evaluating a reduced
* number of candidates.
*
* For each subscription, the topic hash and a hash mask is stored with the
* subscription, and for each application message received, the hash is calculated
* for the published topic before attempting to find matching subscriptions. The
* hash calculation itself is simple and does not have a large performance impact.
*
* We'll first explain how topic hashes and hash masks are constructed and then how
* they are used.
*
* Topic hash
* ----------
* Topic hashes are stored as 64-bit numbers. Each byte within the 64-bit number
* relates to one MQTT topic level. A checksum is calculated for each topic level
* by iterating over the characters within the topic level (cast to byte) and the
* result is stored into the corresponding byte of the 64-bit number. If a topic
* level contains a wildcard character, then 0x00 is stored instead of the
* checksum.
*
* If there are less than 8 levels then the rest of the 64-bit number is filled
* with 0xff. If there are more than 8 levels then topics where the first 8 MQTT
* topic levels are identical will have the same hash value.
*
* This is the topic hash for the MQTT topic below: 0x655D4AF1FFFFFF
*
* client1/building1/level1/sensor1 (empty) (empty) (empty) (empty)
* \_____/ \_______/ \____/ \_____/ \_____/ \_____/ \_____/ \_____/
* | | | | | | | |
* 0x65 0x5D 0x4A 0xF1 0xFF 0xFF 0xFF 0xFF
*
* This is the topic hash for an MQTT topic containing a wildcard: 0x655D00F1FFFFFF
*
* client1/building1/ + /sensor1 (empty) (empty) (empty) (empty)
* \_____/ \_______/ \_/ \_____/ \_____/ \_____/ \_____/ \_____/
* | | | | | | | |
* 0x65 0x5D 0 0xF1 0xFF 0xFF 0xFF 0xFF
*
* For topics that contain the multi level wildcard # at the end, the topic hash
* is filled with 0x00: 0x65004A00000000
*
* client1/ + /level1/ # (empty) (empty) (empty) (empty)
* \_____/ \_/ \____/ \_/ \_____/ \_____/ \_____/ \_____/
* | | | | | | | |
* 0x65 0 0x4A 0 0 0 0 0
*
*
* Topic hash mask
* ---------------
* The hash mask simply contains 0xFF for non-wildcard topic levels and 0x00 for
* wildcard topic levels. Here are the topic hash masks for the examples above.
*
* client1/building1/level1/sensor1 (empty) (empty) (empty) (empty)
* \_____/ \_______/ \____/ \_____/ \_____/ \_____/ \_____/ \_____/
* | | | | | | | |
* 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF
*
* client1/building1/ + /sensor1 (empty) (empty) (empty) (empty)
* \_____/ \_______/ \_/ \_____/ \_____/ \_____/ \_____/ \_____/
* | | | | | | | |
* 0xFF 0xFF 0 0xFF 0xFF 0xFF 0xFF 0xFF
*
* client1/ + /level1/ # (empty) (empty) (empty) (empty)
* \_____/ \_/ \____/ \_/ \_____/ \_____/ \_____/ \_____/
* | | | | | | | |
* 0xFF 0 0xFF 0 0 0 0 0
*
*
* Topic hash and hash mask properties
* -----------------------------------
* The following properties of topic hashes and hash masks can be exploited to
* find potentially matching subscribed topics for a given published topic.
*
* (1) If a subscribed topic does not contain wildcards then the topic hash of the
* subscribed topic must be equal to the topic hash of the published topic,
* otherwise the subscribed topic cannot be a candidate for a match.
*
* (2) If a subscribed topic contains wildcards then the hash of the published
* topic masked with the subscribed topic's hash mask must be equal to the hash of
* the subscribed topic. I.e. a subscribed topic is a candidate for a match if:
* (publishedTopicHash & subscribedTopicHashMask) == subscribedTopicHash
*
* (3) If a subscribed topic contains wildcards then any potentially matching
* published topic must have a hash value that is greater than or equal to the
* hash value of the subscribed topic (because the subscribed topic contains
* zeroes in wildcard positions).
*
* Match finding
* -------------
* The subscription manager maintains two separate dictionaries to assist finding
* matches using topic hashes: a _noWildcardSubscriptionsByTopicHash dictionary
* containing all subscriptions that do not have wildcards, and a
* _wildcardSubscriptionsByTopicHash dictionary containing subscriptions with
* wildcards.
*
* For subscriptions without wildcards, all potential candidates for a match are
* obtained by a single look-up (exploiting point 1 above).
*
* For subscriptions with wildcards, the subscription manager loops through the
* wildcard subscriptions and selects candidates that satisfy condition
* (publishedTopicHash & subscribedTopicMask) == subscribedTopicHash (point 2).
* The loop could exit early if wildcard subscriptions were stored into a sorted
* dictionary (utilizing point 3), but, after testing, there does not seem to be
* any real benefit doing so.
*
* Other considerations
* --------------------
* Characters in the topic string are cast to byte and any additional bytes in a
* multi-byte character are disregarded. Best guess is that this does not impact
* performance in practice.
*
* Instead of one-byte checksums per topic level, one-word checksums per topic
* level could be used. If most topics contained four levels or less then hash
* buckets would be shallower.
*
* For very large numbers of topics, performing a parallel search may help further.
*
* To also handle a larger number of subscribers, it may be beneficial to maintain
* a subscribers-by-subscription-topic dictionary.
*/
public sealed class MqttSubscription
{
public MqttSubscription(
@@ -180,7 +23,7 @@ namespace MQTTnet.Server
GrantedQualityOfServiceLevel = qualityOfServiceLevel;
Identifier = identifier;
CalculateTopicHash(Topic, out var hash, out var hashMask, out var hasWildcard);
MqttTopicHash.Calculate(Topic, out var hash, out var hashMask, out var hasWildcard);
TopicHash = hash;
TopicHashMask = hashMask;
TopicHasWildcard = hasWildcard;
@@ -203,107 +46,5 @@ namespace MQTTnet.Server
public ulong TopicHashMask { get; }
public bool TopicHasWildcard { get; }
public static void CalculateTopicHash(string topic, out ulong resultHash, out ulong resultHashMask, out bool resultHasWildcard)
{
// calculate topic hash
ulong hash = 0;
ulong hashMaskInverted = 0;
ulong levelBitMask = 0;
ulong fillLevelBitMask = 0;
var hasWildcard = false;
byte checkSum = 0;
var level = 0;
var i = 0;
while (i < topic.Length)
{
var c = topic[i];
if (c == MqttTopicFilterComparer.LevelSeparator)
{
// done with this level
hash <<= 8;
hash |= checkSum;
hashMaskInverted <<= 8;
hashMaskInverted |= levelBitMask;
checkSum = 0;
levelBitMask = 0;
++level;
if (level >= 8)
{
break;
}
}
else if (c == MqttTopicFilterComparer.SingleLevelWildcard)
{
levelBitMask = 0xff;
hasWildcard = true;
}
else if (c == MqttTopicFilterComparer.MultiLevelWildcard)
{
// checksum is zero for a valid topic
levelBitMask = 0xff;
// fill rest with this fillLevelBitMask
fillLevelBitMask = 0xff;
hasWildcard = true;
break;
}
else
{
// The checksum should be designed to reduce the hash bucket depth for the expected
// fairly regularly named MQTT topics that don't differ much,
// i.e. "room1/sensor1"
// "room1/sensor2"
// "room1/sensor3"
// etc.
if ((c & 1) == 0)
{
checkSum += (byte)c;
}
else
{
checkSum ^= (byte)(c >> 1);
}
}
++i;
}
// Shift hash left and leave zeroes to fill ulong
if (level < 8)
{
hash <<= 8;
hash |= checkSum;
hashMaskInverted <<= 8;
hashMaskInverted |= levelBitMask;
++level;
while (level < 8)
{
hash <<= 8;
hashMaskInverted <<= 8;
hashMaskInverted |= fillLevelBitMask;
++level;
}
}
if (!hasWildcard)
{
while (i < topic.Length)
{
var c = topic[i];
if (c == MqttTopicFilterComparer.SingleLevelWildcard || c == MqttTopicFilterComparer.MultiLevelWildcard)
{
hasWildcard = true;
break;
}
++i;
}
}
resultHash = hash;
resultHashMask = ~hashMaskInverted;
resultHasWildcard = hasWildcard;
}
}
}
@@ -0,0 +1,268 @@
// 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.
namespace MQTTnet.Server
{
/*
* The MqttSubscription object stores subscription parameters and calculates
* topic hashes.
*
* Use of Topic Hashes to improve message processing performance
* =============================================================
*
* Motivation
* -----------
* In a typical use case for MQTT there may be many publishers (sensors or
* other devices in the field) and few subscribers (monitoring all or many topics).
* Each publisher may have one or more topic(s) to publish and therefore both, the
* number of publishers and the number of topics may be large.
*
* Maintaining subscribers in a separate container
* -----------------------------------------------
* Instead of placing all sessions into a single _sessions container, subscribers
* are added into another _subscriberSessions container (if a client is a
* subscriber and a publisher then the client is present in both containers). The
* cost is some additional bookkeeping work upon subscription where each client
* session needs to maintain a list of subscribed topics.
*
* When the client subscribes to the first topic, then the session manager adds
* the client to the _subscriberSessions container, and when the client
* unsubscribes from the last topic then the session manager removes the client
* from the container. Now, when an application message arrives, only the list of
* subscribers need processing instead of looping through potentially thousands of
* publishers.
*
* Improving subscriber topic lookup
* ---------------------------------
* For each subscriber, it needs to be determined whether an application message
* matches any topic the subscriber has subscribed to. There may only be few
* subscribers but there may be many subscribed topics, including wildcard topics.
*
* The implemented approach uses a topic hash and a hash mask calculated on the
* subscribed topic and the published topic (the application message topic) to
* find candidates for a match, with the existing match logic evaluating a reduced
* number of candidates.
*
* For each subscription, the topic hash and a hash mask is stored with the
* subscription, and for each application message received, the hash is calculated
* for the published topic before attempting to find matching subscriptions. The
* hash calculation itself is simple and does not have a large performance impact.
*
* We'll first explain how topic hashes and hash masks are constructed and then how
* they are used.
*
* Topic hash
* ----------
* Topic hashes are stored as 64-bit numbers. Each byte within the 64-bit number
* relates to one MQTT topic level. A checksum is calculated for each topic level
* by iterating over the characters within the topic level (cast to byte) and the
* result is stored into the corresponding byte of the 64-bit number. If a topic
* level contains a wildcard character, then 0x00 is stored instead of the
* checksum.
*
* If there are less than 8 levels then the rest of the 64-bit number is filled
* with 0xff. If there are more than 8 levels then topics where the first 8 MQTT
* topic levels are identical will have the same hash value.
*
* This is the topic hash for the MQTT topic below: 0x655D4AF1FFFFFF
*
* client1/building1/level1/sensor1 (empty) (empty) (empty) (empty)
* \_____/ \_______/ \____/ \_____/ \_____/ \_____/ \_____/ \_____/
* | | | | | | | |
* 0x65 0x5D 0x4A 0xF1 0xFF 0xFF 0xFF 0xFF
*
* This is the topic hash for an MQTT topic containing a wildcard: 0x655D00F1FFFFFF
*
* client1/building1/ + /sensor1 (empty) (empty) (empty) (empty)
* \_____/ \_______/ \_/ \_____/ \_____/ \_____/ \_____/ \_____/
* | | | | | | | |
* 0x65 0x5D 0 0xF1 0xFF 0xFF 0xFF 0xFF
*
* For topics that contain the multi level wildcard # at the end, the topic hash
* is filled with 0x00: 0x65004A00000000
*
* client1/ + /level1/ # (empty) (empty) (empty) (empty)
* \_____/ \_/ \____/ \_/ \_____/ \_____/ \_____/ \_____/
* | | | | | | | |
* 0x65 0 0x4A 0 0 0 0 0
*
*
* Topic hash mask
* ---------------
* The hash mask simply contains 0xFF for non-wildcard topic levels and 0x00 for
* wildcard topic levels. Here are the topic hash masks for the examples above.
*
* client1/building1/level1/sensor1 (empty) (empty) (empty) (empty)
* \_____/ \_______/ \____/ \_____/ \_____/ \_____/ \_____/ \_____/
* | | | | | | | |
* 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF
*
* client1/building1/ + /sensor1 (empty) (empty) (empty) (empty)
* \_____/ \_______/ \_/ \_____/ \_____/ \_____/ \_____/ \_____/
* | | | | | | | |
* 0xFF 0xFF 0 0xFF 0xFF 0xFF 0xFF 0xFF
*
* client1/ + /level1/ # (empty) (empty) (empty) (empty)
* \_____/ \_/ \____/ \_/ \_____/ \_____/ \_____/ \_____/
* | | | | | | | |
* 0xFF 0 0xFF 0 0 0 0 0
*
*
* Topic hash and hash mask properties
* -----------------------------------
* The following properties of topic hashes and hash masks can be exploited to
* find potentially matching subscribed topics for a given published topic.
*
* (1) If a subscribed topic does not contain wildcards then the topic hash of the
* subscribed topic must be equal to the topic hash of the published topic,
* otherwise the subscribed topic cannot be a candidate for a match.
*
* (2) If a subscribed topic contains wildcards then the hash of the published
* topic masked with the subscribed topic's hash mask must be equal to the hash of
* the subscribed topic. I.e. a subscribed topic is a candidate for a match if:
* (publishedTopicHash & subscribedTopicHashMask) == subscribedTopicHash
*
* (3) If a subscribed topic contains wildcards then any potentially matching
* published topic must have a hash value that is greater than or equal to the
* hash value of the subscribed topic (because the subscribed topic contains
* zeroes in wildcard positions).
*
* Match finding
* -------------
* The subscription manager maintains two separate dictionaries to assist finding
* matches using topic hashes: a _noWildcardSubscriptionsByTopicHash dictionary
* containing all subscriptions that do not have wildcards, and a
* _wildcardSubscriptionsByTopicHash dictionary containing subscriptions with
* wildcards.
*
* For subscriptions without wildcards, all potential candidates for a match are
* obtained by a single look-up (exploiting point 1 above).
*
* For subscriptions with wildcards, the subscription manager loops through the
* wildcard subscriptions and selects candidates that satisfy condition
* (publishedTopicHash & subscribedTopicMask) == subscribedTopicHash (point 2).
* The loop could exit early if wildcard subscriptions were stored into a sorted
* dictionary (utilizing point 3), but, after testing, there does not seem to be
* any real benefit doing so.
*
* Other considerations
* --------------------
* Characters in the topic string are cast to byte and any additional bytes in a
* multi-byte character are disregarded. Best guess is that this does not impact
* performance in practice.
*
* Instead of one-byte checksums per topic level, one-word checksums per topic
* level could be used. If most topics contained four levels or less then hash
* buckets would be shallower.
*
* For very large numbers of topics, performing a parallel search may help further.
*
* To also handle a larger number of subscribers, it may be beneficial to maintain
* a subscribers-by-subscription-topic dictionary.
*/
public static class MqttTopicHash
{
public static void Calculate(string topic, out ulong resultHash, out ulong resultHashMask, out bool resultHasWildcard)
{
// calculate topic hash
ulong hash = 0;
ulong hashMaskInverted = 0;
ulong levelBitMask = 0;
ulong fillLevelBitMask = 0;
var hasWildcard = false;
byte checkSum = 0;
var level = 0;
var i = 0;
while (i < topic.Length)
{
var c = topic[i];
if (c == MqttTopicFilterComparer.LevelSeparator)
{
// done with this level
hash <<= 8;
hash |= checkSum;
hashMaskInverted <<= 8;
hashMaskInverted |= levelBitMask;
checkSum = 0;
levelBitMask = 0;
++level;
if (level >= 8)
{
break;
}
}
else if (c == MqttTopicFilterComparer.SingleLevelWildcard)
{
levelBitMask = 0xff;
hasWildcard = true;
}
else if (c == MqttTopicFilterComparer.MultiLevelWildcard)
{
// checksum is zero for a valid topic
levelBitMask = 0xff;
// fill rest with this fillLevelBitMask
fillLevelBitMask = 0xff;
hasWildcard = true;
break;
}
else
{
// The checksum should be designed to reduce the hash bucket depth for the expected
// fairly regularly named MQTT topics that don't differ much,
// i.e. "room1/sensor1"
// "room1/sensor2"
// "room1/sensor3"
// etc.
if ((c & 1) == 0)
{
checkSum += (byte)c;
}
else
{
checkSum ^= (byte)(c >> 1);
}
}
++i;
}
// Shift hash left and leave zeroes to fill ulong
if (level < 8)
{
hash <<= 8;
hash |= checkSum;
hashMaskInverted <<= 8;
hashMaskInverted |= levelBitMask;
++level;
while (level < 8)
{
hash <<= 8;
hashMaskInverted <<= 8;
hashMaskInverted |= fillLevelBitMask;
++level;
}
}
if (!hasWildcard)
{
while (i < topic.Length)
{
var c = topic[i];
if (c == MqttTopicFilterComparer.SingleLevelWildcard || c == MqttTopicFilterComparer.MultiLevelWildcard)
{
hasWildcard = true;
break;
}
++i;
}
}
resultHash = hash;
resultHashMask = ~hashMaskInverted;
resultHasWildcard = hasWildcard;
}
}
}
@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using MQTTnet.Packets;
using MQTTnet.Protocol;
namespace MQTTnet.Server
@@ -10,7 +11,9 @@ namespace MQTTnet.Server
public sealed class UnsubscribeResult
{
public List<MqttUnsubscribeReasonCode> ReasonCodes { get; } = new List<MqttUnsubscribeReasonCode>(128);
public bool CloseConnection { get; set; }
public List<MqttUserProperty> UserProperties { get; set; }
}
}
+19 -1
View File
@@ -85,6 +85,12 @@ namespace MQTTnet.Server
remove => _eventContainer.ClientUnsubscribedTopicEvent.RemoveHandler(value);
}
public event Func<InterceptingClientApplicationMessageEnqueueEventArgs, Task> InterceptingClientEnqueueAsync
{
add => _eventContainer.InterceptingClientEnqueueEvent.AddHandler(value);
remove => _eventContainer.InterceptingClientEnqueueEvent.RemoveHandler(value);
}
public event Func<InterceptingPacketEventArgs, Task> InterceptingInboundPacketAsync
{
add => _eventContainer.InterceptingInboundPacketEvent.AddHandler(value);
@@ -204,6 +210,18 @@ namespace MQTTnet.Server
return _retainedMessagesManager.GetMessages();
}
public Task<MqttApplicationMessage> GetRetainedMessageAsync(string topic)
{
if (topic == null)
{
throw new ArgumentNullException(nameof(topic));
}
ThrowIfNotStarted();
return _retainedMessagesManager.GetMessage(topic);
}
public Task<IList<MqttSessionStatus>> GetSessionsAsync()
{
ThrowIfNotStarted();
@@ -233,7 +251,7 @@ namespace MQTTnet.Server
}
var sessionItems = injectedApplicationMessage.CustomSessionItems ?? ServerSessionItems;
return _clientSessionsManager.DispatchApplicationMessage(
injectedApplicationMessage.SenderClientId,
sessionItems,
@@ -41,7 +41,7 @@ namespace MQTTnet.Server
new MqttApplicationMessage
{
Topic = topic,
Payload = payloadBuffer,
PayloadSegment = new ArraySegment<byte>(payloadBuffer),
QualityOfServiceLevel = qualityOfServiceLevel,
Retain = retain
}));
@@ -18,44 +18,48 @@ namespace MQTTnet.Server
{
_session = session ?? throw new ArgumentNullException(nameof(session));
}
public string Id => _session.Id;
public long PendingApplicationMessagesCount => _session.PendingDataPacketsCount;
public DateTime CreatedTimestamp => _session.CreatedTimestamp;
public string Id => _session.Id;
public IDictionary Items => _session.Items;
public Task EnqueueApplicationMessageAsync(MqttApplicationMessage applicationMessage)
{
if (applicationMessage == null) throw new ArgumentNullException(nameof(applicationMessage));
var publishPacketFactory = new MqttPublishPacketFactory();
_session.EnqueueDataPacket(new MqttPacketBusItem(publishPacketFactory.Create(applicationMessage)));
return CompletedTask.Instance;
}
public long PendingApplicationMessagesCount => _session.PendingDataPacketsCount;
public Task DeliverApplicationMessageAsync(MqttApplicationMessage applicationMessage)
{
if (applicationMessage == null) throw new ArgumentNullException(nameof(applicationMessage));
var publishPacketFactory = new MqttPublishPacketFactory();
var packetBusItem = new MqttPacketBusItem(publishPacketFactory.Create(applicationMessage));
_session.EnqueueDataPacket(packetBusItem);
return packetBusItem.WaitAsync();
}
public Task DeleteAsync()
{
return _session.DeleteAsync();
}
public Task ClearApplicationMessagesQueueAsync()
{
throw new NotImplementedException();
}
public Task DeleteAsync()
{
return _session.DeleteAsync();
}
public Task DeliverApplicationMessageAsync(MqttApplicationMessage applicationMessage)
{
if (applicationMessage == null)
{
throw new ArgumentNullException(nameof(applicationMessage));
}
var packetBusItem = new MqttPacketBusItem(MqttPacketFactories.Publish.Create(applicationMessage));
_session.EnqueueDataPacket(packetBusItem);
return packetBusItem.WaitAsync();
}
public Task EnqueueApplicationMessageAsync(MqttApplicationMessage applicationMessage)
{
if (applicationMessage == null)
{
throw new ArgumentNullException(nameof(applicationMessage));
}
_session.EnqueueDataPacket(new MqttPacketBusItem(MqttPacketFactories.Publish.Create(applicationMessage)));
return CompletedTask.Instance;
}
}
}
}