Merge branch 'master' into 1547-use-mqttclientdefaultcertificatevalidationhandlerhandle-by-default

This commit is contained in:
Christian
2022-11-13 18:06:28 +01:00
59 changed files with 1514 additions and 903 deletions
+1 -3
View File
@@ -1,3 +1 @@
* [Core] Fixed several dead locks by implementing a new async signal (#1552)
* [Server] Fixed duplicated invocation of the event _ClientAcknowledgedPublishPacketAsync_ for QoS level 2 (#1550)
* [Server] Fixed issue in upgrading and downgrading of QoS levels for subscriptions and retained messages (#1560)
@@ -1,26 +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.Collections.Generic;
using System.IO.Pipelines;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Http.Features;
namespace MQTTnet.AspNetCore.Tests.Mockups
{
public class ConnectionContextMockup : ConnectionContext
{
public override string ConnectionId { get; set; }
public override IFeatureCollection Features { get; }
public override IDictionary<object, object> Items { get; set; }
public override IDuplexPipe Transport { get; set; }
public ConnectionContextMockup()
{
//Transport = new DefaultConnectionContext
}
}
}
@@ -2,20 +2,24 @@
// The .NET Foundation licenses this file to you under the MIT license. // The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using Microsoft.AspNetCore.Connections;
using MQTTnet.Adapter;
using MQTTnet.Formatter;
using MQTTnet.Server;
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Connections;
using MQTTnet.Adapter;
using MQTTnet.Diagnostics; using MQTTnet.Diagnostics;
using MQTTnet.Formatter;
using MQTTnet.Server;
namespace MQTTnet.AspNetCore.Tests.Mockups namespace MQTTnet.AspNetCore.Tests.Mockups
{ {
public class ConnectionHandlerMockup: IMqttServerAdapter public sealed class ConnectionHandlerMockup : IMqttServerAdapter
{ {
public TaskCompletionSource<MqttConnectionContext> Context { get; } = new TaskCompletionSource<MqttConnectionContext>();
public Func<IMqttChannelAdapter, Task> ClientHandler { get; set; } public Func<IMqttChannelAdapter, Task> ClientHandler { get; set; }
public TaskCompletionSource<MqttConnectionContext> Context { get; } = new TaskCompletionSource<MqttConnectionContext>();
public void Dispose()
{
}
public async Task OnConnectedAsync(ConnectionContext connection) public async Task OnConnectedAsync(ConnectionContext connection)
{ {
@@ -42,9 +46,5 @@ namespace MQTTnet.AspNetCore.Tests.Mockups
{ {
return Task.CompletedTask; return Task.CompletedTask;
} }
public void Dispose()
{
}
} }
} }
@@ -6,7 +6,7 @@ using System.IO.Pipelines;
namespace MQTTnet.AspNetCore.Tests.Mockups namespace MQTTnet.AspNetCore.Tests.Mockups
{ {
public class DuplexPipeMockup : IDuplexPipe public sealed class DuplexPipeMockup : IDuplexPipe
{ {
public DuplexPipeMockup() public DuplexPipeMockup()
{ {
@@ -20,9 +20,8 @@ namespace MQTTnet.AspNetCore.Tests.Mockups
PipeWriter IDuplexPipe.Output => Send.Writer; PipeWriter IDuplexPipe.Output => Send.Writer;
public Pipe Receive { get; }
public Pipe Receive { get; set; } public Pipe Send { get; }
public Pipe Send { get; set; }
} }
} }
@@ -6,7 +6,7 @@ using System.Buffers;
namespace MQTTnet.AspNetCore.Tests.Mockups namespace MQTTnet.AspNetCore.Tests.Mockups
{ {
public class LimitedMemoryPool : MemoryPool<byte> public sealed class LimitedMemoryPool : MemoryPool<byte>
{ {
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)
{ {
@@ -7,9 +7,9 @@ using System.Buffers;
namespace MQTTnet.AspNetCore.Tests.Mockups namespace MQTTnet.AspNetCore.Tests.Mockups
{ {
public class MemoryOwner : IMemoryOwner<byte> public sealed class MemoryOwner : IMemoryOwner<byte>
{ {
private readonly byte[] _raw; readonly byte[] _raw;
public MemoryOwner(int size) public MemoryOwner(int size)
{ {
@@ -27,11 +27,11 @@ namespace MQTTnet.AspNetCore.Tests.Mockups
Memory = _raw; Memory = _raw;
} }
public Memory<byte> Memory { get; }
public void Dispose() public void Dispose()
{ {
ArrayPool<byte>.Shared.Return(_raw); ArrayPool<byte>.Shared.Return(_raw);
} }
public Memory<byte> Memory { get; }
} }
} }
@@ -2,22 +2,21 @@
// The .NET Foundation licenses this file to you under the MIT license. // The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System; using System;
using System.Net; using System.Net;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MQTTnet.Adapter; using MQTTnet.Adapter;
using MQTTnet.AspNetCore.Tests.Mockups; using MQTTnet.AspNetCore.Tests.Mockups;
using MQTTnet.Client;
using MQTTnet.Exceptions; using MQTTnet.Exceptions;
using MQTTnet.Formatter; using MQTTnet.Formatter;
using MQTTnet.Packets; using MQTTnet.Packets;
using MQTTnet.Client;
using MQTTnet.Protocol; using MQTTnet.Protocol;
using MQTTnet.Tests.Extensions; using MQTTnet.Tests.Extensions;
@@ -26,20 +25,6 @@ namespace MQTTnet.AspNetCore.Tests
[TestClass] [TestClass]
public class MqttConnectionContextTest public class MqttConnectionContextTest
{ {
[TestMethod]
public async Task TestReceivePacketAsyncThrowsWhenReaderCompleted()
{
var serializer = new MqttPacketFormatterAdapter(MqttProtocolVersion.V311, new MqttBufferWriter(4096, 65535));
var pipe = new DuplexPipeMockup();
var connection = new DefaultConnectionContext();
connection.Transport = pipe;
var ctx = new MqttConnectionContext(serializer, connection);
pipe.Receive.Writer.Complete();
await Assert.ThrowsExceptionAsync<MqttCommunicationException>(() => ctx.ReceivePacketAsync(CancellationToken.None));
}
[TestMethod] [TestMethod]
public async Task TestCorruptedConnectPacket() public async Task TestCorruptedConnectPacket()
{ {
@@ -55,7 +40,37 @@ namespace MQTTnet.AspNetCore.Tests
await Assert.ThrowsExceptionAsync<MqttProtocolViolationException>(() => ctx.ReceivePacketAsync(CancellationToken.None)); await Assert.ThrowsExceptionAsync<MqttProtocolViolationException>(() => ctx.ReceivePacketAsync(CancellationToken.None));
// the first exception should complete the pipes so if someone tries to use the connection after that it should throw immidiatly // the first exception should complete the pipes so if someone tries to use the connection after that it should throw immidiatly
await Assert.ThrowsExceptionAsync<InvalidOperationException>(() => ctx.ReceivePacketAsync(CancellationToken.None)); await Assert.ThrowsExceptionAsync<InvalidOperationException>(() => ctx.ReceivePacketAsync(CancellationToken.None));
}
[TestMethod]
public async Task TestEndpoint()
{
var mockup = new ConnectionHandlerMockup();
using (var host = new WebHostBuilder().UseKestrel(kestrel => kestrel.ListenLocalhost(1883, listener => listener.Use((ctx, next) => mockup.OnConnectedAsync(ctx))))
.UseStartup<Startup>()
.ConfigureServices(
(hostContext, services) =>
{
services.AddHostedMqttServer(o => o.WithoutDefaultEndpoint());
services.AddSingleton<IMqttServerAdapter>(mockup);
})
.Build())
using (var client = new MqttFactory().CreateMqttClient())
{
host.Start();
await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("localhost").Build(), CancellationToken.None);
var ctx = await mockup.Context.Task;
#if NETCOREAPP3_1
var ep = IPEndPoint.Parse(ctx.Endpoint);
Assert.IsNotNull(ep);
#endif
Assert.IsNotNull(ctx);
}
} }
// COMMENTED OUT DUE TO DEAD LOCK? OR VERY VERY SLOW PERFORMANCE ON LOCAL DEV MACHINE. TEST WAS STILL RUNNING AFTER SEVERAL MINUTES! // COMMENTED OUT DUE TO DEAD LOCK? OR VERY VERY SLOW PERFORMANCE ON LOCAL DEV MACHINE. TEST WAS STILL RUNNING AFTER SEVERAL MINUTES!
@@ -94,43 +109,25 @@ namespace MQTTnet.AspNetCore.Tests
Assert.IsTrue(readResult.Buffer.Length > 20000); Assert.IsTrue(readResult.Buffer.Length > 20000);
} }
private class Startup [TestMethod]
public async Task TestReceivePacketAsyncThrowsWhenReaderCompleted()
{
var serializer = new MqttPacketFormatterAdapter(MqttProtocolVersion.V311, new MqttBufferWriter(4096, 65535));
var pipe = new DuplexPipeMockup();
var connection = new DefaultConnectionContext();
connection.Transport = pipe;
var ctx = new MqttConnectionContext(serializer, connection);
pipe.Receive.Writer.Complete();
await Assert.ThrowsExceptionAsync<MqttCommunicationException>(() => ctx.ReceivePacketAsync(CancellationToken.None));
}
class Startup
{ {
public void Configure(IApplicationBuilder app) public void Configure(IApplicationBuilder app)
{ {
} }
} }
[TestMethod]
public async Task TestEndpoint()
{
var mockup = new ConnectionHandlerMockup();
using (var host = new WebHostBuilder()
.UseKestrel(kestrel => kestrel.ListenLocalhost(1883, listener => listener.Use((ctx, next) => mockup.OnConnectedAsync(ctx))))
.UseStartup<Startup>()
.ConfigureServices((hostContext, services) =>
{
services.AddHostedMqttServer(o => o.WithoutDefaultEndpoint());
services.AddSingleton<IMqttServerAdapter>(mockup);
})
.Build())
using (var client = new MqttFactory().CreateMqttClient())
{
host.Start();
await client.ConnectAsync(new MqttClientOptionsBuilder()
.WithTcpServer("localhost")
.Build(), CancellationToken.None);
var ctx = await mockup.Context.Task;
#if NETCOREAPP3_1
var ep = IPEndPoint.Parse(ctx.Endpoint);
Assert.IsNotNull(ep);
#endif
Assert.IsNotNull(ctx);
}
}
} }
} }
@@ -12,7 +12,7 @@ using MQTTnet.Packets;
namespace MQTTnet.AspNetCore.Tests namespace MQTTnet.AspNetCore.Tests
{ {
[TestClass] [TestClass]
public class ReaderExtensionsTest public sealed class ReaderExtensionsTest
{ {
[TestMethod] [TestMethod]
public void TestTryDeserialize() public void TestTryDeserialize()
@@ -43,7 +43,7 @@ namespace MQTTnet.Extensions.ManagedClient
Task StartAsync(ManagedMqttClientOptions options); Task StartAsync(ManagedMqttClientOptions options);
Task StopAsync(); Task StopAsync(bool cleanDisconnect = true);
Task SubscribeAsync(ICollection<MqttTopicFilter> topicFilters); Task SubscribeAsync(ICollection<MqttTopicFilter> topicFilters);
@@ -62,6 +62,7 @@ namespace MQTTnet.Extensions.ManagedClient
CancellationTokenSource _publishingCancellationToken; CancellationTokenSource _publishingCancellationToken;
ManagedMqttClientStorageManager _storageManager; ManagedMqttClientStorageManager _storageManager;
bool _isCleanDisconnect;
public ManagedMqttClient(IMqttClient mqttClient, IMqttNetLogger logger) public ManagedMqttClient(IMqttClient mqttClient, IMqttNetLogger logger)
{ {
@@ -160,7 +161,7 @@ namespace MQTTnet.Extensions.ManagedClient
throw new InvalidOperationException("call StartAsync before publishing messages"); throw new InvalidOperationException("call StartAsync before publishing messages");
} }
MqttTopicValidator.ThrowIfInvalid(applicationMessage.ApplicationMessage.Topic); MqttTopicValidator.ThrowIfInvalid(applicationMessage.ApplicationMessage);
ManagedMqttApplicationMessage removedMessage = null; ManagedMqttApplicationMessage removedMessage = null;
ApplicationMessageSkippedEventArgs applicationMessageSkippedEventArgs = null; ApplicationMessageSkippedEventArgs applicationMessageSkippedEventArgs = null;
@@ -255,10 +256,12 @@ namespace MQTTnet.Extensions.ManagedClient
_logger.Info("Started"); _logger.Info("Started");
} }
public async Task StopAsync() public async Task StopAsync(bool cleanDisconnect = true)
{ {
ThrowIfDisposed(); ThrowIfDisposed();
_isCleanDisconnect = cleanDisconnect;
StopPublishing(); StopPublishing();
StopMaintainingConnection(); StopMaintainingConnection();
@@ -382,9 +385,12 @@ namespace MQTTnet.Extensions.ManagedClient
{ {
try try
{ {
using (var disconnectTimeout = new CancellationTokenSource(Options.ClientOptions.Timeout)) if (_isCleanDisconnect)
{ {
await InternalClient.DisconnectAsync(new MqttClientDisconnectOptions(), disconnectTimeout.Token).ConfigureAwait(false); using (var disconnectTimeout = new CancellationTokenSource(Options.ClientOptions.Timeout))
{
await InternalClient.DisconnectAsync(new MqttClientDisconnectOptions(), disconnectTimeout.Token).ConfigureAwait(false);
}
} }
} }
catch (OperationCanceledException) catch (OperationCanceledException)
@@ -80,6 +80,8 @@ namespace MQTTnet.Tests.Client
{ {
using (var testEnvironment = CreateTestEnvironment()) using (var testEnvironment = CreateTestEnvironment())
{ {
testEnvironment.IgnoreServerLogErrors = true;
testEnvironment.ServerPort = 8364; testEnvironment.ServerPort = 8364;
var server = await testEnvironment.StartServer(); var server = await testEnvironment.StartServer();
@@ -116,6 +118,8 @@ namespace MQTTnet.Tests.Client
{ {
using (var testEnvironment = CreateTestEnvironment()) using (var testEnvironment = CreateTestEnvironment())
{ {
testEnvironment.IgnoreServerLogErrors = true;
var server = await testEnvironment.StartServer(); var server = await testEnvironment.StartServer();
using (var lowLevelClient = testEnvironment.CreateLowLevelClient()) using (var lowLevelClient = testEnvironment.CreateLowLevelClient())
@@ -11,6 +11,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting;
using MQTTnet.Client; using MQTTnet.Client;
using MQTTnet.Diagnostics; using MQTTnet.Diagnostics;
using MQTTnet.Extensions.ManagedClient; using MQTTnet.Extensions.ManagedClient;
using MQTTnet.Formatter;
using MQTTnet.Internal; using MQTTnet.Internal;
using MQTTnet.Protocol; using MQTTnet.Protocol;
using MQTTnet.Server; using MQTTnet.Server;
@@ -21,6 +22,65 @@ namespace MQTTnet.Tests.Client
[TestClass] [TestClass]
public sealed class ManagedMqttClient_Tests : BaseTestClass public sealed class ManagedMqttClient_Tests : BaseTestClass
{ {
[TestMethod]
public async Task Receive_While_Not_Cleanly_Disconnected()
{
using (var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500))
{
await testEnvironment.StartServer(o => o.WithPersistentSessions());
var senderClient = await testEnvironment.ConnectClient();
// Prepare managed client.
var managedClient = testEnvironment.Factory.CreateManagedMqttClient();
await managedClient.SubscribeAsync("#");
var receivedMessages = testEnvironment.CreateApplicationMessageHandler(managedClient);
var managedClientOptions = new ManagedMqttClientOptions
{
ClientOptions = testEnvironment.Factory.CreateClientOptionsBuilder()
.WithTcpServer("127.0.0.1", testEnvironment.ServerPort)
.WithClientId(nameof(Receive_While_Not_Cleanly_Disconnected) + "_managed")
.WithCleanSession(false)
.Build()
};
await managedClient.StartAsync(managedClientOptions);
await LongTestDelay();
// Send test data.
await senderClient.PublishStringAsync("topic1");
await LongTestDelay();
receivedMessages.AssertReceivedCountEquals(1);
// Stop the managed client but keep session at server (not clean disconnect required).
await managedClient.StopAsync(false);
await LongTestDelay();
// Send new messages in the meantime.
await senderClient.PublishStringAsync("topic2", qualityOfServiceLevel: MqttQualityOfServiceLevel.ExactlyOnce);
await LongTestDelay();
// Start the managed client, it should receive the new message.
await managedClient.StartAsync(managedClientOptions);
await LongTestDelay();
receivedMessages.AssertReceivedCountEquals(2);
// Stop and start again, no new message should be received.
for (var i = 0; i < 3; i++)
{
await managedClient.StopAsync(false);
await LongTestDelay();
await managedClient.StartAsync(managedClientOptions);
await LongTestDelay();
}
receivedMessages.AssertReceivedCountEquals(2);
}
}
[TestMethod] [TestMethod]
public async Task Connect_To_Invalid_Server() public async Task Connect_To_Invalid_Server()
{ {
@@ -33,7 +93,6 @@ namespace MQTTnet.Tests.Client
var managedClient = testEnvironment.Factory.CreateManagedMqttClient(mqttClient); var managedClient = testEnvironment.Factory.CreateManagedMqttClient(mqttClient);
ConnectingFailedEventArgs connectingFailedEventArgs = null; ConnectingFailedEventArgs connectingFailedEventArgs = null;
managedClient.ConnectingFailedAsync += args => managedClient.ConnectingFailedAsync += args =>
{ {
connectingFailedEventArgs = args; connectingFailedEventArgs = args;
@@ -13,13 +13,12 @@ using MQTTnet.Adapter;
using MQTTnet.Diagnostics; using MQTTnet.Diagnostics;
using MQTTnet.Exceptions; using MQTTnet.Exceptions;
using MQTTnet.Formatter; using MQTTnet.Formatter;
using MQTTnet.Internal;
using MQTTnet.Packets; using MQTTnet.Packets;
using MQTTnet.Protocol; using MQTTnet.Protocol;
using MQTTnet.Tests.Extensions; using MQTTnet.Tests.Extensions;
using MQTTnet.Tests.Mockups; using MQTTnet.Tests.Mockups;
namespace MQTTnet.Tests namespace MQTTnet.Tests.Formatter
{ {
[TestClass] [TestClass]
public sealed class MqttPacketSerialization_V3_Binary_Tests public sealed class MqttPacketSerialization_V3_Binary_Tests
@@ -10,7 +10,7 @@ using MQTTnet.Formatter;
using MQTTnet.Packets; using MQTTnet.Packets;
using MQTTnet.Protocol; using MQTTnet.Protocol;
namespace MQTTnet.Tests namespace MQTTnet.Tests.Formatter
{ {
[TestClass] [TestClass]
public sealed class MqttPacketSerialization_V3_Tests public sealed class MqttPacketSerialization_V3_Tests
@@ -9,7 +9,7 @@ using MQTTnet.Formatter;
using MQTTnet.Packets; using MQTTnet.Packets;
using MQTTnet.Protocol; using MQTTnet.Protocol;
namespace MQTTnet.Tests namespace MQTTnet.Tests.Formatter
{ {
[TestClass] [TestClass]
public sealed class MqttPacketSerialization_V5_Tests public sealed class MqttPacketSerialization_V5_Tests
@@ -52,12 +52,26 @@ namespace MQTTnet.Tests.Internal
} }
[TestMethod] [TestMethod]
public async Task Loop_Signal() public async Task Reset_Signal()
{ {
var asyncSignal = new AsyncSignal(); var asyncSignal = new AsyncSignal();
// WaitAsync should fail because no signal is available.
for (var i = 0; i < 10; i++) for (var i = 0; i < 10; i++)
{ {
try
{
using (var timeout = new CancellationTokenSource(TimeSpan.FromMilliseconds(100)))
{
await asyncSignal.WaitAsync(timeout.Token);
}
Assert.Fail("This must fail because the signal is not yet set.");
}
catch (OperationCanceledException)
{
}
asyncSignal.Set(); asyncSignal.Set();
// WaitAsync should return directly because the signal is available. // WaitAsync should return directly because the signal is available.
@@ -0,0 +1,43 @@
// 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.Internal;
using MQTTnet.Packets;
namespace MQTTnet.Tests.Internal
{
[TestClass]
public sealed class MqttPacketBusItem_Tests
{
[TestMethod]
public void Fire_Completed_Event()
{
var eventFired = false;
var item = new MqttPacketBusItem(new MqttPublishPacket());
item.Completed += (_, __) =>
{
eventFired = true;
};
item.Complete();
Assert.IsTrue(eventFired);
}
[TestMethod]
[ExpectedException(typeof(TaskCanceledException))]
public async Task Wait_Packet_Bus_Item_After_Already_Canceled()
{
var item = new MqttPacketBusItem(new MqttPublishPacket());
// Finish the item before the actual
item.Cancel();
await item.WaitAsync();
}
}
}
@@ -14,49 +14,6 @@ namespace MQTTnet.Tests.Internal
[TestClass] [TestClass]
public sealed class MqttPacketBus_Tests public sealed class MqttPacketBus_Tests
{ {
[TestMethod]
[ExpectedException(typeof(TaskCanceledException))]
public async Task Wait_With_Empty_Bus()
{
var bus = new MqttPacketBus();
using (var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(1)))
{
await bus.DequeueItemAsync(timeout.Token);
}
}
[TestMethod]
public async Task Fill_From_Different_Task()
{
const int MessageCount = 500;
var delayRandom = new Random();
var bus = new MqttPacketBus();
_ = Task.Run(
() =>
{
for (int i = 0; i < MessageCount; i++)
{
bus.EnqueueItem(new MqttPacketBusItem(MqttPingReqPacket.Instance), MqttPacketBusPartition.Health);
Thread.Sleep(delayRandom.Next(0, 10));
}
});
for (var i = 0; i < MessageCount; i++)
{
using (var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10)))
{
await bus.DequeueItemAsync(timeout.Token);
}
}
Assert.AreEqual(0, bus.TotalItemsCount);
}
[TestMethod] [TestMethod]
public void Alternate_Priorities() public void Alternate_Priorities()
{ {
@@ -89,6 +46,42 @@ namespace MQTTnet.Tests.Internal
Assert.AreEqual(0, bus.TotalItemsCount); Assert.AreEqual(0, bus.TotalItemsCount);
} }
[TestMethod]
public void Await_Single_Packet()
{
var bus = new MqttPacketBus();
var delivered = false;
var item1 = new MqttPacketBusItem(new MqttPublishPacket());
var item2 = new MqttPacketBusItem(new MqttPublishPacket());
var item3 = new MqttPacketBusItem(new MqttPublishPacket());
item3.Completed += (_, __) =>
{
delivered = true;
};
bus.EnqueueItem(item1, MqttPacketBusPartition.Data);
bus.EnqueueItem(item2, MqttPacketBusPartition.Data);
bus.EnqueueItem(item3, MqttPacketBusPartition.Data);
Assert.IsFalse(delivered);
bus.DequeueItemAsync(CancellationToken.None).Result.Complete();
Assert.IsFalse(delivered);
bus.DequeueItemAsync(CancellationToken.None).Result.Complete();
Assert.IsFalse(delivered);
bus.DequeueItemAsync(CancellationToken.None).Result.Complete();
// The third packet has the event attached.
Assert.IsTrue(delivered);
}
[TestMethod] [TestMethod]
public void Export_Packets_Without_Dequeue() public void Export_Packets_Without_Dequeue()
{ {
@@ -113,39 +106,46 @@ namespace MQTTnet.Tests.Internal
} }
[TestMethod] [TestMethod]
public void Await_Single_Packet() public async Task Fill_From_Different_Task()
{
const int MessageCount = 500;
var delayRandom = new Random();
var bus = new MqttPacketBus();
_ = Task.Run(
() =>
{
for (var i = 0; i < MessageCount; i++)
{
bus.EnqueueItem(new MqttPacketBusItem(MqttPingReqPacket.Instance), MqttPacketBusPartition.Health);
Thread.Sleep(delayRandom.Next(0, 10));
}
});
for (var i = 0; i < MessageCount; i++)
{
using (var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10)))
{
await bus.DequeueItemAsync(timeout.Token);
}
}
Assert.AreEqual(0, bus.TotalItemsCount);
}
[TestMethod]
[ExpectedException(typeof(TaskCanceledException))]
public async Task Wait_With_Empty_Bus()
{ {
var bus = new MqttPacketBus(); var bus = new MqttPacketBus();
var delivered = false; using (var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(1)))
var item1 = new MqttPacketBusItem(new MqttPublishPacket());
var item2 = new MqttPacketBusItem(new MqttPublishPacket());
var item3 = new MqttPacketBusItem(new MqttPublishPacket());
item3.Delivered += (_, __) =>
{ {
delivered = true; await bus.DequeueItemAsync(timeout.Token);
}; }
bus.EnqueueItem(item1, MqttPacketBusPartition.Data);
bus.EnqueueItem(item2, MqttPacketBusPartition.Data);
bus.EnqueueItem(item3, MqttPacketBusPartition.Data);
Assert.IsFalse(delivered);
bus.DequeueItemAsync(CancellationToken.None).Result.MarkAsDelivered();
Assert.IsFalse(delivered);
bus.DequeueItemAsync(CancellationToken.None).Result.MarkAsDelivered();
Assert.IsFalse(delivered);
bus.DequeueItemAsync(CancellationToken.None).Result.MarkAsDelivered();
// The third packet has the event attached.
Assert.IsTrue(delivered);
} }
} }
} }
+2 -2
View File
@@ -154,7 +154,7 @@ namespace MQTTnet.Tests.MQTTv5
var result = await client.PublishStringAsync("a", "b", MqttQualityOfServiceLevel.AtLeastOnce); var result = await client.PublishStringAsync("a", "b", MqttQualityOfServiceLevel.AtLeastOnce);
await client.DisconnectAsync(); await client.DisconnectAsync();
Assert.AreEqual(MqttClientPublishReasonCode.Success, result.ReasonCode); Assert.AreEqual(MqttClientPublishReasonCode.NoMatchingSubscribers, result.ReasonCode);
} }
} }
@@ -169,7 +169,7 @@ namespace MQTTnet.Tests.MQTTv5
var result = await client.PublishStringAsync("a", "b", MqttQualityOfServiceLevel.ExactlyOnce); var result = await client.PublishStringAsync("a", "b", MqttQualityOfServiceLevel.ExactlyOnce);
await client.DisconnectAsync(); await client.DisconnectAsync();
Assert.AreEqual(MqttClientPublishReasonCode.Success, result.ReasonCode); Assert.AreEqual(MqttClientPublishReasonCode.NoMatchingSubscribers, result.ReasonCode);
} }
} }
@@ -2,8 +2,10 @@
// The .NET Foundation licenses this file to you under the MIT license. // The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.VisualStudio.TestTools.UnitTesting;
using MQTTnet.Client; using MQTTnet.Client;
@@ -17,7 +19,12 @@ namespace MQTTnet.Tests.Mockups
public TestApplicationMessageReceivedHandler(IMqttClient mqttClient) public TestApplicationMessageReceivedHandler(IMqttClient mqttClient)
{ {
mqttClient.ApplicationMessageReceivedAsync += MqttClientOnApplicationMessageReceivedAsync; if (mqttClient == null)
{
throw new ArgumentNullException(nameof(mqttClient));
}
mqttClient.ApplicationMessageReceivedAsync += OnApplicationMessageReceivedAsync;
} }
public List<MqttApplicationMessageReceivedEventArgs> ReceivedEventArgs public List<MqttApplicationMessageReceivedEventArgs> ReceivedEventArgs
@@ -39,7 +46,25 @@ namespace MQTTnet.Tests.Mockups
} }
} }
Task MqttClientOnApplicationMessageReceivedAsync(MqttApplicationMessageReceivedEventArgs eventArgs) public string GeneratePayloadSequence()
{
var sequence = new StringBuilder();
lock (_receivedEventArgs)
{
foreach (var receivedEventArg in _receivedEventArgs)
{
var payload = receivedEventArg.ApplicationMessage.ConvertPayloadToString();
// An empty payload is not part of the sequence!
sequence.Append(payload);
}
}
return sequence.ToString();
}
Task OnApplicationMessageReceivedAsync(MqttApplicationMessageReceivedEventArgs eventArgs)
{ {
lock (_receivedEventArgs) lock (_receivedEventArgs)
{ {
@@ -11,6 +11,7 @@ using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.VisualStudio.TestTools.UnitTesting;
using MQTTnet.Client; using MQTTnet.Client;
using MQTTnet.Diagnostics; using MQTTnet.Diagnostics;
using MQTTnet.Extensions.ManagedClient;
using MQTTnet.Extensions.Rpc; using MQTTnet.Extensions.Rpc;
using MQTTnet.Formatter; using MQTTnet.Formatter;
using MQTTnet.Internal; using MQTTnet.Internal;
@@ -199,6 +200,16 @@ namespace MQTTnet.Tests.Mockups
return new TestApplicationMessageReceivedHandler(mqttClient); return new TestApplicationMessageReceivedHandler(mqttClient);
} }
public TestApplicationMessageReceivedHandler CreateApplicationMessageHandler(IManagedMqttClient managedClient)
{
if (managedClient == null)
{
throw new ArgumentNullException(nameof(managedClient));
}
return new TestApplicationMessageReceivedHandler(managedClient.InternalClient);
}
public IMqttClient CreateClient() public IMqttClient CreateClient()
{ {
lock (_clients) lock (_clients)
+1 -1
View File
@@ -55,7 +55,7 @@ namespace MQTTnet.Tests.Server
}); });
} }
SpinWait.SpinUntil(() => receivedMessages == 100000, TimeSpan.FromSeconds(90)); SpinWait.SpinUntil(() => receivedMessages == 100000, TimeSpan.FromSeconds(120));
Assert.AreEqual(100000, receivedMessages); Assert.AreEqual(100000, receivedMessages);
} }
@@ -130,9 +130,7 @@ namespace MQTTnet.Tests.Server
CheckSubscriptionsResult CheckSubscriptions(string topic, MqttQualityOfServiceLevel applicationMessageQoSLevel, string senderClientId) CheckSubscriptionsResult CheckSubscriptions(string topic, MqttQualityOfServiceLevel applicationMessageQoSLevel, string senderClientId)
{ {
ulong topicHashMask; // not needed MqttSubscription.CalculateTopicHash(topic, out var topicHash, out _, out _);
bool hasWildcard; // not needed
MqttSubscription.CalculateTopicHash(topic, out var topicHash, out topicHashMask, out hasWildcard);
return _subscriptionsManager.CheckSubscriptions(topic, topicHash, applicationMessageQoSLevel, senderClientId); return _subscriptionsManager.CheckSubscriptions(topic, topicHash, applicationMessageQoSLevel, senderClientId);
} }
} }
@@ -0,0 +1,58 @@
// 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);
}
}
}
}
@@ -0,0 +1,31 @@
// 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.Protocol;
namespace MQTTnet.Tests.Server
{
[TestClass]
public sealed class Publishing_Tests : BaseTestClass
{
[TestMethod]
[ExpectedException(typeof(MqttClientDisconnectedException))]
public async Task Disconnect_While_Publishing()
{
using (var testEnvironment = CreateTestEnvironment())
{
var server = await testEnvironment.StartServer();
// The client will be disconnect directly after subscribing!
server.InterceptingPublishAsync += ev => server.DisconnectClientAsync(ev.ClientId, MqttDisconnectReasonCode.NormalDisconnection);
var client = await testEnvironment.ConnectClient();
await client.PublishStringAsync("test", qualityOfServiceLevel: MqttQualityOfServiceLevel.AtLeastOnce);
}
}
}
}
+51
View File
@@ -15,6 +15,57 @@ namespace MQTTnet.Tests.Server
[TestClass] [TestClass]
public sealed class QoS_Tests : BaseTestClass public sealed class QoS_Tests : BaseTestClass
{ {
[TestMethod]
public async Task Preserve_Message_Order_For_Queued_Messages()
{
using (var testEnvironment = CreateTestEnvironment())
{
var server = await testEnvironment.StartServer(o => o.WithPersistentSessions());
// Create a session which will contain the messages.
var dummyClient = await testEnvironment.ConnectClient(o => o.WithClientId("A").WithCleanSession(false));
await dummyClient.SubscribeAsync("#", MqttQualityOfServiceLevel.AtLeastOnce);
dummyClient.Dispose();
// Now inject messages which are appended to the queue of the client.
await server.InjectApplicationMessage("T", "0", qualityOfServiceLevel: MqttQualityOfServiceLevel.AtLeastOnce);
await server.InjectApplicationMessage("T", "2", qualityOfServiceLevel: MqttQualityOfServiceLevel.AtLeastOnce);
await server.InjectApplicationMessage("T", "1", qualityOfServiceLevel: MqttQualityOfServiceLevel.AtLeastOnce);
await server.InjectApplicationMessage("T", "4", qualityOfServiceLevel: MqttQualityOfServiceLevel.AtLeastOnce);
await server.InjectApplicationMessage("T", "3", qualityOfServiceLevel: MqttQualityOfServiceLevel.AtLeastOnce);
await server.InjectApplicationMessage("T", "6", qualityOfServiceLevel: MqttQualityOfServiceLevel.AtLeastOnce);
await server.InjectApplicationMessage("T", "5", qualityOfServiceLevel: MqttQualityOfServiceLevel.AtLeastOnce);
await server.InjectApplicationMessage("T", "8", qualityOfServiceLevel: MqttQualityOfServiceLevel.AtLeastOnce);
await server.InjectApplicationMessage("T", "7", qualityOfServiceLevel: MqttQualityOfServiceLevel.AtLeastOnce);
await server.InjectApplicationMessage("T", "9", qualityOfServiceLevel: MqttQualityOfServiceLevel.AtLeastOnce);
await LongTestDelay();
// Create a new client for the existing message.
var client = await testEnvironment.ConnectClient(o => o.WithClientId("A").WithCleanSession(false));
var messages = testEnvironment.CreateApplicationMessageHandler(client);
await LongTestDelay();
var payloadSequence = messages.GeneratePayloadSequence();
Assert.AreEqual("0214365879", payloadSequence);
// Disconnect and reconnect to make sure that the server will not send the messages twice.
await client.DisconnectAsync();
await LongTestDelay();
await client.ReconnectAsync();
await LongTestDelay();
payloadSequence = messages.GeneratePayloadSequence();
Assert.AreEqual("0214365879", payloadSequence);
}
}
[TestMethod] [TestMethod]
public async Task Fire_Event_On_Client_Acknowledges_QoS_0() public async Task Fire_Event_On_Client_Acknowledges_QoS_0()
{ {
+2 -2
View File
@@ -150,7 +150,7 @@ namespace MQTTnet.Tests.Server
// Try to connect 50 clients at the same time. // Try to connect 50 clients at the same time.
var clients = await Task.WhenAll(Enumerable.Range(0, 50).Select(i => ConnectAndSubscribe(testEnvironment, options, OnReceive))); var clients = await Task.WhenAll(Enumerable.Range(0, 50).Select(i => ConnectAndSubscribe(testEnvironment, options, OnReceive)));
await Task.Delay(5000); await Task.Delay(TimeSpan.FromSeconds(10));
var connectedClients = clients.Where(c => c?.TryPingAsync().GetAwaiter().GetResult() == true).ToList(); var connectedClients = clients.Where(c => c?.TryPingAsync().GetAwaiter().GetResult() == true).ToList();
@@ -162,7 +162,7 @@ namespace MQTTnet.Tests.Server
var sendClient = await testEnvironment.ConnectClient(option2); var sendClient = await testEnvironment.ConnectClient(option2);
await sendClient.PublishStringAsync("aaa", "1"); await sendClient.PublishStringAsync("aaa", "1");
await Task.Delay(3000); await Task.Delay(TimeSpan.FromSeconds(5));
Assert.AreEqual(true, hasReceive); Assert.AreEqual(true, hasReceive);
} }
@@ -46,6 +46,22 @@ namespace MQTTnet.Tests.Server
} }
} }
[TestMethod]
[ExpectedException(typeof(MqttClientDisconnectedException))]
public async Task Disconnect_While_Subscribing()
{
using (var testEnvironment = CreateTestEnvironment())
{
var server = await testEnvironment.StartServer();
// The client will be disconnect directly after subscribing!
server.ClientSubscribedTopicAsync += ev => server.DisconnectClientAsync(ev.ClientId, MqttDisconnectReasonCode.NormalDisconnection);
var client = await testEnvironment.ConnectClient();
await client.SubscribeAsync("#");
}
}
[TestMethod] [TestMethod]
public async Task Enqueue_Message_After_Subscription() public async Task Enqueue_Message_After_Subscription()
{ {
@@ -1,9 +1,11 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.VisualStudio.TestTools.UnitTesting;
using MQTTnet.Packets;
using MQTTnet.Protocol; using MQTTnet.Protocol;
using MQTTnet.Server; using MQTTnet.Server;
using MQTTnet.Tests.Mockups; using MQTTnet.Tests.Mockups;
@@ -13,43 +15,299 @@ namespace MQTTnet.Tests.Server
[TestClass] [TestClass]
public sealed class SubscriptionTopicHashTests public sealed class SubscriptionTopicHashTests
{ {
enum TopicHashSelector { NoWildcard, SingleWildcard, MultiWildcard };
MqttSession _clientSession; MqttSession _clientSession;
/// <summary>
/// Even fairly regularly named topics as generated by the topic generator should result in shallow hash buckets
/// </summary>
/// <returns></returns>
[TestMethod] [TestMethod]
public void Match_Hash_Test_SingleWildCard() public void Check_Hash_Bucket_Depth()
{ {
var l0 = "pub0"; const int numPublishers = 5000;
var l1 = "topic1"; const int numTopicsPerPublisher = 10;
var l2 = "+";
var l3 = "prop1"; TopicGenerator.Generate(
var topic = $"{l0}/{l1}/{l2}/{l3}"; numPublishers,
numTopicsPerPublisher,
out var topicsByPublisher,
out _,
out _);
// There will be many 'similar' topics ending with, i.e. "sensor100", "sensor101", ...
// Hash bucket depths should remain low.
var bucketDepths = new Dictionary<ulong, int>();
var maxBucketDepth = 0;
ulong maxBucketDepthHash = 0;
var topicsByHash = new Dictionary<ulong, List<string>>();
foreach (var t in topicsByPublisher)
{
var topics = t.Value;
foreach (var topic in topics)
{
MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var hashMask, out var hasWildcard);
bucketDepths.TryGetValue(topicHash, out var currentValue);
++currentValue;
bucketDepths[topicHash] = currentValue;
if (currentValue > maxBucketDepth)
{
maxBucketDepth = currentValue;
maxBucketDepthHash = topicHash;
}
if (!topicsByHash.TryGetValue(topicHash, out var topicList))
{
topicList = new List<string>();
topicsByHash.Add(topicHash, topicList);
}
topicList.Add(topic);
}
}
var maxDepthTopics = topicsByHash[maxBucketDepthHash];
Console.Write("Max bucket depth is " + maxBucketDepth);
// for the test case the bucket depth should be less than 100
Assert.IsTrue(maxBucketDepth < 100, "Unexpected high topic hash bucket depth");
}
[TestMethod]
public void Check_Selected_Topic_Hashes()
{
CheckTopicHash("client1/building1/level1/sensor1", 0x655D4AF100000000, 0xFFFFFFFFFFFFFFFF);
CheckTopicHash("client1/building1/+/sensor1", 0x655D00F100000000, 0xFFFF00FFFFFFFFFF);
CheckTopicHash("client1/+/level1/+", 0x65004A0000000000, 0xFF00FF00FFFFFFFF);
CheckTopicHash("client1/building1/level1/#", 0x655D4A0000000000, 0xFFFFFF0000000000);
CheckTopicHash("client1/+/level1/#", 0x65004A0000000000, 0xFF00FF0000000000);
}
/// <summary>
/// Test long topic name with last level being #
/// </summary>
[TestMethod]
public void Match_Hash_Test_LongTopic_DetectMultiLevelWildcard()
{
var topic = "asdfasdf/asdfasdf/asdfasdf/asdfasdf/asdfas/dfaf/assfdgsdfgdf/#";
MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard);
Assert.IsTrue(topicHasWildcard, "Wildcard not detected");
}
/// <summary>
/// Test long topic name with last level being +
/// </summary>
[TestMethod]
public void Match_Hash_Test_LongTopic_DetectSingleLevelWildcard()
{
var topic = "asdfasdf/asdfasdf/asdfasdf/asdfasdf/asdfas/dfaf/assfdgsdfgdf/+";
MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard);
Assert.IsTrue(topicHasWildcard, "Wildcard not detected");
}
/// <summary>
/// Test long topic name with last level being #
/// </summary>
[TestMethod]
public void Match_Hash_Test_LongTopic_MultiWildcard()
{
var sb = new StringBuilder();
const int numLevels = 8;
var levelNames = new string[numLevels];
for (var i = 0; i < numLevels; ++i)
{
if (i > 0)
{
sb.Append("/");
}
string levelName;
if (i == numLevels - 1)
{
// last one is #
levelName = "#";
}
else
{
levelName = "level" + i;
}
levelNames[i] = levelName;
sb.Append(levelName);
}
var topic = sb.ToString();
// UInt64 is limited to 8 levels
MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard); MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard);
Assert.IsTrue(topicHasWildcard, "Wildcard not detected"); Assert.IsTrue(topicHasWildcard, "Wildcard not detected");
var hashBytes = GetBytes(topicHash); var hashBytes = GetBytes(topicHash);
Assert.AreNotEqual(hashBytes[0], 0, "checksum 0 mismatch"); // all bytes should contain checksum
Assert.AreNotEqual(hashBytes[1], 0, "checksum 1 mismatch"); var count = 0;
Assert.AreEqual(hashBytes[2], 0, "checksum 2 mismatch"); foreach (var h in hashBytes)
Assert.AreNotEqual(hashBytes[3], 0, "checksum 3 mismatch"); {
Assert.AreEqual(hashBytes[4], 0, "checksum 4 mismatch"); if (count < 7)
Assert.AreEqual(hashBytes[5], 0, "checksum 5 mismatch"); {
Assert.AreEqual(hashBytes[6], 0, "checksum 6 mismatch"); Assert.AreNotEqual(h, 0, "checksum mismatch");
Assert.AreEqual(hashBytes[7], 0, "checksum 7 mismatch"); }
else
{
Assert.AreEqual(h, 0, "checksum mismatch");
}
// The mask should have zeroes where the wildcard and ff at the end ++count;
}
// The mask should have ff except for last level
var hashMaskBytes = GetBytes(topicHashMask); var hashMaskBytes = GetBytes(topicHashMask);
Assert.AreEqual(hashMaskBytes[0], 0xff, "mask 0 mismatch"); count = 0;
Assert.AreEqual(hashMaskBytes[1], 0xff, "mask 1 mismatch"); foreach (var h in hashMaskBytes)
Assert.AreEqual(hashMaskBytes[2], 0, "mask 2 mismatch"); {
Assert.AreEqual(hashMaskBytes[3], 0xff, "mask 3 mismatch"); if (count < 7)
Assert.AreEqual(hashMaskBytes[4], 0xff, "mask 4 mismatch"); {
Assert.AreEqual(hashMaskBytes[5], 0xff, "mask 5 mismatch"); Assert.AreEqual(h, 0xff, "mask mismatch");
Assert.AreEqual(hashMaskBytes[6], 0xff, "mask 6 mismatch"); }
Assert.AreEqual(hashMaskBytes[7], 0xff, "mask 7 mismatch"); else
{
Assert.AreEqual(h, 0, "last mask mismatch");
}
++count;
}
}
/// <summary>
/// Test long topic name exceeding 8 levels
/// </summary>
[TestMethod]
public void Match_Hash_Test_LongTopic_NoWildCard()
{
var sb = new StringBuilder();
const int numLevels = 9;
var levelNames = new string[numLevels];
for (var i = 0; i < numLevels; ++i)
{
if (i > 0)
{
sb.Append("/");
}
var levelName = "level" + i;
levelNames[i] = levelName;
sb.Append(levelName);
}
var topic = sb.ToString();
MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard);
Assert.IsFalse(topicHasWildcard, "Wildcard detected when not present");
var hashBytes = GetBytes(topicHash);
// all bytes should contain checksum
var count = 0;
foreach (var h in hashBytes)
{
Assert.AreNotEqual(h, 0, "checksum mismatch");
++count;
}
// The mask should have ff
var hashMaskBytes = GetBytes(topicHashMask);
count = 0;
foreach (var h in hashMaskBytes)
{
Assert.AreEqual(h, 0xff, "mask mismatch");
++count;
}
}
/// <summary>
/// Test long topic name with last level being +
/// </summary>
[TestMethod]
public void Match_Hash_Test_LongTopic_SingleWildCard()
{
var sb = new StringBuilder();
const int numLevels = 8;
var levelNames = new string[numLevels];
for (var i = 0; i < numLevels; ++i)
{
if (i > 0)
{
sb.Append("/");
}
string levelName;
if (i == numLevels - 1)
{
// last one is +
levelName = "+";
}
else
{
levelName = "level" + i;
}
levelNames[i] = levelName;
sb.Append(levelName);
}
var topic = sb.ToString();
MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard);
Assert.IsTrue(topicHasWildcard, "Wildcard not detected");
var hashBytes = GetBytes(topicHash);
// all bytes should contain checksum
var count = 0;
foreach (var h in hashBytes)
{
if (count < 7)
{
Assert.AreNotEqual(h, 0, "checksum mismatch");
}
else
{
// wildcard position
Assert.AreEqual(h, 0, "checksum mismatch");
}
++count;
}
// The mask should have ff
var hashMaskBytes = GetBytes(topicHashMask);
count = 0;
foreach (var h in hashMaskBytes)
{
if (count < 7)
{
Assert.AreEqual(h, 0xff, "mask mismatch");
}
else
{
Assert.AreEqual(h, 0, "last mask mismatch");
}
++count;
}
} }
[TestMethod] [TestMethod]
@@ -123,205 +381,13 @@ namespace MQTTnet.Tests.Server
Assert.AreEqual(hashMaskBytes[7], 0xff, "mask 7 mismatch"); Assert.AreEqual(hashMaskBytes[7], 0xff, "mask 7 mismatch");
} }
/// <summary>
/// Test long topic name with last level being #
/// </summary>
[TestMethod] [TestMethod]
public void Match_Hash_Test_LongTopic_MultiWildcard() public async Task Match_Hash_Test_Search_MultiWildcard()
{ {
var sb = new StringBuilder(); var topics = await PrepareTopicHashSubscriptions(TopicHashSelector.MultiWildcard);
const int numLevels = 8; var matchCount = CheckTopicSubscriptions(_clientSession, topics, "Multi Wildcard");
var levelNames = new string[numLevels]; // Should match all topics
for (var i = 0; i < numLevels; ++i) Assert.AreEqual(topics.Count, matchCount, "Topics not matched");
{
if (i > 0)
sb.Append("/");
string levelName;
if (i == numLevels - 1)
{
// last one is #
levelName = "#";
}
else
{
levelName = "level" + i;
}
levelNames[i] = levelName;
sb.Append(levelName);
}
var topic = sb.ToString();
// UInt64 is limited to 8 levels
MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard);
Assert.IsTrue(topicHasWildcard, "Wildcard not detected");
var hashBytes = GetBytes(topicHash);
// all bytes should contain checksum
int count = 0;
foreach (var h in hashBytes)
{
if (count < 7)
{
Assert.AreNotEqual(h, 0, "checksum mismatch");
}
else
{
Assert.AreEqual(h, 0, "checksum mismatch");
}
++count;
}
// The mask should have ff except for last level
var hashMaskBytes = GetBytes(topicHashMask);
count = 0;
foreach (var h in hashMaskBytes)
{
if (count < 7)
{
Assert.AreEqual(h, 0xff, "mask mismatch");
}
else
{
Assert.AreEqual(h, 0, "last mask mismatch");
}
++count;
}
}
/// <summary>
/// Test long topic name with last level being +
/// </summary>
[TestMethod]
public void Match_Hash_Test_LongTopic_SingleWildCard()
{
var sb = new StringBuilder();
const int numLevels = 8;
var levelNames = new string[numLevels];
for (var i = 0; i < numLevels; ++i)
{
if (i > 0)
sb.Append("/");
string levelName;
if (i == numLevels - 1)
{
// last one is +
levelName = "+";
}
else
{
levelName = "level" + i;
}
levelNames[i] = levelName;
sb.Append(levelName);
}
var topic = sb.ToString();
MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard);
Assert.IsTrue(topicHasWildcard, "Wildcard not detected");
var hashBytes = GetBytes(topicHash);
// all bytes should contain checksum
int count = 0;
foreach (var h in hashBytes)
{
if (count < 7)
{
Assert.AreNotEqual(h, 0, "checksum mismatch");
}
else
{
// wildcard position
Assert.AreEqual(h, 0, "checksum mismatch");
}
++count;
}
// The mask should have ff
var hashMaskBytes = GetBytes(topicHashMask);
count = 0;
foreach (var h in hashMaskBytes)
{
if (count < 7)
{
Assert.AreEqual(h, 0xff, "mask mismatch");
}
else
{
Assert.AreEqual(h, 0, "last mask mismatch");
}
++count;
}
}
/// <summary>
/// Test long topic name exceeding 8 levels
/// </summary>
[TestMethod]
public void Match_Hash_Test_LongTopic_NoWildCard()
{
var sb = new StringBuilder();
const int numLevels = 9;
var levelNames = new string[numLevels];
for(var i=0; i < numLevels; ++i)
{
if (i > 0)
sb.Append("/");
var levelName = "level" + i;
levelNames[i] = levelName;
sb.Append(levelName);
}
var topic = sb.ToString();
MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard);
Assert.IsFalse(topicHasWildcard, "Wildcard detected when not present");
var hashBytes = GetBytes(topicHash);
// all bytes should contain checksum
int count = 0;
foreach(var h in hashBytes)
{
Assert.AreNotEqual(h, 0, "checksum mismatch");
++count;
}
// The mask should have ff
var hashMaskBytes = GetBytes(topicHashMask);
count = 0;
foreach (var h in hashMaskBytes)
{
Assert.AreEqual(h, 0xff, "mask mismatch");
++count;
}
}
/// <summary>
/// Test long topic name with last level being +
/// </summary>
[TestMethod]
public void Match_Hash_Test_LongTopic_DetectSingleLevelWildcard()
{
var topic = "asdfasdf/asdfasdf/asdfasdf/asdfasdf/asdfas/dfaf/assfdgsdfgdf/+";
MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard);
Assert.IsTrue(topicHasWildcard, "Wildcard not detected");
}
/// <summary>
/// Test long topic name with last level being #
/// </summary>
[TestMethod]
public void Match_Hash_Test_LongTopic_DetectMultiLevelWildcard()
{
var topic = "asdfasdf/asdfasdf/asdfasdf/asdfasdf/asdfas/dfaf/assfdgsdfgdf/#";
MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard);
Assert.IsTrue(topicHasWildcard, "Wildcard not detected");
} }
[TestMethod] [TestMethod]
@@ -373,77 +439,38 @@ namespace MQTTnet.Tests.Server
} }
[TestMethod] [TestMethod]
public async Task Match_Hash_Test_Search_MultiWildcard() public void Match_Hash_Test_SingleWildCard()
{ {
var topics = await PrepareTopicHashSubscriptions(TopicHashSelector.MultiWildcard); var l0 = "pub0";
var matchCount = CheckTopicSubscriptions(_clientSession, topics, "Multi Wildcard"); var l1 = "topic1";
// Should match all topics var l2 = "+";
Assert.AreEqual(topics.Count, matchCount, "Topics not matched"); var l3 = "prop1";
} var topic = $"{l0}/{l1}/{l2}/{l3}";
/// <summary> MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard);
/// Even fairly regularly named topics as generated by the topic generator should result in shallow hash buckets
/// </summary>
/// <returns></returns>
[TestMethod]
public void Check_Hash_Bucket_Depth()
{
const int numPublishers = 5000;
const int numTopicsPerPublisher = 10;
TopicGenerator.Generate(numPublishers, numTopicsPerPublisher, out var topicsByPublisher, out var singleWildcardTopicsByPublisher, out var multiWildcardTopicsByPublisher); Assert.IsTrue(topicHasWildcard, "Wildcard not detected");
// There will be many 'similar' topics ending with, i.e. "sensor100", "sensor101", ... var hashBytes = GetBytes(topicHash);
// Hash bucket depths should remain low. Assert.AreNotEqual(hashBytes[0], 0, "checksum 0 mismatch");
var bucketDepths = new Dictionary<UInt64, int>(); Assert.AreNotEqual(hashBytes[1], 0, "checksum 1 mismatch");
int maxBucketDepth = 0; Assert.AreEqual(hashBytes[2], 0, "checksum 2 mismatch");
UInt64 maxBucketDepthHash = 0; Assert.AreNotEqual(hashBytes[3], 0, "checksum 3 mismatch");
Assert.AreEqual(hashBytes[4], 0, "checksum 4 mismatch");
Assert.AreEqual(hashBytes[5], 0, "checksum 5 mismatch");
Assert.AreEqual(hashBytes[6], 0, "checksum 6 mismatch");
Assert.AreEqual(hashBytes[7], 0, "checksum 7 mismatch");
var topicsByHash = new Dictionary<UInt64, List<string>>(); // The mask should have zeroes where the wildcard and ff at the end
var hashMaskBytes = GetBytes(topicHashMask);
foreach (var t in topicsByPublisher) Assert.AreEqual(hashMaskBytes[0], 0xff, "mask 0 mismatch");
{ Assert.AreEqual(hashMaskBytes[1], 0xff, "mask 1 mismatch");
var topics = t.Value; Assert.AreEqual(hashMaskBytes[2], 0, "mask 2 mismatch");
foreach (var topic in topics) Assert.AreEqual(hashMaskBytes[3], 0xff, "mask 3 mismatch");
{ Assert.AreEqual(hashMaskBytes[4], 0xff, "mask 4 mismatch");
MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var hashMask, out var hasWildcard); Assert.AreEqual(hashMaskBytes[5], 0xff, "mask 5 mismatch");
Assert.AreEqual(hashMaskBytes[6], 0xff, "mask 6 mismatch");
bucketDepths.TryGetValue(topicHash, out var currentValue); Assert.AreEqual(hashMaskBytes[7], 0xff, "mask 7 mismatch");
++currentValue;
bucketDepths[topicHash] = currentValue;
if (currentValue > maxBucketDepth)
{
maxBucketDepth = currentValue;
maxBucketDepthHash = topicHash;
}
if (!topicsByHash.TryGetValue(topicHash, out var topicList))
{
topicList = new List<string>();
topicsByHash.Add(topicHash, topicList);
}
topicList.Add(topic);
}
}
var maxDepthTopics = topicsByHash[maxBucketDepthHash];
Console.Write("Max bucket depth is " + maxBucketDepth);
// for the test case the bucket depth should be less than 100
Assert.IsTrue(maxBucketDepth < 100, "Unexpected high topic hash bucket depth");
}
[TestMethod]
public void Check_Selected_Topic_Hashes()
{
CheckTopicHash("client1/building1/level1/sensor1", 0x655D4AF100000000, 0xFFFFFFFFFFFFFFFF);
CheckTopicHash("client1/building1/+/sensor1", 0x655D00F100000000, 0xFFFF00FFFFFFFFFF);
CheckTopicHash("client1/+/level1/+", 0x65004A0000000000, 0xFF00FF00FFFFFFFF);
CheckTopicHash("client1/building1/level1/#", 0x655D4A0000000000, 0xFFFFFF0000000000);
CheckTopicHash("client1/+/level1/#", 0x65004A0000000000, 0xFF00FF0000000000);
} }
void CheckTopicHash(string topic, ulong expectedHash, ulong expectedHashMask) void CheckTopicHash(string topic, ulong expectedHash, ulong expectedHashMask)
@@ -459,13 +486,66 @@ namespace MQTTnet.Tests.Server
Assert.AreEqual(expectedHashMask, hashMask, "Topic hash mask not as expected"); Assert.AreEqual(expectedHashMask, hashMask, "Topic hash mask not as expected");
} }
int CheckTopicSubscriptions(MqttSession clientSession, List<string> topicsToFind, string subject)
{
var matchCount = 0;
{
var resultCount = 0;
var stopWatch = new Stopwatch();
stopWatch.Start();
var countUp = 0;
var countDown = topicsToFind.Count - 1;
for (; countUp < topicsToFind.Count; ++countUp, --countDown)
{
var topicToFind = topicsToFind[countDown];
MqttSubscription.CalculateTopicHash(topicToFind, out var topicHash, out _, out _);
clientSession.TryCheckSubscriptions(topicToFind, topicHash, MqttQualityOfServiceLevel.AtMostOnce, "OtherClient", out var result);
if (result.IsSubscribed)
{
++matchCount;
}
++resultCount;
}
stopWatch.Stop();
Console.Write("Match count: " + matchCount + "; ");
Console.WriteLine(subject + " lookup milliseconds: " + stopWatch.ElapsedMilliseconds);
}
return matchCount;
}
byte[] GetBytes(ulong value)
{
var bytes = BitConverter.GetBytes(value);
// Ensure that highest byte comes first for comparison left to right
if (BitConverter.IsLittleEndian)
{
return bytes.Reverse().ToArray();
}
return bytes;
}
async Task<List<string>> PrepareTopicHashSubscriptions(TopicHashSelector selector) async Task<List<string>> PrepareTopicHashSubscriptions(TopicHashSelector selector)
{ {
const int numPublishers = 1; const int numPublishers = 1;
const int numTopicsPerPublisher = 10000; const int numTopicsPerPublisher = 10000;
TopicGenerator.Generate(numPublishers, numTopicsPerPublisher, out var topicsByPublisher, out var singleWildcardTopicsByPublisher, out var multiWildcardTopicsByPublisher); TopicGenerator.Generate(
numPublishers,
numTopicsPerPublisher,
out var topicsByPublisher,
out var singleWildcardTopicsByPublisher,
out var multiWildcardTopicsByPublisher);
var topics = topicsByPublisher.FirstOrDefault().Value; var topics = topicsByPublisher.FirstOrDefault().Value;
var singleWildcardTopics = singleWildcardTopicsByPublisher.FirstOrDefault().Value; var singleWildcardTopics = singleWildcardTopicsByPublisher.FirstOrDefault().Value;
@@ -477,15 +557,7 @@ namespace MQTTnet.Tests.Server
var eventContainer = new MqttServerEventContainer(); var eventContainer = new MqttServerEventContainer();
var retainedMessagesManager = new MqttRetainedMessagesManager(eventContainer, logger); var retainedMessagesManager = new MqttRetainedMessagesManager(eventContainer, logger);
var sessionManager = new MqttClientSessionsManager(serverOptions, retainedMessagesManager, eventContainer, logger); var sessionManager = new MqttClientSessionsManager(serverOptions, retainedMessagesManager, eventContainer, logger);
_clientSession = new MqttSession( _clientSession = new MqttSession(clientId, false, new Dictionary<object, object>(), serverOptions, eventContainer, retainedMessagesManager, sessionManager);
clientId,
false,
new Dictionary<object, object>(),
serverOptions,
eventContainer,
retainedMessagesManager,
sessionManager
);
List<string> topicsToSubscribe; List<string> topicsToSubscribe;
@@ -501,62 +573,26 @@ namespace MQTTnet.Tests.Server
topicsToSubscribe = topics; topicsToSubscribe = topics;
break; break;
} }
foreach (var t in topicsToSubscribe) foreach (var t in topicsToSubscribe)
{ {
var subPacket = new Packets.MqttSubscribePacket(); var subPacket = new MqttSubscribePacket();
var filter = new Packets.MqttTopicFilter var filter = new MqttTopicFilter
{ {
Topic = t Topic = t
}; };
subPacket.TopicFilters.Add(filter); subPacket.TopicFilters.Add(filter);
await _clientSession.SubscriptionsManager.Subscribe(subPacket, default); await _clientSession.Subscribe(subPacket, default);
} }
return topics; return topics;
} }
int CheckTopicSubscriptions(MqttSession clientSession, List<string> topicsToFind, string subject) enum TopicHashSelector
{ {
var matchCount = 0; NoWildcard,
SingleWildcard,
{ MultiWildcard
int resultCount = 0;
var stopWatch = new System.Diagnostics.Stopwatch();
stopWatch.Start();
var countUp = 0;
var countDown = topicsToFind.Count - 1;
for (; countUp < topicsToFind.Count; ++countUp, --countDown)
{
var topicToFind = topicsToFind[countDown];
MqttSubscription.CalculateTopicHash(topicToFind, out var topicHash, out var hashMask, out var hasWildcard);
var result = clientSession.SubscriptionsManager.CheckSubscriptions(topicToFind, topicHash, MqttQualityOfServiceLevel.AtMostOnce, "OtherClient");
if (result.IsSubscribed)
{
++matchCount;
}
++resultCount;
}
stopWatch.Stop();
Console.Write("Match count: " + matchCount + "; ");
Console.WriteLine(subject + " lookup milliseconds: " + stopWatch.ElapsedMilliseconds);
}
return matchCount;
}
byte[] GetBytes(UInt64 value)
{
var bytes = BitConverter.GetBytes(value);
// Ensure that highest byte comes first for comparison left to right
if (BitConverter.IsLittleEndian)
return bytes.Reverse().ToArray();
return bytes;
} }
} }
} }
@@ -0,0 +1,32 @@
// 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.Protocol;
namespace MQTTnet.Tests.Server
{
[TestClass]
public sealed class Unsubscribe_Tests : BaseTestClass
{
[TestMethod]
[ExpectedException(typeof(MqttClientDisconnectedException))]
public async Task Disconnect_While_Unsubscribing()
{
using (var testEnvironment = CreateTestEnvironment())
{
var server = await testEnvironment.StartServer();
// The client will be disconnect directly after subscribing!
server.ClientUnsubscribedTopicAsync += ev => server.DisconnectClientAsync(ev.ClientId, MqttDisconnectReasonCode.NormalDisconnection);
var client = await testEnvironment.ConnectClient();
await client.SubscribeAsync("#");
await client.UnsubscribeAsync("#");
}
}
}
}
+1 -2
View File
@@ -1,8 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text;
namespace MQTTnet.Tests.Server namespace MQTTnet.Tests
{ {
public class TopicGenerator public class TopicGenerator
{ {
+4 -4
View File
@@ -86,7 +86,7 @@ namespace MQTTnet.Adapter
await Task.WhenAny(connectTask, timeout.Task).ConfigureAwait(false); await Task.WhenAny(connectTask, timeout.Task).ConfigureAwait(false);
if (timeout.Task.IsCompleted && !connectTask.IsCompleted) if (timeout.Task.IsCompleted && !connectTask.IsCompleted)
{ {
throw new OperationCanceledException("MQTT connect cancelled.", cancellationToken); throw new OperationCanceledException("MQTT connect canceled.", cancellationToken);
} }
// Make sure that the exception from the connect task gets thrown. // Make sure that the exception from the connect task gets thrown.
@@ -298,7 +298,7 @@ namespace MQTTnet.Adapter
// Check two times for cancellation because the call to _ReadAsync_ might block for some time. // Check two times for cancellation because the call to _ReadAsync_ might block for some time.
if (cancellationToken.IsCancellationRequested) if (cancellationToken.IsCancellationRequested)
{ {
return ReadFixedHeaderResult.Cancelled; return ReadFixedHeaderResult.Canceled;
} }
int bytesRead; int bytesRead;
@@ -308,7 +308,7 @@ namespace MQTTnet.Adapter
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
return ReadFixedHeaderResult.Cancelled; return ReadFixedHeaderResult.Canceled;
} }
catch (SocketException) catch (SocketException)
{ {
@@ -317,7 +317,7 @@ namespace MQTTnet.Adapter
if (cancellationToken.IsCancellationRequested) if (cancellationToken.IsCancellationRequested)
{ {
return ReadFixedHeaderResult.Cancelled; return ReadFixedHeaderResult.Canceled;
} }
if (bytesRead == 0) if (bytesRead == 0)
@@ -0,0 +1,16 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using MQTTnet.Exceptions;
namespace MQTTnet.Client
{
public sealed class MqttClientDisconnectedException : MqttCommunicationException
{
public MqttClientDisconnectedException(Exception innerException) : base("The MQTT client is disconnected.", innerException)
{
}
}
}
@@ -3,14 +3,15 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System.Collections.Generic; using System.Collections.Generic;
using MQTTnet.Exceptions;
using MQTTnet.Packets; using MQTTnet.Packets;
using MQTTnet.Protocol; using MQTTnet.Protocol;
namespace MQTTnet.Exceptions namespace MQTTnet.Client
{ {
public class MqttUnexpectedDisconnectReceivedException : MqttCommunicationException public sealed class MqttClientUnexpectedDisconnectReceivedException : MqttCommunicationException
{ {
public MqttUnexpectedDisconnectReceivedException(MqttDisconnectPacket disconnectPacket) public MqttClientUnexpectedDisconnectReceivedException(MqttDisconnectPacket disconnectPacket)
: base($"Unexpected DISCONNECT (Reason code={disconnectPacket.ReasonCode}) received.") : base($"Unexpected DISCONNECT (Reason code={disconnectPacket.ReasonCode}) received.")
{ {
ReasonCode = disconnectPacket.ReasonCode; ReasonCode = disconnectPacket.ReasonCode;
+10 -8
View File
@@ -32,25 +32,21 @@ namespace MQTTnet.Client
readonly object _disconnectLock = new object(); readonly object _disconnectLock = new object();
readonly AsyncEvent<InspectMqttPacketEventArgs> _inspectPacketEvent = new AsyncEvent<InspectMqttPacketEventArgs>(); readonly AsyncEvent<InspectMqttPacketEventArgs> _inspectPacketEvent = new AsyncEvent<InspectMqttPacketEventArgs>();
readonly MqttNetSourceLogger _logger; readonly MqttNetSourceLogger _logger;
readonly MqttPacketDispatcher _packetDispatcher = new MqttPacketDispatcher();
readonly MqttPacketFactories _packetFactories = new MqttPacketFactories(); readonly MqttPacketFactories _packetFactories = new MqttPacketFactories();
readonly MqttPacketIdentifierProvider _packetIdentifierProvider = new MqttPacketIdentifierProvider(); readonly MqttPacketIdentifierProvider _packetIdentifierProvider = new MqttPacketIdentifierProvider();
readonly IMqttNetLogger _rootLogger; readonly IMqttNetLogger _rootLogger;
IMqttChannelAdapter _adapter; IMqttChannelAdapter _adapter;
CancellationTokenSource _backgroundCancellationTokenSource; CancellationTokenSource _backgroundCancellationTokenSource;
bool _cleanDisconnectInitiated; bool _cleanDisconnectInitiated;
volatile int _connectionStatus; volatile int _connectionStatus;
MqttClientDisconnectReason _disconnectReason; MqttClientDisconnectReason _disconnectReason;
string _disconnectReasonString; string _disconnectReasonString;
Task _keepAlivePacketsSenderTask; Task _keepAlivePacketsSenderTask;
DateTime _lastPacketSentTimestamp; DateTime _lastPacketSentTimestamp;
MqttPacketDispatcher _packetDispatcher;
Task _packetReceiverTask; Task _packetReceiverTask;
AsyncQueue<MqttPublishPacket> _publishPacketReceiverQueue; AsyncQueue<MqttPublishPacket> _publishPacketReceiverQueue;
Task _publishPacketReceiverTask; Task _publishPacketReceiverTask;
@@ -128,7 +124,7 @@ namespace MQTTnet.Client
} }
_packetIdentifierProvider.Reset(); _packetIdentifierProvider.Reset();
_packetDispatcher.CancelAll(); _packetDispatcher = new MqttPacketDispatcher();
_backgroundCancellationTokenSource = new CancellationTokenSource(); _backgroundCancellationTokenSource = new CancellationTokenSource();
var backgroundCancellationToken = _backgroundCancellationTokenSource.Token; var backgroundCancellationToken = _backgroundCancellationTokenSource.Token;
@@ -414,6 +410,10 @@ namespace MQTTnet.Client
_publishPacketReceiverQueue = null; _publishPacketReceiverQueue = null;
_adapter?.Dispose(); _adapter?.Dispose();
_adapter = null;
_packetDispatcher?.Dispose();
_packetDispatcher = null;
} }
} }
@@ -447,6 +447,8 @@ namespace MQTTnet.Client
try try
{ {
_packetDispatcher.Dispose(new MqttClientDisconnectedException(exception));
var receiverTask = WaitForTaskAsync(_packetReceiverTask, sender); var receiverTask = WaitForTaskAsync(_packetReceiverTask, sender);
var publishPacketReceiverTask = WaitForTaskAsync(_publishPacketReceiverTask, sender); var publishPacketReceiverTask = WaitForTaskAsync(_publishPacketReceiverTask, sender);
var keepAliveTask = WaitForTaskAsync(_keepAlivePacketsSenderTask, sender); var keepAliveTask = WaitForTaskAsync(_keepAlivePacketsSenderTask, sender);
@@ -558,7 +560,7 @@ namespace MQTTnet.Client
_disconnectReasonString = disconnectPacket.ReasonString; _disconnectReasonString = disconnectPacket.ReasonString;
// Also dispatch disconnect to waiting threads to generate a proper exception. // Also dispatch disconnect to waiting threads to generate a proper exception.
_packetDispatcher.FailAll(new MqttUnexpectedDisconnectReceivedException(disconnectPacket)); _packetDispatcher.Dispose(new MqttClientUnexpectedDisconnectReceivedException(disconnectPacket));
return DisconnectInternalAsync(_packetReceiverTask, null, null); return DisconnectInternalAsync(_packetReceiverTask, null, null);
} }
@@ -776,7 +778,7 @@ namespace MQTTnet.Client
} }
else else
{ {
_logger.Error(exception, "Error while receiving packets."); _logger.Error(exception, $"Error while processing received packet ({packet.GetType().Name}).");
} }
_packetDispatcher.FailAll(exception); _packetDispatcher.FailAll(exception);
+31 -20
View File
@@ -14,26 +14,6 @@ namespace MQTTnet.Client
{ {
public static class MqttClientExtensions public static class MqttClientExtensions
{ {
public static async Task<bool> TryPingAsync(this IMqttClient client, CancellationToken cancellationToken = default)
{
if (client == null)
{
throw new ArgumentNullException(nameof(client));
}
try
{
await client.PingAsync(cancellationToken).ConfigureAwait(false);
return true;
}
catch
{
// Ignore errors.
}
return false;
}
public static Task DisconnectAsync(this IMqttClient client, MqttClientDisconnectReason reason = MqttClientDisconnectReason.NormalDisconnection, string reasonString = null) public static Task DisconnectAsync(this IMqttClient client, MqttClientDisconnectReason reason = MqttClientDisconnectReason.NormalDisconnection, string reasonString = null)
{ {
if (client == null) if (client == null)
@@ -89,6 +69,17 @@ namespace MQTTnet.Client
return mqttClient.PublishBinaryAsync(topic, payloadBuffer, qualityOfServiceLevel, retain, cancellationToken); return mqttClient.PublishBinaryAsync(topic, payloadBuffer, qualityOfServiceLevel, retain, cancellationToken);
} }
public static Task ReconnectAsync(this IMqttClient client, CancellationToken cancellationToken = default)
{
if (client.Options == null)
{
throw new InvalidOperationException(
"The MQTT client was not connected before. A reconnect is only permitted when the client was already connected or at least tried to.");
}
return client.ConnectAsync(client.Options, cancellationToken);
}
public static Task SendExtendedAuthenticationExchangeDataAsync(this IMqttClient client, MqttExtendedAuthenticationExchangeData data) public static Task SendExtendedAuthenticationExchangeDataAsync(this IMqttClient client, MqttExtendedAuthenticationExchangeData data)
{ {
if (client == null) if (client == null)
@@ -137,6 +128,26 @@ namespace MQTTnet.Client
return mqttClient.SubscribeAsync(subscribeOptions, cancellationToken); return mqttClient.SubscribeAsync(subscribeOptions, cancellationToken);
} }
public static async Task<bool> TryPingAsync(this IMqttClient client, CancellationToken cancellationToken = default)
{
if (client == null)
{
throw new ArgumentNullException(nameof(client));
}
try
{
await client.PingAsync(cancellationToken).ConfigureAwait(false);
return true;
}
catch
{
// Ignore errors.
}
return false;
}
public static Task<MqttClientUnsubscribeResult> UnsubscribeAsync(this IMqttClient mqttClient, string topic, CancellationToken cancellationToken = default) public static Task<MqttClientUnsubscribeResult> UnsubscribeAsync(this IMqttClient mqttClient, string topic, CancellationToken cancellationToken = default)
{ {
if (mqttClient == null) if (mqttClient == null)
@@ -10,28 +10,37 @@ namespace MQTTnet.Client
public sealed class MqttClientPublishResult public sealed class MqttClientPublishResult
{ {
/// <summary> /// <summary>
/// Gets the packet identifier which was used for this publish. /// Returns if the overall status of the publish is a success. This can be the reason code _Success_ or
/// _NoMatchingSubscribers_. _NoMatchingSubscribers_ usually indicates only that no other client is interested in the topic but overall transmit
/// to the server etc. was a success.
/// </summary>
public bool IsSuccess => ReasonCode == MqttClientPublishReasonCode.Success || ReasonCode == MqttClientPublishReasonCode.NoMatchingSubscribers;
/// <summary>
/// Gets the packet identifier which was used for this publish.
/// </summary> /// </summary>
public ushort? PacketIdentifier { get; set; } public ushort? PacketIdentifier { get; set; }
/// <summary> /// <summary>
/// Gets or sets the reason code. /// Gets or sets the reason code.
/// Hint: MQTT 5 feature only. /// Hint: MQTT 5 feature only.
/// </summary> /// </summary>
public MqttClientPublishReasonCode ReasonCode { get; set; } = MqttClientPublishReasonCode.Success; public MqttClientPublishReasonCode ReasonCode { get; set; } = MqttClientPublishReasonCode.Success;
/// <summary> /// <summary>
/// Gets or sets the reason string. /// Gets or sets the reason string.
/// Hint: MQTT 5 feature only. /// Hint: MQTT 5 feature only.
/// </summary> /// </summary>
public string ReasonString { get; set; } public string ReasonString { get; set; }
/// <summary> /// <summary>
/// Gets or sets the user properties. /// Gets or sets the user properties.
/// In MQTT 5, user properties are basic UTF-8 string key-value pairs that you can append to almost every type of MQTT packet. /// In MQTT 5, user properties are basic UTF-8 string key-value pairs that you can append to almost every type of MQTT
/// As long as you don’t exceed the maximum message size, you can use an unlimited number of user properties to add metadata to MQTT messages and pass information between publisher, broker, and subscriber. /// packet.
/// The feature is very similar to the HTTP header concept. /// As long as you don’t exceed the maximum message size, you can use an unlimited number of user properties to add
/// Hint: MQTT 5 feature only. /// metadata to MQTT messages and pass information between publisher, broker, and subscriber.
/// The feature is very similar to the HTTP header concept.
/// Hint: MQTT 5 feature only.
/// </summary> /// </summary>
public IReadOnlyCollection<MqttUserProperty> UserProperties { get; internal set; } public IReadOnlyCollection<MqttUserProperty> UserProperties { get; internal set; }
} }
@@ -8,12 +8,8 @@ namespace MQTTnet.Exceptions
{ {
public class MqttCommunicationException : Exception public class MqttCommunicationException : Exception
{ {
protected MqttCommunicationException()
{
}
public MqttCommunicationException(Exception innerException) public MqttCommunicationException(Exception innerException)
: base(innerException.Message, innerException) : base(innerException?.Message ?? "MQTT communication failed.", innerException)
{ {
} }
@@ -12,7 +12,10 @@ namespace MQTTnet.Formatter
{ {
public sealed class MqttPubAckPacketFactory public sealed class MqttPubAckPacketFactory
{ {
public MqttPubAckPacket Create(MqttPublishPacket publishPacket, InterceptingPublishEventArgs interceptingPublishEventArgs) public MqttPubAckPacket Create(
MqttPublishPacket publishPacket,
InterceptingPublishEventArgs interceptingPublishEventArgs,
DispatchApplicationMessageResult dispatchApplicationMessageResult)
{ {
if (publishPacket == null) if (publishPacket == null)
{ {
@@ -21,10 +24,19 @@ namespace MQTTnet.Formatter
var pubAckPacket = new MqttPubAckPacket var pubAckPacket = new MqttPubAckPacket
{ {
PacketIdentifier = publishPacket.PacketIdentifier, PacketIdentifier = publishPacket.PacketIdentifier
ReasonCode = MqttPubAckReasonCode.Success
}; };
if (dispatchApplicationMessageResult.MatchingSubscribersCount == 0)
{
// NoMatchingSubscribers is ONLY sent by the server!
pubAckPacket.ReasonCode = MqttPubAckReasonCode.NoMatchingSubscribers;
}
else
{
pubAckPacket.ReasonCode = MqttPubAckReasonCode.Success;
}
if (interceptingPublishEventArgs != null) if (interceptingPublishEventArgs != null)
{ {
pubAckPacket.ReasonCode = (MqttPubAckReasonCode)(int)interceptingPublishEventArgs.Response.ReasonCode; pubAckPacket.ReasonCode = (MqttPubAckReasonCode)(int)interceptingPublishEventArgs.Response.ReasonCode;
@@ -42,24 +54,12 @@ namespace MQTTnet.Formatter
throw new ArgumentNullException(nameof(applicationMessageReceivedEventArgs)); throw new ArgumentNullException(nameof(applicationMessageReceivedEventArgs));
} }
var pubAckPacket = Create(applicationMessageReceivedEventArgs.PublishPacket, applicationMessageReceivedEventArgs.ReasonCode);
pubAckPacket.UserProperties = applicationMessageReceivedEventArgs.ResponseUserProperties;
pubAckPacket.ReasonString = applicationMessageReceivedEventArgs.ResponseReasonString;
return pubAckPacket;
}
static MqttPubAckPacket Create(MqttPublishPacket publishPacket, MqttApplicationMessageReceivedReasonCode applicationMessageReceivedReasonCode)
{
if (publishPacket == null)
{
throw new ArgumentNullException(nameof(publishPacket));
}
var pubAckPacket = new MqttPubAckPacket var pubAckPacket = new MqttPubAckPacket
{ {
PacketIdentifier = publishPacket.PacketIdentifier, PacketIdentifier = applicationMessageReceivedEventArgs.PublishPacket.PacketIdentifier,
ReasonCode = (MqttPubAckReasonCode)(int)applicationMessageReceivedReasonCode ReasonCode = (MqttPubAckReasonCode)(int)applicationMessageReceivedEventArgs.ReasonCode,
UserProperties = applicationMessageReceivedEventArgs.ResponseUserProperties,
ReasonString = applicationMessageReceivedEventArgs.ResponseReasonString
}; };
return pubAckPacket; return pubAckPacket;
@@ -25,7 +25,10 @@ namespace MQTTnet.Formatter
return pubRecPacket; return pubRecPacket;
} }
public MqttPacket Create(MqttPublishPacket publishPacket, InterceptingPublishEventArgs interceptingPublishEventArgs) public MqttPacket Create(
MqttPublishPacket publishPacket,
InterceptingPublishEventArgs interceptingPublishEventArgs,
DispatchApplicationMessageResult dispatchApplicationMessageResult)
{ {
if (publishPacket == null) if (publishPacket == null)
{ {
@@ -38,6 +41,11 @@ namespace MQTTnet.Formatter
ReasonCode = MqttPubRecReasonCode.Success ReasonCode = MqttPubRecReasonCode.Success
}; };
if (dispatchApplicationMessageResult.MatchingSubscribersCount == 0)
{
pubRecPacket.ReasonCode = MqttPubRecReasonCode.NoMatchingSubscribers;
}
if (interceptingPublishEventArgs != null) if (interceptingPublishEventArgs != null)
{ {
pubRecPacket.ReasonCode = (MqttPubRecReasonCode)(int)interceptingPublishEventArgs.Response.ReasonCode; pubRecPacket.ReasonCode = (MqttPubRecReasonCode)(int)interceptingPublishEventArgs.Response.ReasonCode;
@@ -10,16 +10,16 @@ namespace MQTTnet.Formatter
{ {
public sealed class MqttUnsubAckPacketFactory public sealed class MqttUnsubAckPacketFactory
{ {
public MqttUnsubAckPacket Create(MqttUnsubscribePacket unsubscribePacket, MqttUnsubscribeResult mqttUnsubscribeResult) public MqttUnsubAckPacket Create(MqttUnsubscribePacket unsubscribePacket, UnsubscribeResult unsubscribeResult)
{ {
if (unsubscribePacket == null) if (unsubscribePacket == null)
{ {
throw new ArgumentNullException(nameof(unsubscribePacket)); throw new ArgumentNullException(nameof(unsubscribePacket));
} }
if (mqttUnsubscribeResult == null) if (unsubscribeResult == null)
{ {
throw new ArgumentNullException(nameof(mqttUnsubscribeResult)); throw new ArgumentNullException(nameof(unsubscribeResult));
} }
var unsubAckPacket = new MqttUnsubAckPacket var unsubAckPacket = new MqttUnsubAckPacket
@@ -28,7 +28,7 @@ namespace MQTTnet.Formatter
}; };
// MQTTv5.0.0 only. // MQTTv5.0.0 only.
unsubAckPacket.ReasonCodes = mqttUnsubscribeResult.ReasonCodes; unsubAckPacket.ReasonCodes = unsubscribeResult.ReasonCodes;
return unsubAckPacket; return unsubAckPacket;
} }
@@ -6,9 +6,9 @@ namespace MQTTnet.Formatter
{ {
public struct ReadFixedHeaderResult public struct ReadFixedHeaderResult
{ {
public static ReadFixedHeaderResult Cancelled { get; } = new ReadFixedHeaderResult public static ReadFixedHeaderResult Canceled { get; } = new ReadFixedHeaderResult
{ {
IsCancelled = true IsCanceled = true
}; };
public static ReadFixedHeaderResult ConnectionClosed { get; } = new ReadFixedHeaderResult public static ReadFixedHeaderResult ConnectionClosed { get; } = new ReadFixedHeaderResult
@@ -16,7 +16,7 @@ namespace MQTTnet.Formatter
IsConnectionClosed = true IsConnectionClosed = true
}; };
public bool IsCancelled { get; set; } public bool IsCanceled { get; set; }
public bool IsConnectionClosed { get; set; } public bool IsConnectionClosed { get; set; }
+1 -1
View File
@@ -123,7 +123,7 @@ namespace MQTTnet.Internal
} }
catch (Exception exception) catch (Exception exception)
{ {
logger.Warning(exception, $"Error while invoking event ({typeof(TEventArgs)})."); logger.Warning(exception, $"Error while invoking event with arguments of type {typeof(TEventArgs)}.");
} }
} }
} }
+17 -11
View File
@@ -29,7 +29,7 @@ namespace MQTTnet.Internal
{ {
lock (_syncRoot) lock (_syncRoot)
{ {
_signal?.Dispose(); _signal.Dispose();
_isDisposed = true; _isDisposed = true;
} }
@@ -48,10 +48,10 @@ namespace MQTTnet.Internal
{ {
if (_queue.TryDequeue(out var item)) if (_queue.TryDequeue(out var item))
{ {
return new AsyncQueueDequeueResult<TItem>(true, item); return AsyncQueueDequeueResult<TItem>.Success(item);
} }
return new AsyncQueueDequeueResult<TItem>(false, default); return AsyncQueueDequeueResult<TItem>.NonSuccess;
} }
public async Task<AsyncQueueDequeueResult<TItem>> TryDequeueAsync(CancellationToken cancellationToken) public async Task<AsyncQueueDequeueResult<TItem>> TryDequeueAsync(CancellationToken cancellationToken)
@@ -60,36 +60,42 @@ namespace MQTTnet.Internal
{ {
try try
{ {
Task task; Task task = null;
lock (_syncRoot) lock (_syncRoot)
{ {
if (_isDisposed) if (_isDisposed)
{ {
return new AsyncQueueDequeueResult<TItem>(false, default); return AsyncQueueDequeueResult<TItem>.NonSuccess;
} }
task = _signal.WaitAsync(cancellationToken); if (_queue.IsEmpty)
{
task = _signal.WaitAsync(cancellationToken);
}
} }
await task.ConfigureAwait(false); if (task != null)
{
await task.ConfigureAwait(false);
}
if (cancellationToken.IsCancellationRequested) if (cancellationToken.IsCancellationRequested)
{ {
return new AsyncQueueDequeueResult<TItem>(false, default); return AsyncQueueDequeueResult<TItem>.NonSuccess;
} }
if (_queue.TryDequeue(out var item)) if (_queue.TryDequeue(out var item))
{ {
return new AsyncQueueDequeueResult<TItem>(true, item); return AsyncQueueDequeueResult<TItem>.Success(item);
} }
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
return new AsyncQueueDequeueResult<TItem>(false, default); return AsyncQueueDequeueResult<TItem>.NonSuccess;
} }
} }
return new AsyncQueueDequeueResult<TItem>(false, default); return AsyncQueueDequeueResult<TItem>.NonSuccess;
} }
} }
} }
@@ -4,8 +4,10 @@
namespace MQTTnet.Internal namespace MQTTnet.Internal
{ {
public class AsyncQueueDequeueResult<TItem> public sealed class AsyncQueueDequeueResult<TItem>
{ {
public static readonly AsyncQueueDequeueResult<TItem> NonSuccess = new AsyncQueueDequeueResult<TItem>(false, default);
public AsyncQueueDequeueResult(bool isSuccess, TItem item) public AsyncQueueDequeueResult(bool isSuccess, TItem item)
{ {
IsSuccess = isSuccess; IsSuccess = isSuccess;
@@ -15,5 +17,10 @@ namespace MQTTnet.Internal
public bool IsSuccess { get; } public bool IsSuccess { get; }
public TItem Item { get; } public TItem Item { get; }
public static AsyncQueueDequeueResult<TItem> Success(TItem item)
{
return new AsyncQueueDequeueResult<TItem>(true, item);
}
} }
} }
+22 -2
View File
@@ -33,9 +33,17 @@ namespace MQTTnet.Internal
{ {
_isSignaled = true; _isSignaled = true;
Cleanup();
// If there is already a waiting task let it run. // If there is already a waiting task let it run.
_waiter?.Approve(); if (_waiter != null)
_waiter = null; {
_waiter.Approve();
_waiter = null;
// Since we already got a waiter the signal must be reset right now!
_isSignaled = false;
}
} }
} }
@@ -47,8 +55,11 @@ namespace MQTTnet.Internal
{ {
ThrowIfDisposed(); ThrowIfDisposed();
Cleanup();
if (_isSignaled) if (_isSignaled)
{ {
_isSignaled = false;
return CompletedTask.Instance; return CompletedTask.Instance;
} }
@@ -67,6 +78,15 @@ namespace MQTTnet.Internal
} }
} }
void Cleanup()
{
// Cleanup if the previous waiter was cancelled.
if (_waiter != null && _waiter.Task.IsCanceled)
{
_waiter = null;
}
}
void ThrowIfDisposed() void ThrowIfDisposed()
{ {
if (_isDisposed) if (_isDisposed)
+20 -24
View File
@@ -10,40 +10,36 @@ namespace MQTTnet.Internal
{ {
public sealed class MqttPacketBusItem public sealed class MqttPacketBusItem
{ {
readonly AsyncTaskCompletionSource<int> _promise = new AsyncTaskCompletionSource<int>(); readonly AsyncTaskCompletionSource<bool> _promise = new AsyncTaskCompletionSource<bool>();
public MqttPacketBusItem(MqttPacket packet) public MqttPacketBusItem(MqttPacket packet)
{ {
Packet = packet ?? throw new ArgumentNullException(nameof(packet)); Packet = packet ?? throw new ArgumentNullException(nameof(packet));
} }
public event EventHandler Completed;
public MqttPacket Packet { get; } public MqttPacket Packet { get; }
public event EventHandler Delivered; public void Cancel()
public Task WaitForDeliveryAsync()
{
return _promise.Task;
}
public void MarkAsDelivered()
{
if (_promise.TrySetResult(0))
{
Delivered?.Invoke(this, EventArgs.Empty);
}
}
public void MarkAsFailed(Exception exception)
{
if (exception == null) throw new ArgumentNullException(nameof(exception));
_promise.TrySetException(exception);
}
public void MarkAsCancelled()
{ {
_promise.TrySetCanceled(); _promise.TrySetCanceled();
} }
public void Complete()
{
_promise.TrySetResult(true);
Completed?.Invoke(this, EventArgs.Empty);
}
public void Fail(Exception exception)
{
_promise.TrySetException(exception);
}
public Task WaitAsync()
{
return _promise.Task;
}
} }
} }
@@ -2,6 +2,7 @@
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=client_005Cconnecting/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=client_005Cconnecting/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=client_005Cdiagnostics/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=client_005Cdiagnostics/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=client_005Cdisconnecting/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=client_005Cdisconnecting/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=client_005Cexceptions/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=client_005Cextendedauthenticationexchange/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=client_005Cextendedauthenticationexchange/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=client_005Coptions/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=client_005Coptions/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=client_005Cpublishing/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=client_005Cpublishing/@EntryIndexedValue">True</s:Boolean>
@@ -2,47 +2,102 @@
// The .NET Foundation licenses this file to you under the MIT license. // The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using MQTTnet.Packets;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using MQTTnet.Packets;
namespace MQTTnet.PacketDispatcher namespace MQTTnet.PacketDispatcher
{ {
public sealed class MqttPacketDispatcher public sealed class MqttPacketDispatcher : IDisposable
{ {
readonly List<IMqttPacketAwaitable> _awaitables = new List<IMqttPacketAwaitable>(); readonly List<IMqttPacketAwaitable> _waiters = new List<IMqttPacketAwaitable>();
public void FailAll(Exception exception) bool _isDisposed;
public MqttPacketAwaitable<TResponsePacket> AddAwaitable<TResponsePacket>(ushort packetIdentifier) where TResponsePacket : MqttPacket
{ {
if (exception == null) throw new ArgumentNullException(nameof(exception)); var awaitable = new MqttPacketAwaitable<TResponsePacket>(packetIdentifier, this);
lock (_awaitables) lock (_waiters)
{ {
foreach (var awaitable in _awaitables) _waiters.Add(awaitable);
{
awaitable.Fail(exception);
}
_awaitables.Clear();
} }
return awaitable;
} }
public void CancelAll() public void CancelAll()
{ {
lock (_awaitables) lock (_waiters)
{ {
foreach (var awaitable in _awaitables) foreach (var awaitable in _waiters)
{ {
awaitable.Cancel(); awaitable.Cancel();
} }
_awaitables.Clear(); _waiters.Clear();
}
}
public void Dispose()
{
Dispose(new ObjectDisposedException(nameof(MqttPacketDispatcher)));
}
public void Dispose(Exception exception)
{
if (exception == null)
{
throw new ArgumentNullException(nameof(exception));
}
lock (_waiters)
{
FailAll(exception);
// Make sure that no task can start waiting after this instance is already disposed.
// This will prevent unexpected freezes.
_isDisposed = true;
}
}
public void FailAll(Exception exception)
{
if (exception == null)
{
throw new ArgumentNullException(nameof(exception));
}
lock (_waiters)
{
foreach (var awaitable in _waiters)
{
awaitable.Fail(exception);
}
_waiters.Clear();
}
}
public void RemoveAwaitable(IMqttPacketAwaitable awaitable)
{
if (awaitable == null)
{
throw new ArgumentNullException(nameof(awaitable));
}
lock (_waiters)
{
_waiters.Remove(awaitable);
} }
} }
public bool TryDispatch(MqttPacket packet) public bool TryDispatch(MqttPacket packet)
{ {
if (packet == null) throw new ArgumentNullException(nameof(packet)); if (packet == null)
{
throw new ArgumentNullException(nameof(packet));
}
ushort identifier = 0; ushort identifier = 0;
if (packet is MqttPacketWithIdentifier packetWithIdentifier) if (packet is MqttPacketWithIdentifier packetWithIdentifier)
@@ -51,13 +106,15 @@ namespace MQTTnet.PacketDispatcher
} }
var packetType = packet.GetType(); var packetType = packet.GetType();
var awaitables = new List<IMqttPacketAwaitable>(); var waiters = new List<IMqttPacketAwaitable>();
lock (_awaitables) lock (_waiters)
{ {
for (var i = _awaitables.Count - 1; i >= 0; i--) ThrowIfDisposed();
for (var i = _waiters.Count - 1; i >= 0; i--)
{ {
var entry = _awaitables[i]; var entry = _waiters[i];
// Note: The PingRespPacket will also arrive here and has NO identifier but there // Note: The PingRespPacket will also arrive here and has NO identifier but there
// is code which waits for it. So the code must be able to deal with filters which // is code which waits for it. So the code must be able to deal with filters which
@@ -67,38 +124,24 @@ namespace MQTTnet.PacketDispatcher
continue; continue;
} }
awaitables.Add(entry); waiters.Add(entry);
_awaitables.RemoveAt(i); _waiters.RemoveAt(i);
} }
} }
foreach (var matchingEntry in awaitables) foreach (var matchingEntry in waiters)
{ {
matchingEntry.Complete(packet); matchingEntry.Complete(packet);
} }
return awaitables.Count > 0; return waiters.Count > 0;
} }
public MqttPacketAwaitable<TResponsePacket> AddAwaitable<TResponsePacket>(ushort packetIdentifier) where TResponsePacket : MqttPacket void ThrowIfDisposed()
{ {
var awaitable = new MqttPacketAwaitable<TResponsePacket>(packetIdentifier, this); if (_isDisposed)
lock (_awaitables)
{ {
_awaitables.Add(awaitable); throw new ObjectDisposedException(nameof(MqttPacketDispatcher));
}
return awaitable;
}
public void RemoveAwaitable(IMqttPacketAwaitable awaitable)
{
if (awaitable == null) throw new ArgumentNullException(nameof(awaitable));
lock (_awaitables)
{
_awaitables.Remove(awaitable);
} }
} }
} }
@@ -7,7 +7,12 @@ namespace MQTTnet.Protocol
public enum MqttPubAckReasonCode public enum MqttPubAckReasonCode
{ {
Success = 0, Success = 0,
/// <summary>
/// The message is accepted but there are no subscribers. This is sent only by the Server. If the Server knows that there are no matching subscribers, it MAY use this Reason Code instead of 0x00 (Success).
/// </summary>
NoMatchingSubscribers = 16, NoMatchingSubscribers = 16,
UnspecifiedError = 128, UnspecifiedError = 128,
ImplementationSpecificError = 131, ImplementationSpecificError = 131,
NotAuthorized = 135, NotAuthorized = 135,
@@ -0,0 +1,16 @@
// 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
{
public sealed class DispatchApplicationMessageResult
{
public DispatchApplicationMessageResult(int matchingSubscribersCount)
{
MatchingSubscribersCount = matchingSubscribersCount;
}
public int MatchingSubscribersCount { get; }
}
}
+29 -21
View File
@@ -33,6 +33,7 @@ namespace MQTTnet.Server
readonly Dictionary<ushort, string> _topicAlias = new Dictionary<ushort, string>(); readonly Dictionary<ushort, string> _topicAlias = new Dictionary<ushort, string>();
CancellationTokenSource _cancellationToken; CancellationTokenSource _cancellationToken;
bool _disconnectPacketSent;
public MqttClient( public MqttClient(
MqttConnectPacket connectPacket, MqttConnectPacket connectPacket,
@@ -146,19 +147,22 @@ namespace MQTTnet.Server
{ {
IsRunning = false; IsRunning = false;
// Sending DISCONNECT packets from the server to the client is only supported when using MQTTv5+. if (!_disconnectPacketSent)
if (ChannelAdapter.PacketFormatterAdapter.ProtocolVersion == MqttProtocolVersion.V500)
{ {
// The Client or Server MAY send a DISCONNECT packet before closing the Network Connection. // Sending DISCONNECT packets from the server to the client is only supported when using MQTTv5+.
// This library does not sent a DISCONNECT packet for a normal disconnection. Maybe adding if (ChannelAdapter.PacketFormatterAdapter.ProtocolVersion == MqttProtocolVersion.V500)
// a configuration option is requested in the future.
if (reason != MqttDisconnectReasonCode.NormalDisconnection)
{ {
// Is is very important to send the DISCONNECT packet here BEFORE cancelling the // The Client or Server MAY send a DISCONNECT packet before closing the Network Connection.
// token because the entire connection is closed (disposed) as soon as the cancellation // This library does not sent a DISCONNECT packet for a normal disconnection. Maybe adding
// token is cancelled. To there is no chance that the DISCONNECT packet will ever arrive // a configuration option is requested in the future.
// at the client! if (reason != MqttDisconnectReasonCode.NormalDisconnection)
await TrySendDisconnectPacket(reason).ConfigureAwait(false); {
// Is is very important to send the DISCONNECT packet here BEFORE cancelling the
// token because the entire connection is closed (disposed) as soon as the cancellation
// token is cancelled. To there is no chance that the DISCONNECT packet will ever arrive
// at the client!
await TrySendDisconnectPacket(reason).ConfigureAwait(false);
}
} }
} }
@@ -233,9 +237,10 @@ namespace MQTTnet.Server
return; return;
} }
DispatchApplicationMessageResult dispatchResult = null;
if (processPublish && applicationMessage != null) if (processPublish && applicationMessage != null)
{ {
await _sessionsManager.DispatchApplicationMessage(Id, applicationMessage).ConfigureAwait(false); dispatchResult = await _sessionsManager.DispatchApplicationMessage(Id, applicationMessage).ConfigureAwait(false);
} }
switch (publishPacket.QualityOfServiceLevel) switch (publishPacket.QualityOfServiceLevel)
@@ -247,13 +252,13 @@ namespace MQTTnet.Server
} }
case MqttQualityOfServiceLevel.AtLeastOnce: case MqttQualityOfServiceLevel.AtLeastOnce:
{ {
var pubAckPacket = _packetFactories.PubAck.Create(publishPacket, interceptingPublishEventArgs); var pubAckPacket = _packetFactories.PubAck.Create(publishPacket, interceptingPublishEventArgs, dispatchResult);
Session.EnqueueControlPacket(new MqttPacketBusItem(pubAckPacket)); Session.EnqueueControlPacket(new MqttPacketBusItem(pubAckPacket));
break; break;
} }
case MqttQualityOfServiceLevel.ExactlyOnce: case MqttQualityOfServiceLevel.ExactlyOnce:
{ {
var pubRecPacket = _packetFactories.PubRec.Create(publishPacket, interceptingPublishEventArgs); var pubRecPacket = _packetFactories.PubRec.Create(publishPacket, interceptingPublishEventArgs, dispatchResult);
Session.EnqueueControlPacket(new MqttPacketBusItem(pubRecPacket)); Session.EnqueueControlPacket(new MqttPacketBusItem(pubRecPacket));
break; break;
} }
@@ -282,7 +287,7 @@ namespace MQTTnet.Server
async Task HandleIncomingSubscribePacket(MqttSubscribePacket subscribePacket, CancellationToken cancellationToken) async Task HandleIncomingSubscribePacket(MqttSubscribePacket subscribePacket, CancellationToken cancellationToken)
{ {
var subscribeResult = await Session.SubscriptionsManager.Subscribe(subscribePacket, cancellationToken).ConfigureAwait(false); var subscribeResult = await Session.Subscribe(subscribePacket, cancellationToken).ConfigureAwait(false);
var subAckPacket = _packetFactories.SubAck.Create(subscribePacket, subscribeResult); var subAckPacket = _packetFactories.SubAck.Create(subscribePacket, subscribeResult);
@@ -306,7 +311,7 @@ namespace MQTTnet.Server
async Task HandleIncomingUnsubscribePacket(MqttUnsubscribePacket unsubscribePacket, CancellationToken cancellationToken) async Task HandleIncomingUnsubscribePacket(MqttUnsubscribePacket unsubscribePacket, CancellationToken cancellationToken)
{ {
var unsubscribeResult = await Session.SubscriptionsManager.Unsubscribe(unsubscribePacket, cancellationToken).ConfigureAwait(false); var unsubscribeResult = await Session.Unsubscribe(unsubscribePacket, cancellationToken).ConfigureAwait(false);
var unsubAckPacket = _packetFactories.UnsubAck.Create(unsubscribePacket, unsubscribeResult); var unsubAckPacket = _packetFactories.UnsubAck.Create(unsubscribePacket, unsubscribeResult);
@@ -484,7 +489,7 @@ namespace MQTTnet.Server
try try
{ {
while (!cancellationToken.IsCancellationRequested) while (!cancellationToken.IsCancellationRequested && !IsTakenOver && IsRunning)
{ {
packetBusItem = await Session.DequeuePacketAsync(cancellationToken).ConfigureAwait(false); packetBusItem = await Session.DequeuePacketAsync(cancellationToken).ConfigureAwait(false);
@@ -502,15 +507,15 @@ namespace MQTTnet.Server
try try
{ {
await SendPacketAsync(packetBusItem.Packet, cancellationToken).ConfigureAwait(false); await SendPacketAsync(packetBusItem.Packet, cancellationToken).ConfigureAwait(false);
packetBusItem.MarkAsDelivered(); packetBusItem.Complete();
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
packetBusItem.MarkAsCancelled(); packetBusItem.Cancel();
} }
catch (Exception exception) catch (Exception exception)
{ {
packetBusItem.MarkAsFailed(exception); packetBusItem.Fail(exception);
} }
finally finally
{ {
@@ -558,7 +563,7 @@ namespace MQTTnet.Server
catch (ObjectDisposedException) catch (ObjectDisposedException)
{ {
// This can happen when connections are created and dropped very quickly. // This can happen when connections are created and dropped very quickly.
// It is not an issue if the cancellation token cannot be cancelled multiple times. // It is not an issue if the cancellation token cannot be canceled multiple times.
} }
} }
@@ -566,6 +571,9 @@ namespace MQTTnet.Server
{ {
try try
{ {
// This also indicates that it was tried at least!
_disconnectPacketSent = true;
var disconnectPacket = _packetFactories.Disconnect.Create(reasonCode); var disconnectPacket = _packetFactories.Disconnect.Create(reasonCode);
using (var timeout = new CancellationTokenSource(_serverOptions.DefaultCommunicationTimeout)) using (var timeout = new CancellationTokenSource(_serverOptions.DefaultCommunicationTimeout))
@@ -128,8 +128,9 @@ namespace MQTTnet.Server
_logger.Verbose("Session for client '{0}' deleted.", clientId); _logger.Verbose("Session for client '{0}' deleted.", clientId);
} }
public async Task DispatchApplicationMessage(string senderId, MqttApplicationMessage applicationMessage) public async Task<DispatchApplicationMessageResult> DispatchApplicationMessage(string senderId, MqttApplicationMessage applicationMessage)
{ {
var matchingSubscribersCount = 0;
try try
{ {
if (applicationMessage.Retain) if (applicationMessage.Retain)
@@ -137,11 +138,9 @@ namespace MQTTnet.Server
await _retainedMessagesManager.UpdateMessage(senderId, applicationMessage).ConfigureAwait(false); await _retainedMessagesManager.UpdateMessage(senderId, applicationMessage).ConfigureAwait(false);
} }
var deliveryCount = 0;
List<MqttSession> subscriberSessions; List<MqttSession> subscriberSessions;
lock (_sessionsManagementLock) lock (_sessionsManagementLock)
{ {
// only subscriber clients are of interest here.
subscriberSessions = _subscriberSessions.ToList(); subscriberSessions = _subscriberSessions.ToList();
} }
@@ -150,11 +149,17 @@ namespace MQTTnet.Server
foreach (var session in subscriberSessions) foreach (var session in subscriberSessions)
{ {
var checkSubscriptionsResult = session.SubscriptionsManager.CheckSubscriptions( if(!session.TryCheckSubscriptions(
applicationMessage.Topic, applicationMessage.Topic,
topicHash, topicHash,
applicationMessage.QualityOfServiceLevel, applicationMessage.QualityOfServiceLevel,
senderId); senderId,
out var checkSubscriptionsResult))
{
// Checking the subscriptions has failed for the session. The session
// will be ignored.
continue;
}
if (!checkSubscriptionsResult.IsSubscribed) if (!checkSubscriptionsResult.IsSubscribed)
{ {
@@ -181,17 +186,22 @@ namespace MQTTnet.Server
} }
session.EnqueueDataPacket(new MqttPacketBusItem(newPublishPacket)); session.EnqueueDataPacket(new MqttPacketBusItem(newPublishPacket));
deliveryCount++; matchingSubscribersCount++;
_logger.Verbose("Client '{0}': Queued PUBLISH packet with topic '{1}'.", session.Id, applicationMessage.Topic); _logger.Verbose("Client '{0}': Queued PUBLISH packet with topic '{1}'.", session.Id, applicationMessage.Topic);
} }
await FireApplicationMessageNotConsumedEvent(applicationMessage, deliveryCount, senderId); if (matchingSubscribersCount == 0)
{
await FireApplicationMessageNotConsumedEvent(applicationMessage, senderId).ConfigureAwait(false);
}
} }
catch (Exception exception) catch (Exception exception)
{ {
_logger.Error(exception, "Unhandled exception while processing next queued application message."); _logger.Error(exception, "Unhandled exception while processing next queued application message.");
} }
return new DispatchApplicationMessageResult(matchingSubscribersCount);
} }
public void Dispose() public void Dispose()
@@ -301,7 +311,7 @@ namespace MQTTnet.Server
channelAdapter.Endpoint, channelAdapter.Endpoint,
client.Session.Items); client.Session.Items);
await _eventContainer.ClientConnectedEvent.InvokeAsync(eventArgs).ConfigureAwait(false); await _eventContainer.ClientConnectedEvent.TryInvokeAsync(eventArgs, _logger).ConfigureAwait(false);
} }
await client.RunAsync().ConfigureAwait(false); await client.RunAsync().ConfigureAwait(false);
@@ -414,7 +424,7 @@ namespace MQTTnet.Server
var clientSession = GetClientSession(clientId); var clientSession = GetClientSession(clientId);
var subscribeResult = await clientSession.SubscriptionsManager.Subscribe(fakeSubscribePacket, CancellationToken.None).ConfigureAwait(false); var subscribeResult = await clientSession.Subscribe(fakeSubscribePacket, CancellationToken.None).ConfigureAwait(false);
if (subscribeResult.RetainedMessages != null) if (subscribeResult.RetainedMessages != null)
{ {
@@ -441,7 +451,7 @@ namespace MQTTnet.Server
var fakeUnsubscribePacket = new MqttUnsubscribePacket(); var fakeUnsubscribePacket = new MqttUnsubscribePacket();
fakeUnsubscribePacket.TopicFilters.AddRange(topicFilters); fakeUnsubscribePacket.TopicFilters.AddRange(topicFilters);
return GetClientSession(clientId).SubscriptionsManager.Unsubscribe(fakeUnsubscribePacket, CancellationToken.None); return GetClientSession(clientId).Unsubscribe(fakeUnsubscribePacket, CancellationToken.None);
} }
MqttClient CreateClient(MqttConnectPacket connectPacket, IMqttChannelAdapter channelAdapter, MqttSession session) MqttClient CreateClient(MqttConnectPacket connectPacket, IMqttChannelAdapter channelAdapter, MqttSession session)
@@ -484,10 +494,14 @@ namespace MQTTnet.Server
using (await _createConnectionSyncRoot.EnterAsync().ConfigureAwait(false)) using (await _createConnectionSyncRoot.EnterAsync().ConfigureAwait(false))
{ {
MqttSession session;
MqttSession oldSession; MqttSession oldSession;
MqttClient oldClient;
lock (_sessionsManagementLock) lock (_sessionsManagementLock)
{ {
MqttSession session;
// Create a new session (if required).
if (!_sessions.TryGetValue(connectPacket.ClientId, out oldSession)) if (!_sessions.TryGetValue(connectPacket.ClientId, out oldSession))
{ {
session = CreateSession(connectPacket.ClientId, validatingConnectionEventArgs.SessionItems, sessionShouldPersist); session = CreateSession(connectPacket.ClientId, validatingConnectionEventArgs.SessionItems, sessionShouldPersist);
@@ -505,6 +519,7 @@ namespace MQTTnet.Server
_logger.Verbose("Reusing existing session of client '{0}'.", connectPacket.ClientId); _logger.Verbose("Reusing existing session of client '{0}'.", connectPacket.ClientId);
session = oldSession; session = oldSession;
oldSession = null; oldSession = null;
// Session persistence could change for MQTT 5 clients that reconnect with different SessionExpiryInterval // Session persistence could change for MQTT 5 clients that reconnect with different SessionExpiryInterval
session.IsPersistent = sessionShouldPersist; session.IsPersistent = sessionShouldPersist;
connAckPacket.IsSessionPresent = true; connAckPacket.IsSessionPresent = true;
@@ -513,39 +528,41 @@ namespace MQTTnet.Server
} }
_sessions[connectPacket.ClientId] = session; _sessions[connectPacket.ClientId] = session;
// Create a new client (always required).
_clients.TryGetValue(connectPacket.ClientId, out oldClient);
if (oldClient != null)
{
// This will stop the current client from sending and receiving but remains the connection active
// for a later DISCONNECT packet.
oldClient.IsTakenOver = true;
}
client = CreateClient(connectPacket, channelAdapter, session);
_clients[connectPacket.ClientId] = client;
} }
if (!connAckPacket.IsSessionPresent) if (!connAckPacket.IsSessionPresent)
{ {
// TODO: This event is not yet final. It can already be used but restoring sessions from storage will be added later! // TODO: This event is not yet final. It can already be used but restoring sessions from storage will be added later!
var preparingSessionEventArgs = new PreparingSessionEventArgs(); var preparingSessionEventArgs = new PreparingSessionEventArgs();
await _eventContainer.PreparingSessionEvent.InvokeAsync(preparingSessionEventArgs).ConfigureAwait(false); await _eventContainer.PreparingSessionEvent.TryInvokeAsync(preparingSessionEventArgs, _logger).ConfigureAwait(false);
} }
MqttClient existingClient; if (oldClient != null)
lock (_clients)
{ {
_clients.TryGetValue(connectPacket.ClientId, out existingClient); await oldClient.StopAsync(MqttDisconnectReasonCode.SessionTakenOver).ConfigureAwait(false);
client = CreateClient(connectPacket, channelAdapter, session);
_clients[connectPacket.ClientId] = client;
}
if (existingClient != null)
{
existingClient.IsTakenOver = true;
await existingClient.StopAsync(MqttDisconnectReasonCode.SessionTakenOver).ConfigureAwait(false);
if (_eventContainer.ClientDisconnectedEvent.HasHandlers) if (_eventContainer.ClientDisconnectedEvent.HasHandlers)
{ {
var eventArgs = new ClientDisconnectedEventArgs( var eventArgs = new ClientDisconnectedEventArgs(
existingClient.Id, oldClient.Id,
MqttClientDisconnectType.Takeover, MqttClientDisconnectType.Takeover,
existingClient.Endpoint, oldClient.Endpoint,
existingClient.Session.Items); oldClient.Session.Items);
await _eventContainer.ClientDisconnectedEvent.InvokeAsync(eventArgs).ConfigureAwait(false); await _eventContainer.ClientDisconnectedEvent.TryInvokeAsync(eventArgs, _logger).ConfigureAwait(false);
} }
} }
@@ -562,13 +579,8 @@ namespace MQTTnet.Server
return new MqttSession(clientId, isPersistent, sessionItems, _options, _eventContainer, _retainedMessagesManager, this); return new MqttSession(clientId, isPersistent, sessionItems, _options, _eventContainer, _retainedMessagesManager, this);
} }
async Task FireApplicationMessageNotConsumedEvent(MqttApplicationMessage applicationMessage, int deliveryCount, string senderId) async Task FireApplicationMessageNotConsumedEvent(MqttApplicationMessage applicationMessage, string senderId)
{ {
if (deliveryCount > 0)
{
return;
}
if (!_eventContainer.ApplicationMessageNotConsumedEvent.HasHandlers) if (!_eventContainer.ApplicationMessageNotConsumedEvent.HasHandlers)
{ {
return; return;
@@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MQTTnet.Internal;
using MQTTnet.Packets; using MQTTnet.Packets;
using MQTTnet.Protocol; using MQTTnet.Protocol;
@@ -30,7 +31,7 @@ namespace MQTTnet.Server
readonly Dictionary<string, MqttSubscription> _subscriptions = new Dictionary<string, MqttSubscription>(); readonly Dictionary<string, MqttSubscription> _subscriptions = new Dictionary<string, MqttSubscription>();
// Use subscription lock to maintain consistency across subscriptions and topic hash dictionaries // Use subscription lock to maintain consistency across subscriptions and topic hash dictionaries
readonly SemaphoreSlim _subscriptionsLock = new SemaphoreSlim(1); readonly AsyncLock _subscriptionsLock = new AsyncLock();
readonly Dictionary<ulong, TopicHashMaskSubscriptions> _wildcardSubscriptionsByTopicHash = new Dictionary<ulong, TopicHashMaskSubscriptions>(); readonly Dictionary<ulong, TopicHashMaskSubscriptions> _wildcardSubscriptionsByTopicHash = new Dictionary<ulong, TopicHashMaskSubscriptions>();
public MqttClientSubscriptionsManager( public MqttClientSubscriptionsManager(
@@ -45,13 +46,12 @@ namespace MQTTnet.Server
_subscriptionChangedNotification = subscriptionChangedNotification; _subscriptionChangedNotification = subscriptionChangedNotification;
} }
public CheckSubscriptionsResult CheckSubscriptions(string topic, ulong topicHash, MqttQualityOfServiceLevel applicationMessageQoSLevel, string senderClientId) public CheckSubscriptionsResult CheckSubscriptions(string topic, ulong topicHash, MqttQualityOfServiceLevel qualityOfServiceLevel, string senderId)
{ {
var possibleSubscriptions = new List<MqttSubscription>(); var possibleSubscriptions = new List<MqttSubscription>();
// Check for possible subscriptions. They might have collisions but this is fine. // Check for possible subscriptions. They might have collisions but this is fine.
_subscriptionsLock.Wait(); using (_subscriptionsLock.EnterAsync(CancellationToken.None).GetAwaiter().GetResult())
try
{ {
if (_noWildcardSubscriptionsByTopicHash.TryGetValue(topicHash, out var noWildcardSubscriptions)) if (_noWildcardSubscriptionsByTopicHash.TryGetValue(topicHash, out var noWildcardSubscriptions))
{ {
@@ -70,10 +70,6 @@ namespace MQTTnet.Server
} }
} }
} }
finally
{
_subscriptionsLock.Release();
}
// The pre check has evaluated that nothing is subscribed. // The pre check has evaluated that nothing is subscribed.
// If there were some possible candidates they get checked below // If there were some possible candidates they get checked below
@@ -83,7 +79,7 @@ namespace MQTTnet.Server
return CheckSubscriptionsResult.NotSubscribed; return CheckSubscriptionsResult.NotSubscribed;
} }
var senderIsReceiver = string.Equals(senderClientId, _session.Id); var senderIsReceiver = string.Equals(senderId, _session.Id);
var maxQoSLevel = -1; // Not subscribed. var maxQoSLevel = -1; // Not subscribed.
HashSet<uint> subscriptionIdentifiers = null; HashSet<uint> subscriptionIdentifiers = null;
@@ -136,7 +132,7 @@ namespace MQTTnet.Server
SubscriptionIdentifiers = subscriptionIdentifiers?.ToList() ?? EmptySubscriptionIdentifiers, SubscriptionIdentifiers = subscriptionIdentifiers?.ToList() ?? EmptySubscriptionIdentifiers,
// Start with the same QoS as the publisher. // Start with the same QoS as the publisher.
QualityOfServiceLevel = applicationMessageQoSLevel QualityOfServiceLevel = qualityOfServiceLevel
}; };
// Now downgrade if required. // Now downgrade if required.
@@ -148,7 +144,7 @@ namespace MQTTnet.Server
// Subscribing to a Topic Filter at QoS 2 is equivalent to saying "I would like to receive Messages matching this filter at the QoS with which they were published". // Subscribing to a Topic Filter at QoS 2 is equivalent to saying "I would like to receive Messages matching this filter at the QoS with which they were published".
// This means a publisher is responsible for determining the maximum QoS a Message can be delivered at, but a subscriber is able to require that the Server // This means a publisher is responsible for determining the maximum QoS a Message can be delivered at, but a subscriber is able to require that the Server
// downgrades the QoS to one more suitable for its usage. // downgrades the QoS to one more suitable for its usage.
if (maxQoSLevel < (int)applicationMessageQoSLevel) if (maxQoSLevel < (int)qualityOfServiceLevel)
{ {
result.QualityOfServiceLevel = (MqttQualityOfServiceLevel)maxQoSLevel; result.QualityOfServiceLevel = (MqttQualityOfServiceLevel)maxQoSLevel;
} }
@@ -222,19 +218,18 @@ namespace MQTTnet.Server
return result; return result;
} }
public async Task<MqttUnsubscribeResult> Unsubscribe(MqttUnsubscribePacket unsubscribePacket, CancellationToken cancellationToken) public async Task<UnsubscribeResult> Unsubscribe(MqttUnsubscribePacket unsubscribePacket, CancellationToken cancellationToken)
{ {
if (unsubscribePacket == null) if (unsubscribePacket == null)
{ {
throw new ArgumentNullException(nameof(unsubscribePacket)); throw new ArgumentNullException(nameof(unsubscribePacket));
} }
var result = new MqttUnsubscribeResult(); var result = new UnsubscribeResult();
var removedSubscriptions = new List<string>(); var removedSubscriptions = new List<string>();
await _subscriptionsLock.WaitAsync(cancellationToken).ConfigureAwait(false); using (await _subscriptionsLock.EnterAsync(cancellationToken).ConfigureAwait(false))
try
{ {
foreach (var topicFilter in unsubscribePacket.TopicFilters) foreach (var topicFilter in unsubscribePacket.TopicFilters)
{ {
@@ -294,11 +289,8 @@ namespace MQTTnet.Server
} }
} }
} }
finally
{ _subscriptionChangedNotification?.OnSubscriptionsRemoved(_session, removedSubscriptions);
_subscriptionsLock.Release();
_subscriptionChangedNotification?.OnSubscriptionsRemoved(_session, removedSubscriptions);
}
if (_eventContainer.ClientUnsubscribedTopicEvent.HasHandlers) if (_eventContainer.ClientUnsubscribedTopicEvent.HasHandlers)
{ {
@@ -345,8 +337,7 @@ namespace MQTTnet.Server
// Add to subscriptions and maintain topic hash dictionaries // Add to subscriptions and maintain topic hash dictionaries
_subscriptionsLock.Wait(); using (_subscriptionsLock.EnterAsync(CancellationToken.None).GetAwaiter().GetResult())
try
{ {
MqttSubscription.CalculateTopicHash(topicFilter.Topic, out var topicHash, out var topicHashMask, out var hasWildcard); MqttSubscription.CalculateTopicHash(topicFilter.Topic, out var topicHash, out var topicHashMask, out var hasWildcard);
@@ -396,10 +387,6 @@ namespace MQTTnet.Server
subscriptions.Add(subscription); subscriptions.Add(subscription);
} }
} }
finally
{
_subscriptionsLock.Release();
}
return new CreateSubscriptionResult return new CreateSubscriptionResult
{ {
@@ -493,23 +480,20 @@ namespace MQTTnet.Server
async Task<InterceptingUnsubscriptionEventArgs> InterceptUnsubscribe(string topicFilter, MqttSubscription mqttSubscription, CancellationToken cancellationToken) async Task<InterceptingUnsubscriptionEventArgs> InterceptUnsubscribe(string topicFilter, MqttSubscription mqttSubscription, CancellationToken cancellationToken)
{ {
var clientUnsubscribingTopicEventArgs = new InterceptingUnsubscriptionEventArgs(cancellationToken, topicFilter, _session.Items, topicFilter); var clientUnsubscribingTopicEventArgs = new InterceptingUnsubscriptionEventArgs(cancellationToken, topicFilter, _session.Items, topicFilter)
if (mqttSubscription == null)
{ {
clientUnsubscribingTopicEventArgs.Response.ReasonCode = MqttUnsubscribeReasonCode.NoSubscriptionExisted; Response =
} {
else ReasonCode = mqttSubscription == null ? MqttUnsubscribeReasonCode.NoSubscriptionExisted : MqttUnsubscribeReasonCode.Success
{ }
clientUnsubscribingTopicEventArgs.Response.ReasonCode = MqttUnsubscribeReasonCode.Success; };
}
await _eventContainer.InterceptingUnsubscriptionEvent.InvokeAsync(clientUnsubscribingTopicEventArgs).ConfigureAwait(false); await _eventContainer.InterceptingUnsubscriptionEvent.InvokeAsync(clientUnsubscribingTopicEventArgs).ConfigureAwait(false);
return clientUnsubscribingTopicEventArgs; return clientUnsubscribingTopicEventArgs;
} }
public sealed class CreateSubscriptionResult sealed class CreateSubscriptionResult
{ {
public bool IsNewSubscription { get; set; } public bool IsNewSubscription { get; set; }
+78 -18
View File
@@ -4,7 +4,6 @@
using System; using System;
using System.Collections; using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
@@ -23,8 +22,10 @@ namespace MQTTnet.Server
readonly MqttPacketIdentifierProvider _packetIdentifierProvider = new MqttPacketIdentifierProvider(); readonly MqttPacketIdentifierProvider _packetIdentifierProvider = new MqttPacketIdentifierProvider();
readonly MqttServerOptions _serverOptions; readonly MqttServerOptions _serverOptions;
readonly MqttClientSubscriptionsManager _subscriptionsManager;
readonly ConcurrentDictionary<ushort, MqttPublishPacket> _unacknowledgedPublishPackets = new ConcurrentDictionary<ushort, MqttPublishPacket>(); // Do not use a dictionary in order to keep the ordering of the messages.
readonly List<MqttPublishPacket> _unacknowledgedPublishPackets = new List<MqttPublishPacket>();
// Bookkeeping to know if this is a subscribing client; lazy initialize later. // Bookkeeping to know if this is a subscribing client; lazy initialize later.
HashSet<string> _subscribedTopics; HashSet<string> _subscribedTopics;
@@ -45,7 +46,7 @@ namespace MQTTnet.Server
_serverOptions = serverOptions ?? throw new ArgumentNullException(nameof(serverOptions)); _serverOptions = serverOptions ?? throw new ArgumentNullException(nameof(serverOptions));
_clientSessionsManager = clientSessionsManager ?? throw new ArgumentNullException(nameof(clientSessionsManager)); _clientSessionsManager = clientSessionsManager ?? throw new ArgumentNullException(nameof(clientSessionsManager));
SubscriptionsManager = new MqttClientSubscriptionsManager(this, eventContainer, retainedMessagesManager, clientSessionsManager); _subscriptionsManager = new MqttClientSubscriptionsManager(this, eventContainer, retainedMessagesManager, clientSessionsManager);
} }
public DateTime CreatedTimestamp { get; } = DateTime.UtcNow; public DateTime CreatedTimestamp { get; } = DateTime.UtcNow;
@@ -67,21 +68,18 @@ namespace MQTTnet.Server
public long PendingDataPacketsCount => _packetBus.PartitionItemsCount(MqttPacketBusPartition.Data); public long PendingDataPacketsCount => _packetBus.PartitionItemsCount(MqttPacketBusPartition.Data);
public MqttClientSubscriptionsManager SubscriptionsManager { get; }
public bool WillMessageSent { get; set; } public bool WillMessageSent { get; set; }
public MqttPublishPacket PeekAcknowledgePublishPacket(ushort packetIdentifier)
{
// This will only return the matching PUBLISH packet but does not remove it.
// This is required for QoS 2.
_unacknowledgedPublishPackets.TryGetValue(packetIdentifier, out var publishPacket);
return publishPacket;
}
public MqttPublishPacket AcknowledgePublishPacket(ushort packetIdentifier) public MqttPublishPacket AcknowledgePublishPacket(ushort packetIdentifier)
{ {
_unacknowledgedPublishPackets.TryRemove(packetIdentifier, out var publishPacket); MqttPublishPacket publishPacket;
lock (_unacknowledgedPublishPackets)
{
publishPacket = _unacknowledgedPublishPackets.FirstOrDefault(p => p.PacketIdentifier.Equals(packetIdentifier));
_unacknowledgedPublishPackets.Remove(publishPacket);
}
return publishPacket; return publishPacket;
} }
@@ -107,8 +105,8 @@ namespace MQTTnet.Server
public void Dispose() public void Dispose()
{ {
_packetBus?.Dispose(); _packetBus.Dispose();
SubscriptionsManager.Dispose(); _subscriptionsManager.Dispose();
} }
public void EnqueueControlPacket(MqttPacketBusItem packetBusItem) public void EnqueueControlPacket(MqttPacketBusItem packetBusItem)
@@ -139,7 +137,10 @@ namespace MQTTnet.Server
{ {
publishPacket.PacketIdentifier = _packetIdentifierProvider.GetNextPacketIdentifier(); publishPacket.PacketIdentifier = _packetIdentifierProvider.GetNextPacketIdentifier();
_unacknowledgedPublishPackets[publishPacket.PacketIdentifier] = publishPacket; lock (_unacknowledgedPublishPackets)
{
_unacknowledgedPublishPackets.Add(publishPacket);
}
} }
_packetBus.EnqueueItem(packetBusItem, MqttPacketBusPartition.Data); _packetBus.EnqueueItem(packetBusItem, MqttPacketBusPartition.Data);
@@ -150,14 +151,48 @@ namespace MQTTnet.Server
_packetBus.EnqueueItem(packetBusItem, MqttPacketBusPartition.Health); _packetBus.EnqueueItem(packetBusItem, MqttPacketBusPartition.Health);
} }
public MqttPublishPacket PeekAcknowledgePublishPacket(ushort packetIdentifier)
{
// This will only return the matching PUBLISH packet but does not remove it.
// This is required for QoS 2.
lock (_unacknowledgedPublishPackets)
{
return _unacknowledgedPublishPackets.FirstOrDefault(p => p.PacketIdentifier.Equals(packetIdentifier));
}
}
public void Recover() public void Recover()
{ {
// TODO: Keep the bus and only insert pending items again. // TODO: Keep the bus and only insert pending items again.
// TODO: Check if packet identifier must be restarted or not. // TODO: Check if packet identifier must be restarted or not.
// TODO: Recover package identifier. // TODO: Recover package identifier.
/*
The Session state in the Client consists of:
· QoS 1 and QoS 2 messages which have been sent to the Server, but have not been completely acknowledged.
· QoS 2 messages which have been received from the Server, but have not been completely acknowledged.
The Session state in the Server consists of:
· The existence of a Session, even if the rest of the Session state is empty.
· The Client’s subscriptions.
· QoS 1 and QoS 2 messages which have been sent to the Client, but have not been completely acknowledged.
· QoS 1 and QoS 2 messages pending transmission to the Client.
· QoS 2 messages which have been received from the Client, but have not been completely acknowledged.
· Optionally, QoS 0 messages pending transmission to the Client.
*/
// Create a copy of all currently unacknowledged publish packets and clear the storage.
// We must re-enqueue them in order to trigger other code.
List<MqttPublishPacket> unacknowledgedPublishPackets;
lock (_unacknowledgedPublishPackets)
{
unacknowledgedPublishPackets = _unacknowledgedPublishPackets.ToList();
_unacknowledgedPublishPackets.Clear();
}
_packetBus.Clear(); _packetBus.Clear();
foreach (var publishPacket in _unacknowledgedPublishPackets.Values.ToList()) foreach (var publishPacket in unacknowledgedPublishPackets)
{ {
EnqueueDataPacket(new MqttPacketBusItem(publishPacket)); EnqueueDataPacket(new MqttPacketBusItem(publishPacket));
} }
@@ -167,5 +202,30 @@ namespace MQTTnet.Server
{ {
_subscribedTopics?.Remove(topic); _subscribedTopics?.Remove(topic);
} }
public Task<SubscribeResult> Subscribe(MqttSubscribePacket subscribePacket, CancellationToken cancellationToken)
{
return _subscriptionsManager.Subscribe(subscribePacket, cancellationToken);
}
public bool TryCheckSubscriptions(string topic, ulong topicHash, MqttQualityOfServiceLevel qualityOfServiceLevel, string senderId, out CheckSubscriptionsResult result)
{
result = null;
try
{
result = _subscriptionsManager.CheckSubscriptions(topic, topicHash, qualityOfServiceLevel, senderId);
return true;
}
catch
{
return false;
}
}
public Task<UnsubscribeResult> Unsubscribe(MqttUnsubscribePacket unsubscribePacket, CancellationToken cancellationToken)
{
return _subscriptionsManager.Unsubscribe(unsubscribePacket, cancellationToken);
}
} }
} }
@@ -7,7 +7,7 @@ using MQTTnet.Protocol;
namespace MQTTnet.Server namespace MQTTnet.Server
{ {
public sealed class MqttUnsubscribeResult public sealed class UnsubscribeResult
{ {
public List<MqttUnsubscribeReasonCode> ReasonCodes { get; } = new List<MqttUnsubscribeReasonCode>(128); public List<MqttUnsubscribeReasonCode> ReasonCodes { get; } = new List<MqttUnsubscribeReasonCode>(128);
+67 -7
View File
@@ -3,29 +3,89 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System; using System;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using MQTTnet.Internal;
using MQTTnet.Packets; using MQTTnet.Packets;
using MQTTnet.Protocol;
namespace MQTTnet.Server namespace MQTTnet.Server
{ {
public static class MqttServerExtensions public static class MqttServerExtensions
{ {
public static Task InjectApplicationMessage(
this MqttServer server,
string topic,
string payload = null,
MqttQualityOfServiceLevel qualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce,
bool retain = false)
{
if (server == null)
{
throw new ArgumentNullException(nameof(server));
}
if (topic == null)
{
throw new ArgumentNullException(nameof(topic));
}
var payloadBuffer = EmptyBuffer.Array;
if (payload is string stringPayload)
{
payloadBuffer = Encoding.UTF8.GetBytes(stringPayload);
}
return server.InjectApplicationMessage(
new InjectedMqttApplicationMessage(
new MqttApplicationMessage
{
Topic = topic,
Payload = payloadBuffer,
QualityOfServiceLevel = qualityOfServiceLevel,
Retain = retain
}));
}
public static Task SubscribeAsync(this MqttServer server, string clientId, params MqttTopicFilter[] topicFilters) public static Task SubscribeAsync(this MqttServer server, string clientId, params MqttTopicFilter[] topicFilters)
{ {
if (server == null) throw new ArgumentNullException(nameof(server)); if (server == null)
if (clientId == null) throw new ArgumentNullException(nameof(clientId)); {
if (topicFilters == null) throw new ArgumentNullException(nameof(topicFilters)); throw new ArgumentNullException(nameof(server));
}
if (clientId == null)
{
throw new ArgumentNullException(nameof(clientId));
}
if (topicFilters == null)
{
throw new ArgumentNullException(nameof(topicFilters));
}
return server.SubscribeAsync(clientId, topicFilters); return server.SubscribeAsync(clientId, topicFilters);
} }
public static Task SubscribeAsync(this MqttServer server, string clientId, string topic) public static Task SubscribeAsync(this MqttServer server, string clientId, string topic)
{ {
if (server == null) throw new ArgumentNullException(nameof(server)); if (server == null)
if (clientId == null) throw new ArgumentNullException(nameof(clientId)); {
if (topic == null) throw new ArgumentNullException(nameof(topic)); throw new ArgumentNullException(nameof(server));
}
return server.SubscribeAsync(clientId, new MqttTopicFilterBuilder().WithTopic(topic).Build()); if (clientId == null)
{
throw new ArgumentNullException(nameof(clientId));
}
if (topic == null)
{
throw new ArgumentNullException(nameof(topic));
}
var topicFilters = new MqttTopicFilterBuilder().WithTopic(topic).Build();
return server.SubscribeAsync(clientId, topicFilters);
} }
} }
} }
@@ -45,7 +45,7 @@ namespace MQTTnet.Server
var packetBusItem = new MqttPacketBusItem(publishPacketFactory.Create(applicationMessage)); var packetBusItem = new MqttPacketBusItem(publishPacketFactory.Create(applicationMessage));
_session.EnqueueDataPacket(packetBusItem); _session.EnqueueDataPacket(packetBusItem);
return packetBusItem.WaitForDeliveryAsync(); return packetBusItem.WaitAsync();
} }
public Task DeleteAsync() public Task DeleteAsync()