Compare commits

...
Author SHA1 Message Date
christian 74bb68ad73 Improve CI 2024-09-22 20:15:50 +02:00
Christian 81a4e7cb86 Pass cancellation token to TCP listeners (#2082) 2024-09-15 16:08:21 +02:00
Christian 2fdbb02dc8 Fix parsing error of AUTH packet with empty body (#2070)
* Reformat code

* Fix issue

* Fix AUTH packet serialization
2024-09-07 09:49:10 +02:00
christian c4eb05105b Update release notes 2024-08-24 12:04:24 +02:00
christian bda21f0fcd Fix build errors 2024-08-24 12:04:14 +02:00
980a5d0a6d Improve performance and drop support for old frameworks (#2068)
* Remove WebSocket4Net extension library

* Remove UWP support

* Drop support for all old frameworks

* Update CI

* Update CI

* Update CI

* Update CI

* Remove obsolete code

* Set default protocol version to 5.0.0

* Remove obsolete tests

* Remove old dependencies

* Reset release notes

* Move server to dedicated project

* Fix unit tests

* Update and cleanup nuget packages

* Fix unit tests
Refactor code

* No longer throw exception when receiving non success CONNACK

* Enable nuget signing

* Enable nuget signing

* Enable nuget signing

* Enable nuget signing

* Enable nuget signing

* Enable nuget signing

* Enable nuget signing

* Enable nuget signing

* Enable code signing

* Enable code signing

* Enable code signing

* Fix client endpoint handling

* Improve CI

* Fix CI

* Fix CI

* Fix CI

* Fix CI

* Import changes from version 4

* Cleanup target frameworks and remove obsolete code

* Drop support for dotnet7.0

* Remove managed client

* Version5 auditmode (#2016)

Added NuGet audit mode and some other settings to get informed about security issues.

* Address warnings from analyzers

* Support for ReadOnlySequence as payload (#2046)

* use ReadOnlySequence as payload

* fix build

* fix build

* skip sign in forks

* remove unnecessary owner

* tests and extensions

* remove test

* simplify WritePacketBuffer, remove unused helper

* Adjust namespaces

* Fix build errors

* Import latest fixes from main branch

* Adjust namespaces

---------

Co-authored-by: SeppPenner <HansM2013@gmx.de>
Co-authored-by: Martin Regen <mregen@microsoft.com>
2024-08-24 11:47:21 +02:00
pigwing e18a91a4b5 fix LoadingRetainedMessageAsync not exec (#2025)
because IHostedService StartAsync before startup Configure
see https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/generic-host?view=aspnetcore-8.0#ihostapplicationlifetime
2024-06-22 15:24:25 +02:00
drelidan7 b95d352236 Switch SubscribeAsync/UnsubscribeAsync to IEnumerable<string> (#2026)
The function currently requires an ICollection<string>, but doesn't actually depend on the ICollection<string> interface.  Relax the requirement to be IEnumerable<string> for better flexibility.
2024-06-22 14:55:20 +02:00
simonthum c5f4cf1f9b Topic template extension maintenance (#2022)
* Topic templates: Ensure parameter values are checked for wildcard

* Topic templates: Complement extension methods

- add template.BuildMessage()
- add subscribeOptionsBuilder.WithTopicTemplate()

* Topic templates: switch some samples to use a topic template

The changes should be self-explanatory and are
intended to provide more examples.

* Topic templates: add README
2024-06-22 14:54:11 +02:00
Josua Jäger 8650002523 fix Managed_Client_Subscribe_Samples.Connect_Client sample: (#2023)
- Result was never logged (`bool subscribed`)
- `subscribed` was never set, as it was not passed as a reference but as a value.
2024-06-21 19:05:12 +02:00
Christian 25c1da6a21 Restore behavior of _Server_ and _Port_ in client options (#2009)
* Restore old server and port behavior

* Fix unit test
2024-05-23 19:32:22 +02:00
christian 4e3ede42c6 Increase version 2024-05-21 22:36:34 +02:00
Christian 918a3c8d8b Restore old signing to get out hotfix release (#2004) 2024-05-19 16:39:49 +02:00
Christian 290419b3b6 [Client] Fix endpoint handling (#1999)
* Fix endpoint handling

* Fix different behavior for Mono framework.

* Restore old properties to avoid breaking changes
2024-05-19 12:18:12 +02:00
Christian 32b6146532 Setup nuget signing (#2001)
* Setup signing

* Update release notes
2024-05-18 20:21:09 +02:00
Christian 3a73ec21f7 [Client] Restore _WithConnectionUri_ methods (#1998)
Remove obsolete attributes
2024-05-17 17:49:33 +02:00
Christian 41d5b70344 Fix support for unix sockets (#1995)
* Add support for changing the protocol type on socket level

* Do not set NoDelay when not supported by protocol

* Update release notes

* Fix release notes
2024-05-14 18:09:33 +02:00
yztandChristian b4769778af DISCONNECT packet should allow zero-length body (#1994)
* DISCONNECT packet should allow zero-length body

> [If the Remaining Length is less than 1 the value of 0x00 (Normal disconnection) is used.](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901208)

* Update release notes and add comment

---------

Co-authored-by: Christian <6939810+chkr1011@users.noreply.github.com>
2024-05-14 18:08:54 +02:00
christian 473f9c31b1 Fix unit tests 2024-05-12 11:37:33 +02:00
christian ae4f168066 Fix build 2024-05-12 11:23:03 +02:00
christian c5578a93e8 Fix Unit Tests 2024-05-11 11:21:11 +02:00
christian f4b2a10616 Fix build errors and several warnings 2024-05-09 18:39:08 +02:00
ed63aa4910 Fix comment typos/text, refactor if-else chains to switch statements, optimize return statements, and invert conditional logic (#1961)
* Fix comment typos and refactor if-else chains to switch statements.

* Refactor code

---------

Co-authored-by: Wilmar Arias <wilmar.arias@keylinx.se>
Co-authored-by: christian <6939810+chkr1011@users.noreply.github.com>
2024-05-09 18:21:00 +02:00
dcharlesandchristian 4481869d42 fix: time out and cancel propagation in managed client (#1987)
* fix: time out and cancel propagation in managed client

* Update release notes

* Refactor code

---------

Co-authored-by: christian <6939810+chkr1011@users.noreply.github.com>
2024-05-07 19:16:32 +02:00
fazhoandchristian abbf14b1ec rethrow MqttClientUnexpectedDisconnectReceivedException when Publish … (#1974)
* rethrow MqttClientUnexpectedDisconnectReceivedException when Publish errors out because server disconnects the client and QoS is 0

* use a local var to cache _unexpectedDisconnectPacket

* Add inner exception to new exception

* Update release notes

* set the default inner exception as null

---------

Co-authored-by: christian <6939810+chkr1011@users.noreply.github.com>
2024-05-07 18:31:16 +02:00
Christian 4f9e54ba1d Expose certificate selection in client options (#1984)
* Expose certificate selection in MQTTnet options

* Fix .NET Framework builds

* Update release notes
2024-05-02 22:05:33 +02:00
Christian d80188167a Restore support for null in _AddServer_ method in ASP.NET integration (#1981)
* Upgrade ASP test project to dotnet8.

* Fix broken null support for _AddMqttServer_ method.

* Fix wrong code style

* Downgrade to dotnet7.0

* Expose certificate selection in MQTTnet options

* Revert "Expose certificate selection in MQTTnet options"

This reverts commit f7d098a119.

* Update release notes
2024-05-01 20:08:25 +02:00
Christian 4ab956abd1 Change behavior of NoMatchingSubscribers (#1975)
Change behavior of NoMatchingSubscribers.
2024-04-26 18:35:39 +02:00
simonthum 1aa91e3828 Fix unit tests for topic templates (#1976) 2024-04-26 18:35:27 +02:00
simonthumandchristian 2410d7b4c1 Utility class supporting dynamic topics using topic templates (#1932)
* Add MqttTopicTemplate

The topic template class is a way to deal with dynamic topics that
is less cumbersome and error-prone than String.Format or
other string fu one might use to handle dynamic topics.

Example:

  MyCity/{street}/{crossing}/lights

'street' and 'crossing' would be parameters.

The class and its extension methods are intended to support
dynamic subscription/publication, message-topic matching and
routing. The template syntax is modelled after the well-known
moustache syntax, which conincidentally is also published
as the 'AsyncAPI channel address expressions'.

* Add MQTTnet.Extensions.TopicTemplate to solution

* Apply code style from team shared resharper settings.
Add missing file headers.

* Update release notes

---------

Co-authored-by: christian <6939810+chkr1011@users.noreply.github.com>
2024-04-23 20:09:56 +02:00
Guiorgy b7328e5fd2 fixed a comment typo (#1973) 2024-04-23 20:09:39 +02:00
christian 7f5c437c46 Fix unit tests 2024-04-11 22:26:39 +02:00
christian ae5e038548 Merge remote-tracking branch 'origin/master' 2024-04-09 21:34:31 +02:00
christian 67e0fb8b0b Fix build errors by no longer using not supported language features for certain TFMs. 2024-04-09 21:34:15 +02:00
Marc PiulachsandChristian b763542329 Move "Client is not connected" error to it's own exception (#1954)
* Move "Client is not connected" error to it's own exception

* Update MqttClientNotConnectedException.cs

* Update release notes

---------

Co-authored-by: Christian <6939810+chkr1011@users.noreply.github.com>
2024-04-09 21:26:59 +02:00
christian d061da6077 Update release notes 2024-04-09 21:24:39 +02:00
Andriy Tolstoy 82a235aef4 remove select method (#1959) 2024-04-09 21:18:09 +02:00
James Thompson fd55982d98 #1957 Invert the logic to improve maintainance (#1958)
#1957 Invert the logic to improve maintaince
2024-04-01 22:13:48 +02:00
aabf180b23 1862 Add subscribe and unsubscribe results event for Managed Mqtt Client. (#1894)
* See the issue #1862

Adding an event when ManagedMqttClient actually subscribe/unsubscribe to topics.
This event has to be used in parallel with connected event to know the exact state.

* Rename SubscriptionsResultAsync to SubscriptionsChangedAsync.

* Rename internal field for event

* Update ReleaseNotes.md

---------

Co-authored-by: Pierre Dufrene <pierre.dufrene@se.com>
Co-authored-by: Christian <6939810+chkr1011@users.noreply.github.com>
2024-03-02 09:58:42 +01:00
Christian 1245d8299a Merge branch 'master' of https://github.com/dotnet/MQTTnet 2024-03-02 08:45:03 +01:00
yztandChristian 1ffffaff47 Fix minor bug in MqttV5PacketEncoder.cs (#1939)
* Fix minor bug in MqttV5PacketEncoder.cs

* Update ReleaseNotes.md

---------

Co-authored-by: Christian <6939810+chkr1011@users.noreply.github.com>
2024-03-02 09:44:14 +01:00
Christian c204f6d8b2 Make packet inspector fully async (#1941)
* Make packet inspector methods async.

* Update release notes
2024-03-02 09:28:56 +01:00
Christian b775fb256b Update version 2024-03-02 08:44:57 +01:00
Christian f82d382f23 Merge remote-tracking branch 'origin/master' 2024-02-14 17:54:04 +01:00
Christian 4214af283a Update ReleaseNotes.md 2024-02-14 17:47:29 +01:00
Christian b32a5c9fa0 Update ReleaseNotes.md 2024-02-14 17:44:16 +01:00
Sean Hanna e7e13a631f implement intercept publish message event on ManagedMqttClient (#1915)
* implement intercept publish message event on ManagedMqttClient

* rename ShouldPublish to ACceptPublish per PR feedback
2024-02-14 17:43:45 +01:00
Christian c12096705f 1908 support unix domain sockets (#1919)
* Add support for custom endpoints

Add overlaods etc. to allow for passing other endpoint implementations

* Extend publich broker test to also probe with different protocol versions

* Fix UWP build

* Update ReleaseNotes.md
2024-02-06 17:37:07 +01:00
Christian eb25500f50 1889 no service for type mqttnetservermqttserver has been registered (#1892)
* Add missing service to service collection

* Update ReleaseNotes.md

* Restore service registrations
2023-12-08 18:01:19 +01:00
Christian 4df06980a1 1883 in a weak network environment the mqtt broker is unable to detect disconnections (#1891)
* Add new setting

* Align keep alive handling with RFC

Only completely sent packets are treated as keep alive

* Update ReleaseNotes.md
2023-12-07 17:49:03 +01:00
Christian f3499013f4 Increase version 2023-12-07 17:48:14 +01:00
Christian 158a7fca46 Reset release notes 2023-12-07 17:47:57 +01:00
kallayjandChristian 09ab30fb0f Adds events for delivered and dropped messages. (#1866)
* Adds events for delivered and dropped messages.

* Rename new events to match other namings

* Update ReleaseNotes.md

---------

Co-authored-by: Christian <6939810+chkr1011@users.noreply.github.com>
2023-12-06 18:02:48 +01:00
Christian ae84aa43d2 1846 server mqttserverstopasync doesnt indicate correct reason (#1872) 2023-11-25 16:17:16 +01:00
RidoandChristian b431a5d3fe Add ReasonCode to SendExtendedAuthenticationExchangeDataAsync (#1882)
* add reason code to ext auth

* Update ReleaseNotes.md

---------

Co-authored-by: Christian <6939810+chkr1011@users.noreply.github.com>
2023-11-25 15:45:10 +01:00
Christian b9683c2943 Improve client disconnect (#1873)
Imrpove client disconnect
2023-11-05 15:15:36 +01:00
Christian 1ba8f74555 Update version 2023-11-04 13:40:28 +01:00
Christian ec637f7946 Handle unobserved tasks exceptions (#1871)
* Access exception so that it will not treated as unhandled

* Refactor code

* Improve unit tests

* Fix null reference exception

* Update ReleaseNotes.md
2023-11-04 13:39:16 +01:00
Christian 2599b3dfdc Update settings for tests project so that it will build on linux (#1860)
* Update settings for tests project so that it will build on linux

* Improve unit tests and samples

* Remove no longer required target frameworks for unit tests

* Update ci.yml
2023-11-01 09:34:18 +01:00
Christian d22caa7b70 Update readme (#1870)
* Update ReleaseNotes.md

* Apply project code style
2023-11-01 09:26:18 +01:00
Rido 01c90badf9 Support custom CA chain validation (#1851)
* Add CaFile based on SslStream CustomTrust

* change to cert collection

* do not require to disable revocation checks
2023-11-01 09:22:13 +01:00
Diego2098andChristian e4f605fbd5 Update MqttServer.cs (#1858)
* Update MqttServer.cs

clientID is null => RetainedMessageChangedEventArgs ctor throw new ArgumentNullException(nameof(clientId)

* Update ReleaseNotes.md

---------

Co-authored-by: Christian <6939810+chkr1011@users.noreply.github.com>
2023-11-01 08:29:19 +01:00
Christian 6fdee9e0db Fix wrong TLS usage (#1833)
* Fix wrong usage of TLS options

* Fix public broker tests

* Update ReleaseNotes.md
2023-09-06 20:08:35 +02:00
Christian cae7206733 Fix NullReferenceException when calling Ping (#1834)
* Check connection status before doing ping

* Fix broken reconnect sample

* Update ReleaseNotes.md
2023-09-06 20:08:13 +02:00
Christian e2e7aacc09 Update version and release notes 2023-09-06 20:07:45 +02:00
Ramon Smits 0d3dd96f2a Samples that show concurrent processing (#1803)
Samples that show concurrent processing with a limit and concurrent processing without auto ACK
2023-09-01 19:01:49 +02:00
Tomáš Jecha e0660ba887 #1810 Close connection after failed connect. (#1811) 2023-09-01 18:58:36 +02:00
Ivan BeňovicandChristian 5965ded488 Add possibility to specify parameter to RPC invocation (#1798)
* Add possibility to specify parameter to RPC invocation which will be accessible in TopicGenerationContext.

* Make parameters optional

* Update ReleaseNotes.md

---------

Co-authored-by: Christian <6939810+chkr1011@users.noreply.github.com>
2023-09-01 18:57:22 +02:00
Christian fc31c3077c Fix Unit Tests 2023-08-22 20:02:47 +02:00
Christian e604345e65 Update version 2023-08-19 15:23:10 +02:00
ChristianandSean Hanna 111a0d69cf Add support for hot reloading of client certificates (#1783)
* Refactor Unit Tests

* Add new certificate provider interface instead of read only certificates list.

* Update ReleaseNotes.md

* Fix Unit Tests

* Fix build

* unit testing hot swappable certificates (#1787)

* Move test classes to correct namespace

* Apply code style

---------

Co-authored-by: Sean Hanna <hannasm@users.noreply.github.com>
2023-08-19 15:14:05 +02:00
trokyandChristian 1a08ec17ba Added certificate validation callback for .NET framework 4.x (#1806)
* Added certificate validation callback for .NET framework 4.x

* Update ReleaseNotes.md

* Also expose sender to event arguments

---------

Co-authored-by: Christian <6939810+chkr1011@users.noreply.github.com>
2023-08-19 14:20:01 +02:00
Ramon SmitsandChristian c537d290c3 Only logging "Client will now throw an _MqttConnectingFailedException_." warning when actually throwing the exception (#1801)
* Warning was always logged

Warning was always logged while the text states it would throw a `MqttConnectingFailedException` exception

* Update ReleaseNotes.md

---------

Co-authored-by: Christian <6939810+chkr1011@users.noreply.github.com>
2023-08-19 13:59:14 +02:00
Cynthia MacLeodandChristian 2180f59725 Address issue #1818 where MqttConnectionContext throws an error when passed a TcpConnection. (#1819)
* Address issue #1818 where MqttConnectionContext throws and error when passed a TcpConnection.

* Update ReleaseNotes.md

---------

Co-authored-by: Christian <6939810+chkr1011@users.noreply.github.com>
2023-08-19 13:48:01 +02:00
Ramon SmitsandChristian b1dc8607af Moved check on Options.ValidateFeatures below ThrowIf methods to prevent NullReferenceException (#1800)
* Moved check on Options.ValidateFeatures below ThrowIf methods to prevent NullReferenceException

* Add Unit Test

* Update ReleaseNotes.md

---------

Co-authored-by: Christian <6939810+chkr1011@users.noreply.github.com>
2023-08-13 18:38:37 +02:00
simonthum 101898695e Add will message expiry interval to builder (#1804)
This was missing from builder but not regular options.
2023-08-13 18:06:16 +02:00
Christian 9295626503 [Server] Add initial support for session expiration (#1776) 2023-06-27 19:32:08 +02:00
Christian 18614d4e81 Expose configuration of packet fragmentation in server options. (#1769) 2023-06-15 18:51:43 +02:00
Christian 560e80455f Update samples (#1768) 2023-06-15 18:32:28 +02:00
HansM a97e88be29 Update Client_Subscribe_Samples.cs (#1766)
Fixes https://github.com/dotnet/MQTTnet/issues/1765.
2023-06-15 17:31:01 +02:00
logicaloudandChristian 3b5667c882 Fix #1761: MQTTRetainedMessageManager, return task with NULL result instead of NULL (#1762)
* Return task with NULL result instead of NULL

* move Task.FromResult call out of lock

* Update ReleaseNotes.md

---------

Co-authored-by: Christian <6939810+chkr1011@users.noreply.github.com>
2023-06-12 21:52:54 +02:00
Nicke BroströmandChristian 0790e54794 Websockets no longer working in Blazor WebAssembly (#1755)
* Fix for websockets not applying properties unless explicitly set

* Added comment indicating why properties can't be set

* Update ReleaseNotes.md

---------

Co-authored-by: Christian <6939810+chkr1011@users.noreply.github.com>
2023-06-12 21:48:27 +02:00
Christian 94f9fac2bd Update ReleaseNotes.md 2023-05-29 16:48:50 +02:00
Christian 8ad86928f1 1681 misleading signature of mqttclientconnectasync mqttclientconnectresultcode expected but throws exception (#1749) 2023-05-29 16:29:16 +02:00
Christian d8e5936d45 1724 nullreferenceexception on mqttclientconnectasync (#1748)
Update MqttClient.cs
2023-05-29 13:55:36 +02:00
Christian caa717544b 1742 some bug in mqttclientconnectionvalidatorhandle (#1746) 2023-05-29 13:53:42 +02:00
Christian 66b77f8d6d Merge branch 'master' of https://github.com/dotnet/MQTTnet 2023-05-29 13:47:09 +02:00
Christian ab5264c8ca 1738 userproperties in clientconnectedeventargs (#1747) 2023-05-29 13:45:44 +02:00
Christian db12309bae Update ReleaseNotes.md 2023-05-29 12:41:28 +02:00
Christian 2953e0dcc5 Improve payload to string conversion 2023-05-29 12:41:24 +02:00
Ramon Smits 1b18ac9fe3 Resolve ConvertPayloadToString resulting in ArgumentNullException (#1745)
Resolve breaking change where previously ConvertPayloadToString returned null if the payload was null
2023-05-29 12:38:33 +02:00
Christian 2cdad6622a Fix build 2023-05-14 09:14:37 +02:00
Andrei Kurosh 7601d11a7a UseDefaultCredentials options for WebSockets proxy configuration. (#1734) 2023-05-14 08:58:57 +02:00
Christian 0bb4c6ef09 Expose more TLS options (#1737) 2023-05-13 12:08:52 +02:00
Christian 449624fd72 Expose proper connect result in clients Disconnected event (#1728) 2023-05-06 12:49:41 +02:00
Christian 99f4f46062 Add new overloads for packet payload and will payload (#1721) 2023-05-04 22:19:06 +02:00
b08d11c069 replace lock/semaphore by readerwriterlockslim to avoid lock contenti… (#1716)
* replace lock/semaphore by readerwriterlockslim to avoid lock contention while dispatch messages

* Update MqttClientSubscriptionsManager.cs

removed comment out codes

* Update ReleaseNotes.md

---------

Co-authored-by: Zeheng Feng <hf944813@broadcom.com>
Co-authored-by: Christian <6939810+chkr1011@users.noreply.github.com>
2023-05-01 13:53:15 +02:00
Christian ac41e838ec Merge branch 'master' of https://github.com/dotnet/MQTTnet 2023-05-01 12:20:26 +02:00
Christian 836ddf3bbc Update version 2023-05-01 12:20:20 +02:00
Christian 0e815c626c Add validation of string lengths (#1718) 2023-05-01 12:03:23 +02:00
Christian 4b7d035e9e Reset release notes 2023-04-29 12:37:36 +02:00
Christian cb3c44f48d Rename InspectPackage method (#1713) 2023-04-23 13:57:59 +02:00
Christian 8b3b9d33de Add overloads for options builder (#1711)
* Add new overload for options builder

* Refactor code

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

* Update ReleaseNotes.md

---------

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

* Update readme

---------

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

* Fix UWP build

* Fix comment in MqttClientOptions (#1706)

* Refactor naming

* Fix wrong option value

---------

Co-authored-by: Michi <michael@volz.dev>
2023-04-06 17:54:36 +02:00
Christian d6cab76728 Add user properties to event args to interceptors for subscribing and unsubscribing (#1707)
Add user properties to event args
2023-04-06 17:31:42 +02:00
Christian b4f740f338 Update readme 2023-04-05 22:12:58 +02:00
Christian a004de25ac Fix unit test 2023-02-22 21:46:33 +01:00
老九 d0584413ca Add PayloadSegmentable feature (#1585) 2023-02-22 20:53:55 +01:00
Christian ba57537108 Fix running state handling in server (#1680)
Fix running state handling
2023-02-17 21:17:31 +01:00
Christian 6dcb110737 Add InterceptingClientEnqueueAsync event (#1679) 2023-02-17 19:13:06 +01:00
Christian b599a832da Add new method for reading a single retained message (#1676) 2023-02-15 19:46:37 +01:00
Christian 9cbd5dd778 Update ReleaseNotes.md 2023-01-28 13:08:24 +01:00
Christian ed31c51237 Code cleanup 2023-01-28 13:08:21 +01:00
Christian e01707d25e Improve samples 2023-01-28 13:08:08 +01:00
Jeffery MichaelandJeff Scherrer 73f118fe1c Moved the usage of certificate provider into the server listener (#1652)
* Moved the usage of certificate provider into the server listener so that each connection has the posibility of receiving a new SSL cert.

* Removed warning from unused field.

* Reduced changeds on original codebase.

---------

Co-authored-by: Jeff Scherrer <Jeff.Scherrer@vw.com>
2023-01-28 12:18:31 +01:00
Int32Overflow 0234918f30 Performance impact fix (#1661) 2023-01-28 12:12:20 +01:00
Jeffery Michael c90ba7e061 Fixed broken UWP build (#1655)
Added compiler declaritive to check for WINDOWS_UWP on IsAssignableFrom call.
2023-01-28 12:02:22 +01:00
467 changed files with 16368 additions and 16162 deletions
+5 -6
View File
@@ -21,14 +21,14 @@ dotnet_naming_rule.static_readonly_rule.symbols = static_readonly_symbols
dotnet_naming_style.lower_camel_case_style.capitalization = camel_case
dotnet_naming_style.lower_camel_case_style.required_prefix = _
dotnet_naming_style.upper_camel_case_style.capitalization = pascal_case
dotnet_naming_symbols.constants_symbols.applicable_accessibilities = public,internal,protected,protected_internal,private_protected
dotnet_naming_symbols.constants_symbols.applicable_accessibilities = public, internal, protected, protected_internal, private_protected
dotnet_naming_symbols.constants_symbols.applicable_kinds = field
dotnet_naming_symbols.constants_symbols.required_modifiers = const
dotnet_naming_symbols.public_fields_symbols.applicable_accessibilities = public,internal,protected,protected_internal,private_protected
dotnet_naming_symbols.public_fields_symbols.applicable_accessibilities = public, internal, protected, protected_internal, private_protected
dotnet_naming_symbols.public_fields_symbols.applicable_kinds = field
dotnet_naming_symbols.static_readonly_symbols.applicable_accessibilities = public,internal,protected,protected_internal,private_protected
dotnet_naming_symbols.static_readonly_symbols.applicable_accessibilities = public, internal, protected, protected_internal, private_protected
dotnet_naming_symbols.static_readonly_symbols.applicable_kinds = field
dotnet_naming_symbols.static_readonly_symbols.required_modifiers = static,readonly
dotnet_naming_symbols.static_readonly_symbols.required_modifiers = static, readonly
dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:none
dotnet_style_parentheses_in_other_binary_operators = never_if_unnecessary:none
dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:none
@@ -40,7 +40,6 @@ dotnet_style_qualification_for_method = false:suggestion
dotnet_style_qualification_for_property = false:suggestion
# ReSharper properties
resharper_autodetect_indent_settings = true
resharper_default_private_modifier = implicit
resharper_place_accessorholder_attribute_on_same_line = false
resharper_use_indent_from_vs = false
@@ -61,7 +60,7 @@ resharper_web_config_wrong_module_highlighting = warning
[*]
charset = utf-8
end_of_line = crlf
trim_trailing_whitespace = false
trim_trailing_whitespace = true
insert_final_newline = false
indent_style = space
indent_size = 4
-8
View File
@@ -1,8 +0,0 @@
* [Core] Added net7.0 builds (#1635, thanks to @YAJeff).
* [Client] Added support for passing MQTT v5 options (User properties etc.) for disconnects.
* [Client] An internal exception is no longer caught silently when calling _DisconnectAsync_ to indicate that the disconnect is not clean (BREAKING CHANGE).
* [Client] MQTTv5 features are now checked and an exception is thrown if they are used when using protocol version 3.1.1 and lower. These checks can be disabled in client options. (BREAKING CHANGE!).
* [Server] Exposed MQTT v5 sent properties from the affected client in _ClientDisconnectedAsync_ event.
* [Server] Fixed wrong client ID passed to _InterceptingUnsubscriptionEventArgs_ (#1631, thanks to @ghord).
* [Server] Exposed socket settings for TCP keep alive in TCP options (#1544).
* [Server] Exposed server session items at server level and allow custom session items for injected application messages (#1638).
+99 -41
View File
@@ -3,62 +3,120 @@ name: CI
on: [push, pull_request]
env:
VERSION: "4.2.0.${{github.run_number}}"
VERSION: "5.0.0.${{github.run_number}}"
PACKAGE_SUFFIX: "-RC"
jobs:
build:
runs-on: windows-2022
runs-on: ubuntu-latest
steps:
- name: Setup Windows SDK
uses: GuillaumeFalourd/setup-windows10-sdk-action@v1
with:
sdk-version: 18362
- name: Setup .NET SDK
uses: actions/setup-dotnet@v1.9.0
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
3.1.x
6.0.x
7.0.x
8.0.x
- name: Setup MSBuild
uses: microsoft/setup-msbuild@v1.1
- name: Checkout code
uses: actions/checkout@v4
- name: Checkout Code
uses: actions/checkout@v2
- name: Build package
run: dotnet build MQTTnet.sln --configuration Release /p:FileVersion=${{ env.VERSION }} /p:AssemblyVersion=${{ env.VERSION }} /p:PackageVersion=${{ env.VERSION }}${{ env.PACKAGE_SUFFIX }}
- name: Setup Signing Certificate
run: |
$secret = '${{ secrets.SNC_BASE64 }}'
$decoded = [System.Convert]::FromBase64CharArray($secret, 0, $secret.Length)
Set-Content -Path ${{ github.workspace }}\certificate.snk -Value $decoded -AsByteStream
- name: Restore nuget packages
run: msbuild MQTTnet.sln /t:Restore /p:Configuration="Release" /verbosity:m
- name: Build solution
run: msbuild MQTTnet.sln /t:Build /p:Configuration="Release" /verbosity:m /p:FileVersion=${{ env.VERSION }} /p:AssemblyVersion=${{ env.VERSION }} /p:PackageVersion=${{ env.VERSION }} /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=${{ github.workspace }}\certificate.snk
- name: Collect nuget Packages
uses: actions/upload-artifact@v2
- name: Upload nuget packages
uses: actions/upload-artifact@v4
with:
name: nuget Packages
name: nugets
path: |
**\*.nupkg
**\*.snupkg
${{ github.workspace }}/Source/**/*.nupkg
${{ github.workspace }}/Source/**/*.snupkg
- name: Setup VSTest
uses: darenm/Setup-VSTest@v1
test:
runs-on: ubuntu-latest
- name: Core Tests
run: vstest.console.exe Source\MQTTnet.Tests\bin\Release\net6.0\MQTTnet.Tests.dll
steps:
- name: Setup .NET SDK
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
8.0.x
- name: ASP.NET Tests
run: vstest.console.exe Source\MQTTnet.AspNetCore.Tests\bin\Release\netcoreapp3.1\MQTTnet.AspNetCore.Tests.dll
- name: Checkout code
uses: actions/checkout@v4
- name: Publish MyGet nugets
if: ${{ github.event_name == 'push' }}
run: dotnet nuget push **/*.nupkg -k ${{ secrets.MYGET_API_KEY }} -s https://www.myget.org/F/mqttnet/api/v3/index.json --skip-duplicate
- name: Execute tests
run: dotnet test --framework net8.0 Source/MQTTnet.Tests/MQTTnet.Tests.csproj
sign:
needs: build
runs-on: windows-latest # Code signing must run on a Windows agent for Authenticode signing (dll/exe)
if: github.repository == 'dotnet/MQTTnet'
steps:
- name: Setup .NET SDK
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
6.0.x
- name: Download nuget packages
uses: actions/download-artifact@v4
with:
name: nugets
path: nugets
- name: Install sign CLI tool
run: dotnet tool install --tool-path . sign --version 0.9.0-beta.23127.3
- name: Sign nugets
shell: pwsh
run: >
./sign code azure-key-vault `
"**/*.nupkg" `
--base-directory "${{ github.workspace }}\nugets" `
--publisher-name "MQTTnet" `
--description "MQTTnet" `
--description-url "https://github.com/dotnet/MQTTnet" `
--azure-key-vault-tenant-id "${{ secrets.AZURE_TENANT_ID }}" `
--azure-key-vault-client-id "${{ secrets.AZURE_CLIENT_ID }}" `
--azure-key-vault-client-secret "${{ secrets.AZURE_CLIENT_SECRET }}" `
--azure-key-vault-certificate "${{ secrets.KEY_VAULT_CERTIFICATE_ID }}" `
--azure-key-vault-url "${{ secrets.KEY_VAULT_URL }}"
- name: Upload signed nuget packages
uses: actions/upload-artifact@v4
with:
name: signed-nugets
path: |
${{ github.workspace }}\nugets\**\*.nupkg
${{ github.workspace }}\nugets\**\*.snupkg
publish-myget:
if: ${{ github.event_name == 'push' }}
needs: sign
runs-on: ubuntu-latest
steps:
- name: Download signed nuget packages
uses: actions/download-artifact@v4
with:
name: signed-nugets
path: nugets
- name: Publish myget.com nugets
run: dotnet nuget push ${{ github.workspace }}/nugets/**/*.nupkg -k ${{ secrets.MYGET_API_KEY }} -s https://www.myget.org/F/mqttnet/api/v3/index.json --skip-duplicate
publish-nuget:
if: ${{ github.event_name == 'published' }}
needs: [sign, test]
runs-on: ubuntu-latest
steps:
- name: Download signed nuget packages
uses: actions/download-artifact@v4
with:
name: signed-nugets
path: nugets
- name: Publish nuget.org nugets
run: dotnet nuget push ${{ github.workspace }}/nugets/**/*.nupkg -k ${{ secrets.NUGET_API_KEY }} -s https://api.nuget.org/v3/index.json --skip-duplicate
+20 -24
View File
@@ -7,30 +7,30 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet", "Source\MQTTnet\M
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{B3F60ECB-45BA-4C66-8903-8BB89CA67998}"
ProjectSection(SolutionItems) = preProject
CODE-OF-CONDUCT.md = CODE-OF-CONDUCT.md
LICENSE = LICENSE
README.md = README.md
CODE-OF-CONDUCT.md = CODE-OF-CONDUCT.md
Source\ReleaseNotes.md = Source\ReleaseNotes.md
.github\workflows\ci.yml = .github\workflows\ci.yml
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.AspNetCore", "Source\MQTTnet.AspnetCore\MQTTnet.AspNetCore.csproj", "{F10C4060-F7EE-4A83-919F-FF723E72F94A}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Extensions.Rpc", "Source\MQTTnet.Extensions.Rpc\MQTTnet.Extensions.Rpc.csproj", "{C444E9C8-95FA-430E-9126-274129DE16CD}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Extensions.ManagedClient", "Source\MQTTnet.Extensions.ManagedClient\MQTTnet.Extensions.ManagedClient.csproj", "{C400533A-8EBA-4F0B-BF4D-295C3708604B}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Samples", "Samples\MQTTnet.Samples.csproj", "{71CF35F5-3327-4A91-AAF4-5340F6701771}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Extensions.WebSocket4Net", "Source\MQTTnet.Extensions.WebSocket4Net\MQTTnet.Extensions.WebSocket4Net.csproj", "{2BD01D53-4CA5-4142-BE8D-313876395E3E}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Tests", "Source\MQTTnet.Tests\MQTTnet.Tests.csproj", "{B270F32A-9F3E-42EE-A989-813E35E29ADB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MQTTnet.Samples", "Samples\MQTTnet.Samples.csproj", "{71CF35F5-3327-4A91-AAF4-5340F6701771}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Benchmarks", "Source\MQTTnet.Benchmarks\MQTTnet.Benchmarks.csproj", "{2F516E76-AAC4-4219-B7D1-34CDD3CFF381}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MQTTnet.Tests", "Source\MQTTnet.Tests\MQTTnet.Tests.csproj", "{B270F32A-9F3E-42EE-A989-813E35E29ADB}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.TestApp", "Source\MQTTnet.TestApp\MQTTnet.TestApp.csproj", "{175D5340-CC5B-4542-939D-4E7D15A0BC8D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MQTTnet.AspNetCore.Tests", "Source\MQTTnet.AspNetCore.Tests\MQTTnet.AspNetCore.Tests.csproj", "{A238BBBF-C75F-482D-9CC3-BB34ABA9B675}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.AspTestApp", "Source\MQTTnet.AspTestApp\MQTTnet.AspTestApp.csproj", "{72867E4C-4E15-4E8E-8FAB-AE9253286BBC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MQTTnet.Benchmarks", "Source\MQTTnet.Benchmarks\MQTTnet.Benchmarks.csproj", "{2F516E76-AAC4-4219-B7D1-34CDD3CFF381}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Extensions.TopicTemplate", "Source\MQTTnet.Extensions.TopicTemplate\MQTTnet.Extensions.TopicTemplate.csproj", "{374D5861-F7A6-42A2-8CBE-040846000EF9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MQTTnet.TestApp", "Source\MQTTnet.TestApp\MQTTnet.TestApp.csproj", "{175D5340-CC5B-4542-939D-4E7D15A0BC8D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MQTTnet.AspTestApp", "Source\MQTTnet.AspTestApp\MQTTnet.AspTestApp.csproj", "{72867E4C-4E15-4E8E-8FAB-AE9253286BBC}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MQTTnet.Server", "Source\MQTTnet.Server\MQTTnet.Server.csproj", "{C876EFFD-C5AD-4E26-BD9F-4C48252C02BC}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -50,14 +50,6 @@ Global
{C444E9C8-95FA-430E-9126-274129DE16CD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C444E9C8-95FA-430E-9126-274129DE16CD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C444E9C8-95FA-430E-9126-274129DE16CD}.Release|Any CPU.Build.0 = Release|Any CPU
{C400533A-8EBA-4F0B-BF4D-295C3708604B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C400533A-8EBA-4F0B-BF4D-295C3708604B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C400533A-8EBA-4F0B-BF4D-295C3708604B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C400533A-8EBA-4F0B-BF4D-295C3708604B}.Release|Any CPU.Build.0 = Release|Any CPU
{2BD01D53-4CA5-4142-BE8D-313876395E3E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2BD01D53-4CA5-4142-BE8D-313876395E3E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2BD01D53-4CA5-4142-BE8D-313876395E3E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2BD01D53-4CA5-4142-BE8D-313876395E3E}.Release|Any CPU.Build.0 = Release|Any CPU
{71CF35F5-3327-4A91-AAF4-5340F6701771}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{71CF35F5-3327-4A91-AAF4-5340F6701771}.Debug|Any CPU.Build.0 = Debug|Any CPU
{71CF35F5-3327-4A91-AAF4-5340F6701771}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -66,10 +58,6 @@ Global
{B270F32A-9F3E-42EE-A989-813E35E29ADB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B270F32A-9F3E-42EE-A989-813E35E29ADB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B270F32A-9F3E-42EE-A989-813E35E29ADB}.Release|Any CPU.Build.0 = Release|Any CPU
{A238BBBF-C75F-482D-9CC3-BB34ABA9B675}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A238BBBF-C75F-482D-9CC3-BB34ABA9B675}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A238BBBF-C75F-482D-9CC3-BB34ABA9B675}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A238BBBF-C75F-482D-9CC3-BB34ABA9B675}.Release|Any CPU.Build.0 = Release|Any CPU
{2F516E76-AAC4-4219-B7D1-34CDD3CFF381}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2F516E76-AAC4-4219-B7D1-34CDD3CFF381}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2F516E76-AAC4-4219-B7D1-34CDD3CFF381}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -82,13 +70,21 @@ Global
{72867E4C-4E15-4E8E-8FAB-AE9253286BBC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{72867E4C-4E15-4E8E-8FAB-AE9253286BBC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{72867E4C-4E15-4E8E-8FAB-AE9253286BBC}.Release|Any CPU.Build.0 = Release|Any CPU
{374D5861-F7A6-42A2-8CBE-040846000EF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{374D5861-F7A6-42A2-8CBE-040846000EF9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{374D5861-F7A6-42A2-8CBE-040846000EF9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{374D5861-F7A6-42A2-8CBE-040846000EF9}.Release|Any CPU.Build.0 = Release|Any CPU
{C876EFFD-C5AD-4E26-BD9F-4C48252C02BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C876EFFD-C5AD-4E26-BD9F-4C48252C02BC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C876EFFD-C5AD-4E26-BD9F-4C48252C02BC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C876EFFD-C5AD-4E26-BD9F-4C48252C02BC}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {07536672-5CBC-4BE3-ACE0-708A431A7894}
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
EndGlobalSection
EndGlobal
+3
View File
@@ -230,16 +230,19 @@
<s:String x:Key="/Default/CodeStyle/FileHeader/FileHeaderText/@EntryValue">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.</s:String>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EFeature_002EServices_002ECodeCleanup_002EFileHeader_002EFileHeaderSettingsMigrate/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpAttributeForSingleLineMethodUpgrade/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpKeepExistingMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpPlaceEmbeddedOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpRenamePlacementToArrangementMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpUseContinuousIndentInsideBracesMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002EMemberReordering_002EMigrations_002ECSharpFileLayoutPatternRemoveIsAttributeUpgrade/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EAddAccessorOwnerDeclarationBracesMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EAlwaysTreatStructAsNotReorderableMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002ECSharpPlaceAttributeOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateThisQualifierSettings/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=catched/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=CONNACK/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=PINGREQ/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=PINGRESP/@EntryIndexedValue">True</s:Boolean>
+27 -4
View File
@@ -12,7 +12,9 @@
# MQTTnet
MQTTnet is a high performance .NET library for MQTT based communication. It provides a MQTT client and a MQTT server (broker) and supports the MQTT protocol up to version 5. It is compatible with mostly any supported .NET Framework version and CPU architecture.
MQTTnet is a high performance .NET library for MQTT based communication. It provides a MQTT client and a MQTT server (
broker) and supports the MQTT protocol up to version 5. It is compatible with mostly any supported .NET Framework
version and CPU architecture.
## Features
@@ -28,13 +30,15 @@ MQTTnet is a high performance .NET library for MQTT based communication. It prov
* Unit tested (~636 tests)
* No external dependencies
\* Tested on local machine (Intel i7 8700K) with MQTTnet client and server running in the same process using the TCP channel. The app for verification is part of this repository and stored in _/Tests/MQTTnet.TestApp.NetCore_.
\* Tested on local machine (Intel i7 8700K) with MQTTnet client and server running in the same process using the TCP
channel. The app for verification is part of this repository and stored in _/Tests/MQTTnet.TestApp.NetCore_.
### Client
* Communication via TCP (+TLS) or WS (WebSocket) supported
* Included core _LowLevelMqttClient_ with low level functionality
* Also included _ManagedMqttClient_ which maintains the connection and subscriptions automatically. Also application messages are queued and re-scheduled for higher QoS levels automatically.
* Also included _ManagedMqttClient_ which maintains the connection and subscriptions automatically. Also application
messages are queued and re-scheduled for higher QoS levels automatically.
* Rx support (via another project)
* Compatible with Microsoft Azure IoT Hub
@@ -50,9 +54,28 @@ MQTTnet is a high performance .NET library for MQTT based communication. It prov
* A custom message interceptor can be added which allows transforming or extending every received application message
* Validate subscriptions and deny subscribing of certain topics depending on requesting clients
## Getting Started
MQTTnet is delivered via <b>NuGet</b> package manager. You can find the packages
here: https://www.nuget.org/packages/MQTTnet/
Use these command in the Package Manager console to install MQTTnet manually:
```
Install-Package MQTTnet
```
Samples for using MQTTnet are part of this repository. For starters these samples are recommended:
- [Connect with a broker](https://github.com/dotnet/MQTTnet/blob/master/Samples/Client/Client_Connection_Samples.cs)
- [Subscribing to data](https://github.com/dotnet/MQTTnet/blob/master/Samples/Client/Client_Subscribe_Samples.cs)
- [Publishing data](https://github.com/dotnet/MQTTnet/blob/master/Samples/Client/Client_Publish_Samples.cs)
- [Host own broker](https://github.com/dotnet/MQTTnet/blob/master/Samples/Server/Server_Simple_Samples.cs)
## Code of Conduct
This project has adopted the code of conduct defined by the Contributor Covenant to clarify expected behavior in our community.
This project has adopted the code of conduct defined by the Contributor Covenant to clarify expected behavior in our
community.
For more information see the [.NET Foundation Code of Conduct](https://dotnetfoundation.org/code-of-conduct).
## .NET Foundation
+118 -66
View File
@@ -7,8 +7,7 @@
// ReSharper disable InconsistentNaming
using System.Security.Authentication;
using MQTTnet.Client;
using MQTTnet.Extensions.WebSocket4Net;
using System.Security.Cryptography.X509Certificates;
using MQTTnet.Formatter;
using MQTTnet.Samples.Helpers;
@@ -25,14 +24,14 @@ public static class Client_Connection_Samples
* See sample _Connect_Client_ for more details.
*/
var mqttFactory = new MqttFactory();
var mqttFactory = new MqttClientFactory();
using (var mqttClient = mqttFactory.CreateMqttClient())
{
var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("broker.hivemq.com").Build();
await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None);
// This will send the DISCONNECT packet. Calling _Dispose_ without DisconnectAsync the
// This will send the DISCONNECT packet. Calling _Dispose_ without DisconnectAsync the
// connection is closed in a "not clean" way. See MQTT specification for more details.
await mqttClient.DisconnectAsync(new MqttClientDisconnectOptionsBuilder().WithReason(MqttClientDisconnectOptionsReason.NormalDisconnection).Build());
}
@@ -47,7 +46,7 @@ public static class Client_Connection_Samples
* The default version of MQTT is 3.1.1.
*/
var mqttFactory = new MqttFactory();
var mqttFactory = new MqttClientFactory();
using (var mqttClient = mqttFactory.CreateMqttClient())
{
@@ -55,7 +54,7 @@ public static class Client_Connection_Samples
var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("broker.hivemq.com").Build();
// This will throw an exception if the server is not available.
// The result from this message returns additional data which was sent
// The result from this message returns additional data which was sent
// from the server. Please refer to the MQTT protocol specification for details.
var response = await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None);
@@ -75,11 +74,11 @@ public static class Client_Connection_Samples
{
/*
* This sample creates a simple MQTT client and connects to an invalid broker using a timeout.
*
*
* This is a modified version of the sample _Connect_Client_! See other sample for more details.
*/
var mqttFactory = new MqttFactory();
var mqttFactory = new MqttClientFactory();
using (var mqttClient = mqttFactory.CreateMqttClient())
{
@@ -103,11 +102,11 @@ public static class Client_Connection_Samples
{
/*
* This sample creates a simple MQTT client and connects to a public broker using MQTTv5.
*
*
* This is a modified version of the sample _Connect_Client_! See other sample for more details.
*/
var mqttFactory = new MqttFactory();
var mqttFactory = new MqttClientFactory();
using (var mqttClient = mqttFactory.CreateMqttClient())
{
@@ -126,24 +125,24 @@ public static class Client_Connection_Samples
{
/*
* This sample creates a simple MQTT client and connects to a public broker using TLS 1.2 encryption.
*
*
* This is a modified version of the sample _Connect_Client_! See other sample for more details.
*/
var mqttFactory = new MqttFactory();
var mqttFactory = new MqttClientFactory();
using (var mqttClient = mqttFactory.CreateMqttClient())
{
var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("mqtt.fluux.io")
.WithTls(
.WithTlsOptions(
o =>
{
// The used public broker sometimes has invalid certificates. This sample accepts all
// certificates. This should not be used in live environments.
o.CertificateValidationHandler = _ => true;
o.WithCertificateValidationHandler(_ => true);
// The default value is determined by the OS. Set manually to force version.
o.SslProtocol = SslProtocols.Tls12;
o.WithSslProtocols(SslProtocols.Tls12);
})
.Build();
@@ -156,43 +155,19 @@ public static class Client_Connection_Samples
}
}
public static async Task Connect_Client_Using_WebSocket4Net()
{
/*
* This sample creates a simple MQTT client and connects to a public broker using a WebSocket connection.
* Instead of the .NET implementation of WebSockets the implementation from WebSocket4Net is used. It provides more
* encryption algorithms and supports more platforms.
*
* This is a modified version of the sample _Connect_Client_! See other sample for more details.
*/
var mqttFactory = new MqttFactory().UseWebSocket4Net();
using (var mqttClient = mqttFactory.CreateMqttClient())
{
var mqttClientOptions = new MqttClientOptionsBuilder().WithWebSocketServer("broker.hivemq.com:8000/mqtt").Build();
var response = await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None);
Console.WriteLine("The MQTT client is connected.");
response.DumpToConsole();
}
}
public static async Task Connect_Client_Using_WebSockets()
{
/*
* This sample creates a simple MQTT client and connects to a public broker using a WebSocket connection.
*
*
* This is a modified version of the sample _Connect_Client_! See other sample for more details.
*/
var mqttFactory = new MqttFactory();
var mqttFactory = new MqttClientFactory();
using (var mqttClient = mqttFactory.CreateMqttClient())
{
var mqttClientOptions = new MqttClientOptionsBuilder().WithWebSocketServer("broker.hivemq.com:8000/mqtt").Build();
var mqttClientOptions = new MqttClientOptionsBuilder().WithWebSocketServer(o => o.WithUri("broker.hivemq.com:8000/mqtt")).Build();
var response = await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None);
@@ -206,22 +181,20 @@ public static class Client_Connection_Samples
{
/*
* This sample creates a simple MQTT client and connects to a public broker with enabled TLS encryption.
*
*
* This is a modified version of the sample _Connect_Client_! See other sample for more details.
*/
var mqttFactory = new MqttFactory();
var mqttFactory = new MqttClientFactory();
using (var mqttClient = mqttFactory.CreateMqttClient())
{
var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("test.mosquitto.org", 8883)
.WithTls(
o =>
{
.WithTlsOptions(
o => o.WithCertificateValidationHandler(
// The used public broker sometimes has invalid certificates. This sample accepts all
// certificates. This should not be used in live environments.
o.CertificateValidationHandler = _ => true;
})
_ => true))
.Build();
// In MQTTv5 the response contains much more information.
@@ -236,6 +209,31 @@ public static class Client_Connection_Samples
}
}
public static async Task Connect_With_Amazon_AWS()
{
/*
* This sample creates a simple MQTT client and connects to an Amazon Web Services broker.
*
* The broker requires special settings which are set here.
*/
var mqttFactory = new MqttClientFactory();
using (var mqttClient = mqttFactory.CreateMqttClient())
{
var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("amazon.web.services.broker")
// Disabling packet fragmentation is very important!
.WithoutPacketFragmentation()
.Build();
await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None);
Console.WriteLine("The MQTT client is connected.");
await mqttClient.DisconnectAsync();
}
}
public static async Task Disconnect_Clean()
{
/*
@@ -244,7 +242,7 @@ public static class Client_Connection_Samples
* trigger sending the last will etc.
*/
var mqttFactory = new MqttFactory();
var mqttFactory = new MqttClientFactory();
using (var mqttClient = mqttFactory.CreateMqttClient())
{
@@ -266,7 +264,7 @@ public static class Client_Connection_Samples
* trigger sending the last will etc.
*/
var mqttFactory = new MqttFactory();
var mqttFactory = new MqttClientFactory();
var mqttClient = mqttFactory.CreateMqttClient();
@@ -286,23 +284,24 @@ public static class Client_Connection_Samples
* including the reason for that status.
*/
var mqttFactory = new MqttFactory();
var mqttFactory = new MqttClientFactory();
using (var mqttClient = mqttFactory.CreateMqttClient())
{
var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("mqtt.fluux.io", 8883)
.WithTls(
.WithTlsOptions(
o =>
{
o.CertificateValidationHandler = eventArgs =>
{
eventArgs.Certificate.Subject.DumpToConsole();
eventArgs.Certificate.GetExpirationDateString().DumpToConsole();
eventArgs.Chain.ChainPolicy.RevocationMode.DumpToConsole();
eventArgs.Chain.ChainStatus.DumpToConsole();
eventArgs.SslPolicyErrors.DumpToConsole();
return true;
};
o.WithCertificateValidationHandler(
eventArgs =>
{
eventArgs.Certificate.Subject.DumpToConsole();
eventArgs.Certificate.GetExpirationDateString().DumpToConsole();
eventArgs.Chain.ChainPolicy.RevocationMode.DumpToConsole();
eventArgs.Chain.ChainStatus.DumpToConsole();
eventArgs.SslPolicyErrors.DumpToConsole();
return true;
});
})
.Build();
@@ -322,7 +321,7 @@ public static class Client_Connection_Samples
* This is only supported in MQTTv5.0.0+.
*/
var mqttFactory = new MqttFactory();
var mqttFactory = new MqttClientFactory();
using (var mqttClient = mqttFactory.CreateMqttClient())
{
@@ -345,7 +344,7 @@ public static class Client_Connection_Samples
* This approach has a risk of dead locks! Consider using the timer approach (see sample).
*/
var mqttFactory = new MqttFactory();
var mqttFactory = new MqttClientFactory();
using (var mqttClient = mqttFactory.CreateMqttClient())
{
@@ -372,7 +371,7 @@ public static class Client_Connection_Samples
* This is the recommended way but requires more custom code!
*/
var mqttFactory = new MqttFactory();
var mqttFactory = new MqttClientFactory();
using (var mqttClient = mqttFactory.CreateMqttClient())
{
@@ -406,6 +405,59 @@ public static class Client_Connection_Samples
}
}
});
Console.WriteLine("Press <Enter> to exit");
Console.ReadLine();
}
}
}
public static async Task ConnectTls_WithCaFile()
{
var mqttFactory = new MqttClientFactory();
X509Certificate2Collection caChain = new X509Certificate2Collection();
caChain.ImportFromPem(mosquitto_org); // from https://test.mosquitto.org/ssl/mosquitto.org.crt
using (var mqttClient = mqttFactory.CreateMqttClient())
{
var mqttClientOptions = new MqttClientOptionsBuilder()
.WithTcpServer("test.mosquitto.org", 8883)
.WithTlsOptions(new MqttClientTlsOptionsBuilder()
.WithTrustChain(caChain)
.Build())
.Build();
var connAck = await mqttClient.ConnectAsync(mqttClientOptions);
Console.WriteLine("Connected to test.moquitto.org:8883 with CaFile mosquitto.org.crt: " + connAck.ResultCode);
}
}
const string mosquitto_org = @"
-----BEGIN CERTIFICATE-----
MIIEAzCCAuugAwIBAgIUBY1hlCGvdj4NhBXkZ/uLUZNILAwwDQYJKoZIhvcNAQEL
BQAwgZAxCzAJBgNVBAYTAkdCMRcwFQYDVQQIDA5Vbml0ZWQgS2luZ2RvbTEOMAwG
A1UEBwwFRGVyYnkxEjAQBgNVBAoMCU1vc3F1aXR0bzELMAkGA1UECwwCQ0ExFjAU
BgNVBAMMDW1vc3F1aXR0by5vcmcxHzAdBgkqhkiG9w0BCQEWEHJvZ2VyQGF0Y2hv
by5vcmcwHhcNMjAwNjA5MTEwNjM5WhcNMzAwNjA3MTEwNjM5WjCBkDELMAkGA1UE
BhMCR0IxFzAVBgNVBAgMDlVuaXRlZCBLaW5nZG9tMQ4wDAYDVQQHDAVEZXJieTES
MBAGA1UECgwJTW9zcXVpdHRvMQswCQYDVQQLDAJDQTEWMBQGA1UEAwwNbW9zcXVp
dHRvLm9yZzEfMB0GCSqGSIb3DQEJARYQcm9nZXJAYXRjaG9vLm9yZzCCASIwDQYJ
KoZIhvcNAQEBBQADggEPADCCAQoCggEBAME0HKmIzfTOwkKLT3THHe+ObdizamPg
UZmD64Tf3zJdNeYGYn4CEXbyP6fy3tWc8S2boW6dzrH8SdFf9uo320GJA9B7U1FW
Te3xda/Lm3JFfaHjkWw7jBwcauQZjpGINHapHRlpiCZsquAthOgxW9SgDgYlGzEA
s06pkEFiMw+qDfLo/sxFKB6vQlFekMeCymjLCbNwPJyqyhFmPWwio/PDMruBTzPH
3cioBnrJWKXc3OjXdLGFJOfj7pP0j/dr2LH72eSvv3PQQFl90CZPFhrCUcRHSSxo
E6yjGOdnz7f6PveLIB574kQORwt8ePn0yidrTC1ictikED3nHYhMUOUCAwEAAaNT
MFEwHQYDVR0OBBYEFPVV6xBUFPiGKDyo5V3+Hbh4N9YSMB8GA1UdIwQYMBaAFPVV
6xBUFPiGKDyo5V3+Hbh4N9YSMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL
BQADggEBAGa9kS21N70ThM6/Hj9D7mbVxKLBjVWe2TPsGfbl3rEDfZ+OKRZ2j6AC
6r7jb4TZO3dzF2p6dgbrlU71Y/4K0TdzIjRj3cQ3KSm41JvUQ0hZ/c04iGDg/xWf
+pp58nfPAYwuerruPNWmlStWAXf0UTqRtg4hQDWBuUFDJTuWuuBvEXudz74eh/wK
sMwfu1HFvjy5Z0iMDU8PUDepjVolOCue9ashlS4EB5IECdSR2TItnAIiIwimx839
LdUdRudafMu5T5Xma182OC0/u/xRlEm+tvKGGmfFcN0piqVl8OrSPBgIlb+1IKJE
m/XriWr/Cq4h/JfB7NTsezVslgkBaoU=
-----END CERTIFICATE-----
";
}
+8 -10
View File
@@ -6,8 +6,6 @@
// ReSharper disable UnusedMember.Global
// ReSharper disable InconsistentNaming
using MQTTnet.Client;
namespace MQTTnet.Samples.Client;
public static class Client_Publish_Samples
@@ -23,7 +21,7 @@ public static class Client_Publish_Samples
* or at least provides backward compatibility where possible.
*/
var mqttFactory = new MqttFactory();
var mqttFactory = new MqttClientFactory();
using (var mqttClient = mqttFactory.CreateMqttClient())
{
@@ -41,11 +39,11 @@ public static class Client_Publish_Samples
await mqttClient.PublishAsync(applicationMessage, CancellationToken.None);
await mqttClient.DisconnectAsync();
Console.WriteLine("MQTT application message is published.");
}
}
public static async Task Publish_Multiple_Application_Messages()
{
/*
@@ -54,7 +52,7 @@ public static class Client_Publish_Samples
* See sample _Publish_Application_Message_ for more details.
*/
var mqttFactory = new MqttFactory();
var mqttFactory = new MqttClientFactory();
using (var mqttClient = mqttFactory.CreateMqttClient())
{
@@ -70,23 +68,23 @@ public static class Client_Publish_Samples
.Build();
await mqttClient.PublishAsync(applicationMessage, CancellationToken.None);
applicationMessage = new MqttApplicationMessageBuilder()
.WithTopic("samples/temperature/living_room")
.WithPayload("20.0")
.Build();
await mqttClient.PublishAsync(applicationMessage, CancellationToken.None);
applicationMessage = new MqttApplicationMessageBuilder()
.WithTopic("samples/temperature/living_room")
.WithPayload("21.0")
.Build();
await mqttClient.PublishAsync(applicationMessage, CancellationToken.None);
await mqttClient.DisconnectAsync();
Console.WriteLine("MQTT application message is published.");
}
}
+72 -41
View File
@@ -5,8 +5,9 @@
// ReSharper disable UnusedType.Global
// ReSharper disable UnusedMember.Global
// ReSharper disable InconsistentNaming
// ReSharper disable UnusedMember.Local
using MQTTnet.Client;
using MQTTnet.Extensions.TopicTemplate;
using MQTTnet.Packets;
using MQTTnet.Protocol;
using MQTTnet.Samples.Helpers;
@@ -15,13 +16,15 @@ namespace MQTTnet.Samples.Client;
public static class Client_Subscribe_Samples
{
static readonly MqttTopicTemplate sampleTemplate = new("mqttnet/samples/topic/{id}");
public static async Task Handle_Received_Application_Message()
{
/*
* This sample subscribes to a topic and processes the received message.
*/
var mqttFactory = new MqttFactory();
var mqttFactory = new MqttClientFactory();
using (var mqttClient = mqttFactory.CreateMqttClient())
{
@@ -40,13 +43,7 @@ public static class Client_Subscribe_Samples
await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None);
var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder()
.WithTopicFilter(
f =>
{
f.WithTopic("mqttnet/samples/topic/2");
})
.Build();
var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder().WithTopicTemplate(sampleTemplate.WithParameter("id", "2")).Build();
await mqttClient.SubscribeAsync(mqttSubscribeOptions, CancellationToken.None);
@@ -63,7 +60,7 @@ public static class Client_Subscribe_Samples
* This sample subscribes to a topic and sends a response to the broker. This requires at least QoS level 1 to work!
*/
var mqttFactory = new MqttFactory();
var mqttFactory = new MqttClientFactory();
using (var mqttClient = mqttFactory.CreateMqttClient())
{
@@ -86,13 +83,7 @@ public static class Client_Subscribe_Samples
await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None);
var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder()
.WithTopicFilter(
f =>
{
f.WithTopic("mqttnet/samples/topic/1");
})
.Build();
var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder().WithTopicTemplate(sampleTemplate.WithParameter("id", "1")).Build();
var response = await mqttClient.SubscribeAsync(mqttSubscribeOptions, CancellationToken.None);
@@ -109,7 +100,7 @@ public static class Client_Subscribe_Samples
* This sample subscribes to several topics in a single request.
*/
var mqttFactory = new MqttFactory();
var mqttFactory = new MqttClientFactory();
using (var mqttClient = mqttFactory.CreateMqttClient())
{
@@ -120,21 +111,9 @@ public static class Client_Subscribe_Samples
// Create the subscribe options including several topics with different options.
// It is also possible to all of these topics using a dedicated call of _SubscribeAsync_ per topic.
var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder()
.WithTopicFilter(
f =>
{
f.WithTopic("mqttnet/samples/topic/1");
})
.WithTopicFilter(
f =>
{
f.WithTopic("mqttnet/samples/topic/2").WithNoLocal();
})
.WithTopicFilter(
f =>
{
f.WithTopic("mqttnet/samples/topic/3").WithRetainHandling(MqttRetainHandling.SendAtSubscribe);
})
.WithTopicTemplate(sampleTemplate.WithParameter("id", "1"))
.WithTopicTemplate(sampleTemplate.WithParameter("id", "2"), noLocal: true)
.WithTopicTemplate(sampleTemplate.WithParameter("id", "3"), retainHandling: MqttRetainHandling.SendAtSubscribe)
.Build();
var response = await mqttClient.SubscribeAsync(mqttSubscribeOptions, CancellationToken.None);
@@ -152,7 +131,7 @@ public static class Client_Subscribe_Samples
* This sample subscribes to a topic.
*/
var mqttFactory = new MqttFactory();
var mqttFactory = new MqttClientFactory();
using (var mqttClient = mqttFactory.CreateMqttClient())
{
@@ -160,13 +139,7 @@ public static class Client_Subscribe_Samples
await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None);
var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder()
.WithTopicFilter(
f =>
{
f.WithTopic("mqttnet/samples/topic/1");
})
.Build();
var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder().WithTopicTemplate(sampleTemplate.WithParameter("id", "1")).Build();
var response = await mqttClient.SubscribeAsync(mqttSubscribeOptions, CancellationToken.None);
@@ -176,4 +149,62 @@ public static class Client_Subscribe_Samples
response.DumpToConsole();
}
}
static void ConcurrentProcessingDisableAutoAcknowledge(CancellationToken shutdownToken, IMqttClient mqttClient)
{
/*
* This sample shows how to achieve concurrent processing and not have message AutoAcknowledged
* This to have a proper QoS1 (at-least-once) experience for what at least MQTT specification can provide
*/
mqttClient.ApplicationMessageReceivedAsync += ea =>
{
ea.AutoAcknowledge = false;
async Task ProcessAsync()
{
// DO YOUR WORK HERE!
await Task.Delay(1000, shutdownToken);
await ea.AcknowledgeAsync(shutdownToken);
// WARNING: If process failures are not transient the message will be retried on every restart of the client
// A failed message will not be dispatched again to the client as MQTT does not have a NACK packet to let
// the broker know processing failed
//
// Optionally: Use a framework like Polly to create a retry policy: https://github.com/App-vNext/Polly#retry
}
_ = Task.Run(ProcessAsync, shutdownToken);
return Task.CompletedTask;
};
}
static void ConcurrentProcessingWithLimit(CancellationToken shutdownToken, IMqttClient mqttClient)
{
/*
* This sample shows how to achieve concurrent processing, with:
* - a maximum concurrency limit based on Environment.ProcessorCount
*/
var concurrent = new SemaphoreSlim(Environment.ProcessorCount);
mqttClient.ApplicationMessageReceivedAsync += async ea =>
{
await concurrent.WaitAsync(shutdownToken).ConfigureAwait(false);
async Task ProcessAsync()
{
try
{
// DO YOUR WORK HERE!
await Task.Delay(1000, shutdownToken);
}
finally
{
concurrent.Release();
}
}
_ = Task.Run(ProcessAsync, shutdownToken);
};
}
}
+3 -3
View File
@@ -7,7 +7,7 @@
// ReSharper disable InconsistentNaming
using System.Text;
using MQTTnet.Diagnostics;
using MQTTnet.Diagnostics.Logger;
namespace MQTTnet.Samples.Diagnostics;
@@ -20,7 +20,7 @@ public static class Logger_Samples
* to other loggers like Microsoft logger or Serilog or log4net etc.
*/
var mqttFactory = new MqttFactory(new MyLogger());
var mqttFactory = new MqttClientFactory(new MyLogger());
var mqttClientOptions = mqttFactory.CreateClientOptionsBuilder()
.WithTcpServer("broker.hivemq.com")
@@ -62,7 +62,7 @@ public static class Logger_Samples
Console.Write(output);
};
var mqttFactory = new MqttFactory(mqttEventLogger);
var mqttFactory = new MqttClientFactory(mqttEventLogger);
var mqttClientOptions = mqttFactory.CreateClientOptionsBuilder()
.WithTcpServer("broker.hivemq.com")
@@ -6,7 +6,7 @@
// ReSharper disable UnusedMember.Global
// ReSharper disable InconsistentNaming
using MQTTnet.Diagnostics;
using MQTTnet.Diagnostics.PacketInspection;
namespace MQTTnet.Samples.Diagnostics;
@@ -17,19 +17,19 @@ public static class PackageInspection_Samples
/*
* This sample covers the inspection of outgoing packages from the client.
*/
var mqttFactory = new MqttFactory();
var mqttFactory = new MqttClientFactory();
using (var mqttClient = mqttFactory.CreateMqttClient())
{
var mqttClientOptions = mqttFactory.CreateClientOptionsBuilder()
.WithTcpServer("broker.hivemq.com")
.Build();
mqttClient.InspectPackage += OnInspectPackage;
mqttClient.InspectPacketAsync += OnInspectPacket;
await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None);
Console.WriteLine("MQTT client is connected.");
var mqttClientDisconnectOptions = mqttFactory.CreateClientDisconnectOptionsBuilder()
@@ -39,7 +39,7 @@ public static class PackageInspection_Samples
}
}
static Task OnInspectPackage(InspectMqttPacketEventArgs eventArgs)
static Task OnInspectPacket(InspectMqttPacketEventArgs eventArgs)
{
if (eventArgs.Direction == MqttPacketFlowDirection.Inbound)
{
+12 -7
View File
@@ -2,23 +2,28 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<DisableImplicitAspNetCoreAnalyzers>true</DisableImplicitAspNetCoreAnalyzers>
<EnableNETAnalyzers>false</EnableNETAnalyzers>
<EnforceCodeStyleInBuild>false</EnforceCodeStyleInBuild>
<NoWarn>1591;NETSDK1138</NoWarn>
<NoWarn>1591;NETSDK1138;NU1803;NU1901;NU1902</NoWarn>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<NuGetAuditMode>all</NuGetAuditMode>
<NuGetAudit>true</NuGetAudit>
<NuGetAuditLevel>low</NuGetAuditLevel>
<AnalysisLevel>latest-Recommended</AnalysisLevel>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Source\MQTTnet.AspnetCore\MQTTnet.AspNetCore.csproj" />
<ProjectReference Include="..\Source\MQTTnet.Extensions.ManagedClient\MQTTnet.Extensions.ManagedClient.csproj" />
<ProjectReference Include="..\Source\MQTTnet.Extensions.Rpc\MQTTnet.Extensions.Rpc.csproj" />
<ProjectReference Include="..\Source\MQTTnet.Extensions.WebSocket4Net\MQTTnet.Extensions.WebSocket4Net.csproj" />
<ProjectReference Include="..\Source\MQTTnet\MQTTnet.csproj" />
<ProjectReference Include="..\Source\MQTTnet.AspnetCore\MQTTnet.AspNetCore.csproj"/>
<ProjectReference Include="..\Source\MQTTnet.Extensions.Rpc\MQTTnet.Extensions.Rpc.csproj"/>
<ProjectReference Include="..\Source\MQTTnet.Extensions.TopicTemplate\MQTTnet.Extensions.TopicTemplate.csproj" />
<ProjectReference Include="..\Source\MQTTnet.Server\MQTTnet.Server.csproj"/>
<ProjectReference Include="..\Source\MQTTnet\MQTTnet.csproj"/>
</ItemGroup>
</Project>
@@ -1,52 +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.
// ReSharper disable UnusedType.Global
// ReSharper disable UnusedMember.Global
// ReSharper disable InconsistentNaming
using MQTTnet.Client;
using MQTTnet.Extensions.ManagedClient;
namespace MQTTnet.Samples.ManagedClient;
public sealed class Managed_Client_Simple_Samples
{
public static async Task Connect_Client()
{
/*
* This sample creates a simple managed MQTT client and connects to a public broker.
*
* The managed client extends the existing _MqttClient_. It adds the following features.
* - Reconnecting when connection is lost.
* - Storing pending messages in an internal queue so that an enqueue is possible while the client remains not connected.
*/
var mqttFactory = new MqttFactory();
using (var managedMqttClient = mqttFactory.CreateManagedMqttClient())
{
var mqttClientOptions = new MqttClientOptionsBuilder()
.WithTcpServer("broker.hivemq.com")
.Build();
var managedMqttClientOptions = new ManagedMqttClientOptionsBuilder()
.WithClientOptions(mqttClientOptions)
.Build();
await managedMqttClient.StartAsync(managedMqttClientOptions);
// The application message is not sent. It is stored in an internal queue and
// will be sent when the client is connected.
await managedMqttClient.EnqueueAsync("Topic", "Payload");
Console.WriteLine("The managed MQTT client is connected.");
// Wait until the queue is fully processed.
SpinWait.SpinUntil(() => managedMqttClient.PendingApplicationMessagesCount == 0, 10000);
Console.WriteLine($"Pending messages = {managedMqttClient.PendingApplicationMessagesCount}");
}
}
}
+18 -1
View File
@@ -4,7 +4,13 @@
using System.Reflection;
Console.WriteLine("Welcome to MQTTnet samples!");
Console.BackgroundColor = ConsoleColor.White;
Console.BackgroundColor = ConsoleColor.Red;
Console.Title = "Samples - MQTTnet";
Console.WriteLine("----------------------------------------------");
Console.WriteLine("- Welcome to MQTTnet samples! -");
Console.WriteLine("----------------------------------------------");
Console.ResetColor();
Console.WriteLine();
var sampleClasses = Assembly.GetExecutingAssembly().GetExportedTypes().OrderBy(c => c.Name).ToList();
@@ -16,7 +22,11 @@ foreach (var sampleClass in sampleClasses)
index++;
}
Console.WriteLine();
Console.ForegroundColor = ConsoleColor.Green;
Console.Write("Please choose sample class (press Enter to continue): ");
Console.ResetColor();
var input = Console.ReadLine();
var selectedIndex = int.Parse(input ?? "0");
var selectedSampleClass = sampleClasses[selectedIndex];
@@ -29,12 +39,19 @@ foreach (var sampleMethod in sampleMethods)
index++;
}
Console.WriteLine();
Console.ForegroundColor = ConsoleColor.Green;
Console.Write("Please choose sample (press Enter to continue): ");
Console.ResetColor();
input = Console.ReadLine();
selectedIndex = int.Parse(input ?? "0");
var selectedSampleMethod = sampleMethods[selectedIndex];
Console.WriteLine();
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine("Executing sample...");
Console.ResetColor();
Console.WriteLine();
try
+8 -9
View File
@@ -6,7 +6,6 @@
// ReSharper disable UnusedMember.Global
// ReSharper disable InconsistentNaming
using MQTTnet.Client;
using MQTTnet.Extensions.Rpc;
using MQTTnet.Protocol;
@@ -19,14 +18,14 @@ public static class RpcClient_Samples
* This is done via defining a pattern which uses the topic to correlate the request and the response.
* From client usage it is possible to define a timeout.
*/
public static async Task Send_Request()
{
var mqttFactory = new MqttFactory();
var mqttFactory = new MqttClientFactory();
// The RPC client is an addon for the existing client. So we need a regular client
// which is wrapped later.
using (var mqttClient = mqttFactory.CreateMqttClient())
{
var mqttClientOptions = new MqttClientOptionsBuilder()
@@ -34,23 +33,23 @@ public static class RpcClient_Samples
.Build();
await mqttClient.ConnectAsync(mqttClientOptions);
using (var mqttRpcClient = mqttFactory.CreateMqttRpcClient(mqttClient))
{
// Access to a fully featured application message is not supported for RPC calls!
// The method will throw an exception when the response was not received in time.
await mqttRpcClient.ExecuteAsync(TimeSpan.FromSeconds(2), "ping", "", MqttQualityOfServiceLevel.AtMostOnce);
}
Console.WriteLine("The RPC call was successful.");
}
}
/*
* The device must respond to the request using the correct topic. The following C code shows how a
* smart device like an ESP8266 must respond to the above sample.
*
// If using the MQTT client PubSubClient it must be ensured
// If using the MQTT client PubSubClient it must be ensured
// that the request topic for each method is subscribed like the following.
mqttClient.subscribe("MQTTnet.RPC/+/ping");
mqttClient.subscribe("MQTTnet.RPC/+/do_something");
@@ -17,7 +17,7 @@ using MQTTnet.Server;
namespace MQTTnet.Samples.Server;
public static class Server_Asp_Net_Samples
public static class Server_ASP_NET_Samples
{
public static Task Start_Server_With_WebSockets_Support()
{
@@ -51,7 +51,7 @@ public static class Server_Asp_Net_Samples
{
// Inject other services via constructor.
}
public Task OnClientConnected(ClientConnectedEventArgs eventArgs)
{
Console.WriteLine($"Client '{eventArgs.ClientId}' connected.");
+7 -8
View File
@@ -13,7 +13,6 @@ namespace MQTTnet.Samples.Server;
// ReSharper disable InconsistentNaming
// ReSharper disable EmptyConstructor
// ReSharper disable MemberCanBeMadeStatic.Local
public static class Server_Diagnostics_Samples
{
public static async Task Get_Notified_When_Client_Received_Message()
@@ -22,26 +21,26 @@ public static class Server_Diagnostics_Samples
* This sample starts a MQTT server and attaches an event which gets fired whenever
* a client received and acknowledged a PUBLISH packet.
*/
var mqttFactory = new MqttFactory();
var mqttServerFactory = new MqttServerFactory();
var mqttServerOptions = new MqttServerOptionsBuilder().WithDefaultEndpoint().Build();
using (var mqttServer = mqttFactory.CreateMqttServer(mqttServerOptions))
using (var mqttServer = mqttServerFactory.CreateMqttServer(mqttServerOptions))
{
// Attach the event handler.
mqttServer.ClientAcknowledgedPublishPacketAsync += e =>
{
Console.WriteLine($"Client '{e.ClientId}' acknowledged packet {e.PublishPacket.PacketIdentifier} with topic '{e.PublishPacket.Topic}'");
// It is also possible to read additional data from the client response. This requires casting the response packet.
var qos1AcknowledgePacket = e.AcknowledgePacket as MqttPubAckPacket;
Console.WriteLine($"QoS 1 reason code: {qos1AcknowledgePacket?.ReasonCode}");
var qos2AcknowledgePacket = e.AcknowledgePacket as MqttPubCompPacket;
Console.WriteLine($"QoS 2 reason code: {qos1AcknowledgePacket?.ReasonCode}");
return CompletedTask.Instance;
};
await mqttServer.StartAsync();
Console.WriteLine("Press Enter to exit.");
@@ -22,10 +22,10 @@ public static class Server_Intercepting_Samples
* Please see _Server_Simple_Samples_ for more details on how to start a server.
*/
var mqttFactory = new MqttFactory();
var mqttServerFactory = new MqttServerFactory();
var mqttServerOptions = new MqttServerOptionsBuilder().WithDefaultEndpoint().Build();
using (var mqttServer = mqttFactory.CreateMqttServer(mqttServerOptions))
using (var mqttServer = mqttServerFactory.CreateMqttServer(mqttServerOptions))
{
mqttServer.InterceptingPublishAsync += args =>
{
@@ -6,7 +6,11 @@
// ReSharper disable UnusedMember.Global
// ReSharper disable InconsistentNaming
using System.Buffers;
using System.Text.Json;
using MQTTnet.Packets;
using MQTTnet.Protocol;
using MQTTnet.Server;
namespace MQTTnet.Samples.Server;
@@ -20,19 +24,22 @@ public static class Server_Retained_Messages_Samples
var storePath = Path.Combine(Path.GetTempPath(), "RetainedMessages.json");
var mqttFactory = new MqttFactory();
var mqttServerFactory = new MqttServerFactory();
// Due to security reasons the "default" endpoint (which is unencrypted) is not enabled by default!
var mqttServerOptions = mqttFactory.CreateServerOptionsBuilder().WithDefaultEndpoint().Build();
var mqttServerOptions = mqttServerFactory.CreateServerOptionsBuilder().WithDefaultEndpoint().Build();
using (var server = mqttFactory.CreateMqttServer(mqttServerOptions))
using (var server = mqttServerFactory.CreateMqttServer(mqttServerOptions))
{
// Make sure that the server will load the retained messages.
server.LoadingRetainedMessageAsync += async eventArgs =>
{
try
{
eventArgs.LoadedRetainedMessages = await JsonSerializer.DeserializeAsync<List<MqttApplicationMessage>>(File.OpenRead(storePath));
var models = await JsonSerializer.DeserializeAsync<List<MqttRetainedMessageModel>>(File.OpenRead(storePath)) ?? new List<MqttRetainedMessageModel>();
var retainedMessages = models.Select(m => m.ToApplicationMessage()).ToList();
eventArgs.LoadedRetainedMessages = retainedMessages;
Console.WriteLine("Retained messages loaded.");
}
catch (FileNotFoundException)
@@ -56,7 +63,9 @@ public static class Server_Retained_Messages_Samples
// used to write all retained messages to dedicated files etc. Then all files must be loaded and a full list
// of retained messages must be provided in the loaded event.
var buffer = JsonSerializer.SerializeToUtf8Bytes(eventArgs.StoredRetainedMessages);
var models = eventArgs.StoredRetainedMessages.Select(MqttRetainedMessageModel.Create);
var buffer = JsonSerializer.SerializeToUtf8Bytes(models);
await File.WriteAllBytesAsync(storePath, buffer);
Console.WriteLine("Retained messages saved.");
}
@@ -79,4 +88,59 @@ public static class Server_Retained_Messages_Samples
Console.ReadLine();
}
}
sealed class MqttRetainedMessageModel
{
public string? ContentType { get; set; }
public byte[]? CorrelationData { get; set; }
public byte[]? Payload { get; set; }
public MqttPayloadFormatIndicator PayloadFormatIndicator { get; set; }
public MqttQualityOfServiceLevel QualityOfServiceLevel { get; set; }
public string? ResponseTopic { get; set; }
public string? Topic { get; set; }
public List<MqttUserProperty>? UserProperties { get; set; }
public static MqttRetainedMessageModel Create(MqttApplicationMessage message)
{
if (message == null)
{
throw new ArgumentNullException(nameof(message));
}
return new MqttRetainedMessageModel
{
Topic = message.Topic,
// Create a copy of the buffer from the payload segment because
// it cannot be serialized and deserialized with the JSON serializer.
Payload = message.Payload.ToArray(),
UserProperties = message.UserProperties,
ResponseTopic = message.ResponseTopic,
CorrelationData = message.CorrelationData,
ContentType = message.ContentType,
PayloadFormatIndicator = message.PayloadFormatIndicator,
QualityOfServiceLevel = message.QualityOfServiceLevel
// Other properties like "Retain" are not if interest in the storage.
// That's why a custom model makes sense.
};
}
public MqttApplicationMessage ToApplicationMessage()
{
return new MqttApplicationMessage
{
Topic = Topic,
PayloadSegment = new ArraySegment<byte>(Payload ?? Array.Empty<byte>()),
PayloadFormatIndicator = PayloadFormatIndicator,
ResponseTopic = ResponseTopic,
CorrelationData = CorrelationData,
ContentType = ContentType,
UserProperties = UserProperties,
QualityOfServiceLevel = QualityOfServiceLevel,
Dup = false,
Retain = true
};
}
}
}
+12 -12
View File
@@ -6,7 +6,7 @@
// ReSharper disable UnusedMember.Global
// ReSharper disable InconsistentNaming
using MQTTnet.Diagnostics;
using MQTTnet.Diagnostics.Logger;
using MQTTnet.Protocol;
using MQTTnet.Server;
@@ -64,7 +64,7 @@ public static class Server_Simple_Samples
* This sample starts a simple MQTT server which will accept any TCP connection.
*/
var mqttFactory = new MqttFactory();
var mqttServerFactory = new MqttServerFactory();
// The port for the default endpoint is 1883.
// The default endpoint is NOT encrypted!
@@ -77,7 +77,7 @@ public static class Server_Simple_Samples
// .WithDefaultEndpointPort(1234)
// .Build();
using (var mqttServer = mqttFactory.CreateMqttServer(mqttServerOptions))
using (var mqttServer = mqttServerFactory.CreateMqttServer(mqttServerOptions))
{
await mqttServer.StartAsync();
@@ -99,11 +99,11 @@ public static class Server_Simple_Samples
* See sample "Run_Minimal_Server" for more details.
*/
var mqttFactory = new MqttFactory(new ConsoleLogger());
var mqttServerFactory = new MqttServerFactory(new ConsoleLogger());
var mqttServerOptions = new MqttServerOptionsBuilder().WithDefaultEndpoint().Build();
using (var mqttServer = mqttFactory.CreateMqttServer(mqttServerOptions))
using (var mqttServer = mqttServerFactory.CreateMqttServer(mqttServerOptions))
{
await mqttServer.StartAsync();
@@ -123,13 +123,13 @@ public static class Server_Simple_Samples
* See _Run_Minimal_Server_ for more information.
*/
var mqttFactory = new MqttFactory();
var mqttServerFactory = new MqttServerFactory();
var mqttServerOptions = new MqttServerOptionsBuilder().WithDefaultEndpoint().Build();
using (var mqttServer = mqttFactory.CreateMqttServer(mqttServerOptions))
using (var mqttServer = mqttServerFactory.CreateMqttServer(mqttServerOptions))
{
// Setup connection validation before starting the server so that there is
// Setup connection validation before starting the server so that there is
// no change to connect without valid credentials.
mqttServer.ValidatingConnectionAsync += e =>
{
@@ -162,11 +162,11 @@ public static class Server_Simple_Samples
static async Task<MqttServer> StartMqttServer()
{
var mqttFactory = new MqttFactory();
var mqttServerFactory = new MqttServerFactory();
// Due to security reasons the "default" endpoint (which is unencrypted) is not enabled by default!
var mqttServerOptions = mqttFactory.CreateServerOptionsBuilder().WithDefaultEndpoint().Build();
var server = mqttFactory.CreateMqttServer(mqttServerOptions);
var mqttServerOptions = mqttServerFactory.CreateServerOptionsBuilder().WithDefaultEndpoint().Build();
var server = mqttServerFactory.CreateMqttServer(mqttServerOptions);
await server.StartAsync();
return server;
}
@@ -216,4 +216,4 @@ public static class Server_Simple_Samples
}
}
}
}
}
+75
View File
@@ -0,0 +1,75 @@
// 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.
// ReSharper disable UnusedType.Global
// ReSharper disable UnusedMember.Global
// ReSharper disable InconsistentNaming
using System.Net;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using MQTTnet.Server;
namespace MQTTnet.Samples.Server;
public static class Server_TLS_Samples
{
public static async Task Run_Server_With_Self_Signed_Certificate()
{
/*
* This sample starts a simple MQTT server which will accept any TCP connection.
* It also has an encrypted connection using a self signed TLS certificate.
*
* See sample "Run_Minimal_Server" for more details.
*/
var mqttServerFactory = new MqttServerFactory();
// This certificate is self signed so that
var certificate = CreateSelfSignedCertificate("1.3.6.1.5.5.7.3.1");
var mqttServerOptions = new MqttServerOptionsBuilder().WithEncryptionCertificate(certificate).WithEncryptedEndpoint().Build();
using (var mqttServer = mqttServerFactory.CreateMqttServer(mqttServerOptions))
{
await mqttServer.StartAsync();
Console.WriteLine("Press Enter to exit.");
Console.ReadLine();
// Stop and dispose the MQTT server if it is no longer needed!
await mqttServer.StopAsync();
}
}
static X509Certificate2 CreateSelfSignedCertificate(string oid)
{
var sanBuilder = new SubjectAlternativeNameBuilder();
sanBuilder.AddIpAddress(IPAddress.Loopback);
sanBuilder.AddIpAddress(IPAddress.IPv6Loopback);
sanBuilder.AddDnsName("localhost");
using (var rsa = RSA.Create())
{
var certRequest = new CertificateRequest("CN=localhost", rsa, HashAlgorithmName.SHA512, RSASignaturePadding.Pkcs1);
certRequest.CertificateExtensions.Add(
new X509KeyUsageExtension(X509KeyUsageFlags.DataEncipherment | X509KeyUsageFlags.KeyEncipherment | X509KeyUsageFlags.DigitalSignature, false));
certRequest.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(new OidCollection { new(oid) }, false));
certRequest.CertificateExtensions.Add(sanBuilder.Build());
using (var certificate = certRequest.CreateSelfSigned(DateTimeOffset.Now.AddMinutes(-10), DateTimeOffset.Now.AddMinutes(10)))
{
var pfxCertificate = new X509Certificate2(
certificate.Export(X509ContentType.Pfx),
(string)null!,
X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.Exportable);
return pfxCertificate;
}
}
}
}
@@ -1,33 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<IsPackable>false</IsPackable>
<DisableImplicitAspNetCoreAnalyzers>true</DisableImplicitAspNetCoreAnalyzers>
<EnableNETAnalyzers>false</EnableNETAnalyzers>
<EnforceCodeStyleInBuild>false</EnforceCodeStyleInBuild>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.10" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.10" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'netcoreapp3.1' ">
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.2.7" />
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' != 'netcoreapp3.1' ">
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.1.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MQTTnet\MQTTnet.csproj" />
<ProjectReference Include="..\MQTTnet.AspnetCore\MQTTnet.AspNetCore.csproj" />
<ProjectReference Include="..\MQTTnet.Tests\MQTTnet.Tests.csproj" />
</ItemGroup>
</Project>
@@ -1,50 +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;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Connections;
using MQTTnet.Adapter;
using MQTTnet.Diagnostics;
using MQTTnet.Formatter;
using MQTTnet.Server;
namespace MQTTnet.AspNetCore.Tests.Mockups
{
public sealed class ConnectionHandlerMockup : IMqttServerAdapter
{
public Func<IMqttChannelAdapter, Task> ClientHandler { get; set; }
public TaskCompletionSource<MqttConnectionContext> Context { get; } = new TaskCompletionSource<MqttConnectionContext>();
public void Dispose()
{
}
public async Task OnConnectedAsync(ConnectionContext connection)
{
try
{
var formatter = new MqttPacketFormatterAdapter(new MqttBufferWriter(4096, 65535));
var context = new MqttConnectionContext(formatter, connection);
Context.TrySetResult(context);
await ClientHandler(context);
}
catch (Exception ex)
{
Context.TrySetException(ex);
}
}
public Task StartAsync(MqttServerOptions options, IMqttNetLogger logger)
{
return Task.CompletedTask;
}
public Task StopAsync()
{
return Task.CompletedTask;
}
}
}
@@ -1,51 +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.
#if NETCOREAPP3_1
using System.Buffers;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MQTTnet.AspNetCore;
using MQTTnet.Formatter;
using MQTTnet.Packets;
namespace MQTTnet.AspNetCore.Tests
{
[TestClass]
public sealed class ReaderExtensionsTest
{
[TestMethod]
public void TestTryDeserialize()
{
var serializer = new MqttPacketFormatterAdapter(MqttProtocolVersion.V311, new MqttBufferWriter(4096, 65535));
var buffer = serializer.Encode(new MqttPublishPacket {Topic = "a", Payload = new byte[5]}).Join();
var sequence = new ReadOnlySequence<byte>(buffer.Array, buffer.Offset, buffer.Count);
var part = sequence;
MqttPacket packet;
var consumed = part.Start;
var observed = part.Start;
var result = false;
var read = 0;
part = sequence.Slice(sequence.Start, 0); // empty message should fail
result = serializer.TryDecode(part, out packet, out consumed, out observed, out read);
Assert.IsFalse(result);
part = sequence.Slice(sequence.Start, 1); // partial fixed header should fail
result = serializer.TryDecode(part, out packet, out consumed, out observed, out read);
Assert.IsFalse(result);
part = sequence.Slice(sequence.Start, 4); // partial body should fail
result = serializer.TryDecode(part, out packet, out consumed, out observed, out read);
Assert.IsFalse(result);
part = sequence; // complete msg should work
result = serializer.TryDecode(part, out packet, out consumed, out observed, out read);
Assert.IsTrue(result);
}
}
}
#endif
@@ -1,22 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable>
<DisableImplicitAspNetCoreAnalyzers>true</DisableImplicitAspNetCoreAnalyzers>
<EnableNETAnalyzers>false</EnableNETAnalyzers>
<EnforceCodeStyleInBuild>false</EnforceCodeStyleInBuild>
<NoWarn>1591;NETSDK1138</NoWarn>
<NoWarn>1591;NETSDK1138;NU1803;NU1901;NU1902</NoWarn>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<NuGetAuditMode>all</NuGetAuditMode>
<NuGetAudit>true</NuGetAudit>
<NuGetAuditLevel>low</NuGetAuditLevel>
<AnalysisLevel>latest-Recommended</AnalysisLevel>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Source\MQTTnet.AspnetCore\MQTTnet.AspNetCore.csproj" />
<ProjectReference Include="..\..\Source\MQTTnet.AspnetCore\MQTTnet.AspNetCore.csproj"/>
<ProjectReference Include="..\..\Source\MQTTnet.Server\MQTTnet.Server.csproj"/>
<ProjectReference Include="..\MQTTnet\MQTTnet.csproj"/>
</ItemGroup>
<ItemGroup>
<Folder Include="wwwroot\" />
<Folder Include="wwwroot\"/>
</ItemGroup>
</Project>
+1 -1
View File
@@ -17,7 +17,7 @@
clientId: 'mqttnet_web_client'
}
const client = mqtt.connect('ws://localhost:5049/mqtt')
const client = mqtt.connect('ws://localhost:5000/mqtt')
client.on('connect', function () {
console.log('Connected')
+2 -5
View File
@@ -30,10 +30,7 @@ app.UseAuthorization();
app.MapRazorPages();
// Setup MQTT stuff.
app.UseEndpoints(endpoints =>
{
endpoints.MapMqtt("/mqtt");
});
app.MapMqtt("/mqtt");
app.UseMqttServer(server =>
{
@@ -42,7 +39,7 @@ app.UseMqttServer(server =>
_ = Task.Run(async () =>
{
var mqttApplicationMessage = new MqttApplicationMessageBuilder()
.WithPayload($"Test application message from MQTTnet server.")
.WithPayload("Test application message from MQTTnet server.")
.WithTopic("message")
.Build();
@@ -7,15 +7,17 @@ using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using MQTTnet.Server;
namespace MQTTnet.AspNetCore
namespace MQTTnet.AspNetCore;
public static class ApplicationBuilderExtensions
{
public static class ApplicationBuilderExtensions
[Obsolete(
"This class is obsolete and will be removed in a future version. The recommended alternative is to use MapMqtt inside Microsoft.AspNetCore.Builder.UseEndpoints(...).")]
public static IApplicationBuilder UseMqttEndpoint(this IApplicationBuilder app, string path = "/mqtt")
{
[Obsolete("This class is obsolete and will be removed in a future version. The recommended alternative is to use MapMqtt inside Microsoft.AspNetCore.Builder.UseEndpoints(...).")]
public static IApplicationBuilder UseMqttEndpoint(this IApplicationBuilder app, string path = "/mqtt")
{
app.UseWebSockets();
app.Use(async (context, next) =>
app.UseWebSockets();
app.Use(
async (context, next) =>
{
if (!context.WebSockets.IsWebSocketRequest || context.Request.Path != path)
{
@@ -37,16 +39,15 @@ namespace MQTTnet.AspNetCore
}
});
return app;
}
public static IApplicationBuilder UseMqttServer(this IApplicationBuilder app, Action<MqttServer> configure)
{
var server = app.ApplicationServices.GetRequiredService<MqttServer>();
configure(server);
return app;
}
return app;
}
}
public static IApplicationBuilder UseMqttServer(this IApplicationBuilder app, Action<MqttServer> configure)
{
var server = app.ApplicationServices.GetRequiredService<MqttServer>();
configure(server);
return app;
}
}
@@ -2,18 +2,17 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using MQTTnet.Server;
using System;
using MQTTnet.Server;
namespace MQTTnet.AspNetCore
namespace MQTTnet.AspNetCore;
public sealed class AspNetMqttServerOptionsBuilder : MqttServerOptionsBuilder
{
public sealed class AspNetMqttServerOptionsBuilder : MqttServerOptionsBuilder
public AspNetMqttServerOptionsBuilder(IServiceProvider serviceProvider)
{
public AspNetMqttServerOptionsBuilder(IServiceProvider serviceProvider)
{
ServiceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
}
public IServiceProvider ServiceProvider { get; }
ServiceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
}
}
public IServiceProvider ServiceProvider { get; }
}
@@ -0,0 +1,21 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Runtime.InteropServices;
namespace MQTTnet.AspNetCore;
public static class BufferExtensions
{
public static ArraySegment<byte> GetArray(this ReadOnlyMemory<byte> memory)
{
if (!MemoryMarshal.TryGetArray(memory, out var result))
{
throw new InvalidOperationException("Buffer backed by array was expected");
}
return result;
}
}
@@ -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;
using System.Runtime.InteropServices;
namespace MQTTnet.AspNetCore.Client.Tcp
{
public static class BufferExtensions
{
public static ArraySegment<byte> GetArray(this Memory<byte> memory)
{
return ((ReadOnlyMemory<byte>)memory).GetArray();
}
public static ArraySegment<byte> GetArray(this ReadOnlyMemory<byte> memory)
{
if (!MemoryMarshal.TryGetArray(memory, out var result))
{
throw new InvalidOperationException("Buffer backed by array was expected");
}
return result;
}
}
}
@@ -1,45 +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.IO.Pipelines;
namespace MQTTnet.AspNetCore.Client.Tcp
{
public class DuplexPipe : IDuplexPipe
{
public DuplexPipe(PipeReader reader, PipeWriter writer)
{
Input = reader;
Output = writer;
}
public PipeReader Input { get; }
public PipeWriter Output { get; }
public static DuplexPipePair CreateConnectionPair(PipeOptions inputOptions, PipeOptions outputOptions)
{
var input = new Pipe(inputOptions);
var output = new Pipe(outputOptions);
var transportToApplication = new DuplexPipe(output.Reader, input.Writer);
var applicationToTransport = new DuplexPipe(input.Reader, output.Writer);
return new DuplexPipePair(applicationToTransport, transportToApplication);
}
// This class exists to work around issues with value tuple on .NET Framework
public readonly struct DuplexPipePair
{
public IDuplexPipe Transport { get; }
public IDuplexPipe Application { get; }
public DuplexPipePair(IDuplexPipe transport, IDuplexPipe application)
{
Transport = transport;
Application = application;
}
}
}
}
@@ -1,74 +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;
using System.Diagnostics;
using System.IO.Pipelines;
using System.Net.Sockets;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
namespace MQTTnet.AspNetCore.Client.Tcp
{
public class SocketAwaitable : ICriticalNotifyCompletion
{
private static readonly Action _callbackCompleted = () => { };
private readonly PipeScheduler _ioScheduler;
private Action _callback;
private int _bytesTransferred;
private SocketError _error;
public SocketAwaitable(PipeScheduler ioScheduler)
{
_ioScheduler = ioScheduler;
}
public bool IsCompleted => ReferenceEquals(_callback, _callbackCompleted);
public SocketAwaitable GetAwaiter() => this;
public int GetResult()
{
Debug.Assert(ReferenceEquals(_callback, _callbackCompleted));
_callback = null;
if (_error != SocketError.Success)
{
throw new SocketException((int)_error);
}
return _bytesTransferred;
}
public void OnCompleted(Action continuation)
{
if (ReferenceEquals(_callback, _callbackCompleted) ||
ReferenceEquals(Interlocked.CompareExchange(ref _callback, continuation, null), _callbackCompleted))
{
Task.Run(continuation);
}
}
public void UnsafeOnCompleted(Action continuation)
{
OnCompleted(continuation);
}
public void Complete(int bytesTransferred, SocketError socketError)
{
_error = socketError;
_bytesTransferred = bytesTransferred;
var continuation = Interlocked.Exchange(ref _callback, _callbackCompleted);
if (continuation != null)
{
_ioScheduler.Schedule(state => ((Action)state)(), continuation);
}
}
}
}
@@ -1,41 +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;
using System.IO.Pipelines;
using System.Net.Sockets;
namespace MQTTnet.AspNetCore.Client.Tcp
{
public class SocketReceiver
{
private readonly Socket _socket;
private readonly SocketAsyncEventArgs _eventArgs = new SocketAsyncEventArgs();
private readonly SocketAwaitable _awaitable;
public SocketReceiver(Socket socket, PipeScheduler scheduler)
{
_socket = socket;
_awaitable = new SocketAwaitable(scheduler);
_eventArgs.UserToken = _awaitable;
_eventArgs.Completed += (_, e) => ((SocketAwaitable)e.UserToken).Complete(e.BytesTransferred, e.SocketError);
}
public SocketAwaitable ReceiveAsync(Memory<byte> buffer)
{
#if NETCOREAPP2_1
_eventArgs.SetBuffer(buffer);
#else
var segment = buffer.GetArray();
_eventArgs.SetBuffer(segment.Array, segment.Offset, segment.Count);
#endif
if (!_socket.ReceiveAsync(_eventArgs))
{
_awaitable.Complete(_eventArgs.BytesTransferred, _eventArgs.SocketError);
}
return _awaitable;
}
}
}
@@ -1,105 +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;
using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO.Pipelines;
using System.Net.Sockets;
#if NETCOREAPP2_1
using System.Runtime.InteropServices;
#endif
namespace MQTTnet.AspNetCore.Client.Tcp
{
public class SocketSender
{
private readonly Socket _socket;
private readonly SocketAsyncEventArgs _eventArgs = new SocketAsyncEventArgs();
private readonly SocketAwaitable _awaitable;
private List<ArraySegment<byte>> _bufferList;
public SocketSender(Socket socket, PipeScheduler scheduler)
{
_socket = socket;
_awaitable = new SocketAwaitable(scheduler);
_eventArgs.UserToken = _awaitable;
_eventArgs.Completed += (_, e) => ((SocketAwaitable)e.UserToken).Complete(e.BytesTransferred, e.SocketError);
}
public SocketAwaitable SendAsync(in ReadOnlySequence<byte> buffers)
{
if (buffers.IsSingleSegment)
{
return SendAsync(buffers.First);
}
#if NETCOREAPP2_1
if (!_eventArgs.MemoryBuffer.Equals(Memory<byte>.Empty))
#else
if (_eventArgs.Buffer != null)
#endif
{
_eventArgs.SetBuffer(null, 0, 0);
}
_eventArgs.BufferList = GetBufferList(buffers);
if (!_socket.SendAsync(_eventArgs))
{
_awaitable.Complete(_eventArgs.BytesTransferred, _eventArgs.SocketError);
}
return _awaitable;
}
private SocketAwaitable SendAsync(ReadOnlyMemory<byte> memory)
{
// The BufferList getter is much less expensive then the setter.
if (_eventArgs.BufferList != null)
{
_eventArgs.BufferList = null;
}
#if NETCOREAPP2_1
_eventArgs.SetBuffer(MemoryMarshal.AsMemory(memory));
#else
var segment = memory.GetArray();
_eventArgs.SetBuffer(segment.Array, segment.Offset, segment.Count);
#endif
if (!_socket.SendAsync(_eventArgs))
{
_awaitable.Complete(_eventArgs.BytesTransferred, _eventArgs.SocketError);
}
return _awaitable;
}
private List<ArraySegment<byte>> GetBufferList(in ReadOnlySequence<byte> buffer)
{
Debug.Assert(!buffer.IsEmpty);
Debug.Assert(!buffer.IsSingleSegment);
if (_bufferList == null)
{
_bufferList = new List<ArraySegment<byte>>();
}
else
{
// Buffers are pooled, so it's OK to root them until the next multi-buffer write.
_bufferList.Clear();
}
foreach (var b in buffer)
{
_bufferList.Add(b.GetArray());
}
return _bufferList;
}
}
}
@@ -1,272 +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 Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Http.Features;
using MQTTnet.Exceptions;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Pipelines;
using System.Net;
using System.Net.Sockets;
using System.Threading.Tasks;
namespace MQTTnet.AspNetCore.Client.Tcp
{
public class TcpConnection : ConnectionContext
{
private volatile bool _aborted;
private readonly EndPoint _endPoint;
private SocketSender _sender;
private SocketReceiver _receiver;
private Socket _socket;
private IDuplexPipe _application;
public bool IsConnected { get; private set; }
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 TcpConnection(EndPoint endPoint)
{
_endPoint = endPoint;
}
public TcpConnection(Socket socket)
{
_socket = socket;
_endPoint = socket.RemoteEndPoint;
_sender = new SocketSender(_socket, PipeScheduler.ThreadPool);
_receiver = new SocketReceiver(_socket, PipeScheduler.ThreadPool);
}
#if NETCOREAPP3_1_OR_GREATER
public override ValueTask DisposeAsync()
#else
public Task DisposeAsync()
#endif
{
IsConnected = false;
Transport?.Output.Complete();
Transport?.Input.Complete();
_socket?.Dispose();
#if NETCOREAPP3_1_OR_GREATER
return base.DisposeAsync();
}
#else
return Task.CompletedTask;
}
#endif
public async Task StartAsync()
{
if (_socket == null)
{
_socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
_sender = new SocketSender(_socket, PipeScheduler.ThreadPool);
_receiver = new SocketReceiver(_socket, PipeScheduler.ThreadPool);
await _socket.ConnectAsync(_endPoint);
}
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
Transport = pair.Transport;
_application = pair.Application;
_ = ExecuteAsync();
IsConnected = true;
}
private async Task ExecuteAsync()
{
Exception sendError = null;
try
{
// Spawn send and receive logic
var receiveTask = DoReceive();
var sendTask = DoSend();
// If the sending task completes then close the receive
// We don't need to do this in the other direction because the kestrel
// will trigger the output closing once the input is complete.
if (await Task.WhenAny(receiveTask, sendTask) == sendTask)
{
// Tell the reader it's being aborted
_socket.Dispose();
}
// Now wait for both to complete
await receiveTask;
sendError = await sendTask;
// Dispose the socket(should noop if already called)
_socket.Dispose();
}
catch (Exception ex)
{
Console.WriteLine($"Unexpected exception in {nameof(TcpConnection)}.{nameof(StartAsync)}: " + ex);
}
finally
{
// Complete the output after disposing the socket
_application.Input.Complete(sendError);
}
}
private async Task DoReceive()
{
Exception error = null;
try
{
await ProcessReceives();
}
catch (SocketException ex) when (ex.SocketErrorCode == SocketError.ConnectionReset)
{
error = new MqttCommunicationException(ex);
}
catch (SocketException ex) when (ex.SocketErrorCode == SocketError.OperationAborted ||
ex.SocketErrorCode == SocketError.ConnectionAborted ||
ex.SocketErrorCode == SocketError.Interrupted ||
ex.SocketErrorCode == SocketError.InvalidArgument)
{
if (!_aborted)
{
// Calling Dispose after ReceiveAsync can cause an "InvalidArgument" error on *nix.
error = ConnectionAborted();
}
}
catch (ObjectDisposedException)
{
if (!_aborted)
{
error = ConnectionAborted();
}
}
catch (IOException ex)
{
error = ex;
}
catch (Exception ex)
{
error = new IOException(ex.Message, ex);
}
finally
{
if (_aborted)
{
error = error ?? ConnectionAborted();
}
_application.Output.Complete(error);
}
}
private async Task ProcessReceives()
{
while (true)
{
// Ensure we have some reasonable amount of buffer space
var buffer = _application.Output.GetMemory();
var bytesReceived = await _receiver.ReceiveAsync(buffer);
if (bytesReceived == 0)
{
// FIN
break;
}
_application.Output.Advance(bytesReceived);
var flushTask = _application.Output.FlushAsync();
if (!flushTask.IsCompleted)
{
await flushTask;
}
var result = flushTask.GetAwaiter().GetResult();
if (result.IsCompleted)
{
// Pipe consumer is shut down, do we stop writing
break;
}
}
}
private Exception ConnectionAborted()
{
return new MqttCommunicationException("Connection Aborted");
}
private async Task<Exception> DoSend()
{
Exception error = null;
try
{
await ProcessSends();
}
catch (SocketException ex) when (ex.SocketErrorCode == SocketError.OperationAborted)
{
}
catch (ObjectDisposedException)
{
}
catch (IOException ex)
{
error = ex;
}
catch (Exception ex)
{
error = new IOException(ex.Message, ex);
}
finally
{
_aborted = true;
_socket.Shutdown(SocketShutdown.Both);
}
return error;
}
private async Task ProcessSends()
{
while (true)
{
// Wait for data to write from the pipe producer
var result = await _application.Input.ReadAsync();
var buffer = result.Buffer;
if (result.IsCanceled)
{
break;
}
var end = buffer.End;
var isCompleted = result.IsCompleted;
if (!buffer.IsEmpty)
{
await _sender.SendAsync(buffer);
}
_application.Input.AdvanceTo(end);
if (isCompleted)
{
break;
}
}
}
}
}
@@ -1,29 +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 Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Connections;
#if NETCOREAPP3_1
using System;
#endif
namespace MQTTnet.AspNetCore
{
public static class ConnectionRouteBuilderExtensions
{
#if NETCOREAPP3_1
[Obsolete("This class is obsolete and will be removed in a future version. The recommended alternative is to use MapMqtt inside Microsoft.AspNetCore.Builder.UseEndpoints(...).")]
#endif
#if NETCOREAPP3_1 || NETCOREAPP2_1 || NETSTANDARD
public static void MapMqtt(this ConnectionsRouteBuilder connection, PathString path)
{
connection.MapConnectionHandler<MqttConnectionHandler>(path, options =>
{
options.WebSockets.SubProtocolSelector = MqttSubProtocolSelector.SelectSubProtocol;
});
}
#endif
}
}
+44
View File
@@ -0,0 +1,44 @@
// 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.IO.Pipelines;
namespace MQTTnet.AspNetCore;
public class DuplexPipe : IDuplexPipe
{
public DuplexPipe(PipeReader reader, PipeWriter writer)
{
Input = reader;
Output = writer;
}
public PipeReader Input { get; }
public PipeWriter Output { get; }
public static DuplexPipePair CreateConnectionPair(PipeOptions inputOptions, PipeOptions outputOptions)
{
var input = new Pipe(inputOptions);
var output = new Pipe(outputOptions);
var transportToApplication = new DuplexPipe(output.Reader, input.Writer);
var applicationToTransport = new DuplexPipe(input.Reader, output.Writer);
return new DuplexPipePair(applicationToTransport, transportToApplication);
}
// This class exists to work around issues with value tuple on .NET Framework
public readonly struct DuplexPipePair
{
public IDuplexPipe Transport { get; }
public IDuplexPipe Application { get; }
public DuplexPipePair(IDuplexPipe transport, IDuplexPipe application)
{
Transport = transport;
Application = application;
}
}
}
@@ -2,9 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#if NETCOREAPP3_1_OR_GREATER
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing;
@@ -12,8 +10,13 @@ namespace MQTTnet.AspNetCore
{
public static class EndpointRouterExtensions
{
public static void MapMqtt(this IEndpointRouteBuilder endpoints, string pattern)
public static void MapMqtt(this IEndpointRouteBuilder endpoints, string pattern)
{
if (endpoints == null)
{
throw new ArgumentNullException(nameof(endpoints));
}
endpoints.MapConnectionHandler<MqttConnectionHandler>(pattern, options =>
{
options.WebSockets.SubProtocolSelector = MqttSubProtocolSelector.SelectSubProtocol;
@@ -22,4 +25,3 @@ namespace MQTTnet.AspNetCore
}
}
#endif
@@ -1,8 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;netcoreapp2.1;netcoreapp3.1;net5.0;net6.0;net7.0</TargetFrameworks>
<TargetFramework>net8.0</TargetFramework>
<AssemblyName>MQTTnet.AspNetCore</AssemblyName>
<RootNamespace>MQTTnet.AspNetCore</RootNamespace>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
@@ -32,8 +31,13 @@
<DisableImplicitAspNetCoreAnalyzers>true</DisableImplicitAspNetCoreAnalyzers>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>1591;NETSDK1138</NoWarn>
<LangVersion>7.3</LangVersion>
<NoWarn>1591;NETSDK1138;NU1803;NU1901;NU1902</NoWarn>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<NuGetAuditMode>all</NuGetAuditMode>
<NuGetAudit>true</NuGetAudit>
<NuGetAuditLevel>low</NuGetAuditLevel>
<NuGetAuditLevel>low</NuGetAuditLevel>
<AnalysisLevel>latest-Recommended</AnalysisLevel>
</PropertyGroup>
<ItemGroup>
@@ -42,25 +46,18 @@
<PackagePath>\</PackagePath>
</None>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All"/>
<FrameworkReference Include="Microsoft.AspNetCore.App"/>
</ItemGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<DefineConstants>RELEASE;NETSTANDARD2_0</DefineConstants>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release'">
</PropertyGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp3.1' or '$(TargetFramework)' == 'net5.0' or '$(TargetFramework)' == 'net6.0' or '$(TargetFramework)' == 'net7.0' ">
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' != 'netcoreapp3.1' and '$(TargetFramework)' != 'net5.0' and '$(TargetFramework)' != 'net6.0' and '$(TargetFramework)' != 'net7.0' ">
<PackageReference Include="Microsoft.AspNetCore.Http.Connections" Version="1.1.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MQTTnet\MQTTnet.csproj" />
<ProjectReference Include="..\MQTTnet.Server\MQTTnet.Server.csproj"/>
<ProjectReference Include="..\MQTTnet\MQTTnet.csproj"/>
</ItemGroup>
</Project>
@@ -2,15 +2,12 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using MQTTnet.Adapter;
using MQTTnet.AspNetCore.Client.Tcp;
using MQTTnet.Formatter;
using System;
using System.Net;
using MQTTnet.Client;
using MQTTnet.Diagnostics;
using MQTTnet.Adapter;
using MQTTnet.Diagnostics.Logger;
using MQTTnet.Formatter;
namespace MQTTnet.AspNetCore.Client
namespace MQTTnet.AspNetCore
{
public sealed class MqttClientConnectionContextFactory : IMqttClientAdapterFactory
{
@@ -22,9 +19,8 @@ namespace MQTTnet.AspNetCore.Client
{
case MqttClientTcpOptions tcpOptions:
{
var endpoint = new DnsEndPoint(tcpOptions.Server, tcpOptions.GetPort());
var tcpConnection = new TcpConnection(endpoint);
var tcpConnection = new SocketConnection(tcpOptions.RemoteEndpoint);
var formatter = new MqttPacketFormatterAdapter(options.ProtocolVersion, new MqttBufferWriter(4096, 65535));
return new MqttConnectionContext(formatter, tcpConnection);
}
+171 -180
View File
@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Buffers;
using System.IO.Pipelines;
using System.Net;
using System.Security.Cryptography.X509Certificates;
@@ -12,237 +13,227 @@ using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Http.Connections.Features;
using Microsoft.AspNetCore.Http.Features;
using MQTTnet.Adapter;
using MQTTnet.AspNetCore.Client.Tcp;
using MQTTnet.Exceptions;
using MQTTnet.Formatter;
using MQTTnet.Internal;
using MQTTnet.Packets;
namespace MQTTnet.AspNetCore
namespace MQTTnet.AspNetCore;
public sealed class MqttConnectionContext : IMqttChannelAdapter
{
public sealed class MqttConnectionContext : IMqttChannelAdapter
readonly ConnectionContext _connection;
readonly AsyncLock _writerLock = new();
PipeReader _input;
PipeWriter _output;
public MqttConnectionContext(MqttPacketFormatterAdapter packetFormatterAdapter, ConnectionContext connection)
{
readonly ConnectionContext _connection;
readonly AsyncLock _writerLock = new AsyncLock();
PacketFormatterAdapter = packetFormatterAdapter ?? throw new ArgumentNullException(nameof(packetFormatterAdapter));
_connection = connection ?? throw new ArgumentNullException(nameof(connection));
PipeReader _input;
PipeWriter _output;
public MqttConnectionContext(MqttPacketFormatterAdapter packetFormatterAdapter, ConnectionContext connection)
if (!(_connection is SocketConnection tcp) || tcp.IsConnected)
{
PacketFormatterAdapter = packetFormatterAdapter ?? throw new ArgumentNullException(nameof(packetFormatterAdapter));
_connection = connection ?? throw new ArgumentNullException(nameof(connection));
_input = connection.Transport.Input;
_output = connection.Transport.Output;
}
}
public long BytesReceived { get; private set; }
public long BytesReceived { get; private set; }
public long BytesSent { get; private set; }
public long BytesSent { get; private set; }
public X509Certificate2 ClientCertificate
public X509Certificate2 ClientCertificate
{
get
{
get
// mqtt over tcp
var tlsFeature = _connection.Features.Get<ITlsConnectionFeature>();
if (tlsFeature != null)
{
// mqtt over tcp
var tlsFeature = _connection.Features.Get<ITlsConnectionFeature>();
if (tlsFeature != null)
{
return tlsFeature.ClientCertificate;
}
// mqtt over websocket
var httpFeature = _connection.Features.Get<IHttpContextFeature>();
return httpFeature?.HttpContext?.Connection.ClientCertificate;
return tlsFeature.ClientCertificate;
}
// mqtt over websocket
var httpFeature = _connection.Features.Get<IHttpContextFeature>();
return httpFeature?.HttpContext?.Connection.ClientCertificate;
}
}
public string Endpoint
public string Endpoint
{
get
{
get
// mqtt over tcp
if (_connection.RemoteEndPoint != null)
{
// mqtt over tcp
#if NETCOREAPP3_1_OR_GREATER
if (_connection.RemoteEndPoint != null)
{
return _connection.RemoteEndPoint.ToString();
}
#endif
// mqtt over websocket
var httpFeature = _connection.Features.Get<IHttpConnectionFeature>();
if (httpFeature?.RemoteIpAddress != null)
{
return new IPEndPoint(httpFeature.RemoteIpAddress, httpFeature.RemotePort).ToString();
}
return null;
}
}
public bool IsReadingPacket { get; private set; }
public bool IsSecureConnection
{
get
{
// mqtt over tcp
var tlsFeature = _connection.Features.Get<ITlsConnectionFeature>();
if (tlsFeature != null)
{
return true;
}
// mqtt over websocket
var httpFeature = _connection.Features.Get<IHttpContextFeature>();
if (httpFeature?.HttpContext != null)
{
return httpFeature.HttpContext.Request.IsHttps;
}
return false;
}
}
public MqttPacketFormatterAdapter PacketFormatterAdapter { get; }
public async Task ConnectAsync(CancellationToken cancellationToken)
{
if (_connection is TcpConnection tcp && !tcp.IsConnected)
{
await tcp.StartAsync().ConfigureAwait(false);
return _connection.RemoteEndPoint.ToString();
}
_input = _connection.Transport.Input;
_output = _connection.Transport.Output;
}
public Task DisconnectAsync(CancellationToken cancellationToken)
{
_input?.Complete();
_output?.Complete();
return Task.CompletedTask;
}
public void Dispose()
{
_writerLock.Dispose();
}
public async Task<MqttPacket> ReceivePacketAsync(CancellationToken cancellationToken)
{
try
// mqtt over websocket
var httpFeature = _connection.Features.Get<IHttpConnectionFeature>();
if (httpFeature?.RemoteIpAddress != null)
{
while (!cancellationToken.IsCancellationRequested)
{
ReadResult readResult;
var readTask = _input.ReadAsync(cancellationToken);
if (readTask.IsCompleted)
{
readResult = readTask.Result;
}
else
{
readResult = await readTask.ConfigureAwait(false);
}
var buffer = readResult.Buffer;
var consumed = buffer.Start;
var observed = buffer.Start;
try
{
if (!buffer.IsEmpty)
{
if (PacketFormatterAdapter.TryDecode(buffer, out var packet, out consumed, out observed, out var received))
{
BytesReceived += received;
return packet;
}
else
{
// we did receive something but the message is not yet complete
IsReadingPacket = true;
}
}
else if (readResult.IsCompleted)
{
throw new MqttCommunicationException("Connection Aborted");
}
}
finally
{
// The buffer was sliced up to where it was consumed, so we can just advance to the start.
// We mark examined as buffer.End so that if we didn't receive a full frame, we'll wait for more data
// before yielding the read again.
_input.AdvanceTo(consumed, observed);
}
}
}
catch (Exception exception)
{
// completing the channel makes sure that there is no more data read after a protocol error
_input?.Complete(exception);
_output?.Complete(exception);
throw;
}
finally
{
IsReadingPacket = false;
return new IPEndPoint(httpFeature.RemoteIpAddress, httpFeature.RemotePort).ToString();
}
cancellationToken.ThrowIfCancellationRequested();
return null;
}
}
public void ResetStatistics()
public bool IsSecureConnection
{
get
{
BytesReceived = 0;
BytesSent = 0;
// mqtt over tcp
var tlsFeature = _connection.Features.Get<ITlsConnectionFeature>();
if (tlsFeature != null)
{
return true;
}
// mqtt over websocket
var httpFeature = _connection.Features.Get<IHttpContextFeature>();
if (httpFeature?.HttpContext != null)
{
return httpFeature.HttpContext.Request.IsHttps;
}
return false;
}
}
public MqttPacketFormatterAdapter PacketFormatterAdapter { get; }
public async Task ConnectAsync(CancellationToken cancellationToken)
{
if (_connection is SocketConnection tcp && !tcp.IsConnected)
{
await tcp.StartAsync().ConfigureAwait(false);
}
public async Task SendPacketAsync(MqttPacket packet, CancellationToken cancellationToken)
_input = _connection.Transport.Input;
_output = _connection.Transport.Output;
}
public Task DisconnectAsync(CancellationToken cancellationToken)
{
_input?.Complete();
_output?.Complete();
return Task.CompletedTask;
}
public void Dispose()
{
_writerLock.Dispose();
}
public async Task<MqttPacket> ReceivePacketAsync(CancellationToken cancellationToken)
{
try
{
using (await _writerLock.EnterAsync(cancellationToken).ConfigureAwait(false))
while (!cancellationToken.IsCancellationRequested)
{
ReadResult readResult;
var readTask = _input.ReadAsync(cancellationToken);
if (readTask.IsCompleted)
{
readResult = readTask.Result;
}
else
{
readResult = await readTask.ConfigureAwait(false);
}
var buffer = readResult.Buffer;
var consumed = buffer.Start;
var observed = buffer.Start;
try
{
var buffer = PacketFormatterAdapter.Encode(packet);
if (buffer.Payload.Count == 0)
if (!buffer.IsEmpty)
{
// zero copy
// https://github.com/dotnet/runtime/blob/e31ddfdc4f574b26231233dc10c9a9c402f40590/src/libraries/System.IO.Pipelines/src/System/IO/Pipelines/StreamPipeWriter.cs#L279
await _output.WriteAsync(buffer.Packet, cancellationToken).ConfigureAwait(false);
if (PacketFormatterAdapter.TryDecode(buffer, out var packet, out consumed, out observed, out var received))
{
BytesReceived += received;
return packet;
}
}
else
else if (readResult.IsCompleted)
{
WritePacketBuffer(_output, buffer);
await _output.FlushAsync(cancellationToken).ConfigureAwait(false);
throw new MqttCommunicationException("Connection Aborted");
}
BytesSent += buffer.Length;
}
finally
{
PacketFormatterAdapter.Cleanup();
// The buffer was sliced up to where it was consumed, so we can just advance to the start.
// We mark examined as buffer.End so that if we didn't receive a full frame, we'll wait for more data
// before yielding the read again.
_input.AdvanceTo(consumed, observed);
}
}
}
static void WritePacketBuffer(PipeWriter output, MqttPacketBuffer buffer)
catch (Exception exception)
{
// copy MqttPacketBuffer's Packet and Payload to the same buffer block of PipeWriter
// MqttPacket will be transmitted within the bounds of a WebSocket frame after PipeWriter.FlushAsync
// completing the channel makes sure that there is no more data read after a protocol error
_input?.Complete(exception);
_output?.Complete(exception);
var span = output.GetSpan(buffer.Length);
throw;
}
buffer.Packet.AsSpan().CopyTo(span);
buffer.Payload.AsSpan().CopyTo(span.Slice(buffer.Packet.Count));
cancellationToken.ThrowIfCancellationRequested();
return null;
}
output.Advance(buffer.Length);
public void ResetStatistics()
{
BytesReceived = 0;
BytesSent = 0;
}
public async Task SendPacketAsync(MqttPacket packet, CancellationToken cancellationToken)
{
using (await _writerLock.EnterAsync(cancellationToken).ConfigureAwait(false))
{
try
{
var buffer = PacketFormatterAdapter.Encode(packet);
if (buffer.Payload.Length == 0)
{
// zero copy
// https://github.com/dotnet/runtime/blob/e31ddfdc4f574b26231233dc10c9a9c402f40590/src/libraries/System.IO.Pipelines/src/System/IO/Pipelines/StreamPipeWriter.cs#L279
await _output.WriteAsync(buffer.Packet, cancellationToken).ConfigureAwait(false);
}
else
{
WritePacketBuffer(_output, buffer);
await _output.FlushAsync(cancellationToken).ConfigureAwait(false);
}
BytesSent += buffer.Length;
}
finally
{
PacketFormatterAdapter.Cleanup();
}
}
}
static void WritePacketBuffer(PipeWriter output, MqttPacketBuffer buffer)
{
// copy MqttPacketBuffer's Packet and Payload to the same buffer block of PipeWriter
// MqttPacket will be transmitted within the bounds of a WebSocket frame after PipeWriter.FlushAsync
var span = output.GetSpan(buffer.Length);
buffer.Packet.AsSpan().CopyTo(span);
int offset = buffer.Packet.Count;
buffer.Payload.CopyTo(destination: span.Slice(offset));
output.Advance(buffer.Length);
}
}
@@ -2,57 +2,56 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Connections.Features;
using MQTTnet.Adapter;
using MQTTnet.Server;
using System;
using System.Threading.Tasks;
using MQTTnet.Diagnostics;
using MQTTnet.Diagnostics.Logger;
using MQTTnet.Formatter;
using MQTTnet.Server;
namespace MQTTnet.AspNetCore
namespace MQTTnet.AspNetCore;
public sealed class MqttConnectionHandler : ConnectionHandler, IMqttServerAdapter
{
public sealed class MqttConnectionHandler : ConnectionHandler, IMqttServerAdapter
MqttServerOptions _serverOptions;
public Func<IMqttChannelAdapter, Task> ClientHandler { get; set; }
public void Dispose()
{
MqttServerOptions _serverOptions;
public Func<IMqttChannelAdapter, Task> ClientHandler { get; set; }
}
public override async Task OnConnectedAsync(ConnectionContext connection)
public override async Task OnConnectedAsync(ConnectionContext connection)
{
// required for websocket transport to work
var transferFormatFeature = connection.Features.Get<ITransferFormatFeature>();
if (transferFormatFeature != null)
{
// required for websocket transport to work
var transferFormatFeature = connection.Features.Get<ITransferFormatFeature>();
if (transferFormatFeature != null)
transferFormatFeature.ActiveFormat = TransferFormat.Binary;
}
var formatter = new MqttPacketFormatterAdapter(new MqttBufferWriter(_serverOptions.WriterBufferSize, _serverOptions.WriterBufferSizeMax));
using (var adapter = new MqttConnectionContext(formatter, connection))
{
var clientHandler = ClientHandler;
if (clientHandler != null)
{
transferFormatFeature.ActiveFormat = TransferFormat.Binary;
await clientHandler(adapter).ConfigureAwait(false);
}
var formatter = new MqttPacketFormatterAdapter(new MqttBufferWriter(_serverOptions.WriterBufferSize, _serverOptions.WriterBufferSizeMax));
using (var adapter = new MqttConnectionContext(formatter, connection))
{
var clientHandler = ClientHandler;
if (clientHandler != null)
{
await clientHandler(adapter).ConfigureAwait(false);
}
}
}
public Task StartAsync(MqttServerOptions options, IMqttNetLogger logger)
{
_serverOptions = options;
return Task.CompletedTask;
}
public Task StopAsync()
{
return Task.CompletedTask;
}
public void Dispose()
{
}
}
}
public Task StartAsync(MqttServerOptions options, IMqttNetLogger logger)
{
_serverOptions = options;
return Task.CompletedTask;
}
public Task StopAsync()
{
return Task.CompletedTask;
}
}
+32 -17
View File
@@ -2,32 +2,47 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using MQTTnet.Adapter;
using MQTTnet.Diagnostics;
using MQTTnet.Diagnostics.Logger;
using MQTTnet.Server;
namespace MQTTnet.AspNetCore
namespace MQTTnet.AspNetCore;
public sealed class MqttHostedServer : MqttServer, IHostedService
{
public sealed class MqttHostedServer : MqttServer, IHostedService
readonly IHostApplicationLifetime _hostApplicationLifetime;
readonly MqttServerFactory _mqttFactory;
public MqttHostedServer(
IHostApplicationLifetime hostApplicationLifetime,
MqttServerFactory mqttFactory,
MqttServerOptions options,
IEnumerable<IMqttServerAdapter> adapters,
IMqttNetLogger logger) : base(options, adapters, logger)
{
public MqttHostedServer(MqttServerOptions options, IEnumerable<IMqttServerAdapter> adapters, IMqttNetLogger logger)
: base(options, adapters, logger)
{
}
_mqttFactory = mqttFactory ?? throw new ArgumentNullException(nameof(mqttFactory));
_hostApplicationLifetime = hostApplicationLifetime;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_ = StartAsync();
return Task.CompletedTask;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
// The yield makes sure that the hosted service is considered up and running.
await Task.Yield();
public Task StopAsync(CancellationToken cancellationToken)
{
return StopAsync();
}
_hostApplicationLifetime.ApplicationStarted.Register(OnStarted);
}
public Task StopAsync(CancellationToken cancellationToken)
{
return StopAsync(_mqttFactory.CreateMqttServerStopOptionsBuilder().Build());
}
void OnStarted()
{
_ = StartAsync();
}
}
@@ -7,31 +7,34 @@ using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Http;
namespace MQTTnet.AspNetCore
namespace MQTTnet.AspNetCore;
public static class MqttSubProtocolSelector
{
public static class MqttSubProtocolSelector
public static string SelectSubProtocol(HttpRequest request)
{
public static string SelectSubProtocol(HttpRequest request)
if (request == null)
{
if (request == null) throw new ArgumentNullException(nameof(request));
string subProtocol = null;
if (request.Headers.TryGetValue("Sec-WebSocket-Protocol", out var requestedSubProtocolValues))
{
subProtocol = SelectSubProtocol(requestedSubProtocolValues);
}
return subProtocol;
throw new ArgumentNullException(nameof(request));
}
public static string SelectSubProtocol(IList<string> requestedSubProtocolValues)
string subProtocol = null;
if (request.Headers.TryGetValue("Sec-WebSocket-Protocol", out var requestedSubProtocolValues))
{
if (requestedSubProtocolValues == null) throw new ArgumentNullException(nameof(requestedSubProtocolValues));
// Order the protocols to also match "mqtt", "mqttv-3.1", "mqttv-3.11" etc.
return requestedSubProtocolValues
.OrderByDescending(p => p.Length)
.FirstOrDefault(p => p.ToLower().StartsWith("mqtt"));
subProtocol = SelectSubProtocol(requestedSubProtocolValues);
}
return subProtocol;
}
}
public static string SelectSubProtocol(IList<string> requestedSubProtocolValues)
{
if (requestedSubProtocolValues == null)
{
throw new ArgumentNullException(nameof(requestedSubProtocolValues));
}
// Order the protocols to also match "mqtt", "mqttv-3.1", "mqttv-3.11" etc.
return requestedSubProtocolValues.OrderByDescending(p => p.Length).FirstOrDefault(p => p.ToLower().StartsWith("mqtt"));
}
}
@@ -2,66 +2,68 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.AspNetCore.Http;
using MQTTnet.Adapter;
using MQTTnet.Formatter;
using MQTTnet.Implementations;
using MQTTnet.Server;
using System;
using System.Net.WebSockets;
using System.Threading.Tasks;
using MQTTnet.Diagnostics;
using Microsoft.AspNetCore.Http;
using MQTTnet.Adapter;
using MQTTnet.Diagnostics.Logger;
using MQTTnet.Formatter;
using MQTTnet.Implementations;
using MQTTnet.Server;
namespace MQTTnet.AspNetCore
namespace MQTTnet.AspNetCore;
public sealed class MqttWebSocketServerAdapter : IMqttServerAdapter
{
public sealed class MqttWebSocketServerAdapter : IMqttServerAdapter
IMqttNetLogger _logger = MqttNetNullLogger.Instance;
public Func<IMqttChannelAdapter, Task> ClientHandler { get; set; }
public void Dispose()
{
IMqttNetLogger _logger = new MqttNetNullLogger();
public Func<IMqttChannelAdapter, Task> ClientHandler { get; set; }
}
public Task StartAsync(MqttServerOptions options, IMqttNetLogger logger)
public async Task RunWebSocketConnectionAsync(WebSocket webSocket, HttpContext httpContext)
{
if (webSocket == null)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
return Task.CompletedTask;
throw new ArgumentNullException(nameof(webSocket));
}
public Task StopAsync()
var endpoint = $"{httpContext.Connection.RemoteIpAddress}:{httpContext.Connection.RemotePort}";
var clientCertificate = await httpContext.Connection.GetClientCertificateAsync().ConfigureAwait(false);
try
{
return Task.CompletedTask;
}
var isSecureConnection = clientCertificate != null;
public async Task RunWebSocketConnectionAsync(WebSocket webSocket, HttpContext httpContext)
{
if (webSocket == null) throw new ArgumentNullException(nameof(webSocket));
var endpoint = $"{httpContext.Connection.RemoteIpAddress}:{httpContext.Connection.RemotePort}";
var clientCertificate = await httpContext.Connection.GetClientCertificateAsync().ConfigureAwait(false);
try
var clientHandler = ClientHandler;
if (clientHandler != null)
{
var isSecureConnection = clientCertificate != null;
var formatter = new MqttPacketFormatterAdapter(new MqttBufferWriter(4096, 65535));
var channel = new MqttWebSocketChannel(webSocket, endpoint, isSecureConnection, clientCertificate);
var clientHandler = ClientHandler;
if (clientHandler != null)
using (var channelAdapter = new MqttChannelAdapter(channel, formatter, _logger))
{
var formatter = new MqttPacketFormatterAdapter(new MqttBufferWriter(4096, 65535));
var channel = new MqttWebSocketChannel(webSocket, endpoint, isSecureConnection, clientCertificate);
using (var channelAdapter = new MqttChannelAdapter(channel, formatter, null, _logger))
{
await clientHandler(channelAdapter).ConfigureAwait(false);
}
await clientHandler(channelAdapter).ConfigureAwait(false);
}
}
finally
{
clientCertificate?.Dispose();
}
}
public void Dispose()
finally
{
clientCertificate?.Dispose();
}
}
public Task StartAsync(MqttServerOptions options, IMqttNetLogger logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
return Task.CompletedTask;
}
public Task StopAsync()
{
return Task.CompletedTask;
}
}
+111 -113
View File
@@ -2,139 +2,137 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Buffers;
using System.Runtime.InteropServices;
using MQTTnet.Adapter;
using MQTTnet.Exceptions;
using MQTTnet.Formatter;
using MQTTnet.Packets;
using System;
using System.Buffers;
using System.Linq;
using System.Runtime.InteropServices;
namespace MQTTnet.AspNetCore
namespace MQTTnet.AspNetCore;
public static class ReaderExtensions
{
public static class ReaderExtensions
public static bool TryDecode(
this MqttPacketFormatterAdapter formatter,
in ReadOnlySequence<byte> input,
out MqttPacket packet,
out SequencePosition consumed,
out SequencePosition observed,
out int bytesRead)
{
public static bool TryDecode(this MqttPacketFormatterAdapter formatter,
in ReadOnlySequence<byte> input,
out MqttPacket packet,
out SequencePosition consumed,
out SequencePosition observed,
out int bytesRead)
if (formatter == null)
{
if (formatter == null) throw new ArgumentNullException(nameof(formatter));
packet = null;
consumed = input.Start;
observed = input.End;
bytesRead = 0;
var copy = input;
if (copy.Length < 2)
{
return false;
}
if (!TryReadBodyLength(ref copy, out var headerLength, out var bodyLength))
{
return false;
}
var fixedHeader = copy.First.Span[0];
copy = copy.Slice(headerLength);
if (copy.Length < bodyLength)
{
return false;
}
var bodySlice = copy.Slice(0, bodyLength);
var bodySegment = GetArraySegment(ref bodySlice);
var receivedMqttPacket = new ReceivedMqttPacket(fixedHeader, bodySegment, headerLength + bodyLength);
if (formatter.ProtocolVersion == MqttProtocolVersion.Unknown)
{
formatter.DetectProtocolVersion(receivedMqttPacket);
}
packet = formatter.Decode(receivedMqttPacket);
consumed = bodySlice.End;
observed = bodySlice.End;
bytesRead = headerLength + bodyLength;
return true;
throw new ArgumentNullException(nameof(formatter));
}
static ArraySegment<byte> GetArraySegment(ref ReadOnlySequence<byte> input)
{
if (input.IsSingleSegment && MemoryMarshal.TryGetArray(input.First, out var segment))
{
return segment;
}
packet = null;
consumed = input.Start;
observed = input.End;
bytesRead = 0;
var copy = input;
// Should be rare
var array = input.ToArray();
return new ArraySegment<byte>(array);
if (copy.Length < 2)
{
return false;
}
static bool TryReadBodyLength(ref ReadOnlySequence<byte> input, out int headerLength, out int bodyLength)
if (!TryReadBodyLength(ref copy, out var headerLength, out var bodyLength))
{
var valueSequence = input.Slice(0, Math.Min(5, input.Length));
if (valueSequence.IsSingleSegment)
{
var valueSpan =
#if NET5_0_OR_GREATER
valueSequence.FirstSpan;
#else
valueSequence.First.Span;
#endif
return TryReadBodyLength(valueSpan, out headerLength, out bodyLength);
}
else
{
Span<byte> valueSpan = stackalloc byte[8];
valueSequence.CopyTo(valueSpan);
valueSpan = valueSpan.Slice(0, (int)valueSequence.Length);
return TryReadBodyLength(valueSpan, out headerLength, out bodyLength);
}
return false;
}
static bool TryReadBodyLength(ReadOnlySpan<byte> span, out int headerLength, out int bodyLength)
var fixedHeader = copy.First.Span[0];
copy = copy.Slice(headerLength);
if (copy.Length < bodyLength)
{
// Alorithm taken from https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/errata01/os/mqtt-v3.1.1-errata01-os-complete.html.
var multiplier = 1;
var value = 0;
byte encodedByte;
var index = 1;
headerLength = 0;
bodyLength = 0;
do
{
if (index == span.Length)
{
return false;
}
encodedByte = span[index];
index++;
value += (byte)(encodedByte & 127) * multiplier;
if (multiplier > 128 * 128 * 128)
{
ThrowProtocolViolationException(span, index);
}
multiplier *= 128;
} while ((encodedByte & 128) != 0);
headerLength = index;
bodyLength = value;
return true;
return false;
}
var bodySlice = copy.Slice(0, bodyLength);
var bodySegment = GetArraySegment(ref bodySlice);
static void ThrowProtocolViolationException(ReadOnlySpan<byte> valueSpan, int index)
var receivedMqttPacket = new ReceivedMqttPacket(fixedHeader, bodySegment, headerLength + bodyLength);
if (formatter.ProtocolVersion == MqttProtocolVersion.Unknown)
{
throw new MqttProtocolViolationException($"Remaining length is invalid (Data={string.Join(",", valueSpan.Slice(1, index).ToArray())}).");
formatter.DetectProtocolVersion(receivedMqttPacket);
}
packet = formatter.Decode(receivedMqttPacket);
consumed = bodySlice.End;
observed = bodySlice.End;
bytesRead = headerLength + bodyLength;
return true;
}
static ArraySegment<byte> GetArraySegment(ref ReadOnlySequence<byte> input)
{
if (input.IsSingleSegment && MemoryMarshal.TryGetArray(input.First, out var segment))
{
return segment;
}
// Should be rare
var array = input.ToArray();
return new ArraySegment<byte>(array);
}
static void ThrowProtocolViolationException(ReadOnlySpan<byte> valueSpan, int index)
{
throw new MqttProtocolViolationException($"Remaining length is invalid (Data={string.Join(",", valueSpan.Slice(1, index).ToArray())}).");
}
static bool TryReadBodyLength(ref ReadOnlySequence<byte> input, out int headerLength, out int bodyLength)
{
var valueSequence = input.Slice(0, Math.Min(5, input.Length));
if (valueSequence.IsSingleSegment)
{
var valueSpan = valueSequence.FirstSpan;
return TryReadBodyLength(valueSpan, out headerLength, out bodyLength);
}
else
{
Span<byte> valueSpan = stackalloc byte[8];
valueSequence.CopyTo(valueSpan);
valueSpan = valueSpan.Slice(0, (int)valueSequence.Length);
return TryReadBodyLength(valueSpan, out headerLength, out bodyLength);
}
}
}
static bool TryReadBodyLength(ReadOnlySpan<byte> span, out int headerLength, out int bodyLength)
{
// Alorithm taken from https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/errata01/os/mqtt-v3.1.1-errata01-os-complete.html.
var multiplier = 1;
var value = 0;
byte encodedByte;
var index = 1;
headerLength = 0;
bodyLength = 0;
do
{
if (index == span.Length)
{
return false;
}
encodedByte = span[index];
index++;
value += (byte)(encodedByte & 127) * multiplier;
if (multiplier > 128 * 128 * 128)
{
ThrowProtocolViolationException(span, index);
}
multiplier *= 128;
} while ((encodedByte & 128) != 0);
headerLength = index;
bodyLength = value;
return true;
}
}
@@ -4,102 +4,125 @@
using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using MQTTnet.Adapter;
using MQTTnet.Diagnostics;
using MQTTnet.Implementations;
using MQTTnet.Diagnostics.Logger;
using MQTTnet.Server;
using MQTTnet.Server.Internal.Adapter;
namespace MQTTnet.AspNetCore
namespace MQTTnet.AspNetCore;
public static class ServiceCollectionExtensions
{
public static class ServiceCollectionExtensions
public static IServiceCollection AddHostedMqttServer(this IServiceCollection services, MqttServerOptions options)
{
public static IServiceCollection AddMqttServer(this IServiceCollection serviceCollection, Action<MqttServerOptionsBuilder> configure = null)
if (services == null)
{
if (serviceCollection is null)
{
throw new ArgumentNullException(nameof(serviceCollection));
}
serviceCollection.AddMqttConnectionHandler();
serviceCollection.AddHostedMqttServer(configure);
return serviceCollection;
throw new ArgumentNullException(nameof(services));
}
public static IServiceCollection AddHostedMqttServer(this IServiceCollection services, MqttServerOptions options)
if (options == null)
{
if (options == null) throw new ArgumentNullException(nameof(options));
services.AddSingleton(options);
services.AddHostedMqttServer();
return services;
throw new ArgumentNullException(nameof(options));
}
public static IServiceCollection AddHostedMqttServer(this IServiceCollection services, Action<MqttServerOptionsBuilder> configure = null)
services.AddSingleton(options);
services.AddHostedMqttServer();
return services;
}
public static IServiceCollection AddHostedMqttServer(this IServiceCollection services, Action<MqttServerOptionsBuilder> configure)
{
if (services == null)
{
services.AddSingleton(s =>
{
var serverOptionsBuilder = new MqttServerOptionsBuilder();
configure?.Invoke(serverOptionsBuilder);
return serverOptionsBuilder.Build();
});
services.AddHostedMqttServer();
return services;
throw new ArgumentNullException(nameof(services));
}
public static IServiceCollection AddHostedMqttServerWithServices(this IServiceCollection services, Action<AspNetMqttServerOptionsBuilder> configure)
var serverOptionsBuilder = new MqttServerOptionsBuilder();
configure?.Invoke(serverOptionsBuilder);
var options = serverOptionsBuilder.Build();
return AddHostedMqttServer(services, options);
}
public static void AddHostedMqttServer(this IServiceCollection services)
{
// The user may have these services already registered.
services.TryAddSingleton<IMqttNetLogger>(MqttNetNullLogger.Instance);
services.TryAddSingleton(new MqttServerFactory());
services.AddSingleton<MqttHostedServer>();
services.AddSingleton<IHostedService>(s => s.GetService<MqttHostedServer>());
services.AddSingleton<MqttServer>(s => s.GetService<MqttHostedServer>());
}
public static IServiceCollection AddHostedMqttServerWithServices(this IServiceCollection services, Action<AspNetMqttServerOptionsBuilder> configure)
{
if (services == null)
{
services.AddSingleton(s =>
throw new ArgumentNullException(nameof(services));
}
services.AddSingleton(
s =>
{
var builder = new AspNetMqttServerOptionsBuilder(s);
configure(builder);
return builder.Build();
});
services.AddHostedMqttServer();
services.AddHostedMqttServer();
return services;
}
static IServiceCollection AddHostedMqttServer(this IServiceCollection services)
{
var logger = new MqttNetEventLogger();
services.AddSingleton<IMqttNetLogger>(logger);
services.AddSingleton<MqttHostedServer>();
services.AddSingleton<IHostedService>(s => s.GetService<MqttHostedServer>());
services.AddSingleton<MqttServer>(s => s.GetService<MqttHostedServer>());
return services;
}
public static IServiceCollection AddMqttWebSocketServerAdapter(this IServiceCollection services)
{
services.AddSingleton<MqttWebSocketServerAdapter>();
services.AddSingleton<IMqttServerAdapter>(s => s.GetService<MqttWebSocketServerAdapter>());
return services;
}
public static IServiceCollection AddMqttTcpServerAdapter(this IServiceCollection services)
{
services.AddSingleton<MqttTcpServerAdapter>();
services.AddSingleton<IMqttServerAdapter>(s => s.GetService<MqttTcpServerAdapter>());
return services;
}
public static IServiceCollection AddMqttConnectionHandler(this IServiceCollection services)
{
services.AddSingleton<MqttConnectionHandler>();
services.AddSingleton<IMqttServerAdapter>(s => s.GetService<MqttConnectionHandler>());
return services;
}
return services;
}
}
public static IServiceCollection AddMqttConnectionHandler(this IServiceCollection services)
{
services.AddSingleton<MqttConnectionHandler>();
services.AddSingleton<IMqttServerAdapter>(s => s.GetService<MqttConnectionHandler>());
return services;
}
public static void AddMqttLogger(this IServiceCollection services, IMqttNetLogger logger)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
services.AddSingleton(logger);
}
public static IServiceCollection AddMqttServer(this IServiceCollection serviceCollection, Action<MqttServerOptionsBuilder> configure = null)
{
if (serviceCollection is null)
{
throw new ArgumentNullException(nameof(serviceCollection));
}
serviceCollection.AddMqttConnectionHandler();
serviceCollection.AddHostedMqttServer(configure);
return serviceCollection;
}
public static IServiceCollection AddMqttTcpServerAdapter(this IServiceCollection services)
{
services.AddSingleton<MqttTcpServerAdapter>();
services.AddSingleton<IMqttServerAdapter>(s => s.GetService<MqttTcpServerAdapter>());
return services;
}
public static IServiceCollection AddMqttWebSocketServerAdapter(this IServiceCollection services)
{
services.AddSingleton<MqttWebSocketServerAdapter>();
services.AddSingleton<IMqttServerAdapter>(s => s.GetService<MqttWebSocketServerAdapter>());
return services;
}
}
@@ -0,0 +1,77 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Diagnostics;
using System.IO.Pipelines;
using System.Net.Sockets;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
namespace MQTTnet.AspNetCore;
public class SocketAwaitable : ICriticalNotifyCompletion
{
static readonly Action _callbackCompleted = () =>
{
};
readonly PipeScheduler _ioScheduler;
int _bytesTransferred;
Action _callback;
SocketError _error;
public SocketAwaitable(PipeScheduler ioScheduler)
{
_ioScheduler = ioScheduler;
}
public bool IsCompleted => ReferenceEquals(_callback, _callbackCompleted);
public void Complete(int bytesTransferred, SocketError socketError)
{
_error = socketError;
_bytesTransferred = bytesTransferred;
var continuation = Interlocked.Exchange(ref _callback, _callbackCompleted);
if (continuation != null)
{
_ioScheduler.Schedule(state => ((Action)state)(), continuation);
}
}
public SocketAwaitable GetAwaiter()
{
return this;
}
public int GetResult()
{
Debug.Assert(ReferenceEquals(_callback, _callbackCompleted));
_callback = null;
if (_error != SocketError.Success)
{
throw new SocketException((int)_error);
}
return _bytesTransferred;
}
public void OnCompleted(Action continuation)
{
if (ReferenceEquals(_callback, _callbackCompleted) || ReferenceEquals(Interlocked.CompareExchange(ref _callback, continuation, null), _callbackCompleted))
{
Task.Run(continuation);
}
}
public void UnsafeOnCompleted(Action continuation)
{
OnCompleted(continuation);
}
}
@@ -0,0 +1,261 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Pipelines;
using System.Net;
using System.Net.Sockets;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Http.Features;
using MQTTnet.Exceptions;
namespace MQTTnet.AspNetCore;
public sealed class SocketConnection : ConnectionContext
{
readonly EndPoint _endPoint;
volatile bool _aborted;
IDuplexPipe _application;
SocketReceiver _receiver;
SocketSender _sender;
Socket _socket;
public SocketConnection(EndPoint endPoint)
{
_endPoint = endPoint;
}
public SocketConnection(Socket socket)
{
_socket = socket;
_endPoint = socket.RemoteEndPoint;
_sender = new SocketSender(_socket, PipeScheduler.ThreadPool);
_receiver = new SocketReceiver(_socket, PipeScheduler.ThreadPool);
}
public override string ConnectionId { get; set; }
public override IFeatureCollection Features { get; }
public bool IsConnected { get; private set; }
public override IDictionary<object, object> Items { get; set; }
public override IDuplexPipe Transport { get; set; }
public override ValueTask DisposeAsync()
{
IsConnected = false;
Transport?.Output.Complete();
Transport?.Input.Complete();
_socket?.Dispose();
return base.DisposeAsync();
}
public async Task StartAsync()
{
if (_socket == null)
{
_socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
_sender = new SocketSender(_socket, PipeScheduler.ThreadPool);
_receiver = new SocketReceiver(_socket, PipeScheduler.ThreadPool);
await _socket.ConnectAsync(_endPoint);
}
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
Transport = pair.Transport;
_application = pair.Application;
_ = ExecuteAsync();
IsConnected = true;
}
Exception ConnectionAborted()
{
return new MqttCommunicationException("Connection Aborted");
}
async Task DoReceive()
{
Exception error = null;
try
{
await ProcessReceives();
}
catch (SocketException ex) when (ex.SocketErrorCode == SocketError.ConnectionReset)
{
error = new MqttCommunicationException(ex);
}
catch (SocketException ex) when (ex.SocketErrorCode == SocketError.OperationAborted || ex.SocketErrorCode == SocketError.ConnectionAborted ||
ex.SocketErrorCode == SocketError.Interrupted || ex.SocketErrorCode == SocketError.InvalidArgument)
{
if (!_aborted)
{
// Calling Dispose after ReceiveAsync can cause an "InvalidArgument" error on *nix.
error = ConnectionAborted();
}
}
catch (ObjectDisposedException)
{
if (!_aborted)
{
error = ConnectionAborted();
}
}
catch (IOException ex)
{
error = ex;
}
catch (Exception ex)
{
error = new IOException(ex.Message, ex);
}
finally
{
if (_aborted)
{
error = error ?? ConnectionAborted();
}
_application.Output.Complete(error);
}
}
async Task<Exception> DoSend()
{
Exception error = null;
try
{
await ProcessSends();
}
catch (SocketException ex) when (ex.SocketErrorCode == SocketError.OperationAborted)
{
}
catch (ObjectDisposedException)
{
}
catch (IOException ex)
{
error = ex;
}
catch (Exception ex)
{
error = new IOException(ex.Message, ex);
}
finally
{
_aborted = true;
_socket.Shutdown(SocketShutdown.Both);
}
return error;
}
async Task ExecuteAsync()
{
Exception sendError = null;
try
{
// Spawn send and receive logic
var receiveTask = DoReceive();
var sendTask = DoSend();
// If the sending task completes then close the receive
// We don't need to do this in the other direction because the kestrel
// will trigger the output closing once the input is complete.
if (await Task.WhenAny(receiveTask, sendTask).ConfigureAwait(false) == sendTask)
{
// Tell the reader it's being aborted
_socket.Dispose();
}
// Now wait for both to complete
await receiveTask;
sendError = await sendTask;
// Dispose the socket(should noop if already called)
_socket.Dispose();
}
catch (Exception ex)
{
Console.WriteLine($"Unexpected exception in {nameof(SocketConnection)}.{nameof(StartAsync)}: " + ex);
}
finally
{
// Complete the output after disposing the socket
await _application.Input.CompleteAsync(sendError).ConfigureAwait(false);
}
}
async Task ProcessReceives()
{
while (true)
{
// Ensure we have some reasonable amount of buffer space
var buffer = _application.Output.GetMemory();
var bytesReceived = await _receiver.ReceiveAsync(buffer);
if (bytesReceived == 0)
{
// FIN
break;
}
_application.Output.Advance(bytesReceived);
var flushTask = _application.Output.FlushAsync();
if (!flushTask.IsCompleted)
{
await flushTask;
}
var result = flushTask.GetAwaiter().GetResult();
if (result.IsCompleted)
{
// Pipe consumer is shut down, do we stop writing
break;
}
}
}
async Task ProcessSends()
{
while (true)
{
// Wait for data to write from the pipe producer
var result = await _application.Input.ReadAsync();
var buffer = result.Buffer;
if (result.IsCanceled)
{
break;
}
var end = buffer.End;
var isCompleted = result.IsCompleted;
if (!buffer.IsEmpty)
{
await _sender.SendAsync(buffer);
}
_application.Input.AdvanceTo(end);
if (isCompleted)
{
break;
}
}
}
}
@@ -0,0 +1,36 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.IO.Pipelines;
using System.Net.Sockets;
namespace MQTTnet.AspNetCore;
public sealed class SocketReceiver
{
readonly SocketAwaitable _awaitable;
readonly SocketAsyncEventArgs _eventArgs = new();
readonly Socket _socket;
public SocketReceiver(Socket socket, PipeScheduler scheduler)
{
_socket = socket;
_awaitable = new SocketAwaitable(scheduler);
_eventArgs.UserToken = _awaitable;
_eventArgs.Completed += (_, e) => ((SocketAwaitable)e.UserToken).Complete(e.BytesTransferred, e.SocketError);
}
public SocketAwaitable ReceiveAsync(Memory<byte> buffer)
{
_eventArgs.SetBuffer(buffer);
if (!_socket.ReceiveAsync(_eventArgs))
{
_awaitable.Complete(_eventArgs.BytesTransferred, _eventArgs.SocketError);
}
return _awaitable;
}
}
+93
View File
@@ -0,0 +1,93 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO.Pipelines;
using System.Net.Sockets;
using System.Runtime.InteropServices;
namespace MQTTnet.AspNetCore;
public sealed class SocketSender
{
readonly SocketAwaitable _awaitable;
readonly SocketAsyncEventArgs _eventArgs = new();
readonly Socket _socket;
List<ArraySegment<byte>> _bufferList;
public SocketSender(Socket socket, PipeScheduler scheduler)
{
_socket = socket;
_awaitable = new SocketAwaitable(scheduler);
_eventArgs.UserToken = _awaitable;
_eventArgs.Completed += (_, e) => ((SocketAwaitable)e.UserToken).Complete(e.BytesTransferred, e.SocketError);
}
public SocketAwaitable SendAsync(in ReadOnlySequence<byte> buffers)
{
if (buffers.IsSingleSegment)
{
return SendAsync(buffers.First);
}
if (!_eventArgs.MemoryBuffer.Equals(Memory<byte>.Empty))
{
_eventArgs.SetBuffer(null, 0, 0);
}
_eventArgs.BufferList = GetBufferList(buffers);
if (!_socket.SendAsync(_eventArgs))
{
_awaitable.Complete(_eventArgs.BytesTransferred, _eventArgs.SocketError);
}
return _awaitable;
}
List<ArraySegment<byte>> GetBufferList(in ReadOnlySequence<byte> buffer)
{
Debug.Assert(!buffer.IsEmpty);
Debug.Assert(!buffer.IsSingleSegment);
if (_bufferList == null)
{
_bufferList = new List<ArraySegment<byte>>();
}
else
{
// Buffers are pooled, so it's OK to root them until the next multi-buffer write.
_bufferList.Clear();
}
foreach (var b in buffer)
{
_bufferList.Add(b.GetArray());
}
return _bufferList;
}
SocketAwaitable SendAsync(ReadOnlyMemory<byte> memory)
{
// The BufferList getter is much less expensive then the setter.
if (_eventArgs.BufferList != null)
{
_eventArgs.BufferList = null;
}
_eventArgs.SetBuffer(MemoryMarshal.AsMemory(memory));
if (!_socket.SendAsync(_eventArgs))
{
_awaitable.Complete(_eventArgs.BytesTransferred, _eventArgs.SocketError);
}
return _awaitable;
}
}
@@ -7,7 +7,7 @@ using System.IO;
using System.Threading;
using BenchmarkDotNet.Attributes;
using MQTTnet.Adapter;
using MQTTnet.Diagnostics;
using MQTTnet.Diagnostics.Logger;
using MQTTnet.Formatter;
using MQTTnet.Packets;
using MQTTnet.Tests.Mockups;
@@ -73,7 +73,7 @@ namespace MQTTnet.Benchmarks
var channel = new MemoryMqttChannel(_stream);
_channelAdapter = new MqttChannelAdapter(channel, serializer, null, new MqttNetEventLogger());
_channelAdapter = new MqttChannelAdapter(channel, serializer, new MqttNetEventLogger());
}
static byte[] Join(params ArraySegment<byte>[] chunks)
+11 -11
View File
@@ -4,7 +4,7 @@
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using MQTTnet.Diagnostics;
using MQTTnet.Diagnostics.Logger;
namespace MQTTnet.Benchmarks
{
@@ -15,13 +15,13 @@ namespace MQTTnet.Benchmarks
{
MqttNetNullLogger _nullLogger;
MqttNetSourceLogger _sourceNullLogger;
MqttNetEventLogger _eventLogger;
MqttNetSourceLogger _sourceEventLogger;
MqttNetEventLogger _eventLoggerNoListener;
MqttNetSourceLogger _sourceEventLoggerNoListener;
bool _useHandler;
[GlobalSetup]
@@ -29,11 +29,11 @@ namespace MQTTnet.Benchmarks
{
_nullLogger = new MqttNetNullLogger();
_sourceNullLogger = _nullLogger.WithSource("Source");
_eventLogger = new MqttNetEventLogger();
_eventLogger.LogMessagePublished += OnLogMessagePublished;
_sourceEventLogger = _eventLogger.WithSource("Source");
_eventLoggerNoListener = new MqttNetEventLogger();
_sourceEventLoggerNoListener = _eventLoggerNoListener.WithSource("Source");
}
@@ -50,7 +50,7 @@ namespace MQTTnet.Benchmarks
public void Log_10000_Messages_No_Listener()
{
_useHandler = false;
for (var i = 0; i < 10000; i++)
{
_sourceEventLoggerNoListener.Verbose("test log message {0}", "parameter");
@@ -61,24 +61,24 @@ namespace MQTTnet.Benchmarks
public void Log_10000_Messages_With_To_String()
{
_useHandler = true;
for (var i = 0; i < 10000; i++)
{
_sourceEventLogger.Verbose("test log message {0}", "parameter");
}
}
[Benchmark]
public void Log_10000_Messages_Without_To_String()
{
_useHandler = false;
for (var i = 0; i < 10000; i++)
{
_sourceEventLogger.Verbose("test log message {0}", "parameter");
}
}
[Benchmark]
public void Log_10000_Messages_With_NullLogger()
{
@@ -4,35 +4,28 @@
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<OutputType>Exe</OutputType>
<DebugType>Full</DebugType>
<TargetFrameworks>net6.0;net7.0</TargetFrameworks>
<LangVersion>7.2</LangVersion>
<TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable>
<EnableNETAnalyzers>false</EnableNETAnalyzers>
<EnforceCodeStyleInBuild>false</EnforceCodeStyleInBuild>
<DisableImplicitAspNetCoreAnalyzers>true</DisableImplicitAspNetCoreAnalyzers>
<NoWarn>1591;NETSDK1138</NoWarn>
<NoWarn>1591;NETSDK1138;NU1803;NU1901;NU1902;CS8892</NoWarn>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<NuGetAuditMode>all</NuGetAuditMode>
<NuGetAudit>true</NuGetAudit>
<NuGetAuditLevel>low</NuGetAuditLevel>
<AnalysisLevel>latest-Recommended</AnalysisLevel>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.2" />
<PackageReference Include="Microsoft.AspNetCore" Version="2.2.0" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net5.0'">
<PackageReference Include="System.IO.Pipelines" Version="5.0.0" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net6.0'">
<PackageReference Include="System.IO.Pipelines" Version="5.0.0" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net7.0'">
<PackageReference Include="System.IO.Pipelines" Version="7.0.0" />
<PackageReference Include="BenchmarkDotNet" Version="0.13.12"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Source\MQTTnet.AspnetCore\MQTTnet.AspNetCore.csproj" />
<ProjectReference Include="..\MQTTnet.Tests\MQTTnet.Tests.csproj" />
<ProjectReference Include="..\..\Source\MQTTnet.Server\MQTTnet.Server.csproj"/>
<ProjectReference Include="..\..\Source\MQTTnet.AspnetCore\MQTTnet.AspNetCore.csproj"/>
<ProjectReference Include="..\MQTTnet.Tests\MQTTnet.Tests.csproj"/>
<ProjectReference Include="..\MQTTnet\MQTTnet.csproj"/>
</ItemGroup>
</Project>
@@ -2,232 +2,216 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using BenchmarkDotNet.Attributes;
using MQTTnet.Client;
using MQTTnet.Server;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using MQTTnet.Packets;
using MQTTnet.Server;
namespace MQTTnet.Benchmarks
namespace MQTTnet.Benchmarks;
[MemoryDiagnoser]
public class MessageDeliveryBenchmark : BaseBenchmark
{
[MemoryDiagnoser]
public class MessageDeliveryBenchmark : BaseBenchmark
List<string> _allSubscribedTopics; // Keep track of the subset of topics that are subscribed
CancellationTokenSource _cancellationTokenSource;
object _lockMsgCount;
int _messagesExpectedCount;
int _messagesReceivedCount;
Dictionary<string, IMqttClient> _mqttPublisherClientsByPublisherName;
MqttServer _mqttServer;
List<IMqttClient> _mqttSubscriberClients;
Dictionary<string, string> _publisherByTopic;
List<MqttApplicationMessage> _topicPublishMessages;
Dictionary<string, List<string>> _topicsByPublisher;
[Params(1000, 10000)] public int NumPublishers;
[Params(5, 10, 20, 50)] public int NumSubscribedTopicsPerSubscriber;
[Params(10)] public int NumSubscribers;
[Params(1, 5)] public int NumTopicsPerPublisher;
[GlobalCleanup]
public void Cleanup()
{
List<MqttApplicationMessage> _topicPublishMessages;
[Params(1, 5)]
public int NumTopicsPerPublisher;
[Params(1000, 10000)]
public int NumPublishers;
[Params(10)]
public int NumSubscribers;
[Params(5, 10, 20, 50)]
public int NumSubscribedTopicsPerSubscriber;
object _lockMsgCount;
int _messagesReceivedCount;
int _messagesExpectedCount;
CancellationTokenSource _cancellationTokenSource;
MqttServer _mqttServer;
List<IMqttClient> _mqttSubscriberClients;
Dictionary<string, IMqttClient> _mqttPublisherClientsByPublisherName;
Dictionary<string, List<string>> _topicsByPublisher;
Dictionary<string, string> _publisherByTopic;
List<string> _allSubscribedTopics; // Keep track of the subset of topics that are subscribed
[GlobalSetup]
public void Setup()
foreach (var mp in _mqttPublisherClientsByPublisherName)
{
_lockMsgCount = new object();
Dictionary<string, List<string>> singleWildcardTopicsByPublisher;
Dictionary<string, List<string>> multiWildcardTopicsByPublisher;
TopicGenerator.Generate(NumPublishers, NumTopicsPerPublisher, out _topicsByPublisher, out singleWildcardTopicsByPublisher, out multiWildcardTopicsByPublisher);
var topics = _topicsByPublisher.First().Value;
_topicPublishMessages = new List<MqttApplicationMessage>();
// Prepare messages, same for each publisher
foreach (var topic in topics)
{
var message = new MqttApplicationMessageBuilder()
.WithTopic(topic)
.WithPayload(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 })
.Build();
_topicPublishMessages.Add(message);
}
// Create server
var factory = new MqttFactory();
var serverOptions = new MqttServerOptionsBuilder().WithDefaultEndpoint().Build();
_mqttServer = factory.CreateMqttServer(serverOptions);
_mqttServer.StartAsync().GetAwaiter().GetResult();
// Create publisher clients
_mqttPublisherClientsByPublisherName = new Dictionary<string, IMqttClient>();
foreach (var pt in _topicsByPublisher)
{
var publisherName = pt.Key;
var mqttClient = factory.CreateMqttClient();
var publisherOptions = new MqttClientOptionsBuilder()
.WithTcpServer("localhost")
.WithClientId(publisherName)
.WithKeepAlivePeriod(TimeSpan.FromSeconds(30))
.Build();
mqttClient.ConnectAsync(publisherOptions).GetAwaiter().GetResult();
_mqttPublisherClientsByPublisherName.Add(publisherName, mqttClient);
}
// Create subscriber clients
_mqttSubscriberClients = new List<IMqttClient>();
for (var i = 0; i < NumSubscribers; i++)
{
var mqttSubscriberClient = factory.CreateMqttClient();
_mqttSubscriberClients.Add(mqttSubscriberClient);
var subscriberOptions = new MqttClientOptionsBuilder()
.WithTcpServer("localhost")
.WithClientId("subscriber" + i)
.Build();
mqttSubscriberClient.ApplicationMessageReceivedAsync += r =>
{
// count messages and signal cancellation when expected message count is reached
lock (_lockMsgCount)
{
++_messagesReceivedCount;
if (_messagesReceivedCount == _messagesExpectedCount)
{
_cancellationTokenSource.Cancel();
}
}
return Task.CompletedTask;
};
mqttSubscriberClient.ConnectAsync(subscriberOptions).GetAwaiter().GetResult();
}
List<string> allTopics = new List<string>();
_publisherByTopic = new Dictionary<string, string>();
foreach (var t in _topicsByPublisher)
{
foreach (var topic in t.Value)
{
_publisherByTopic.Add(topic, t.Key);
allTopics.Add(topic);
}
}
// Subscribe to NumSubscribedTopics topics spread across all topics
_allSubscribedTopics = new List<string>();
var totalNumTopics = NumPublishers * NumTopicsPerPublisher;
int topicIndexStep = totalNumTopics / (NumSubscribedTopicsPerSubscriber * NumSubscribers);
if (topicIndexStep * NumSubscribedTopicsPerSubscriber * NumSubscribers != totalNumTopics)
{
throw new System.Exception(
String.Format("The total number of topics must be divisible by the number of subscribed topics across all subscribers. Total number of topics: {0}, topic step: {1}",
totalNumTopics, topicIndexStep
));
}
var topicIndex = 0;
foreach (var mqttSubscriber in _mqttSubscriberClients)
{
for (var i = 0; i < NumSubscribedTopicsPerSubscriber; ++i, topicIndex += topicIndexStep)
{
var topic = allTopics[topicIndex];
_allSubscribedTopics.Add(topic);
var subOptions = new Client.MqttClientSubscribeOptionsBuilder().WithTopicFilter(
new Packets.MqttTopicFilter() { Topic = topic })
.Build();
mqttSubscriber.SubscribeAsync(subOptions).GetAwaiter().GetResult();
}
}
Task.Delay(1000).GetAwaiter().GetResult();
var mqttPublisherClient = mp.Value;
mqttPublisherClient.DisconnectAsync().GetAwaiter().GetResult();
mqttPublisherClient.Dispose();
}
/// <summary>
/// Publish messages and wait for messages sent to subscribers
/// </summary>
[Benchmark]
public void DeliverMessages()
_mqttPublisherClientsByPublisherName.Clear();
foreach (var mqttSubscriber in _mqttSubscriberClients)
{
// There should be one message received per publish for each subscribed topic
_messagesExpectedCount = NumSubscribedTopicsPerSubscriber * NumSubscribers;
// Loop for a while and exchange messages
_messagesReceivedCount = 0;
_cancellationTokenSource = new CancellationTokenSource();
// same payload for all messages
var payload = new byte[] { 1, 2, 3, 4 };
var tasks = new List<Task>();
// publish a message for each subscribed topic
foreach (var topic in _allSubscribedTopics)
{
var message = new MqttApplicationMessageBuilder()
.WithTopic(topic)
.WithPayload(payload)
.Build();
// pick the correct publisher
var publisherName = _publisherByTopic[topic];
var publisherClient = _mqttPublisherClientsByPublisherName[publisherName];
_ = publisherClient.PublishAsync(message);
}
// Wait one message per publish to be received by subscriber (in the subscriber's application message handler)
try
{
Task.Delay(30000, _cancellationTokenSource.Token).GetAwaiter().GetResult();
}
catch
{
}
_cancellationTokenSource.Dispose();
if (_messagesReceivedCount < _messagesExpectedCount)
{
throw new Exception(string.Format("Messages Received Count mismatch, expected {0}, received {1}", _messagesExpectedCount, _messagesReceivedCount));
}
mqttSubscriber.DisconnectAsync().GetAwaiter().GetResult();
mqttSubscriber.Dispose();
}
[GlobalCleanup]
public void Cleanup()
_mqttSubscriberClients.Clear();
_mqttServer.StopAsync().GetAwaiter().GetResult();
_mqttServer.Dispose();
_mqttServer = null;
}
/// <summary>
/// Publish messages and wait for messages sent to subscribers
/// </summary>
[Benchmark]
public void DeliverMessages()
{
// There should be one message received per publish for each subscribed topic
_messagesExpectedCount = NumSubscribedTopicsPerSubscriber * NumSubscribers;
// Loop for a while and exchange messages
_messagesReceivedCount = 0;
_cancellationTokenSource = new CancellationTokenSource();
// same payload for all messages
var payload = new byte[] { 1, 2, 3, 4 };
var tasks = new List<Task>();
// publish a message for each subscribed topic
foreach (var topic in _allSubscribedTopics)
{
foreach (var mp in _mqttPublisherClientsByPublisherName)
{
var mqttPublisherClient = mp.Value;
mqttPublisherClient.DisconnectAsync().GetAwaiter().GetResult();
mqttPublisherClient.Dispose();
}
_mqttPublisherClientsByPublisherName.Clear();
var message = new MqttApplicationMessageBuilder().WithTopic(topic).WithPayload(payload).Build();
// pick the correct publisher
var publisherName = _publisherByTopic[topic];
var publisherClient = _mqttPublisherClientsByPublisherName[publisherName];
_ = publisherClient.PublishAsync(message);
}
foreach (var mqttSubscriber in _mqttSubscriberClients)
{
mqttSubscriber.DisconnectAsync().GetAwaiter().GetResult();
mqttSubscriber.Dispose();
}
_mqttSubscriberClients.Clear();
// Wait one message per publish to be received by subscriber (in the subscriber's application message handler)
try
{
Task.Delay(30000, _cancellationTokenSource.Token).GetAwaiter().GetResult();
}
catch
{
}
_mqttServer.StopAsync().GetAwaiter().GetResult();
_mqttServer.Dispose();
_mqttServer = null;
_cancellationTokenSource.Dispose();
if (_messagesReceivedCount < _messagesExpectedCount)
{
throw new Exception(string.Format("Messages Received Count mismatch, expected {0}, received {1}", _messagesExpectedCount, _messagesReceivedCount));
}
}
}
[GlobalSetup]
public void Setup()
{
_lockMsgCount = new object();
Dictionary<string, List<string>> singleWildcardTopicsByPublisher;
Dictionary<string, List<string>> multiWildcardTopicsByPublisher;
TopicGenerator.Generate(NumPublishers, NumTopicsPerPublisher, out _topicsByPublisher, out singleWildcardTopicsByPublisher, out multiWildcardTopicsByPublisher);
var topics = _topicsByPublisher.First().Value;
_topicPublishMessages = new List<MqttApplicationMessage>();
// Prepare messages, same for each publisher
foreach (var topic in topics)
{
var message = new MqttApplicationMessageBuilder().WithTopic(topic).WithPayload(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }).Build();
_topicPublishMessages.Add(message);
}
// Create server
var serverFactory = new MqttServerFactory();
var clientFactory = new MqttClientFactory();
var serverOptions = new MqttServerOptionsBuilder().WithDefaultEndpoint().Build();
_mqttServer = serverFactory.CreateMqttServer(serverOptions);
_mqttServer.StartAsync().GetAwaiter().GetResult();
// Create publisher clients
_mqttPublisherClientsByPublisherName = new Dictionary<string, IMqttClient>();
foreach (var pt in _topicsByPublisher)
{
var publisherName = pt.Key;
var mqttClient = clientFactory.CreateMqttClient();
var publisherOptions = new MqttClientOptionsBuilder().WithTcpServer("localhost").WithClientId(publisherName).WithKeepAlivePeriod(TimeSpan.FromSeconds(30)).Build();
mqttClient.ConnectAsync(publisherOptions).GetAwaiter().GetResult();
_mqttPublisherClientsByPublisherName.Add(publisherName, mqttClient);
}
// Create subscriber clients
_mqttSubscriberClients = new List<IMqttClient>();
for (var i = 0; i < NumSubscribers; i++)
{
var mqttSubscriberClient = clientFactory.CreateMqttClient();
_mqttSubscriberClients.Add(mqttSubscriberClient);
var subscriberOptions = new MqttClientOptionsBuilder().WithTcpServer("localhost").WithClientId("subscriber" + i).Build();
mqttSubscriberClient.ApplicationMessageReceivedAsync += r =>
{
// count messages and signal cancellation when expected message count is reached
lock (_lockMsgCount)
{
++_messagesReceivedCount;
if (_messagesReceivedCount == _messagesExpectedCount)
{
_cancellationTokenSource.Cancel();
}
}
return Task.CompletedTask;
};
mqttSubscriberClient.ConnectAsync(subscriberOptions).GetAwaiter().GetResult();
}
var allTopics = new List<string>();
_publisherByTopic = new Dictionary<string, string>();
foreach (var t in _topicsByPublisher)
{
foreach (var topic in t.Value)
{
_publisherByTopic.Add(topic, t.Key);
allTopics.Add(topic);
}
}
// Subscribe to NumSubscribedTopics topics spread across all topics
_allSubscribedTopics = new List<string>();
var totalNumTopics = NumPublishers * NumTopicsPerPublisher;
var topicIndexStep = totalNumTopics / (NumSubscribedTopicsPerSubscriber * NumSubscribers);
if (topicIndexStep * NumSubscribedTopicsPerSubscriber * NumSubscribers != totalNumTopics)
{
throw new Exception(
string.Format(
"The total number of topics must be divisible by the number of subscribed topics across all subscribers. Total number of topics: {0}, topic step: {1}",
totalNumTopics,
topicIndexStep));
}
var topicIndex = 0;
foreach (var mqttSubscriber in _mqttSubscriberClients)
{
for (var i = 0; i < NumSubscribedTopicsPerSubscriber; ++i, topicIndex += topicIndexStep)
{
var topic = allTopics[topicIndex];
_allSubscribedTopics.Add(topic);
var subOptions = new MqttClientSubscribeOptionsBuilder().WithTopicFilter(new MqttTopicFilter { Topic = topic }).Build();
mqttSubscriber.SubscribeAsync(subOptions).GetAwaiter().GetResult();
}
}
Task.Delay(1000).GetAwaiter().GetResult();
}
}
@@ -4,48 +4,45 @@
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using MQTTnet.Client;
using MQTTnet.Server;
namespace MQTTnet.Benchmarks
namespace MQTTnet.Benchmarks;
[SimpleJob(RuntimeMoniker.Net60)]
[RPlotExporter]
[RankColumn]
[MemoryDiagnoser]
public class MessageProcessingBenchmark : BaseBenchmark
{
[SimpleJob(RuntimeMoniker.Net60)]
[RPlotExporter, RankColumn]
[MemoryDiagnoser]
public class MessageProcessingBenchmark : BaseBenchmark
MqttApplicationMessage _message;
IMqttClient _mqttClient;
MqttServer _mqttServer;
[Benchmark]
public void Send_10000_Messages()
{
MqttServer _mqttServer;
IMqttClient _mqttClient;
MqttApplicationMessage _message;
[GlobalSetup]
public void Setup()
for (var i = 0; i < 10000; i++)
{
var serverOptions = new MqttServerOptionsBuilder().Build();
var factory = new MqttFactory();
_mqttServer = factory.CreateMqttServer(serverOptions);
_mqttClient = factory.CreateMqttClient();
_mqttServer.StartAsync().GetAwaiter().GetResult();
var clientOptions = new MqttClientOptionsBuilder()
.WithTcpServer("localhost").Build();
_mqttClient.ConnectAsync(clientOptions).GetAwaiter().GetResult();
_message = new MqttApplicationMessageBuilder()
.WithTopic("A")
.Build();
}
[Benchmark]
public void Send_10000_Messages()
{
for (var i = 0; i < 10000; i++)
{
_mqttClient.PublishAsync(_message).GetAwaiter().GetResult();
}
_mqttClient.PublishAsync(_message).GetAwaiter().GetResult();
}
}
}
[GlobalSetup]
public void Setup()
{
var serverOptions = new MqttServerOptionsBuilder().Build();
var serverFactory = new MqttServerFactory();
_mqttServer = serverFactory.CreateMqttServer(serverOptions);
var clientFactory = new MqttClientFactory();
_mqttClient = clientFactory.CreateMqttClient();
_mqttServer.StartAsync().GetAwaiter().GetResult();
var clientOptions = new MqttClientOptionsBuilder().WithTcpServer("localhost").Build();
_mqttClient.ConnectAsync(clientOptions).GetAwaiter().GetResult();
_message = new MqttApplicationMessageBuilder().WithTopic("A").Build();
}
}
@@ -4,12 +4,10 @@
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using MQTTnet.Client;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using MQTTnet.AspNetCore.Client;
using MQTTnet.AspNetCore;
using MQTTnet.Diagnostics;
using MQTTnet.Diagnostics.Logger;
namespace MQTTnet.Benchmarks
{
@@ -38,7 +36,7 @@ namespace MQTTnet.Benchmarks
})
.Build();
var factory = new MqttFactory();
var factory = new MqttClientFactory();
_mqttClient = factory.CreateMqttClient(new MqttNetEventLogger(), new MqttClientConnectionContextFactory());
_host.StartAsync().GetAwaiter().GetResult();
@@ -30,17 +30,17 @@ namespace MQTTnet.Benchmarks
var writer = new MqttBufferWriter(4096, 65535);
writer.WriteString("A relative short string.");
writer.WriteBinaryData(_demoPayload);
writer.WriteBinary(_demoPayload);
writer.WriteByte(0x01);
writer.WriteByte(0x02);
writer.WriteVariableByteInteger(5647382);
writer.WriteString("A relative short string.");
writer.WriteVariableByteInteger(8574489);
writer.WriteBinaryData(_demoPayload);
writer.WriteBinary(_demoPayload);
writer.WriteByte(2);
writer.WriteByte(0x02);
writer.WriteString("fjgffiogfhgfhoihgoireghreghreguhreguireoghreouighreouighreughreguiorehreuiohruiorehreuioghreug");
writer.WriteBinaryData(_demoPayload);
writer.WriteBinary(_demoPayload);
_readPayload = new ArraySegment<byte>(writer.GetBuffer(), 0, writer.Length).ToArray();
}
@@ -83,11 +83,11 @@ namespace MQTTnet.Benchmarks
writer.WriteVariableByteInteger(5647382);
writer.WriteString("A relative short string.");
writer.WriteVariableByteInteger(8574589);
writer.WriteBinaryData(_demoPayload);
writer.WriteBinary(_demoPayload);
writer.WriteByte(2);
writer.WriteByte(0x02);
writer.WriteString("fjgffiogfhgfhoihgoireghreghreguhreguireoghreouighreouighreughreguiorehreuiohruiorehreuioghreug");
writer.WriteBinaryData(_demoPayload);
writer.WriteBinary(_demoPayload);
writer.Reset(0);
}
@@ -3,89 +3,86 @@
// See the LICENSE file in the project root for more information.
using System;
using BenchmarkDotNet.Attributes;
using MQTTnet.Channel;
using MQTTnet.Implementations;
using MQTTnet.Server;
using System.Buffers;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using MQTTnet.Client;
using MQTTnet.Diagnostics;
using MQTTnet.Channel;
using MQTTnet.Diagnostics.Logger;
using MQTTnet.Implementations;
using MQTTnet.Server;
using MQTTnet.Server.Internal.Adapter;
namespace MQTTnet.Benchmarks
namespace MQTTnet.Benchmarks;
[SimpleJob(RuntimeMoniker.Net60)]
[MemoryDiagnoser]
public class MqttTcpChannelBenchmark : BaseBenchmark
{
[SimpleJob(RuntimeMoniker.Net60)]
[MemoryDiagnoser]
public class MqttTcpChannelBenchmark : BaseBenchmark
IMqttChannel _clientChannel;
MqttServer _mqttServer;
IMqttChannel _serverChannel;
[Benchmark]
public async Task Send_10000_Chunks()
{
MqttServer _mqttServer;
IMqttChannel _serverChannel;
IMqttChannel _clientChannel;
var size = 5;
var iterations = 10000;
[GlobalSetup]
public void Setup()
await Task.WhenAll(WriteAsync(iterations, size), ReadAsync(iterations, size));
}
[GlobalSetup]
public void Setup()
{
var serverFactory = new MqttServerFactory();
var tcpServer = new MqttTcpServerAdapter();
tcpServer.ClientHandler += args =>
{
var factory = new MqttFactory();
var tcpServer = new MqttTcpServerAdapter();
tcpServer.ClientHandler += args =>
{
_serverChannel =
(IMqttChannel)args.GetType().GetField("_channel",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
.GetValue(args);
_serverChannel = (IMqttChannel)args.GetType().GetField("_channel", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(args);
return Task.CompletedTask;
};
return Task.CompletedTask;
};
var serverOptions = new MqttServerOptionsBuilder().Build();
_mqttServer = factory.CreateMqttServer(serverOptions, new[] { tcpServer }, new MqttNetEventLogger());
var serverOptions = new MqttServerOptionsBuilder().Build();
_mqttServer = serverFactory.CreateMqttServer(serverOptions, new[] { tcpServer }, new MqttNetEventLogger());
_mqttServer.StartAsync().GetAwaiter().GetResult();
var clientOptions = new MqttClientOptionsBuilder()
.WithTcpServer("localhost").Build();
_mqttServer.StartAsync().GetAwaiter().GetResult();
var tcpOptions = (MqttClientTcpOptions)clientOptions.ChannelOptions;
_clientChannel = new MqttTcpChannel(new MqttClientOptions { ChannelOptions = tcpOptions });
var clientOptions = new MqttClientOptionsBuilder().WithTcpServer("localhost").Build();
_clientChannel.ConnectAsync(CancellationToken.None).GetAwaiter().GetResult();
var tcpOptions = (MqttClientTcpOptions)clientOptions.ChannelOptions;
_clientChannel = new MqttTcpChannel(new MqttClientOptions { ChannelOptions = tcpOptions });
_clientChannel.ConnectAsync(CancellationToken.None).GetAwaiter().GetResult();
}
async Task ReadAsync(int iterations, int size)
{
await Task.Yield();
var expected = iterations * size;
long read = 0;
while (read < expected)
{
var readResult = await _clientChannel.ReadAsync(new byte[size], 0, size, CancellationToken.None).ConfigureAwait(false);
read += readResult;
}
}
[Benchmark]
public async Task Send_10000_Chunks()
async Task WriteAsync(int iterations, int size)
{
await Task.Yield();
var buffer = new ReadOnlySequence<byte>(new byte[size]);
for (var i = 0; i < iterations; i++)
{
var size = 5;
var iterations = 10000;
await Task.WhenAll(WriteAsync(iterations, size), ReadAsync(iterations, size));
}
async Task ReadAsync(int iterations, int size)
{
await Task.Yield();
var expected = iterations * size;
long read = 0;
while (read < expected)
{
var readResult = await _clientChannel.ReadAsync(new byte[size], 0, size, CancellationToken.None).ConfigureAwait(false);
read += readResult;
}
}
async Task WriteAsync(int iterations, int size)
{
await Task.Yield();
var buffer = new ArraySegment<byte>(new byte[size]);
for (var i = 0; i < iterations; i++)
{
await _serverChannel.WriteAsync(buffer, true, CancellationToken.None).ConfigureAwait(false);
}
await _serverChannel.WriteAsync(buffer, true, CancellationToken.None).ConfigureAwait(false);
}
}
}
+3 -3
View File
@@ -71,7 +71,7 @@ namespace MQTTnet.Benchmarks
{
if (type != typeof(BaseBenchmark))
{
benchmarks.Add(type);
benchmarks.Add(type);
}
}
}
@@ -87,7 +87,7 @@ namespace MQTTnet.Benchmarks
}
// Allow for preselection to avoid developer frustration.
if (int.TryParse(arguments[0], out var benchmarkIndex))
{
_selectedBenchmarkIndex = benchmarkIndex;
@@ -111,7 +111,7 @@ namespace MQTTnet.Benchmarks
{
Console.Clear();
Console.WriteLine($"MQTTnet - Benchmarks ({TargetFrameworkProvider.TargetFramework})");
Console.WriteLine($"MQTTnet - Benchmarks");
Console.WriteLine("-----------------------------------------------");
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine("Press arrow keys for benchmark selection");
@@ -31,12 +31,12 @@ namespace MQTTnet.Benchmarks
.WithPayload(new byte[10 * 1024])
.Build();
var packet = MqttPacketFactories.Publish.Create(mqttMessage);
var packet = MqttPublishPacketFactory.Create(mqttMessage);
var buffer = mqttPacketFormatter.Encode(packet);
stream = new MemoryStream();
stream.Write(buffer.Packet);
stream.Write(buffer.Payload);
stream.Write(buffer.Payload.ToArray());
mqttPacketFormatter.Cleanup();
}
@@ -2,6 +2,7 @@ using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using MQTTnet.Formatter;
using System;
using System.Buffers;
using System.IO;
using System.IO.Pipelines;
using System.Threading.Tasks;
@@ -40,7 +41,7 @@ namespace MQTTnet.Benchmarks
stream.Position = 0;
var output = PipeWriter.Create(stream);
if (buffer.Payload.Count == 0)
if (buffer.Payload.Length == 0)
{
await output.WriteAsync(buffer.Packet).ConfigureAwait(false);
}
@@ -60,7 +61,7 @@ namespace MQTTnet.Benchmarks
var span = output.GetSpan(buffer.Length);
buffer.Packet.AsSpan().CopyTo(span);
buffer.Payload.AsSpan().CopyTo(span.Slice(buffer.Packet.Count));
buffer.Payload.CopyTo(span.Slice(buffer.Packet.Count));
output.Advance(buffer.Length);
}
@@ -13,7 +13,8 @@ using MQTTnet.Channel;
using MQTTnet.Formatter;
using MQTTnet.Formatter.V3;
using BenchmarkDotNet.Jobs;
using MQTTnet.Diagnostics;
using MQTTnet.Diagnostics.Logger;
using System.Buffers;
namespace MQTTnet.Benchmarks
{
@@ -54,7 +55,7 @@ namespace MQTTnet.Benchmarks
public void Deserialize_10000_Messages()
{
var channel = new BenchmarkMqttChannel(_serializedPacket);
var reader = new MqttChannelAdapter(channel, new MqttPacketFormatterAdapter(new MqttBufferWriter(4096, 65535)), null, new MqttNetEventLogger());
var reader = new MqttChannelAdapter(channel, new MqttPacketFormatterAdapter(new MqttBufferWriter(4096, 65535)), new MqttNetEventLogger());
for (var i = 0; i < 10000; i++)
{
@@ -104,7 +105,7 @@ namespace MQTTnet.Benchmarks
return Task.FromResult(count);
}
public Task WriteAsync(ArraySegment<byte> buffer, bool isEndOfPacket, CancellationToken cancellationToken)
public Task WriteAsync(ReadOnlySequence<byte> buffer, bool isEndOfPacket, CancellationToken cancellationToken)
{
throw new NotSupportedException();
}
+44 -50
View File
@@ -2,65 +2,59 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using MQTTnet.Client;
using MQTTnet.Server;
using System.Collections.Generic;
using System.Linq;
using BenchmarkDotNet.Attributes;
using MQTTnet.Server;
namespace MQTTnet.Benchmarks
namespace MQTTnet.Benchmarks;
[MemoryDiagnoser]
public class SubscribeBenchmark : BaseBenchmark
{
[MemoryDiagnoser]
public class SubscribeBenchmark : BaseBenchmark
const int NumPublishers = 1;
const int NumTopicsPerPublisher = 10000;
IMqttClient _mqttClient;
MqttServer _mqttServer;
List<string> _topics;
[GlobalCleanup]
public void Cleanup()
{
MqttServer _mqttServer;
IMqttClient _mqttClient;
_mqttClient.DisconnectAsync().GetAwaiter().GetResult();
_mqttServer.StopAsync().GetAwaiter().GetResult();
_mqttServer.Dispose();
}
const int NumPublishers = 1;
const int NumTopicsPerPublisher = 10000;
[GlobalSetup]
public void Setup()
{
TopicGenerator.Generate(NumPublishers, NumTopicsPerPublisher, out var topicsByPublisher, out var singleWildcardTopicsByPublisher, out var multiWildcardTopicsByPublisher);
_topics = topicsByPublisher.Values.First();
List<string> _topics;
var serverOptions = new MqttServerOptionsBuilder().WithDefaultEndpoint().Build();
[GlobalSetup]
public void Setup()
var serverFactory = new MqttServerFactory();
_mqttServer = serverFactory.CreateMqttServer(serverOptions);
var clientFactory = new MqttClientFactory();
_mqttClient = clientFactory.CreateMqttClient();
_mqttServer.StartAsync().GetAwaiter().GetResult();
var clientOptions = new MqttClientOptionsBuilder().WithTcpServer("localhost").Build();
_mqttClient.ConnectAsync(clientOptions).GetAwaiter().GetResult();
}
[Benchmark]
public void Subscribe_10000_Topics()
{
foreach (var topic in _topics)
{
TopicGenerator.Generate(NumPublishers, NumTopicsPerPublisher, out var topicsByPublisher, out var singleWildcardTopicsByPublisher, out var multiWildcardTopicsByPublisher);
_topics = topicsByPublisher.Values.First();
var subscribeOptions = new MqttClientSubscribeOptionsBuilder().WithTopicFilter(topic).Build();
var serverOptions = new MqttServerOptionsBuilder().WithDefaultEndpoint().Build();
var factory = new MqttFactory();
_mqttServer = factory.CreateMqttServer(serverOptions);
_mqttClient = factory.CreateMqttClient();
_mqttServer.StartAsync().GetAwaiter().GetResult();
var clientOptions = new MqttClientOptionsBuilder()
.WithTcpServer("localhost").Build();
_mqttClient.ConnectAsync(clientOptions).GetAwaiter().GetResult();
}
[GlobalCleanup]
public void Cleanup()
{
_mqttClient.DisconnectAsync().GetAwaiter().GetResult();
_mqttServer.StopAsync().GetAwaiter().GetResult();
_mqttServer.Dispose();
}
[Benchmark]
public void Subscribe_10000_Topics()
{
foreach (var topic in _topics)
{
var subscribeOptions = new MqttClientSubscribeOptionsBuilder()
.WithTopicFilter(topic, Protocol.MqttQualityOfServiceLevel.AtMostOnce)
.Build();
_mqttClient.SubscribeAsync(subscribeOptions).GetAwaiter().GetResult();
}
_mqttClient.SubscribeAsync(subscribeOptions).GetAwaiter().GetResult();
}
}
}
}
@@ -9,7 +9,7 @@ using System.Threading;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using MQTTnet.AspNetCore.Client.Tcp;
using MQTTnet.AspNetCore;
namespace MQTTnet.Benchmarks
{
@@ -29,12 +29,12 @@ namespace MQTTnet.Benchmarks
var task = Task.Run(() => server.AcceptSocket());
var clientConnection = new TcpConnection(new IPEndPoint(IPAddress.Loopback, 1883));
var clientConnection = new SocketConnection(new IPEndPoint(IPAddress.Loopback, 1883));
clientConnection.StartAsync().GetAwaiter().GetResult();
_client = clientConnection.Transport;
var serverConnection = new TcpConnection(task.GetAwaiter().GetResult());
var serverConnection = new SocketConnection(task.GetAwaiter().GetResult());
serverConnection.StartAsync().GetAwaiter().GetResult();
_server = serverConnection.Transport;
}
@@ -2,63 +2,56 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using BenchmarkDotNet.Attributes;
using MQTTnet.Client;
using MQTTnet.Server;
using System.Collections.Generic;
using System.Linq;
using BenchmarkDotNet.Attributes;
using MQTTnet.Server;
namespace MQTTnet.Benchmarks
namespace MQTTnet.Benchmarks;
[MemoryDiagnoser]
public class UnsubscribeBenchmark : BaseBenchmark
{
[MemoryDiagnoser]
public class UnsubscribeBenchmark : BaseBenchmark
const int NumPublishers = 1;
const int NumTopicsPerPublisher = 10000;
IMqttClient _mqttClient;
MqttServer _mqttServer;
List<string> _topics;
[GlobalSetup]
public void Setup()
{
MqttServer _mqttServer;
IMqttClient _mqttClient;
TopicGenerator.Generate(NumPublishers, NumTopicsPerPublisher, out var topicsByPublisher, out var singleWildcardTopicsByPublisher, out var multiWildcardTopicsByPublisher);
_topics = topicsByPublisher.Values.First();
const int NumPublishers = 1;
const int NumTopicsPerPublisher = 10000;
var serverOptions = new MqttServerOptionsBuilder().WithDefaultEndpoint().Build();
List<string> _topics;
var serverFactory = new MqttServerFactory();
_mqttServer = serverFactory.CreateMqttServer(serverOptions);
var clientFactory = new MqttClientFactory();
_mqttClient = clientFactory.CreateMqttClient();
[GlobalSetup]
public void Setup()
_mqttServer.StartAsync().GetAwaiter().GetResult();
var clientOptions = new MqttClientOptionsBuilder().WithTcpServer("localhost").Build();
_mqttClient.ConnectAsync(clientOptions).GetAwaiter().GetResult();
foreach (var topic in _topics)
{
TopicGenerator.Generate(NumPublishers, NumTopicsPerPublisher, out var topicsByPublisher, out var singleWildcardTopicsByPublisher, out var multiWildcardTopicsByPublisher);
_topics = topicsByPublisher.Values.First();
var serverOptions = new MqttServerOptionsBuilder().WithDefaultEndpoint().Build();
var factory = new MqttFactory();
_mqttServer = factory.CreateMqttServer(serverOptions);
_mqttClient = factory.CreateMqttClient();
_mqttServer.StartAsync().GetAwaiter().GetResult();
var clientOptions = new MqttClientOptionsBuilder()
.WithTcpServer("localhost").Build();
_mqttClient.ConnectAsync(clientOptions).GetAwaiter().GetResult();
foreach (var topic in _topics)
{
var subscribeOptions = new MqttClientSubscribeOptionsBuilder()
.WithTopicFilter(topic)
.Build();
_mqttClient.SubscribeAsync(subscribeOptions).GetAwaiter().GetResult();
}
}
[Benchmark]
public void Unsubscribe_10000_Topics()
{
foreach (var topic in _topics)
{
var unsubscribeOptions = new MqttClientUnsubscribeOptionsBuilder()
.WithTopicFilter(topic)
.Build();
_mqttClient.UnsubscribeAsync(unsubscribeOptions).GetAwaiter().GetResult();
}
var subscribeOptions = new MqttClientSubscribeOptionsBuilder().WithTopicFilter(topic).Build();
_mqttClient.SubscribeAsync(subscribeOptions).GetAwaiter().GetResult();
}
}
}
[Benchmark]
public void Unsubscribe_10000_Topics()
{
foreach (var topic in _topics)
{
var unsubscribeOptions = new MqttClientUnsubscribeOptionsBuilder().WithTopicFilter(topic).Build();
_mqttClient.UnsubscribeAsync(unsubscribeOptions).GetAwaiter().GetResult();
}
}
}
@@ -1,24 +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;
namespace MQTTnet.Extensions.ManagedClient
{
public sealed class ApplicationMessageProcessedEventArgs : EventArgs
{
public ApplicationMessageProcessedEventArgs(ManagedMqttApplicationMessage applicationMessage, Exception exception)
{
ApplicationMessage = applicationMessage ?? throw new ArgumentNullException(nameof(applicationMessage));
Exception = exception;
}
public ManagedMqttApplicationMessage ApplicationMessage { get; }
/// <summary>
/// Then this is _null_ the message was processed successfully without any error.
/// </summary>
public Exception Exception { get; }
}
}
@@ -1,18 +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;
namespace MQTTnet.Extensions.ManagedClient
{
public sealed class ApplicationMessageSkippedEventArgs : EventArgs
{
public ApplicationMessageSkippedEventArgs(ManagedMqttApplicationMessage applicationMessage)
{
ApplicationMessage = applicationMessage ?? throw new ArgumentNullException(nameof(applicationMessage));
}
public ManagedMqttApplicationMessage ApplicationMessage { get; }
}
}
@@ -1,21 +0,0 @@
using System;
using MQTTnet.Client;
namespace MQTTnet.Extensions.ManagedClient
{
public sealed class ConnectingFailedEventArgs : EventArgs
{
public ConnectingFailedEventArgs(MqttClientConnectResult connectResult, Exception exception)
{
ConnectResult = connectResult;
Exception = exception;
}
/// <summary>
/// This is null when the connection was failing and the server was not reachable.
/// </summary>
public MqttClientConnectResult ConnectResult { get; }
public Exception Exception { get; }
}
}
@@ -1,52 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using MQTTnet.Client;
using MQTTnet.Packets;
namespace MQTTnet.Extensions.ManagedClient
{
public interface IManagedMqttClient : IDisposable
{
event Func<ApplicationMessageProcessedEventArgs, Task> ApplicationMessageProcessedAsync;
event Func<MqttApplicationMessageReceivedEventArgs, Task> ApplicationMessageReceivedAsync;
event Func<ApplicationMessageSkippedEventArgs, Task> ApplicationMessageSkippedAsync;
event Func<MqttClientConnectedEventArgs, Task> ConnectedAsync;
event Func<ConnectingFailedEventArgs, Task> ConnectingFailedAsync;
event Func<EventArgs, Task> ConnectionStateChangedAsync;
event Func<MqttClientDisconnectedEventArgs, Task> DisconnectedAsync;
event Func<ManagedProcessFailedEventArgs, Task> SynchronizingSubscriptionsFailedAsync;
IMqttClient InternalClient { get; }
bool IsConnected { get; }
bool IsStarted { get; }
ManagedMqttClientOptions Options { get; }
int PendingApplicationMessagesCount { get; }
Task EnqueueAsync(MqttApplicationMessage applicationMessage);
Task EnqueueAsync(ManagedMqttApplicationMessage applicationMessage);
Task PingAsync(CancellationToken cancellationToken = default);
Task StartAsync(ManagedMqttClientOptions options);
Task StopAsync(bool cleanDisconnect = true);
Task SubscribeAsync(ICollection<MqttTopicFilter> topicFilters);
Task UnsubscribeAsync(ICollection<string> topics);
}
}
@@ -1,16 +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.Threading.Tasks;
namespace MQTTnet.Extensions.ManagedClient
{
public interface IManagedMqttClientStorage
{
Task SaveQueuedMessagesAsync(IList<ManagedMqttApplicationMessage> messages);
Task<IList<ManagedMqttApplicationMessage>> LoadQueuedMessagesAsync();
}
}
@@ -1,15 +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;
namespace MQTTnet.Extensions.ManagedClient
{
public class ManagedMqttApplicationMessage
{
public Guid Id { get; set; } = Guid.NewGuid();
public MqttApplicationMessage ApplicationMessage { get; set; }
}
}
@@ -1,51 +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;
namespace MQTTnet.Extensions.ManagedClient
{
public class ManagedMqttApplicationMessageBuilder
{
private Guid _id = Guid.NewGuid();
private MqttApplicationMessage _applicationMessage;
public ManagedMqttApplicationMessageBuilder WithId(Guid id)
{
_id = id;
return this;
}
public ManagedMqttApplicationMessageBuilder WithApplicationMessage(MqttApplicationMessage applicationMessage)
{
_applicationMessage = applicationMessage;
return this;
}
public ManagedMqttApplicationMessageBuilder WithApplicationMessage(Action<MqttApplicationMessageBuilder> builder)
{
if (builder == null) throw new ArgumentNullException(nameof(builder));
var internalBuilder = new MqttApplicationMessageBuilder();
builder(internalBuilder);
_applicationMessage = internalBuilder.Build();
return this;
}
public ManagedMqttApplicationMessage Build()
{
if (_applicationMessage == null)
{
throw new InvalidOperationException("The ApplicationMessage cannot be null.");
}
return new ManagedMqttApplicationMessage
{
Id = _id,
ApplicationMessage = _applicationMessage
};
}
}
}
@@ -1,759 +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;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MQTTnet.Client;
using MQTTnet.Diagnostics;
using MQTTnet.Exceptions;
using MQTTnet.Internal;
using MQTTnet.Packets;
using MQTTnet.Protocol;
using MQTTnet.Server;
namespace MQTTnet.Extensions.ManagedClient
{
public sealed class ManagedMqttClient : Disposable, IManagedMqttClient
{
readonly AsyncEvent<ApplicationMessageProcessedEventArgs> _applicationMessageProcessedEvent = new AsyncEvent<ApplicationMessageProcessedEventArgs>();
readonly AsyncEvent<ApplicationMessageSkippedEventArgs> _applicationMessageSkippedEvent = new AsyncEvent<ApplicationMessageSkippedEventArgs>();
readonly AsyncEvent<ConnectingFailedEventArgs> _connectingFailedEvent = new AsyncEvent<ConnectingFailedEventArgs>();
readonly AsyncEvent<EventArgs> _connectionStateChangedEvent = new AsyncEvent<EventArgs>();
readonly AsyncEvent<ManagedProcessFailedEventArgs> _synchronizingSubscriptionsFailedEvent = new AsyncEvent<ManagedProcessFailedEventArgs>();
readonly MqttNetSourceLogger _logger;
readonly BlockingQueue<ManagedMqttApplicationMessage> _messageQueue = new BlockingQueue<ManagedMqttApplicationMessage>();
readonly AsyncLock _messageQueueLock = new AsyncLock();
/// <summary>
/// The subscriptions are managed in 2 separate buckets:
/// <see
/// cref="_subscriptions" />
/// and
/// <see
/// cref="_unsubscriptions" />
/// are processed during normal operation
/// and are moved to the
/// <see
/// cref="_reconnectSubscriptions" />
/// when they get processed. They can be accessed by
/// any thread and are therefore mutex'ed.
/// <see
/// cref="_reconnectSubscriptions" />
/// get sent to the broker
/// at reconnect and are solely owned by
/// <see
/// cref="MaintainConnectionAsync" />
/// .
/// </summary>
readonly Dictionary<string, MqttTopicFilter> _reconnectSubscriptions = new Dictionary<string, MqttTopicFilter>();
readonly Dictionary<string, MqttTopicFilter> _subscriptions = new Dictionary<string, MqttTopicFilter>();
readonly SemaphoreSlim _subscriptionsQueuedSignal = new SemaphoreSlim(0);
readonly HashSet<string> _unsubscriptions = new HashSet<string>();
CancellationTokenSource _connectionCancellationToken;
Task _maintainConnectionTask;
CancellationTokenSource _publishingCancellationToken;
ManagedMqttClientStorageManager _storageManager;
bool _isCleanDisconnect;
public ManagedMqttClient(IMqttClient mqttClient, IMqttNetLogger logger)
{
InternalClient = mqttClient ?? throw new ArgumentNullException(nameof(mqttClient));
if (logger == null)
{
throw new ArgumentNullException(nameof(logger));
}
_logger = logger.WithSource(nameof(ManagedMqttClient));
}
public event Func<ApplicationMessageSkippedEventArgs, Task> ApplicationMessageSkippedAsync
{
add => _applicationMessageSkippedEvent.AddHandler(value);
remove => _applicationMessageSkippedEvent.RemoveHandler(value);
}
public event Func<ApplicationMessageProcessedEventArgs, Task> ApplicationMessageProcessedAsync
{
add => _applicationMessageProcessedEvent.AddHandler(value);
remove => _applicationMessageProcessedEvent.RemoveHandler(value);
}
public event Func<MqttApplicationMessageReceivedEventArgs, Task> ApplicationMessageReceivedAsync
{
add => InternalClient.ApplicationMessageReceivedAsync += value;
remove => InternalClient.ApplicationMessageReceivedAsync -= value;
}
public event Func<MqttClientConnectedEventArgs, Task> ConnectedAsync
{
add => InternalClient.ConnectedAsync += value;
remove => InternalClient.ConnectedAsync -= value;
}
public event Func<ConnectingFailedEventArgs, Task> ConnectingFailedAsync
{
add => _connectingFailedEvent.AddHandler(value);
remove => _connectingFailedEvent.RemoveHandler(value);
}
public event Func<EventArgs, Task> ConnectionStateChangedAsync
{
add => _connectionStateChangedEvent.AddHandler(value);
remove => _connectionStateChangedEvent.RemoveHandler(value);
}
public event Func<MqttClientDisconnectedEventArgs, Task> DisconnectedAsync
{
add => InternalClient.DisconnectedAsync += value;
remove => InternalClient.DisconnectedAsync -= value;
}
public event Func<ManagedProcessFailedEventArgs, Task> SynchronizingSubscriptionsFailedAsync
{
add => _synchronizingSubscriptionsFailedEvent.AddHandler(value);
remove => _synchronizingSubscriptionsFailedEvent.RemoveHandler(value);
}
public IMqttClient InternalClient { get; }
public bool IsConnected => InternalClient.IsConnected;
public bool IsStarted => _connectionCancellationToken != null;
public ManagedMqttClientOptions Options { get; private set; }
public int PendingApplicationMessagesCount => _messageQueue.Count;
public async Task EnqueueAsync(MqttApplicationMessage applicationMessage)
{
ThrowIfDisposed();
if (applicationMessage == null)
{
throw new ArgumentNullException(nameof(applicationMessage));
}
var managedMqttApplicationMessage = new ManagedMqttApplicationMessageBuilder().WithApplicationMessage(applicationMessage);
await EnqueueAsync(managedMqttApplicationMessage.Build()).ConfigureAwait(false);
}
public async Task EnqueueAsync(ManagedMqttApplicationMessage applicationMessage)
{
ThrowIfDisposed();
if (applicationMessage == null)
{
throw new ArgumentNullException(nameof(applicationMessage));
}
if (Options == null)
{
throw new InvalidOperationException("call StartAsync before publishing messages");
}
MqttTopicValidator.ThrowIfInvalid(applicationMessage.ApplicationMessage);
ManagedMqttApplicationMessage removedMessage = null;
ApplicationMessageSkippedEventArgs applicationMessageSkippedEventArgs = null;
try
{
using (await _messageQueueLock.EnterAsync().ConfigureAwait(false))
{
if (_messageQueue.Count >= Options.MaxPendingMessages)
{
if (Options.PendingMessagesOverflowStrategy == MqttPendingMessagesOverflowStrategy.DropNewMessage)
{
_logger.Verbose("Skipping publish of new application message because internal queue is full.");
applicationMessageSkippedEventArgs = new ApplicationMessageSkippedEventArgs(applicationMessage);
return;
}
if (Options.PendingMessagesOverflowStrategy == MqttPendingMessagesOverflowStrategy.DropOldestQueuedMessage)
{
removedMessage = _messageQueue.RemoveFirst();
_logger.Verbose("Removed oldest application message from internal queue because it is full.");
applicationMessageSkippedEventArgs = new ApplicationMessageSkippedEventArgs(removedMessage);
}
}
_messageQueue.Enqueue(applicationMessage);
if (_storageManager != null)
{
if (removedMessage != null)
{
await _storageManager.RemoveAsync(removedMessage).ConfigureAwait(false);
}
await _storageManager.AddAsync(applicationMessage).ConfigureAwait(false);
}
}
}
finally
{
if (applicationMessageSkippedEventArgs != null && _applicationMessageSkippedEvent.HasHandlers)
{
await _applicationMessageSkippedEvent.InvokeAsync(applicationMessageSkippedEventArgs).ConfigureAwait(false);
}
}
}
public Task PingAsync(CancellationToken cancellationToken = default)
{
return InternalClient.PingAsync(cancellationToken);
}
public async Task StartAsync(ManagedMqttClientOptions options)
{
ThrowIfDisposed();
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
if (options.ClientOptions == null)
{
throw new ArgumentException("The client options are not set.", nameof(options));
}
if (!_maintainConnectionTask?.IsCompleted ?? false)
{
throw new InvalidOperationException("The managed client is already started.");
}
Options = options;
if (options.Storage != null)
{
_storageManager = new ManagedMqttClientStorageManager(options.Storage);
var messages = await _storageManager.LoadQueuedMessagesAsync().ConfigureAwait(false);
foreach (var message in messages)
{
_messageQueue.Enqueue(message);
}
}
var cancellationTokenSource = new CancellationTokenSource();
var cancellationToken = cancellationTokenSource.Token;
_connectionCancellationToken = cancellationTokenSource;
_maintainConnectionTask = Task.Run(() => MaintainConnectionAsync(cancellationToken), cancellationToken);
_maintainConnectionTask.RunInBackground(_logger);
_logger.Info("Started");
}
public async Task StopAsync(bool cleanDisconnect = true)
{
ThrowIfDisposed();
_isCleanDisconnect = cleanDisconnect;
StopPublishing();
StopMaintainingConnection();
_messageQueue.Clear();
if (_maintainConnectionTask != null)
{
await Task.WhenAny(_maintainConnectionTask);
_maintainConnectionTask = null;
}
}
public Task SubscribeAsync(ICollection<MqttTopicFilter> topicFilters)
{
ThrowIfDisposed();
if (topicFilters == null)
{
throw new ArgumentNullException(nameof(topicFilters));
}
foreach (var topicFilter in topicFilters)
{
MqttTopicValidator.ThrowIfInvalidSubscribe(topicFilter.Topic);
}
lock (_subscriptions)
{
foreach (var topicFilter in topicFilters)
{
_subscriptions[topicFilter.Topic] = topicFilter;
_unsubscriptions.Remove(topicFilter.Topic);
}
}
_subscriptionsQueuedSignal.Release();
return CompletedTask.Instance;
}
public Task UnsubscribeAsync(ICollection<string> topics)
{
ThrowIfDisposed();
if (topics == null)
{
throw new ArgumentNullException(nameof(topics));
}
lock (_subscriptions)
{
foreach (var topic in topics)
{
_subscriptions.Remove(topic);
_unsubscriptions.Add(topic);
}
}
_subscriptionsQueuedSignal.Release();
return CompletedTask.Instance;
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
StopPublishing();
StopMaintainingConnection();
if (_maintainConnectionTask != null)
{
_maintainConnectionTask.GetAwaiter().GetResult();
_maintainConnectionTask = null;
}
_messageQueue.Dispose();
_messageQueueLock.Dispose();
InternalClient.Dispose();
_subscriptionsQueuedSignal.Dispose();
}
base.Dispose(disposing);
}
static TimeSpan GetRemainingTime(DateTime endTime)
{
var remainingTime = endTime - DateTime.UtcNow;
return remainingTime < TimeSpan.Zero ? TimeSpan.Zero : remainingTime;
}
async Task HandleSubscriptionExceptionAsync(Exception exception)
{
_logger.Warning(exception, "Synchronizing subscriptions failed.");
if (_synchronizingSubscriptionsFailedEvent.HasHandlers)
{
await _synchronizingSubscriptionsFailedEvent.InvokeAsync(new ManagedProcessFailedEventArgs(exception)).ConfigureAwait(false);
}
}
async Task MaintainConnectionAsync(CancellationToken cancellationToken)
{
try
{
while (!cancellationToken.IsCancellationRequested)
{
await TryMaintainConnectionAsync(cancellationToken).ConfigureAwait(false);
}
}
catch (OperationCanceledException)
{
}
catch (Exception exception)
{
_logger.Error(exception, "Error exception while maintaining connection.");
}
finally
{
if (!IsDisposed)
{
try
{
if (_isCleanDisconnect)
{
using (var disconnectTimeout = new CancellationTokenSource(Options.ClientOptions.Timeout))
{
await InternalClient.DisconnectAsync(new MqttClientDisconnectOptions(), disconnectTimeout.Token).ConfigureAwait(false);
}
}
}
catch (OperationCanceledException)
{
_logger.Warning("Timeout while sending DISCONNECT packet.");
}
catch (Exception exception)
{
_logger.Error(exception, "Error while disconnecting.");
}
_logger.Info("Stopped");
}
_reconnectSubscriptions.Clear();
lock (_subscriptions)
{
_subscriptions.Clear();
_unsubscriptions.Clear();
}
}
}
async Task PublishQueuedMessagesAsync(CancellationToken cancellationToken)
{
try
{
while (!cancellationToken.IsCancellationRequested && InternalClient.IsConnected)
{
// Peek at the message without dequeueing in order to prevent the
// possibility of the queue growing beyond the configured cap.
// Previously, messages could be re-enqueued if there was an
// exception, and this re-enqueueing did not honor the cap.
// Furthermore, because re-enqueueing would shuffle the order
// of the messages, the DropOldestQueuedMessage strategy would
// be unable to know which message is actually the oldest and would
// instead drop the first item in the queue.
var message = _messageQueue.PeekAndWait(cancellationToken);
if (message == null)
{
continue;
}
cancellationToken.ThrowIfCancellationRequested();
await TryPublishQueuedMessageAsync(message).ConfigureAwait(false);
}
}
catch (OperationCanceledException)
{
}
catch (Exception exception)
{
_logger.Error(exception, "Error while publishing queued application messages.");
}
finally
{
_logger.Verbose("Stopped publishing messages.");
}
}
async Task PublishReconnectSubscriptionsAsync()
{
_logger.Info("Publishing subscriptions at reconnect");
try
{
if (_reconnectSubscriptions.Any())
{
var topicFilters = new List<MqttTopicFilter>();
foreach (var sub in _reconnectSubscriptions)
{
topicFilters.Add(sub.Value);
if (topicFilters.Count == Options.MaxTopicFiltersInSubscribeUnsubscribePackets)
{
await SendSubscribeUnsubscribe(topicFilters, null).ConfigureAwait(false);
topicFilters.Clear();
}
}
await SendSubscribeUnsubscribe(topicFilters, null).ConfigureAwait(false);
}
}
catch (Exception exception)
{
await HandleSubscriptionExceptionAsync(exception).ConfigureAwait(false);
}
}
async Task PublishSubscriptionsAsync(TimeSpan timeout, CancellationToken cancellationToken)
{
var endTime = DateTime.UtcNow + timeout;
while (await _subscriptionsQueuedSignal.WaitAsync(GetRemainingTime(endTime), cancellationToken).ConfigureAwait(false))
{
List<MqttTopicFilter> subscriptions;
HashSet<string> unsubscriptions;
lock (_subscriptions)
{
subscriptions = _subscriptions.Values.ToList();
_subscriptions.Clear();
unsubscriptions = new HashSet<string>(_unsubscriptions);
_unsubscriptions.Clear();
}
if (!subscriptions.Any() && !unsubscriptions.Any())
{
continue;
}
_logger.Verbose("Publishing {0} added and {1} removed subscriptions", subscriptions.Count, unsubscriptions.Count);
foreach (var unsubscription in unsubscriptions)
{
_reconnectSubscriptions.Remove(unsubscription);
}
foreach (var subscription in subscriptions)
{
_reconnectSubscriptions[subscription.Topic] = subscription;
}
var addedTopicFilters = new List<MqttTopicFilter>();
foreach (var subscription in subscriptions)
{
addedTopicFilters.Add(subscription);
if (addedTopicFilters.Count == Options.MaxTopicFiltersInSubscribeUnsubscribePackets)
{
await SendSubscribeUnsubscribe(addedTopicFilters, null).ConfigureAwait(false);
addedTopicFilters.Clear();
}
}
await SendSubscribeUnsubscribe(addedTopicFilters, null).ConfigureAwait(false);
var removedTopicFilters = new List<string>();
foreach (var unSub in unsubscriptions)
{
removedTopicFilters.Add(unSub);
if (removedTopicFilters.Count == Options.MaxTopicFiltersInSubscribeUnsubscribePackets)
{
await SendSubscribeUnsubscribe(null, removedTopicFilters).ConfigureAwait(false);
removedTopicFilters.Clear();
}
}
await SendSubscribeUnsubscribe(null, removedTopicFilters).ConfigureAwait(false);
}
}
async Task<ReconnectionResult> ReconnectIfRequiredAsync(CancellationToken cancellationToken)
{
if (InternalClient.IsConnected)
{
return ReconnectionResult.StillConnected;
}
MqttClientConnectResult connectResult = null;
try
{
using (var connectTimeout = new CancellationTokenSource(Options.ClientOptions.Timeout))
{
connectResult = await InternalClient.ConnectAsync(Options.ClientOptions, connectTimeout.Token).ConfigureAwait(false);
}
if (connectResult.ResultCode != MqttClientConnectResultCode.Success)
{
throw new MqttCommunicationException($"Client connected but server denied connection with reason '{connectResult.ResultCode}'.");
}
return connectResult.IsSessionPresent ? ReconnectionResult.Recovered : ReconnectionResult.Reconnected;
}
catch (Exception exception)
{
await _connectingFailedEvent.InvokeAsync(new ConnectingFailedEventArgs(connectResult, exception));
return ReconnectionResult.NotConnected;
}
}
async Task SendSubscribeUnsubscribe(List<MqttTopicFilter> addedSubscriptions, List<string> removedSubscriptions)
{
try
{
if (removedSubscriptions != null && removedSubscriptions.Any())
{
var unsubscribeOptionsBuilder = new MqttClientUnsubscribeOptionsBuilder();
foreach (var removedSubscription in removedSubscriptions)
{
unsubscribeOptionsBuilder.WithTopicFilter(removedSubscription);
}
await InternalClient.UnsubscribeAsync(unsubscribeOptionsBuilder.Build()).ConfigureAwait(false);
}
if (addedSubscriptions != null && addedSubscriptions.Any())
{
var subscribeOptionsBuilder = new MqttClientSubscribeOptionsBuilder();
foreach (var addedSubscription in addedSubscriptions)
{
subscribeOptionsBuilder.WithTopicFilter(addedSubscription);
}
await InternalClient.SubscribeAsync(subscribeOptionsBuilder.Build()).ConfigureAwait(false);
}
}
catch (Exception exception)
{
await HandleSubscriptionExceptionAsync(exception).ConfigureAwait(false);
}
}
void StartPublishing()
{
StopPublishing();
var cancellationTokenSource = new CancellationTokenSource();
var cancellationToken = cancellationTokenSource.Token;
_publishingCancellationToken = cancellationTokenSource;
Task.Run(() => PublishQueuedMessagesAsync(cancellationToken), cancellationToken).RunInBackground(_logger);
}
void StopMaintainingConnection()
{
try
{
_connectionCancellationToken?.Cancel(false);
}
finally
{
_connectionCancellationToken?.Dispose();
_connectionCancellationToken = null;
}
}
void StopPublishing()
{
try
{
_publishingCancellationToken?.Cancel(false);
}
finally
{
_publishingCancellationToken?.Dispose();
_publishingCancellationToken = null;
}
}
async Task TryMaintainConnectionAsync(CancellationToken cancellationToken)
{
try
{
var oldConnectionState = InternalClient.IsConnected;
var connectionState = await ReconnectIfRequiredAsync(cancellationToken).ConfigureAwait(false);
if (connectionState == ReconnectionResult.NotConnected)
{
StopPublishing();
await Task.Delay(Options.AutoReconnectDelay, cancellationToken).ConfigureAwait(false);
}
else if (connectionState == ReconnectionResult.Reconnected)
{
await PublishReconnectSubscriptionsAsync().ConfigureAwait(false);
StartPublishing();
}
else if (connectionState == ReconnectionResult.Recovered)
{
StartPublishing();
}
else if (connectionState == ReconnectionResult.StillConnected)
{
await PublishSubscriptionsAsync(Options.ConnectionCheckInterval, cancellationToken).ConfigureAwait(false);
}
if (oldConnectionState != InternalClient.IsConnected)
{
await _connectionStateChangedEvent.InvokeAsync(EventArgs.Empty).ConfigureAwait(false);
}
}
catch (OperationCanceledException)
{
}
catch (MqttCommunicationException exception)
{
_logger.Warning(exception, "Communication error while maintaining connection.");
}
catch (Exception exception)
{
_logger.Error(exception, "Error exception while maintaining connection.");
}
}
async Task TryPublishQueuedMessageAsync(ManagedMqttApplicationMessage message)
{
Exception transmitException = null;
try
{
await InternalClient.PublishAsync(message.ApplicationMessage).ConfigureAwait(false);
using (await _messageQueueLock.EnterAsync().ConfigureAwait(false)) //lock to avoid conflict with this.PublishAsync
{
// While publishing this message, this.PublishAsync could have booted this
// message off the queue to make room for another (when using a cap
// with the DropOldestQueuedMessage strategy). If the first item
// in the queue is equal to this message, then it's safe to remove
// it from the queue. If not, that means this.PublishAsync has already
// removed it, in which case we don't want to do anything.
_messageQueue.RemoveFirst(i => i.Id.Equals(message.Id));
if (_storageManager != null)
{
await _storageManager.RemoveAsync(message).ConfigureAwait(false);
}
}
}
catch (MqttCommunicationException exception)
{
transmitException = exception;
_logger.Warning(exception, "Publishing application message ({0}) failed.", message.Id);
if (message.ApplicationMessage.QualityOfServiceLevel == MqttQualityOfServiceLevel.AtMostOnce)
{
//If QoS 0, we don't want this message to stay on the queue.
//If QoS 1 or 2, it's possible that, when using a cap, this message
//has been booted off the queue by this.PublishAsync, in which case this
//thread will not continue to try to publish it. While this does
//contradict the expected behavior of QoS 1 and 2, that's also true
//for the usage of a message queue cap, so it's still consistent
//with prior behavior in that way.
using (await _messageQueueLock.EnterAsync().ConfigureAwait(false)) //lock to avoid conflict with this.PublishAsync
{
_messageQueue.RemoveFirst(i => i.Id.Equals(message.Id));
if (_storageManager != null)
{
await _storageManager.RemoveAsync(message).ConfigureAwait(false);
}
}
}
}
catch (Exception exception)
{
transmitException = exception;
_logger.Error(exception, "Error while publishing application message ({0}).", message.Id);
}
finally
{
if (_applicationMessageProcessedEvent.HasHandlers)
{
var eventArgs = new ApplicationMessageProcessedEventArgs(message, transmitException);
await _applicationMessageProcessedEvent.InvokeAsync(eventArgs).ConfigureAwait(false);
}
}
}
}
}
@@ -1,78 +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;
using System.Collections.Generic;
using System.Threading.Tasks;
using MQTTnet.Packets;
using MQTTnet.Protocol;
namespace MQTTnet.Extensions.ManagedClient
{
public static class ManagedMqttClientExtensions
{
public static Task EnqueueAsync(
this IManagedMqttClient managedMqttClient,
string topic,
string payload = null,
MqttQualityOfServiceLevel qualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce,
bool retain = false)
{
if (managedMqttClient == null)
{
throw new ArgumentNullException(nameof(managedMqttClient));
}
if (topic == null)
{
throw new ArgumentNullException(nameof(topic));
}
var applicationMessage = new MqttApplicationMessageBuilder().WithTopic(topic)
.WithPayload(payload)
.WithRetainFlag(retain)
.WithQualityOfServiceLevel(qualityOfServiceLevel)
.Build();
return managedMqttClient.EnqueueAsync(applicationMessage);
}
public static Task SubscribeAsync(
this IManagedMqttClient managedMqttClient,
string topic,
MqttQualityOfServiceLevel qualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce)
{
if (managedMqttClient == null)
{
throw new ArgumentNullException(nameof(managedMqttClient));
}
if (topic == null)
{
throw new ArgumentNullException(nameof(topic));
}
return managedMqttClient.SubscribeAsync(
new List<MqttTopicFilter>
{
new MqttTopicFilterBuilder().WithTopic(topic).WithQualityOfServiceLevel(qualityOfServiceLevel).Build()
});
}
public static Task UnsubscribeAsync(this IManagedMqttClient managedMqttClient, string topic)
{
if (managedMqttClient == null)
{
throw new ArgumentNullException(nameof(managedMqttClient));
}
if (topic == null)
{
throw new ArgumentNullException(nameof(topic));
}
return managedMqttClient.UnsubscribeAsync(new List<string> { topic });
}
}
}
@@ -1,31 +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;
using MQTTnet.Client;
using MQTTnet.Server;
namespace MQTTnet.Extensions.ManagedClient
{
public sealed class ManagedMqttClientOptions
{
public MqttClientOptions ClientOptions { get; set; }
public TimeSpan AutoReconnectDelay { get; set; } = TimeSpan.FromSeconds(5);
public TimeSpan ConnectionCheckInterval { get; set; } = TimeSpan.FromSeconds(1);
public IManagedMqttClientStorage Storage { get; set; }
public int MaxPendingMessages { get; set; } = int.MaxValue;
public MqttPendingMessagesOverflowStrategy PendingMessagesOverflowStrategy { get; set; } = MqttPendingMessagesOverflowStrategy.DropNewMessage;
/// <summary>
/// Defines the maximum amount of topic filters which will be sent in a SUBSCRIBE/UNSUBSCRIBE packet.
/// Amazon AWS limits this number to 8. The default is int.MaxValue.
/// </summary>
public int MaxTopicFiltersInSubscribeUnsubscribePackets { get; set; } = int.MaxValue;
}
}
@@ -1,97 +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;
using MQTTnet.Client;
using MQTTnet.Server;
namespace MQTTnet.Extensions.ManagedClient
{
public class ManagedMqttClientOptionsBuilder
{
readonly ManagedMqttClientOptions _options = new ManagedMqttClientOptions();
MqttClientOptionsBuilder _clientOptionsBuilder;
public ManagedMqttClientOptionsBuilder WithMaxPendingMessages(int value)
{
_options.MaxPendingMessages = value;
return this;
}
public ManagedMqttClientOptionsBuilder WithPendingMessagesOverflowStrategy(MqttPendingMessagesOverflowStrategy value)
{
_options.PendingMessagesOverflowStrategy = value;
return this;
}
public ManagedMqttClientOptionsBuilder WithAutoReconnectDelay(TimeSpan value)
{
_options.AutoReconnectDelay = value;
return this;
}
public ManagedMqttClientOptionsBuilder WithStorage(IManagedMqttClientStorage value)
{
_options.Storage = value;
return this;
}
public ManagedMqttClientOptionsBuilder WithClientOptions(MqttClientOptions value)
{
if (_clientOptionsBuilder != null)
{
throw new InvalidOperationException("Cannot use client options builder and client options at the same time.");
}
_options.ClientOptions = value ?? throw new ArgumentNullException(nameof(value));
return this;
}
public ManagedMqttClientOptionsBuilder WithClientOptions(MqttClientOptionsBuilder builder)
{
if (_options.ClientOptions != null)
{
throw new InvalidOperationException("Cannot use client options builder and client options at the same time.");
}
_clientOptionsBuilder = builder;
return this;
}
public ManagedMqttClientOptionsBuilder WithClientOptions(Action<MqttClientOptionsBuilder> options)
{
if (options == null) throw new ArgumentNullException(nameof(options));
if (_clientOptionsBuilder == null)
{
_clientOptionsBuilder = new MqttClientOptionsBuilder();
}
options(_clientOptionsBuilder);
return this;
}
public ManagedMqttClientOptionsBuilder WithMaxTopicFiltersInSubscribeUnsubscribePackets(int value)
{
_options.MaxTopicFiltersInSubscribeUnsubscribePackets = value;
return this;
}
public ManagedMqttClientOptions Build()
{
if (_clientOptionsBuilder != null)
{
_options.ClientOptions = _clientOptionsBuilder.Build();
}
if (_options.ClientOptions == null)
{
throw new InvalidOperationException("The ClientOptions cannot be null.");
}
return _options;
}
}
}
@@ -1,66 +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;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using MQTTnet.Internal;
namespace MQTTnet.Extensions.ManagedClient
{
public class ManagedMqttClientStorageManager
{
private readonly List<ManagedMqttApplicationMessage> _messages = new List<ManagedMqttApplicationMessage>();
private readonly AsyncLock _messagesLock = new AsyncLock();
private readonly IManagedMqttClientStorage _storage;
public ManagedMqttClientStorageManager(IManagedMqttClientStorage storage)
{
_storage = storage ?? throw new ArgumentNullException(nameof(storage));
}
public async Task<List<ManagedMqttApplicationMessage>> LoadQueuedMessagesAsync()
{
var loadedMessages = await _storage.LoadQueuedMessagesAsync().ConfigureAwait(false);
_messages.AddRange(loadedMessages);
return _messages;
}
public async Task AddAsync(ManagedMqttApplicationMessage applicationMessage)
{
if (applicationMessage == null) throw new ArgumentNullException(nameof(applicationMessage));
using (await _messagesLock.EnterAsync().ConfigureAwait(false))
{
_messages.Add(applicationMessage);
await SaveAsync().ConfigureAwait(false);
}
}
public async Task RemoveAsync(ManagedMqttApplicationMessage applicationMessage)
{
if (applicationMessage == null) throw new ArgumentNullException(nameof(applicationMessage));
using (await _messagesLock.EnterAsync().ConfigureAwait(false))
{
var index = _messages.IndexOf(applicationMessage);
if (index == -1)
{
return;
}
_messages.RemoveAt(index);
await SaveAsync().ConfigureAwait(false);
}
}
private Task SaveAsync()
{
return _storage.SaveQueuedMessagesAsync(_messages);
}
}
}
@@ -1,18 +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;
namespace MQTTnet.Extensions.ManagedClient
{
public class ManagedProcessFailedEventArgs : EventArgs
{
public ManagedProcessFailedEventArgs(Exception exception)
{
Exception = exception ?? throw new ArgumentNullException(nameof(exception));
}
public Exception Exception { get; }
}
}
@@ -1,33 +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;
using MQTTnet.Client;
using MQTTnet.Diagnostics;
namespace MQTTnet.Extensions.ManagedClient
{
public static class MqttFactoryExtensions
{
public static IManagedMqttClient CreateManagedMqttClient(this MqttFactory factory, IMqttClient mqttClient = null)
{
if (factory == null) throw new ArgumentNullException(nameof(factory));
if (mqttClient == null)
{
return new ManagedMqttClient(factory.CreateMqttClient(), factory.DefaultLogger);
}
return new ManagedMqttClient(mqttClient, factory.DefaultLogger);
}
public static IManagedMqttClient CreateManagedMqttClient(this MqttFactory factory, IMqttNetLogger logger)
{
if (factory == null) throw new ArgumentNullException(nameof(factory));
if (logger == null) throw new ArgumentNullException(nameof(logger));
return new ManagedMqttClient(factory.CreateMqttClient(logger), logger);
}
}
}
@@ -1,4 +1,9 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using MQTTnet.Protocol;
@@ -7,8 +12,8 @@ namespace MQTTnet.Extensions.Rpc
{
public interface IMqttRpcClient : IDisposable
{
Task<byte[]> ExecuteAsync(TimeSpan timeout, string methodName, byte[] payload, MqttQualityOfServiceLevel qualityOfServiceLevel);
Task<byte[]> ExecuteAsync(TimeSpan timeout, string methodName, byte[] payload, MqttQualityOfServiceLevel qualityOfServiceLevel, IDictionary<string,object> parameters = null);
Task<byte[]> ExecuteAsync(string methodName, byte[] payload, MqttQualityOfServiceLevel qualityOfServiceLevel, CancellationToken cancellationToken = default);
Task<byte[]> ExecuteAsync(string methodName, byte[] payload, MqttQualityOfServiceLevel qualityOfServiceLevel, IDictionary<string, object> parameters = null, CancellationToken cancellationToken = default);
}
}
@@ -1,10 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard1.3;netstandard2.0;netstandard2.1;netcoreapp3.1;net5.0;net6.0;net7.0</TargetFrameworks>
<TargetFrameworks Condition=" '$(OS)' == 'Windows_NT' ">$(TargetFrameworks);net452;net461</TargetFrameworks>
<TargetFrameworks Condition=" '$(OS)' == 'Windows_NT' AND '$(MSBuildRuntimeType)' != 'Core' ">$(TargetFrameworks);uap10.0</TargetFrameworks>
<TargetFramework>net8.0</TargetFramework>
<AssemblyName>MQTTnet.Extensions.Rpc</AssemblyName>
<RootNamespace>MQTTnet.Extensions.Rpc</RootNamespace>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
@@ -33,21 +30,12 @@
<PackageReleaseNotes>For release notes please go to MQTTnet release notes (https://www.nuget.org/packages/MQTTnet/).</PackageReleaseNotes>
<DisableImplicitAspNetCoreAnalyzers>true</DisableImplicitAspNetCoreAnalyzers>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>1591;NETSDK1138</NoWarn>
<LangVersion>7.3</LangVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' == 'uap10.0'">
<CopyLocalLockFileAssemblies>false</CopyLocalLockFileAssemblies>
<NugetTargetMoniker>UAP,Version=v10.0</NugetTargetMoniker>
<TargetPlatformIdentifier>UAP</TargetPlatformIdentifier>
<TargetPlatformVersion>10.0.18362.0</TargetPlatformVersion>
<TargetPlatformMinVersion>10.0.10240.0</TargetPlatformMinVersion>
<TargetFrameworkIdentifier>.NETCore</TargetFrameworkIdentifier>
<TargetFrameworkVersion>v5.0</TargetFrameworkVersion>
<DefineConstants>$(DefineConstants);WINDOWS_UWP</DefineConstants>
<DefaultLanguage>en</DefaultLanguage>
<LanguageTargets>$(MSBuildExtensionsPath)\Microsoft\WindowsXaml\v$(VisualStudioVersion)\Microsoft.Windows.UI.Xaml.CSharp.targets</LanguageTargets>
<NoWarn>1591;NETSDK1138;NU1803;NU1901;NU1902</NoWarn>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<NuGetAuditMode>all</NuGetAuditMode>
<NuGetAudit>true</NuGetAudit>
<NuGetAuditLevel>low</NuGetAuditLevel>
<AnalysisLevel>latest-Recommended</AnalysisLevel>
</PropertyGroup>
<ItemGroup>
@@ -56,13 +44,13 @@
<PackagePath>\</PackagePath>
</None>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MQTTnet\MQTTnet.csproj" />
<ProjectReference Include="..\MQTTnet\MQTTnet.csproj"/>
</ItemGroup>
</Project>
@@ -1,3 +1,7 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=options/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=options_005Ctopicgeneration/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
<wpf:ResourceDictionary xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib"
xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xml:space="preserve">
<s:Boolean
x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=options/@EntryIndexedValue">True</s:Boolean>
<s:Boolean
x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=options_005Ctopicgeneration/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
@@ -3,15 +3,14 @@
// See the LICENSE file in the project root for more information.
using System;
using MQTTnet.Client;
namespace MQTTnet.Extensions.Rpc
{
public static class MqttFactoryExtensions
{
public static IMqttRpcClient CreateMqttRpcClient(this MqttFactory factory, IMqttClient mqttClient)
public static IMqttRpcClient CreateMqttRpcClient(this MqttClientFactory clientFactory, IMqttClient mqttClient)
{
return factory.CreateMqttRpcClient(
return clientFactory.CreateMqttRpcClient(
mqttClient,
new MqttRpcClientOptions
{
@@ -19,7 +18,7 @@ namespace MQTTnet.Extensions.Rpc
});
}
public static IMqttRpcClient CreateMqttRpcClient(this MqttFactory _, IMqttClient mqttClient, MqttRpcClientOptions rpcClientOptions)
public static IMqttRpcClient CreateMqttRpcClient(this MqttClientFactory _, IMqttClient mqttClient, MqttRpcClientOptions rpcClientOptions)
{
if (mqttClient == null)
{
@@ -3,10 +3,12 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Buffers;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MQTTnet.Client;
using MQTTnet.Exceptions;
using MQTTnet.Formatter;
using MQTTnet.Internal;
@@ -41,13 +43,13 @@ namespace MQTTnet.Extensions.Rpc
_waitingCalls.Clear();
}
public async Task<byte[]> ExecuteAsync(TimeSpan timeout, string methodName, byte[] payload, MqttQualityOfServiceLevel qualityOfServiceLevel)
public async Task<byte[]> ExecuteAsync(TimeSpan timeout, string methodName, byte[] payload, MqttQualityOfServiceLevel qualityOfServiceLevel, IDictionary<string, object> parameters = null)
{
using (var timeoutToken = new CancellationTokenSource(timeout))
{
try
{
return await ExecuteAsync(methodName, payload, qualityOfServiceLevel, timeoutToken.Token).ConfigureAwait(false);
return await ExecuteAsync(methodName, payload, qualityOfServiceLevel, parameters, timeoutToken.Token).ConfigureAwait(false);
}
catch (OperationCanceledException exception)
{
@@ -61,14 +63,14 @@ namespace MQTTnet.Extensions.Rpc
}
}
public async Task<byte[]> ExecuteAsync(string methodName, byte[] payload, MqttQualityOfServiceLevel qualityOfServiceLevel, CancellationToken cancellationToken = default)
public async Task<byte[]> ExecuteAsync(string methodName, byte[] payload, MqttQualityOfServiceLevel qualityOfServiceLevel, IDictionary<string, object> parameters = null, CancellationToken cancellationToken = default)
{
if (methodName == null)
{
throw new ArgumentNullException(nameof(methodName));
}
var context = new TopicGenerationContext(_mqttClient, _options, methodName, qualityOfServiceLevel);
var context = new TopicGenerationContext(_mqttClient, _options, methodName, parameters, qualityOfServiceLevel);
var topicNames = _options.TopicGenerationStrategy.CreateRpcTopics(context);
var requestTopic = topicNames.RequestTopic;
@@ -119,7 +121,6 @@ namespace MQTTnet.Extensions.Rpc
finally
{
_waitingCalls.TryRemove(responseTopic, out _);
await _mqttClient.UnsubscribeAsync(responseTopic, CancellationToken.None).ConfigureAwait(false);
}
}
@@ -131,7 +132,8 @@ namespace MQTTnet.Extensions.Rpc
return CompletedTask.Instance;
}
awaitable.TrySetResult(eventArgs.ApplicationMessage.Payload);
var payloadBuffer = eventArgs.ApplicationMessage.Payload.ToArray();
awaitable.TrySetResult(payloadBuffer);
// Set this message to handled to that other code can avoid execution etc.
eventArgs.IsHandled = true;
@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using MQTTnet.Protocol;
@@ -11,13 +12,13 @@ namespace MQTTnet.Extensions.Rpc
{
public static class MqttRpcClientExtensions
{
public static Task<byte[]> ExecuteAsync(this IMqttRpcClient client, TimeSpan timeout, string methodName, string payload, MqttQualityOfServiceLevel qualityOfServiceLevel)
public static Task<byte[]> ExecuteAsync(this IMqttRpcClient client, TimeSpan timeout, string methodName, string payload, MqttQualityOfServiceLevel qualityOfServiceLevel, IDictionary<string,object> parameters = null)
{
if (client == null) throw new ArgumentNullException(nameof(client));
var buffer = Encoding.UTF8.GetBytes(payload ?? string.Empty);
return client.ExecuteAsync(timeout, methodName, buffer, qualityOfServiceLevel);
return client.ExecuteAsync(timeout, methodName, buffer, qualityOfServiceLevel, parameters);
}
}
}
@@ -3,16 +3,18 @@
// See the LICENSE file in the project root for more information.
using System;
using MQTTnet.Client;
using System.Collections.Generic;
using MQTTnet.Protocol;
namespace MQTTnet.Extensions.Rpc
{
public sealed class TopicGenerationContext
{
public TopicGenerationContext(IMqttClient mqttClient, MqttRpcClientOptions options, string methodName, MqttQualityOfServiceLevel qualityOfServiceLevel)
public TopicGenerationContext(IMqttClient mqttClient, MqttRpcClientOptions options, string methodName,
IDictionary<string, object> parameters, MqttQualityOfServiceLevel qualityOfServiceLevel)
{
MethodName = methodName ?? throw new ArgumentNullException(nameof(methodName));
Parameters = parameters;
QualityOfServiceLevel = qualityOfServiceLevel;
MqttClient = mqttClient ?? throw new ArgumentNullException(nameof(mqttClient));
Options = options ?? throw new ArgumentNullException(nameof(options));
@@ -20,6 +22,8 @@ namespace MQTTnet.Extensions.Rpc
public string MethodName { get; }
public IDictionary<string, object> Parameters { get; }
public IMqttClient MqttClient { get; }
public MqttRpcClientOptions Options { get; }
@@ -0,0 +1,57 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AssemblyName>MQTTnet.Extensions.TopicTemplate</AssemblyName>
<RootNamespace>MQTTnet.Extensions.TopicTemplate</RootNamespace>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<Company>The contributors of MQTTnet</Company>
<Product>MQTTnet</Product>
<Description>
Provides mqtt topic templating logic to support dispatch,
routing and similar functionality based on the well known moustache syntax (AsyncAPI compatible).
</Description>
<PackageReadmeFile>README.md</PackageReadmeFile>
<Authors>The contributors of MQTTnet</Authors>
<PackageId>MQTTnet.Extensions.TopicTemplate</PackageId>
<SignAssembly>false</SignAssembly>
<DelaySign>false</DelaySign>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<Copyright>Copyright (c) .NET Foundation and Contributors</Copyright>
<PackageProjectUrl>https://github.com/dotnet/MQTTnet</PackageProjectUrl>
<RepositoryUrl>https://github.com/dotnet/MQTTnet.git</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageTags>MQTT Message Queue Telemetry MQTTClient Messaging Routing AsyncAPI Template</PackageTags>
<NeutralLanguage>en-US</NeutralLanguage>
<PackageIcon>nuget.png</PackageIcon>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageReleaseNotes>For release notes please go to MQTTnet release notes (https://www.nuget.org/packages/MQTTnet/).</PackageReleaseNotes>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>1591;NETSDK1138</NoWarn>
</PropertyGroup>
<ItemGroup>
<None Include="README.md">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
<None Include="..\..\Images\nuget.png">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MQTTnet\MQTTnet.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,355 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using MQTTnet.Protocol;
namespace MQTTnet.Extensions.TopicTemplate;
/// <summary>
/// A topic template is an MQTT topic filter string that may contain
/// segments in curly braces called parameters. This well-known
/// 'moustache' syntax also matches AsyncAPI Channel Address Expressions.
/// The topic template is designed to support dynamic subscription/publication,
/// message-topic matching and routing. It is intended to be more safe and
/// convenient than String.Format() for aforementioned purposes.
/// </summary>
/// <example>
/// topic/subtopic/{parameter}/{otherParameter}
/// </example>
public sealed class MqttTopicTemplate : IEquatable<MqttTopicTemplate>
{
static readonly Regex MoustacheRegex = new("{([^/]+?)}", RegexOptions.Compiled);
readonly string[] _parameterSegments;
string _topicFilter;
/// <summary>
/// Create a topic template from an mqtt topic filter with moustache placeholders.
/// </summary>
/// <param
/// name="topicTemplate">
/// </param>
/// <exception
/// cref="ArgumentNullException">
/// </exception>
public MqttTopicTemplate(string topicTemplate)
{
if (topicTemplate == null)
{
throw new ArgumentNullException(nameof(topicTemplate));
}
MqttTopicValidator.ThrowIfInvalidSubscribe(topicTemplate);
Template = topicTemplate;
_parameterSegments = topicTemplate.Split(MqttTopicFilterComparer.LevelSeparator)
.Select(segment => MoustacheRegex.Match(segment).Groups[1].Value)
.Select(s => s.Length > 0 ? s : null)
.ToArray();
}
/// <summary>
/// Yield the template parameter names.
/// </summary>
public IEnumerable<string> Parameters => _parameterSegments.Where(s => s != null);
/// <summary>
/// The topic template string representation, e.g. A/B/{foo}/D.
/// </summary>
public string Template { get; }
/// <summary>
/// The topic template as an MQTT topic filter (+ substituted for all parameters). If the template
/// ends with a multi-level wildcard (hash), this will be reflected here.
/// </summary>
public string TopicFilter
{
get
{
LazyInitializer.EnsureInitialized(ref _topicFilter, () => MoustacheRegex.Replace(Template, MqttTopicFilterComparer.SingleLevelWildcard.ToString()));
return _topicFilter;
}
}
/// <summary>
/// Return the topic filter of this template, ending with a multi-level wildcard (hash).
/// </summary>
public string TopicTreeRootFilter
{
get
{
var filter = TopicFilter;
// append slash if neccessary
if (filter.Length > 0 && !filter.EndsWith(MqttTopicFilterComparer.LevelSeparator.ToString()) && !filter.EndsWith(MqttTopicFilterComparer.MultiLevelWildcard.ToString()))
{
filter += MqttTopicFilterComparer.LevelSeparator;
}
// append hash if neccessary
if (!filter.EndsWith(MqttTopicFilterComparer.MultiLevelWildcard.ToString()))
{
filter += MqttTopicFilterComparer.MultiLevelWildcard;
}
return filter;
}
}
public bool Equals(MqttTopicTemplate other)
{
return other != null && Template == other.Template;
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj))
{
return false;
}
if (ReferenceEquals(this, obj))
{
return true;
}
if (obj.GetType() != GetType())
{
return false;
}
return Equals((MqttTopicTemplate)obj);
}
/// <summary>
/// Determine the shortest common prefix of the given templates. Partial segments
/// are not returned.
/// </summary>
/// <param
/// name="templates">
/// topic templates
/// </param>
/// <returns></returns>
public static MqttTopicTemplate FindCanonicalPrefix(IEnumerable<MqttTopicTemplate> templates)
{
string root = null;
string CommonPrefix(string a, string b)
{
var maxIndex = Math.Min(a.Length, b.Length) - 1;
for (var i = 0; i <= maxIndex; i++)
{
if (a[i] != b[i])
{
return a.Substring(0, i);
}
}
return a.Substring(0, maxIndex + 1);
}
foreach (var topic in from template in templates select template.Template)
{
root = root == null ? topic : CommonPrefix(root, topic);
}
if (string.IsNullOrEmpty(root))
{
return new MqttTopicTemplate(MqttTopicFilterComparer.MultiLevelWildcard.ToString());
}
if (root.Contains(MqttTopicFilterComparer.LevelSeparator) && !root.EndsWith(MqttTopicFilterComparer.LevelSeparator.ToString()) && !root.EndsWith("}"))
{
root = root.Substring(0, root.LastIndexOf(MqttTopicFilterComparer.LevelSeparator) + 1);
}
if (root.EndsWith(MqttTopicFilterComparer.LevelSeparator.ToString()))
{
root += MqttTopicFilterComparer.SingleLevelWildcard;
}
return new MqttTopicTemplate(root);
}
public override int GetHashCode()
{
return Template.GetHashCode();
}
/// <summary>
/// Test if this topic template matches a given topic.
/// </summary>
/// <param
/// name="topic">
/// a fully specified topic
/// </param>
/// <param
/// name="subtree">
/// true to match including the subtree (multi-level wildcard)
/// </param>
/// <returns>true iff the topic matches the template's filter</returns>
/// <exception
/// cref="InvalidOperationException">
/// </exception>
/// <exception
/// cref="ArgumentException">
/// if the topic is invalid
/// </exception>
public bool MatchesTopic(string topic, bool subtree = false)
{
var comparison = MqttTopicFilterComparer.Compare(topic, subtree ? TopicTreeRootFilter : TopicFilter);
if (comparison == MqttTopicFilterCompareResult.FilterInvalid)
{
throw new InvalidOperationException("Invalid filter");
}
if (comparison == MqttTopicFilterCompareResult.TopicInvalid)
{
throw new ArgumentException("Invalid topic", nameof(topic));
}
return comparison == MqttTopicFilterCompareResult.IsMatch;
}
/// <summary>
/// Extract the parameter values from a topic corresponding to the template
/// parameters. The topic has to match this template.
/// </summary>
/// <param
/// name="topic">
/// the topic
/// </param>
/// <returns>an enumeration of (parameter, index, value)</returns>
public IEnumerable<(string parameter, int index, string value)> ParseParameterValues(string topic)
{
if (!MatchesTopic(topic))
{
throw new ArgumentException("the topic has to match this template", nameof(topic));
}
return parseParameterValuesInternal(topic);
}
/// <summary>
/// Extract the parameter values from the message topic corresponding to the template
/// parameters. The message topic has to match this topic template.
/// </summary>
/// <param
/// name="message">
/// the message
/// </param>
/// <returns>an enumeration of (parameter, index, value)</returns>
public IEnumerable<(string parameter, int index, string value)> ParseParameterValues(MqttApplicationMessage message)
{
return ParseParameterValues(message.Topic);
}
/// <summary>
/// Try to set a parameter to a given value. If the parameter is not present,
/// this is returned. The value must not contain slashes.
/// </summary>
/// <param
/// name="parameter">
/// a template parameter
/// </param>
/// <param
/// name="value">
/// a string
/// </param>
/// <returns></returns>
public MqttTopicTemplate TrySetParameter(string parameter, string value)
{
if (parameter != null && _parameterSegments.Contains(parameter))
{
return WithParameter(parameter, value);
}
return this;
}
/// <summary>
/// Replace the given parameter with a single-level wildcard (plus sign).
/// </summary>
/// <param
/// name="parameter">
/// parameter name
/// </param>
/// <returns>the topic template (without the parameter)</returns>
public MqttTopicTemplate WithoutParameter(string parameter)
{
if (string.IsNullOrEmpty(parameter) || !_parameterSegments.Contains(parameter))
{
throw new ArgumentException("topic template parameter must exist.");
}
return ReplaceInternal(parameter, MqttTopicFilterComparer.SingleLevelWildcard.ToString());
}
/// <summary>
/// Substitute a parameter with a given value, thus removing the parameter. If the parameter is not present,
/// the method trows. The value must not contain slashes or wildcards.
/// </summary>
/// <param
/// name="parameter">
/// a template parameter
/// </param>
/// <param
/// name="value">
/// a string
/// </param>
/// <exception
/// cref="ArgumentException">
/// when the parameter is not present
/// </exception>
/// <returns>the topic template (without the parameter)</returns>
public MqttTopicTemplate WithParameter(string parameter, string value)
{
if (value == null || string.IsNullOrEmpty(parameter) || !_parameterSegments.Contains(parameter) || value.Contains(MqttTopicFilterComparer.LevelSeparator) ||
value.Contains(MqttTopicFilterComparer.SingleLevelWildcard) || value.Contains(MqttTopicFilterComparer.MultiLevelWildcard))
{
throw new ArgumentException("parameter must exist and value must not contain slashes or wildcard.");
}
return ReplaceInternal(parameter, value);
}
/// <summary>
/// Reuse parameters as they are extracted using another topic template on this template
/// when the parameter name matches. Useful
/// for compatibility routing.
/// </summary>
/// <param
/// name="parameters">
/// </param>
/// <returns></returns>
public MqttTopicTemplate WithParameterValuesFrom(IEnumerable<(string parameter, int index, string value)> parameters)
{
return parameters.Aggregate(this, (t, p) => t.TrySetParameter(p.parameter, p.value));
}
IEnumerable<(string parameter, int index, string value)> parseParameterValuesInternal(string topic)
{
// because we have a match, we know the segment array is at least the template's length
var segments = topic.Split(MqttTopicFilterComparer.LevelSeparator);
for (var i = 0; i < _parameterSegments.Length; i++)
{
var name = _parameterSegments[i];
if (name != null)
{
yield return (name, i, segments[i]);
}
}
}
MqttTopicTemplate ReplaceInternal(string parameter, string value)
{
var moustache = "{" + parameter + "}";
return new MqttTopicTemplate(Template.Replace(moustache, value));
}
}
@@ -0,0 +1,79 @@
# MQTTNet.Extensions.TopicTemplate
Provides mqtt topic templating logic to support dispatch, routing and
similar functionality based on the well known moustache syntax, aka
AsyncAPI dynamic channel address.
Generating and parsing MQTT topics and topic filters is often done in
an ad-hoc fashion, leading to error-prone code and subtly introduced
showstoppers like forgetting to strip a slash from a parameter. To
remedy, this extension aids you write safe, maintainable logic
for topic generation, routing and dispatch.
## How it works
The package lets you write MQTTNet code like:
```csharp
var template = new MqttTopicTemplate("App/v1/{sender}/message");
var filter = new MqttTopicFilterBuilder()
.WithTopicTemplate(template) // (1)
.WithAtLeastOnceQoS()
.WithNoLocal()
.Build();
// filter.Topic == "App/v1/+/message"
var myTopic = template
.WithParameter("sender", "me, myself & i"); // (2)
var message = new MqttApplicationMessageBuilder()
.WithTopicTemplate( // (3)
myTopic)
.WithPayload("Hello!")
.Build();
// message.Topic == "App/v1/me, myself & i/message"
Assert.IsTrue(message.MatchesTopicTemplate(template)); // (4)
```
At (1), the parameter "sender" was automatically replaced by a plus sign
(single level wildcard) because the caller is creating a topic filter.
Using the same template (they are immutable), you can send
a message. At (2), we replace the parameter with a known value.
If that value contained a level separator or topic filter only
characters, we'd get an ArgumentException. If we forgot to replace
the parameter "sender" (e.g. when placing template not myTopic),
we would be prevented from sending at (3).
On the receiving side, a message can be matched to a template
using code like at (4).
These daily tasks become much safer and easier to code than
string.Format() and Split() allow for. More elaborate computation is supported
too. You could extract the sender using the template or route
messages based on topic content.
## Features
- Safe handling of topic parameters in moustache syntax
- Simplifies topic generation and parsing logic
- Match topics and extract the values of their parameters
- Translate between topic templates to ease dynamic routing
- Derive a canonical topic filter from a bunch of templates
- Integrates with MQTTNet message, subscription and filter builders
- Extension method friendly (sealed class with one public ctor)
## Additional documentation
- Some of the [unit tests](https://github.com/dotnet/MQTTnet/blob/master/Source/MQTTnet.Tests/Extensions/MqttTopicTemplate_Tests.cs) are instructive
- Some examples feature the extension, e.g. TopicGenerator, Client_Subscribe_Samples
## Feedback
Report any issues with the main project on https://github.com/dotnet/MQTTnet/issues
@@ -0,0 +1,188 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Linq;
using MQTTnet.Packets;
using MQTTnet.Protocol;
namespace MQTTnet.Extensions.TopicTemplate;
public static class TopicTemplateExtensions
{
/// <summary>
/// Modify this message builder to respond to a given message. The
/// message's response topic and correlation data are included
/// in the message builder.
/// </summary>
/// <param
/// name="builder">
/// a message builder
/// </param>
/// <param
/// name="message">
/// a message with a response topic
/// </param>
/// <returns>a message builder</returns>
/// <see
/// cref="BuildResponse" />
public static MqttApplicationMessageBuilder AsResponseTo(this MqttApplicationMessageBuilder builder, MqttApplicationMessage message)
{
if (!string.IsNullOrEmpty(message.ResponseTopic))
{
throw new ArgumentException("message does not have a response topic");
}
return builder.WithTopic(message.ResponseTopic).WithCorrelationData(message.CorrelationData);
}
/// <summary>
/// Set the filter topic according to the template, with
/// remaining template parameters substituted by single-level
/// wildcard.
/// </summary>
/// <param
/// name="topicTemplate">
/// a topic template
/// </param>
/// <param
/// name="subscribeTreeRoot">
/// whether to subscribe to the whole topic tree
/// </param>
/// <returns>the modified topic filter</returns>
public static MqttTopicFilterBuilder BuildFilter(this MqttTopicTemplate topicTemplate, bool subscribeTreeRoot = false)
{
return new MqttTopicFilterBuilder().WithTopicTemplate(topicTemplate, subscribeTreeRoot);
}
/// <summary>
/// Create a message builder from this template. The template must not have
/// remaining parameters.
/// </summary>
/// <param
/// name="topicTemplate">
/// a parameterless topic template
/// </param>
/// <returns>a new message builder</returns>
/// <exception
/// cref="ArgumentException">
/// if the topic template has parameters
/// </exception>
public static MqttApplicationMessageBuilder BuildMessage(this MqttTopicTemplate topicTemplate)
{
return new MqttApplicationMessageBuilder().WithTopicTemplate(topicTemplate);
}
/// <summary>
/// Return a message builder to respond to this message. The
/// message's response topic and correlation data are included
/// in the response message builder.
/// </summary>
/// <param
/// name="message">
/// a message with a response topic
/// </param>
/// <returns>a message builder</returns>
/// <see
/// cref="AsResponseTo" />
public static MqttApplicationMessageBuilder BuildResponse(this MqttApplicationMessage message)
{
return new MqttApplicationMessageBuilder().AsResponseTo(message);
}
/// <summary>
/// Return whether the message matches the given topic template.
/// </summary>
/// <param
/// name="message">
/// a message
/// </param>
/// <param
/// name="topicTemplate">
/// a topic template
/// </param>
/// <param
/// name="subtree">
/// whether to include the topic subtree
/// </param>
/// <returns></returns>
public static bool MatchesTopicTemplate(this MqttApplicationMessage message, MqttTopicTemplate topicTemplate, bool subtree = false)
{
return topicTemplate.MatchesTopic(message.Topic, subtree);
}
/// <summary>
/// Set the filter topic according to the template, with
/// template parameters substituted by a single-level
/// wildcard.
/// </summary>
/// <param
/// name="builder">
/// a filter builder
/// </param>
/// <param
/// name="topicTemplate">
/// a topic template
/// </param>
/// <param
/// name="subscribeTreeRoot">
/// whether to subscribe to the whole topic tree
/// </param>
/// <returns>the modified topic filter</returns>
public static MqttTopicFilterBuilder WithTopicTemplate(this MqttTopicFilterBuilder builder, MqttTopicTemplate topicTemplate, bool subscribeTreeRoot = false)
{
return builder.WithTopic(subscribeTreeRoot ? topicTemplate.TopicTreeRootFilter : topicTemplate.TopicFilter);
}
/// <summary>
/// Set the subscription to the template's topic filter.
/// </summary>
/// <returns>the builder</returns>
public static MqttClientSubscribeOptionsBuilder WithTopicTemplate(
this MqttClientSubscribeOptionsBuilder builder,
MqttTopicTemplate topicTemplate,
MqttQualityOfServiceLevel qualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce,
bool noLocal = false,
bool retainAsPublished = false,
MqttRetainHandling retainHandling = MqttRetainHandling.SendAtSubscribe)
{
return builder.WithTopicFilter(
new MqttTopicFilter
{
Topic = topicTemplate.TopicFilter,
QualityOfServiceLevel = qualityOfServiceLevel,
NoLocal = noLocal,
RetainAsPublished = retainAsPublished,
RetainHandling = retainHandling
});
}
/// <summary>
/// Set the publication topic according to the topic template. The template
/// must not have remaining (unset) parameters or contain wildcards.
/// </summary>
/// <param
/// name="builder">
/// a message builder
/// </param>
/// <param
/// name="topicTemplate">
/// a parameterless topic template
/// </param>
/// <returns>the modified message builder</returns>
/// <exception
/// cref="ArgumentException">
/// if the topic template has parameters
/// </exception>
public static MqttApplicationMessageBuilder WithTopicTemplate(this MqttApplicationMessageBuilder builder, MqttTopicTemplate topicTemplate)
{
if (topicTemplate.Parameters.Any())
{
throw new ArgumentException("topic templates must be parameter-less when sending " + topicTemplate.Template);
}
MqttTopicValidator.ThrowIfInvalid(topicTemplate.Template);
return builder.WithTopic(topicTemplate.Template);
}
}
@@ -1,72 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard1.3;netstandard2.0;netstandard2.1;netcoreapp3.1;net5.0;net6.0;net7.0</TargetFrameworks>
<TargetFrameworks Condition=" '$(OS)' == 'Windows_NT' ">$(TargetFrameworks);net452;net461</TargetFrameworks>
<TargetFrameworks Condition=" '$(OS)' == 'Windows_NT' AND '$(MSBuildRuntimeType)' != 'Core' ">$(TargetFrameworks);uap10.0</TargetFrameworks>
<AssemblyName>MQTTnet.Extensions.WebSocket4Net</AssemblyName>
<RootNamespace>MQTTnet.Extensions.WebSocket4Net</RootNamespace>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<Company>The contributors of MQTTnet</Company>
<Product>MQTTnet</Product>
<Description>This is an extension library which allows using _WebSocket4Net_ as transport for MQTTnet clients.</Description>
<Authors>The contributors of MQTTnet</Authors>
<PackageId>MQTTnet.Extensions.WebSocket4Net</PackageId>
<SignAssembly>false</SignAssembly>
<DelaySign>false</DelaySign>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<Copyright>Copyright (c) .NET Foundation and Contributors</Copyright>
<PackageProjectUrl>https://github.com/dotnet/MQTTnet</PackageProjectUrl>
<RepositoryUrl>https://github.com/dotnet/MQTTnet.git</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageTags>MQTT Message Queue Telemetry Transport MQTTClient MQTTServer Server MQTTBroker Broker NETStandard IoT InternetOfThings Messaging Hardware Arduino Sensor Actuator M2M ESP Smart Home Cities Automation Xamarin Blazor</PackageTags>
<NeutralLanguage>en-US</NeutralLanguage>
<EnableNETAnalyzers>false</EnableNETAnalyzers>
<EnforceCodeStyleInBuild>false</EnforceCodeStyleInBuild>
<PackageIcon>nuget.png</PackageIcon>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageReleaseNotes>For release notes please go to MQTTnet release notes (https://www.nuget.org/packages/MQTTnet/).</PackageReleaseNotes>
<DisableImplicitAspNetCoreAnalyzers>true</DisableImplicitAspNetCoreAnalyzers>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>1591;NETSDK1138</NoWarn>
<LangVersion>7.3</LangVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' == 'uap10.0'">
<CopyLocalLockFileAssemblies>false</CopyLocalLockFileAssemblies>
<NugetTargetMoniker>UAP,Version=v10.0</NugetTargetMoniker>
<TargetPlatformIdentifier>UAP</TargetPlatformIdentifier>
<TargetPlatformVersion>10.0.18362.0</TargetPlatformVersion>
<TargetPlatformMinVersion>10.0.10240.0</TargetPlatformMinVersion>
<TargetFrameworkIdentifier>.NETCore</TargetFrameworkIdentifier>
<TargetFrameworkVersion>v5.0</TargetFrameworkVersion>
<DefineConstants>$(DefineConstants);WINDOWS_UWP</DefineConstants>
<DefaultLanguage>en</DefaultLanguage>
<LanguageTargets>$(MSBuildExtensionsPath)\Microsoft\WindowsXaml\v$(VisualStudioVersion)\Microsoft.Windows.UI.Xaml.CSharp.targets</LanguageTargets>
</PropertyGroup>
<ItemGroup>
<None Include="..\..\Images\nuget.png">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="WebSocket4Net" Version="0.15.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MQTTnet\MQTTnet.csproj" />
</ItemGroup>
</Project>
@@ -1,21 +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;
namespace MQTTnet.Extensions.WebSocket4Net
{
public static class MqttFactoryExtensions
{
public static MqttFactory UseWebSocket4Net(this MqttFactory mqttFactory)
{
if (mqttFactory == null)
{
throw new ArgumentNullException(nameof(mqttFactory));
}
return mqttFactory.UseClientAdapterFactory(new WebSocket4NetMqttClientAdapterFactory());
}
}
}
@@ -1,267 +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;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
using MQTTnet.Channel;
using MQTTnet.Client;
using MQTTnet.Exceptions;
using MQTTnet.Internal;
using SuperSocket.ClientEngine;
using WebSocket4Net;
using DataReceivedEventArgs = WebSocket4Net.DataReceivedEventArgs;
namespace MQTTnet.Extensions.WebSocket4Net
{
public sealed class WebSocket4NetMqttChannel : IMqttChannel
{
readonly MqttClientOptions _clientOptions;
readonly BlockingCollection<byte> _receiveBuffer = new BlockingCollection<byte>();
readonly MqttClientWebSocketOptions _webSocketOptions;
WebSocket _webSocket;
public WebSocket4NetMqttChannel(MqttClientOptions clientOptions, MqttClientWebSocketOptions webSocketOptions)
{
_clientOptions = clientOptions ?? throw new ArgumentNullException(nameof(clientOptions));
_webSocketOptions = webSocketOptions ?? throw new ArgumentNullException(nameof(webSocketOptions));
}
public X509Certificate2 ClientCertificate { get; }
public string Endpoint => _webSocketOptions.Uri;
public bool IsSecureConnection { get; private set; }
public Task ConnectAsync(CancellationToken cancellationToken)
{
var uri = _webSocketOptions.Uri;
if (!uri.StartsWith("ws://", StringComparison.OrdinalIgnoreCase) && !uri.StartsWith("wss://", StringComparison.OrdinalIgnoreCase))
{
if (_webSocketOptions.TlsOptions?.UseTls == false)
{
uri = "ws://" + uri;
}
else
{
uri = "wss://" + uri;
}
}
IsSecureConnection = uri.StartsWith("wss://", StringComparison.OrdinalIgnoreCase);
#if NET48 || NETCOREAPP3_0_OR_GREATER
var sslProtocols = _webSocketOptions.TlsOptions?.SslProtocol ?? SslProtocols.Tls12 | SslProtocols.Tls13;
#else
var sslProtocols = _webSocketOptions.TlsOptions?.SslProtocol ?? SslProtocols.Tls12 | (SslProtocols)0x00003000 /*Tls13*/;
#endif
var subProtocol = _webSocketOptions.SubProtocols.FirstOrDefault() ?? string.Empty;
var cookies = new List<KeyValuePair<string, string>>();
if (_webSocketOptions.CookieContainer != null)
{
throw new NotSupportedException("Cookies are not supported.");
}
List<KeyValuePair<string, string>> customHeaders = null;
if (_webSocketOptions.RequestHeaders != null)
{
customHeaders = _webSocketOptions.RequestHeaders.Select(i => new KeyValuePair<string, string>(i.Key, i.Value)).ToList();
}
EndPoint proxy = null;
if (_webSocketOptions.ProxyOptions != null)
{
throw new NotSupportedException("Proxies are not supported.");
}
// The user agent can be empty always because it is just added to the custom headers as "User-Agent".
var userAgent = string.Empty;
var origin = string.Empty;
var webSocketVersion = WebSocketVersion.None;
var receiveBufferSize = 0;
var certificates = new X509CertificateCollection();
if (_webSocketOptions.TlsOptions?.Certificates != null)
{
foreach (var certificate in _webSocketOptions.TlsOptions.Certificates)
{
#if WINDOWS_UWP
certificates.Add(new X509Certificate(certificate));
#else
certificates.Add(certificate);
#endif
}
}
_webSocket = new WebSocket(uri, subProtocol, cookies, customHeaders, userAgent, origin, webSocketVersion, proxy, sslProtocols, receiveBufferSize)
{
NoDelay = true,
Security =
{
AllowUnstrustedCertificate = _webSocketOptions.TlsOptions?.AllowUntrustedCertificates == true,
AllowCertificateChainErrors = _webSocketOptions.TlsOptions?.IgnoreCertificateChainErrors == true,
Certificates = certificates
}
};
return ConnectInternalAsync(cancellationToken);
}
public Task DisconnectAsync(CancellationToken cancellationToken)
{
if (_webSocket != null && _webSocket.State == WebSocketState.Open)
{
_webSocket.Close();
}
return CompletedTask.Instance;
}
public void Dispose()
{
if (_webSocket == null)
{
return;
}
_webSocket.DataReceived -= OnDataReceived;
_webSocket.Error -= OnError;
_webSocket.Dispose();
_webSocket = null;
}
public Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
var readBytes = 0;
while (count > 0 && !cancellationToken.IsCancellationRequested)
{
if (!_receiveBuffer.TryTake(out var @byte))
{
if (readBytes == 0)
{
// Block until at least one byte was received.
@byte = _receiveBuffer.Take(cancellationToken);
}
else
{
return Task.FromResult(readBytes);
}
}
buffer[offset] = @byte;
offset++;
count--;
readBytes++;
}
return Task.FromResult(readBytes);
}
public Task WriteAsync(ArraySegment<byte> buffer, bool isEndOfPacket, CancellationToken cancellationToken)
{
_webSocket.Send(buffer.Array, buffer.Offset, buffer.Count);
return CompletedTask.Instance;
}
async Task ConnectInternalAsync(CancellationToken cancellationToken)
{
_webSocket.Error += OnError;
_webSocket.DataReceived += OnDataReceived;
var promise = new AsyncTaskCompletionSource<bool>();
void ErrorHandler(object sender, ErrorEventArgs e)
{
promise.TrySetException(e.Exception);
}
void OpenedHandler(object sender, EventArgs e)
{
promise.TrySetResult(true);
}
void ClosedHandler(object sender, EventArgs e)
{
promise.TrySetException(new MqttCommunicationException("Connection closed."));
}
try
{
_webSocket.Opened += OpenedHandler;
_webSocket.Closed += ClosedHandler;
_webSocket.Error += ErrorHandler;
using (var timeout = new CancellationTokenSource(_clientOptions.Timeout))
using (var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, cancellationToken))
{
using (linkedCts.Token.Register(() => promise.TrySetCanceled()))
{
try
{
// OpenAsync does basically the same but without support for a cancellation token.
// So we do it on our own!
_webSocket.Open();
await promise.Task.ConfigureAwait(false);
}
catch (Exception exception)
{
var timeoutReached = timeout.IsCancellationRequested && !cancellationToken.IsCancellationRequested;
if (timeoutReached)
{
throw new MqttCommunicationTimedOutException(exception);
}
if (exception is AuthenticationException authenticationException)
{
throw new MqttCommunicationException(authenticationException.InnerException);
}
if (exception is OperationCanceledException)
{
throw new MqttCommunicationTimedOutException();
}
throw new MqttCommunicationException(exception);
}
}
}
}
catch (OperationCanceledException)
{
throw new MqttCommunicationTimedOutException();
}
finally
{
_webSocket.Opened -= OpenedHandler;
_webSocket.Closed -= ClosedHandler;
_webSocket.Error -= ErrorHandler;
}
}
void OnDataReceived(object sender, DataReceivedEventArgs e)
{
foreach (var @byte in e.Data)
{
_receiveBuffer.Add(@byte);
}
}
static void OnError(object sender, ErrorEventArgs e)
{
Debug.Write(e.Exception.ToString());
}
}
}

Some files were not shown because too many files have changed in this diff Show More