├── .gitattributes ├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── LICENSE ├── MqttFx.sln ├── PROTOCOL.md ├── README.md ├── samples ├── EchoClient │ ├── EchoClient.csproj │ └── Program.cs └── SimpleClient │ ├── Program.cs │ ├── Services.cs │ └── SimpleClient.csproj ├── src ├── DotNetty.Codecs.MqttFx │ ├── ByteBufferExtensions.cs │ ├── DotNetty.Codecs.MqttFx.csproj │ ├── MqttDecoder.cs │ ├── MqttEncoder.cs │ ├── MqttEncoding.cs │ └── Packets │ │ ├── ConnAckPacket.cs │ │ ├── ConnAckVariableHeader.cs │ │ ├── ConnectFlags.cs │ │ ├── ConnectPacket.cs │ │ ├── ConnectPayload.cs │ │ ├── ConnectReturnCode.cs │ │ ├── ConnectVariableHeader.cs │ │ ├── DisconnectPacket.cs │ │ ├── FixedHeader.cs │ │ ├── MqttCodecUtil.cs │ │ ├── MqttQos.cs │ │ ├── MqttVersion.cs │ │ ├── Packet.cs │ │ ├── PacketIdVariableHeader.cs │ │ ├── PacketType.cs │ │ ├── PacketWithId.cs │ │ ├── Payload.cs │ │ ├── PingReqPacket.cs │ │ ├── PingRespPacket.cs │ │ ├── PubAckPacket.cs │ │ ├── PubCompPacket.cs │ │ ├── PubRecPacket.cs │ │ ├── PubRelPacket.cs │ │ ├── PublishPacket.cs │ │ ├── PublishPayload.cs │ │ ├── PublishVariableHeader.cs │ │ ├── SubAckPacket.cs │ │ ├── SubAckPayload.cs │ │ ├── SubscribePacket.cs │ │ ├── SubscribePayload.cs │ │ ├── SubscriptionRequest.cs │ │ ├── UnsubAckPacket.cs │ │ ├── UnsubscribePacket.cs │ │ ├── UnsubscribePayload.cs │ │ └── VariableHeader.cs ├── MqttDecoder.cs ├── MqttEncoder.cs └── MqttFx │ ├── ApplicationMessage.cs │ ├── ApplicationMessageBuilder.cs │ ├── Client │ ├── Channels │ │ ├── MqttChannelHandler.cs │ │ └── MqttPingHandler.cs │ ├── Extensions │ │ ├── MqttClientCredentials.cs │ │ ├── MqttClientOptions.cs │ │ ├── MqttClientOptionsBuilder.cs │ │ ├── MqttClientTlsOptions.cs │ │ └── MqttFxClientServiceCollectionExtensions.cs │ ├── MqttClient.cs │ └── MqttClientExtensions.cs │ ├── ConnectResult.cs │ ├── Formatter │ ├── PublishPacketFactory.cs │ └── PublishPacketFactoryExtensions.cs │ ├── MqttException.cs │ ├── MqttFx - Backup.csproj │ ├── MqttFx.csproj │ ├── MqttFx.csproj.user │ ├── PublishResult.cs │ ├── SubscribeResult.cs │ ├── SubscriptionRequests.cs │ ├── SubscriptionRequestsBuilder.cs │ ├── TopicFilter.cs │ ├── TopicFilterBuilder.cs │ ├── UnsubscribeResult.cs │ └── Utils │ ├── PacketIdProvider.cs │ ├── PendingPublish.cs │ ├── PendingSubscription.cs │ ├── PendingUnsubscription.cs │ └── RetransmissionHandler.cs └── test └── MqttFx.Test ├── MqttCodecTests.cs └── MqttFx.Test.csproj /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows image file caches 2 | Thumbs.db 3 | ehthumbs.db 4 | 5 | # Folder config file 6 | Desktop.ini 7 | 8 | # Recycle Bin used on file shares 9 | $RECYCLE.BIN/ 10 | 11 | # Build results 12 | [Dd]ebug/ 13 | [Dd]ebugPublic/ 14 | [Rr]elease/ 15 | [Rr]eleases/ 16 | x64/ 17 | x86/ 18 | bld/ 19 | [Bb]in/ 20 | [Oo]bj/ 21 | [Ll]og/ 22 | 23 | # Visual Studio 2015 cache/options directory 24 | .vs/ 25 | 26 | # Windows Installer files 27 | *.cab 28 | *.msi 29 | *.msm 30 | *.msp 31 | 32 | # Windows shortcuts 33 | *.lnk 34 | 35 | # ========================= 36 | # Operating System Files 37 | # ========================= 38 | 39 | # OSX 40 | # ========================= 41 | 42 | .DS_Store 43 | .AppleDouble 44 | .LSOverride 45 | 46 | # Thumbnails 47 | ._* 48 | 49 | # Files that might appear on external disk 50 | .Spotlight-V100 51 | .Trashes 52 | 53 | # Directories potentially created on remote AFP share 54 | .AppleDB 55 | .AppleDesktop 56 | Network Trash Folder 57 | Temporary Items 58 | .apdisk 59 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (console)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/examples/Echo.Client/bin/Debug/netcoreapp2.1/Echo.Client.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}/examples/Echo.Client", 16 | // For more information about the 'console' field, see https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md#console-terminal-window 17 | "console": "internalConsole", 18 | "stopAtEntry": false, 19 | "internalConsoleOptions": "openOnSessionStart" 20 | }, 21 | { 22 | "name": ".NET Core Attach", 23 | "type": "coreclr", 24 | "request": "attach", 25 | "processId": "${command:pickProcess}" 26 | } 27 | ,] 28 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/examples/Echo.Client/Echo.Client.csproj" 11 | ], 12 | "problemMatcher": "$msCompile" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 强悍的抽屉 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /MqttFx.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31912.275 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MqttFx", "src\MqttFx\MqttFx.csproj", "{F065401D-3021-402E-BC0D-F28C96E91277}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetty.Codecs.MqttFx", "src\DotNetty.Codecs.MqttFx\DotNetty.Codecs.MqttFx.csproj", "{316B0FD8-15E4-40D9-AFFB-259EBE1E6886}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{541B0DD0-22BE-4B97-B80E-23C75868D1E2}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{64378755-ADF3-471A-905F-08F1D14B9F79}" 13 | EndProject 14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{6FF4C7B2-AC10-40F8-9337-827636A50892}" 15 | EndProject 16 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{4BB60B21-F408-431A-B28A-CF40BDCD5827}" 17 | ProjectSection(SolutionItems) = preProject 18 | PROTOCOL.md = PROTOCOL.md 19 | README.md = README.md 20 | EndProjectSection 21 | EndProject 22 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EchoClient", "samples\EchoClient\EchoClient.csproj", "{662E9323-B645-4B62-9669-E1E15B86ECFC}" 23 | EndProject 24 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SimpleClient", "samples\SimpleClient\SimpleClient.csproj", "{8A48B035-C9D8-4A26-B585-A22F55CC2B8D}" 25 | EndProject 26 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MqttFx.Test", "test\MqttFx.Test\MqttFx.Test.csproj", "{7E9EEB2C-8EB9-4A0A-AD2F-56C6E65848EB}" 27 | EndProject 28 | Global 29 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 30 | Debug|Any CPU = Debug|Any CPU 31 | Release|Any CPU = Release|Any CPU 32 | EndGlobalSection 33 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 34 | {F065401D-3021-402E-BC0D-F28C96E91277}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {F065401D-3021-402E-BC0D-F28C96E91277}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {F065401D-3021-402E-BC0D-F28C96E91277}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {F065401D-3021-402E-BC0D-F28C96E91277}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {316B0FD8-15E4-40D9-AFFB-259EBE1E6886}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {316B0FD8-15E4-40D9-AFFB-259EBE1E6886}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {316B0FD8-15E4-40D9-AFFB-259EBE1E6886}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {316B0FD8-15E4-40D9-AFFB-259EBE1E6886}.Release|Any CPU.Build.0 = Release|Any CPU 42 | {662E9323-B645-4B62-9669-E1E15B86ECFC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 43 | {662E9323-B645-4B62-9669-E1E15B86ECFC}.Debug|Any CPU.Build.0 = Debug|Any CPU 44 | {662E9323-B645-4B62-9669-E1E15B86ECFC}.Release|Any CPU.ActiveCfg = Release|Any CPU 45 | {662E9323-B645-4B62-9669-E1E15B86ECFC}.Release|Any CPU.Build.0 = Release|Any CPU 46 | {8A48B035-C9D8-4A26-B585-A22F55CC2B8D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 47 | {8A48B035-C9D8-4A26-B585-A22F55CC2B8D}.Debug|Any CPU.Build.0 = Debug|Any CPU 48 | {8A48B035-C9D8-4A26-B585-A22F55CC2B8D}.Release|Any CPU.ActiveCfg = Release|Any CPU 49 | {8A48B035-C9D8-4A26-B585-A22F55CC2B8D}.Release|Any CPU.Build.0 = Release|Any CPU 50 | {7E9EEB2C-8EB9-4A0A-AD2F-56C6E65848EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 51 | {7E9EEB2C-8EB9-4A0A-AD2F-56C6E65848EB}.Debug|Any CPU.Build.0 = Debug|Any CPU 52 | {7E9EEB2C-8EB9-4A0A-AD2F-56C6E65848EB}.Release|Any CPU.ActiveCfg = Release|Any CPU 53 | {7E9EEB2C-8EB9-4A0A-AD2F-56C6E65848EB}.Release|Any CPU.Build.0 = Release|Any CPU 54 | EndGlobalSection 55 | GlobalSection(SolutionProperties) = preSolution 56 | HideSolutionNode = FALSE 57 | EndGlobalSection 58 | GlobalSection(NestedProjects) = preSolution 59 | {F065401D-3021-402E-BC0D-F28C96E91277} = {541B0DD0-22BE-4B97-B80E-23C75868D1E2} 60 | {316B0FD8-15E4-40D9-AFFB-259EBE1E6886} = {541B0DD0-22BE-4B97-B80E-23C75868D1E2} 61 | {662E9323-B645-4B62-9669-E1E15B86ECFC} = {64378755-ADF3-471A-905F-08F1D14B9F79} 62 | {8A48B035-C9D8-4A26-B585-A22F55CC2B8D} = {64378755-ADF3-471A-905F-08F1D14B9F79} 63 | {7E9EEB2C-8EB9-4A0A-AD2F-56C6E65848EB} = {6FF4C7B2-AC10-40F8-9337-827636A50892} 64 | EndGlobalSection 65 | GlobalSection(ExtensibilityGlobals) = postSolution 66 | SolutionGuid = {4E0A9EB4-1E55-4389-96A4-EC45F5D31A1C} 67 | EndGlobalSection 68 | EndGlobal 69 | -------------------------------------------------------------------------------- /PROTOCOL.md: -------------------------------------------------------------------------------- 1 | # MQTTX 协议 2 | 3 | ## 概览 4 | #### MQTT是一个轻量的发布订阅模式消息传输协议,专门针对低带宽和不稳定网络环境的物联网应用设计 5 | ``` 6 | MQTT 官网: http://mqtt.org 7 | MQTT V3.1.1协议规范: http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html 8 | MQTT 协议英文版: http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/mqtt-v3.1.1.html 9 | MQTT 协议中文版: https://mcxiaoke.gitbooks.io/mqtt-cn/content/ 10 | ``` 11 | 12 | ## 特点 13 | ``` 14 | 1.开放消息协议,简单易实现 15 | 2.发布订阅模式,一对多消息发布 16 | 3.基于TCP/IP网络连接 17 | 4.1字节固定报头,2字节心跳报文,报文结构紧凑 18 | 5.消息QoS支持,可靠传输保证 19 | ``` 20 | 21 | ## 应用 22 | #### MQTT协议广泛应用于物联网、移动互联网、智能硬件、车联网、电力能源等领域。 23 | ``` 24 | 1.物联网M2M通信,物联网大数据采集 25 | 2.Android消息推送,WEB消息推送 26 | 3.移动即时消息,例如Facebook Messenger 27 | 4.智能硬件、智能家具、智能电器 28 | 5.车联网通信,电动车站桩采集 29 | 6.智慧城市、远程医疗、远程教育 30 | 7.电力、石油与能源等行业市场 31 | ``` 32 | 33 | ## MQTT基于主题(Topic)消息路由 34 | ``` 35 | chat/room/1 36 | sensor/10/temperature 37 | sensor/+/temperature 38 | $SYS/broker/metrics/packets/received 39 | $SYS/broker/metrics/# 40 | ``` 41 | 42 | #### 主题(Topic)通过’/’分割层级,支持’+’, ‘#’通配符: 43 | ``` 44 | '+': 表示通配一个层级,例如a/+,匹配a/x, a/y 45 | '#': 表示通配多个层级,例如a/#,匹配a/x, a/b/c/d 46 | ``` 47 | 48 | #### 订阅者与发布者之间通过主题路由消息进行通信,例如采用mosquitto命令行发布订阅消息: 49 | ``` 50 | mosquitto_sub -t a/b/+ -q 1 51 | mosquitto_pub -t a/b/c -m hello -q 1 52 | ``` 53 | 54 | #### 注解 55 | `订阅者可以订阅含通配符主题,但发布者不允许向含通配符主题发布消息。` 56 | 57 | *************************** 58 | 59 | ## MQTT v3.1.1协议报文 60 | #### 报文结构 61 | ``` 62 | 固定报头(Fixed header) 63 | 可变报头(Variable header) 64 | 报文有效载荷(Payload) 65 | ``` 66 | 67 | #### 固定报头 68 | ``` 69 | | Bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | 70 | | ----- |----|----|----|----|----|----|----|----| 71 | |byte1 | MQTT Packet type | Flags 72 | |byte2… | Remaining Length 73 | ``` 74 | 75 | 76 | #### 报文类型 77 | ``` 78 | | 类型名称 | 类型值 | 报文说明 | 79 | | -------- | ---- | ------------- | 80 | |CONNECT |1 |发起连接 | 81 | |CONNACK |2 |连接回执 | 82 | |PUBLISH |3 |发布消息 | 83 | |PUBACK |4 |发布回执 | 84 | |PUBREC |5 |QoS2消息回执 | 85 | |PUBREL |6 |QoS2消息释放 | 86 | |PUBCOMP |7 |QoS2消息完成 | 87 | |SUBSCRIBE |8 |订阅主题 | 88 | |SUBACK |9 |订阅回执 | 89 | |UNSUBSCRIBE|10 |取消订阅 | 90 | |UNSUBACK |11 |取消订阅回执 | 91 | |PINGREQ |12 |PING请求 | 92 | |PINGRESP |13 |PING响应 | 93 | |DISCONNECT |14 |断开连接 | 94 | ``` 95 | 96 | ### PUBLISH发布消息 97 | ``` 98 | PUBLISH报文承载客户端与服务器间双向的发布消息。 PUBACK报文用于接收端确认QoS1报文,PUBREC/PUBREL/PUBCOMP报文用于QoS2消息流程。 99 | ``` 100 | 101 | ### PINGREQ/PINGRESP心跳 102 | ``` 103 | 客户端在无报文发送时,按保活周期(KeepAlive)定时向服务端发送PINGREQ心跳报文,服务端响应PINGRESP报文。PINGREQ/PINGRESP报文均2个字节。 104 | ``` 105 | 106 | ### MQTT消息QoS 107 | ``` 108 | MQTT发布消息QoS保证不是端到端的,是客户端与服务器之间的。订阅者收到MQTT消息的QoS级别,最终取决于发布消息的QoS和主题订阅的QoS。 109 | ``` 110 | 111 | ### Qos0消息发布订阅 112 | ![Image text](https://docs.emqx.io/broker/v3/cn/_images/qos0_seq.png) 113 | 114 | ### Qos1消息发布订阅 115 | ![Image text](https://docs.emqx.io/broker/v3/cn/_images/qos1_seq.png) 116 | 117 | ### Qos2消息发布订阅 118 | ![Image text](https://docs.emqx.io/broker/v3/cn/_images/qos2_seq.png) 119 | 120 | ### MQTT会话(Clean Session) 121 | ``` 122 | MQTT客户端向服务器发起CONNECT请求时,可以通过’Clean Session’标志设置会话。 123 | 124 | ‘Clean Session’设置为0,表示创建一个持久会话,在客户端断开连接时,会话仍然保持并保存离线消息,直到会话超时注销。 125 | 126 | ‘Clean Session’设置为1,表示创建一个新的临时会话,在客户端断开时,会话自动销毁。 127 | ``` 128 | 129 | ### MQTT连接保活心跳 130 | ``` 131 | MQTT客户端向服务器发起CONNECT请求时,通过KeepAlive参数设置保活周期。 132 | 133 | 客户端在无报文发送时,按KeepAlive周期定时发送2字节的PINGREQ心跳报文,服务端收到PINGREQ报文后,回复2字节的PINGRESP报文。 134 | 135 | 服务端在1.5个心跳周期内,既没有收到客户端发布订阅报文,也没有收到PINGREQ心跳报文时,主动心跳超时断开客户端TCP连接。 136 | ``` 137 | 138 | ### MQTT遗愿消息(Last Will) 139 | ``` 140 | MQTT客户端向服务器端CONNECT请求时,可以设置是否发送遗愿消息(Will Message)标志,和遗愿消息主题(Topic)与内容(Payload)。 141 | 142 | MQTT客户端异常下线时(客户端断开前未向服务器发送DISCONNECT消息),MQTT消息服务器会发布遗愿消息。 143 | ``` 144 | 145 | ### MQTT保留消息(Retained Message) 146 | 147 | MQTT客户端向服务器发布(PUBLISH)消息时,可以设置保留消息(Retained Message)标志。保留消息(Retained Message)会驻留在消息服务器,后来的订阅者订阅主题时仍可以接收该消息。 148 | 149 | 例如mosquitto命令行发布一条保留消息到主题’a/b/c’: 150 | 151 | ``` 152 | mosquitto_pub -r -q 1 -t a/b/c -m 'hello' 153 | ``` 154 | 155 | 之后连接上来的MQTT客户端订阅主题’a/b/c’时候,仍可收到该消息: 156 | 157 | ``` 158 | $ mosquitto_sub -t a/b/c -q 1 159 | hello 160 | ``` 161 | 162 | 保留消息(Retained Message)有两种清除方式: 163 | 164 | 客户端向有保留消息的主题发布一个空消息: 165 | ``` 166 | mosquitto_pub -r -q 1 -t a/b/c -m '' 167 | ``` 168 | 消息服务器设置保留消息的超期时间。 169 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MqttFx 2 | 3 | c# mqtt 3.1.1 client 4 | 5 | *** 6 | 7 | ## Install 8 | 9 | `PM> Install-Package MqttFx` 10 | 11 | 12 | ## Samples 13 | ```c# 14 | 15 | class Program 16 | { 17 | static async Task Main(string[] args) 18 | { 19 | var services = new ServiceCollection(); 20 | services.AddMqttFxClient(options => 21 | { 22 | options.Host = "broker.emqx.io"; 23 | options.Port = 1883; 24 | }); 25 | var container = services.BuildServiceProvider(); 26 | 27 | var client = container.GetService(); 28 | 29 | client.ConnectedAsync += async e => 30 | { 31 | Console.WriteLine("### CONNECTED WITH SERVER ###"); 32 | 33 | Console.WriteLine("### SUBSCRIBED ###"); 34 | 35 | var subscriptionRequests = new SubscriptionRequestsBuilder() 36 | .WithTopicFilter(f => f.WithTopic("testtopic/a")) 37 | .WithTopicFilter(f => f.WithTopic("testtopic/b").WithAtLeastOnceQoS()) 38 | .Build(); 39 | 40 | var subscribeResult = await client.SubscribeAsync(subscriptionRequests); 41 | 42 | foreach (var item in subscribeResult.Items) 43 | { 44 | Console.WriteLine($"+ ResultCode = {item.ResultCode}"); 45 | } 46 | }; 47 | 48 | client.ApplicationMessageReceivedAsync += async message => 49 | { 50 | Console.WriteLine("### RECEIVED APPLICATION MESSAGE ###"); 51 | Console.WriteLine($"+ Topic = {message.Topic}"); 52 | Console.WriteLine($"+ Payload = {Encoding.UTF8.GetString(message.Payload)}"); 53 | Console.WriteLine($"+ QoS = {message.Qos}"); 54 | Console.WriteLine($"+ Retain = {message.Retain}"); 55 | Console.WriteLine(); 56 | 57 | await Task.CompletedTask; 58 | }; 59 | 60 | var connectResult = await client.ConnectAsync(); 61 | if (connectResult.Succeeded) 62 | { 63 | for (int i = 1; i <= 3; i++) 64 | { 65 | await Task.Delay(500); 66 | Console.WriteLine("### Publish Message ###"); 67 | 68 | var mesage = new ApplicationMessageBuilder() 69 | .WithTopic("testtopic/ab") 70 | .WithPayload($"HelloWorld: {i}") 71 | .WithQos(MqttQos.AtLeastOnce) 72 | .Build(); 73 | 74 | await client.PublishAsync(mesage); 75 | } 76 | } 77 | else 78 | Console.WriteLine("Connect Fail!"); 79 | 80 | Console.ReadKey(); 81 | } 82 | } 83 | 84 | ``` 85 | 86 | ## MQTT 规范 87 | 88 | 你可以通过以下链接了解与查阅 MQTT 协议: 89 | 90 | [MQTT 协议中文版](https://mcxiaoke.gitbooks.io/mqtt-cn/content/) 91 | 92 | [MQTT Version 3.1.1](https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html) 93 | 94 | [MQTT Version 5.0](https://docs.oasis-open.org/mqtt/mqtt/v5.0/cs02/mqtt-v5.0-cs02.html) 95 | 96 | [MQTT SN](https://www.oasis-open.org/committees/download.php/66091/MQTT-SN_spec_v1.2.pdf) -------------------------------------------------------------------------------- /samples/EchoClient/EchoClient.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | Exe 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /samples/EchoClient/Program.cs: -------------------------------------------------------------------------------- 1 | using DotNetty.Codecs.MqttFx.Packets; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using MqttFx; 4 | using MqttFx.Client; 5 | using System; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace EchoClient 10 | { 11 | class Program 12 | { 13 | static async Task Main(string[] args) 14 | { 15 | var services = new ServiceCollection(); 16 | services.AddMqttFxClient(options => 17 | { 18 | options.Host = "broker.emqx.io"; 19 | options.Port = 1883; 20 | }); 21 | var container = services.BuildServiceProvider(); 22 | 23 | var client = container.GetService(); 24 | 25 | client.ConnectedAsync += async e => 26 | { 27 | Console.WriteLine("### CONNECTED WITH SERVER ###"); 28 | 29 | Console.WriteLine("### SUBSCRIBED ###"); 30 | 31 | //var subscriptionRequests = new SubscriptionRequestsBuilder() 32 | // .WithTopicFilter(f => f.WithTopic("testtopic/a")) 33 | // .WithTopicFilter(f => f.WithTopic("testtopic/b").WithAtLeastOnceQoS()) 34 | // .Build(); 35 | 36 | //var subscribeResult = await client.SubscribeAsync(subscriptionRequests); 37 | var subscribeResult = await client.SubscribeAsync("testopic/bbb", MqttQos.ExactlyOnce); 38 | 39 | foreach (var item in subscribeResult.Items) 40 | { 41 | Console.WriteLine($"+ ResultCode = {item.ResultCode}"); 42 | } 43 | }; 44 | 45 | client.ApplicationMessageReceivedAsync += async message => 46 | { 47 | Console.WriteLine("### RECEIVED APPLICATION MESSAGE ###"); 48 | Console.WriteLine($"+ Topic = {message.Topic}"); 49 | Console.WriteLine($"+ Payload = {Encoding.UTF8.GetString(message.Payload)}"); 50 | Console.WriteLine($"+ QoS = {message.Qos}"); 51 | Console.WriteLine($"+ Retain = {message.Retain}"); 52 | Console.WriteLine(); 53 | 54 | await Task.CompletedTask; 55 | }; 56 | 57 | var connectResult = await client.ConnectAsync(); 58 | if (connectResult.Succeeded) 59 | { 60 | for (int i = 1; i <= 100; i++) 61 | { 62 | await Task.Delay(500); 63 | Console.WriteLine("### Publish Message ###"); 64 | 65 | var mesage = new ApplicationMessageBuilder() 66 | .WithTopic("testtopic/ab") 67 | .WithPayload($"HelloWorld: {i}") 68 | .WithQos(MqttQos.AtLeastOnce) 69 | .Build(); 70 | 71 | await client.PublishAsync(mesage); 72 | 73 | Console.ReadKey(); 74 | } 75 | } 76 | else 77 | Console.WriteLine("Connect Fail!"); 78 | 79 | Console.ReadKey(); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /samples/SimpleClient/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.Hosting; 3 | using Microsoft.Extensions.Logging; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace SimpleClient 8 | { 9 | class Program 10 | { 11 | static async Task Main(string[] args) 12 | { 13 | var host = HostingHostBuilderExtensions.ConfigureLogging(new HostBuilder(), logging => 14 | { 15 | logging.AddConsole(); 16 | //Microsoft.Extensions.Logging.LoggingBuilderExtensions.SetMinimumLevel(logging, (LogLevel)LogLevel.Debug); 17 | }) 18 | .ConfigureServices(services => 19 | { 20 | services.AddMqttFxClient(options => 21 | { 22 | options.Host = "broker.emqx.io"; 23 | options.Port = 1883; 24 | options.WillTopic = "testtopic/c"; 25 | options.WillPayload = Encoding.UTF8.GetBytes("offline"); 26 | options.WillRetain = true; 27 | }); 28 | services.AddHostedService(); 29 | }); 30 | await host.RunConsoleAsync(); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /samples/SimpleClient/Services.cs: -------------------------------------------------------------------------------- 1 | using DotNetty.Codecs.MqttFx.Packets; 2 | using Microsoft.Extensions.Hosting; 3 | using Microsoft.Extensions.Logging; 4 | using MqttFx; 5 | using MqttFx.Client; 6 | using System; 7 | using System.Text; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | 11 | namespace SimpleClient 12 | { 13 | public class Services : IHostedService 14 | { 15 | private readonly ILogger logger; 16 | private readonly MqttClient client; 17 | 18 | public Services(ILogger logger, MqttClient client) 19 | { 20 | this.logger = logger; 21 | this.client = client; 22 | } 23 | 24 | public async Task StartAsync(CancellationToken cancellationToken) 25 | { 26 | client.ConnectedAsync += async e => 27 | { 28 | Console.WriteLine("### CONNECTED WITH SERVER ###"); 29 | 30 | Console.WriteLine("### SUBSCRIBED ###"); 31 | 32 | var subscriptionRequests = new SubscriptionRequestsBuilder() 33 | .WithTopicFilter(f => f.WithTopic("testtopic/a")) 34 | .WithTopicFilter(f => f.WithTopic("testtopic/b").WithAtLeastOnceQoS()) 35 | .WithTopicFilter(f => f.WithTopic("testtopic/c").WithExactlyOnceQoS()) 36 | .Build(); 37 | 38 | var subscribeResult = await client.SubscribeAsync(subscriptionRequests); 39 | 40 | foreach (var item in subscribeResult.Items) 41 | { 42 | Console.WriteLine($"+ ResultCode = {item.ResultCode}"); 43 | } 44 | 45 | // online 46 | var mesage = new ApplicationMessageBuilder() 47 | .WithTopic("testtopic/s") 48 | .WithPayload($"online") 49 | .WithRetain(true) 50 | .Build(); 51 | 52 | await client.PublishAsync(mesage); 53 | }; 54 | 55 | client.ApplicationMessageReceivedAsync += async message => 56 | { 57 | Console.WriteLine("### RECEIVED APPLICATION MESSAGE ###"); 58 | Console.WriteLine($"+ Topic = {message.Topic}"); 59 | Console.WriteLine($"+ Payload = {Encoding.UTF8.GetString(message.Payload)}"); 60 | Console.WriteLine($"+ QoS = {message.Qos}"); 61 | Console.WriteLine($"+ Retain = {message.Retain}"); 62 | Console.WriteLine(); 63 | 64 | await Task.CompletedTask; 65 | }; 66 | 67 | var connectResult = await client.ConnectAsync(cancellationToken); 68 | if (connectResult.Succeeded) 69 | { 70 | for (int i = 1; i <= 3; i++) 71 | { 72 | await Task.Delay(500); 73 | logger.LogInformation("### Publish Message ###"); 74 | 75 | var mesage = new ApplicationMessageBuilder() 76 | .WithTopic("testtopic/ab") 77 | .WithPayload($"HelloWorld: {i}") 78 | .WithQos(MqttQos.AtLeastOnce) 79 | .Build(); 80 | 81 | await client.PublishAsync(mesage); 82 | } 83 | } 84 | else 85 | logger.LogError("Connect Fail!"); 86 | } 87 | 88 | public async Task StopAsync(CancellationToken cancellationToken) 89 | { 90 | await client.DisconnectAsync(cancellationToken); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /samples/SimpleClient/SimpleClient.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | Exe 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/DotNetty.Codecs.MqttFx/ByteBufferExtensions.cs: -------------------------------------------------------------------------------- 1 | using DotNetty.Buffers; 2 | using System; 3 | using System.IO; 4 | using System.Runtime.CompilerServices; 5 | using System.Text; 6 | 7 | namespace DotNetty.Codecs.MqttFx; 8 | 9 | static class ByteBufferExtensions 10 | { 11 | /// 12 | /// 写入 13 | /// 14 | /// 15 | /// 16 | public static void Write(this Stream stream, byte[] buffer) 17 | { 18 | stream.Write(buffer, 0, buffer.Length); 19 | } 20 | 21 | /// 22 | /// 写入两字节 23 | /// 24 | /// 25 | /// 26 | public static void WriteShort(this Stream stream, short value) 27 | { 28 | stream.WriteByte((byte)(value >> 8)); 29 | stream.WriteByte((byte)(value & 0xFF)); 30 | } 31 | 32 | /// 33 | /// 写入字符串 34 | /// 35 | /// The stream containing the string to write. 36 | /// The string to write. 37 | public static void WriteString(this Stream stream, string value) 38 | { 39 | Encoding enc = new MqttEncoding(); 40 | byte[] stringBytes = enc.GetBytes(value); 41 | stream.Write(stringBytes, 0, stringBytes.Length); 42 | } 43 | 44 | /// 45 | /// 写入字符串(Encode by UTF8) 46 | /// 47 | /// The stream containing the string to write. 48 | /// The string to write. 49 | public static void WriteString(this IByteBuffer buffer, string value) 50 | { 51 | byte[] stringBytes = Encoding.UTF8.GetBytes(value); 52 | buffer.WriteShort(stringBytes.Length); 53 | buffer.WriteBytes(stringBytes); 54 | } 55 | 56 | public static void WriteLengthBytes(this IByteBuffer buffer, byte[] value) 57 | { 58 | //var buf = Unpooled.WrappedBuffer(value); 59 | //buffer.WriteShort(buf.ReadableBytes); 60 | //if(buf.IsReadable()) 61 | //{ 62 | // buffer.WriteBytes(buf); 63 | //} 64 | 65 | buffer.WriteShort(value.Length); 66 | if (value.Length > 0) 67 | buffer.WriteBytes(value); 68 | } 69 | 70 | public static byte ReadByte(this IByteBuffer buffer, ref int remainingLength) 71 | { 72 | DecreaseRemainingLength(ref remainingLength, 1); 73 | return buffer.ReadByte(); 74 | } 75 | 76 | public static byte[] ReadBytes(this IByteBuffer buffer, int length, ref int remainingLength) 77 | { 78 | if (length == 0) 79 | return new byte[0]; 80 | 81 | DecreaseRemainingLength(ref remainingLength, 1); 82 | return buffer.ReadBytes(length).Array; 83 | } 84 | 85 | /// 86 | /// 读取两字节 87 | /// 88 | /// 89 | /// 90 | public static short ReadShort(this Stream stream) 91 | { 92 | byte high, low; 93 | high = (byte)stream.ReadByte(); 94 | low = (byte)stream.ReadByte(); 95 | return (short)((high << 8) + low); 96 | } 97 | 98 | public static short ReadShort(this IByteBuffer buffer, ref int remainingLength) 99 | { 100 | DecreaseRemainingLength(ref remainingLength, 2); 101 | return buffer.ReadShort(); 102 | } 103 | 104 | /// 105 | /// 读取两字节 106 | /// 107 | /// 108 | /// 109 | /// 110 | public static ushort ReadUnsignedShort(this IByteBuffer buffer, ref int remainingLength) 111 | { 112 | DecreaseRemainingLength(ref remainingLength, 2); 113 | return buffer.ReadUnsignedShort(); ; 114 | } 115 | 116 | /// 117 | /// 读取字符串 118 | /// 119 | /// 120 | /// 121 | public static string ReadString(this Stream stream) 122 | { 123 | // read and check the length 124 | var lengthBytes = new byte[2]; 125 | var bytesRead = stream.Read(lengthBytes, 0, 2); 126 | if (bytesRead < 2) 127 | throw new ArgumentException("The stream did not have enough bytes to describe the length of the string", "stringStream"); 128 | 129 | var enc = new MqttEncoding(); 130 | var stringLength = (ushort)enc.GetCharCount(lengthBytes); 131 | 132 | // read the bytes from the string, validate we have enough etc. 133 | var stringBytes = new byte[stringLength]; 134 | var readBuffer = new byte[1 << 10]; // 1KB read buffer 135 | var totalRead = 0; 136 | 137 | // Keep reading until we have all. Intentionally synchronous 138 | while (totalRead < stringLength) 139 | { 140 | var remainingBytes = stringLength - totalRead; 141 | var nextReadSize = remainingBytes > readBuffer.Length ? readBuffer.Length : remainingBytes; 142 | bytesRead = stream.Read(readBuffer, 0, nextReadSize); 143 | Array.Copy(readBuffer, 0, stringBytes, totalRead, bytesRead); 144 | totalRead += bytesRead; 145 | } 146 | 147 | return enc.GetString(stringBytes); 148 | } 149 | 150 | /// 151 | /// 读取字符串 152 | /// 153 | /// 154 | public static string ReadString(this IByteBuffer buffer, ref int remainingLength) 155 | { 156 | int size = ReadUnsignedShort(buffer, ref remainingLength); 157 | 158 | //if (size < minBytes) 159 | // throw new DecoderException($"String value is shorter than minimum allowed {minBytes}. Advertised length: {size}"); 160 | 161 | //if (size > maxBytes) 162 | // throw new DecoderException($"String value is longer than maximum allowed {maxBytes}. Advertised length: {size}"); 163 | 164 | if (size == 0) 165 | return string.Empty; 166 | 167 | DecreaseRemainingLength(ref remainingLength, size); 168 | 169 | var value = buffer.ToString(buffer.ReaderIndex, size, Encoding.UTF8); 170 | buffer.SetReaderIndex(buffer.ReaderIndex + size); 171 | 172 | return value; 173 | } 174 | 175 | /// 176 | /// 读长度字节 177 | /// 178 | /// 179 | /// 180 | /// 181 | public static byte[] ReadBytesArray(this IByteBuffer buffer, ref int remainingLength) 182 | { 183 | int length = ReadUnsignedShort(buffer, ref remainingLength); 184 | return buffer.ReadBytes(length, ref remainingLength); 185 | } 186 | 187 | /// 188 | /// 布尔转字节 189 | /// 190 | /// 191 | /// 192 | public static byte ToByte(this bool input) 193 | { 194 | return input ? (byte)1 : (byte)0; 195 | } 196 | 197 | public static byte[] ReadSliceArray(this IByteBuffer buffer, ref int remainingLength) 198 | { 199 | IByteBuffer buf; 200 | if (remainingLength > 0) 201 | { 202 | buf = buffer.ReadSlice(remainingLength); 203 | buf.Retain(); 204 | remainingLength = 0; 205 | } 206 | else 207 | { 208 | buf = Unpooled.Empty; 209 | } 210 | return buf.ToByteArray(); 211 | } 212 | 213 | public static byte[] ToByteArray(this IByteBuffer buffer) 214 | { 215 | return ((Span)buffer.Array).Slice(buffer.ArrayOffset, buffer.ReadableBytes).ToArray(); 216 | } 217 | 218 | [MethodImpl(MethodImplOptions.AggressiveInlining)] // we don't care about the method being on exception's stack so it's OK to inline 219 | static void DecreaseRemainingLength(ref int remainingLength, int minExpectedLength) 220 | { 221 | if (remainingLength < minExpectedLength) 222 | throw new DecoderException($"Current Remaining Length of {remainingLength} is smaller than expected {minExpectedLength}."); 223 | 224 | remainingLength -= minExpectedLength; 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /src/DotNetty.Codecs.MqttFx/DotNetty.Codecs.MqttFx.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.1 5 | latest 6 | DTXLink 7 | https://github.com/linfx/MqttFx 8 | https://github.com/linfx/MqttFx 9 | git 10 | mqtt 11 | © LinFx. All rights reserved. 12 | 0.7.4 13 | MQTT codec for DotNetty 14 | True 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/DotNetty.Codecs.MqttFx/MqttDecoder.cs: -------------------------------------------------------------------------------- 1 | using DotNetty.Buffers; 2 | using DotNetty.Codecs.MqttFx.Packets; 3 | using DotNetty.Transport.Channels; 4 | using System; 5 | using System.Collections.Generic; 6 | 7 | namespace DotNetty.Codecs.MqttFx; 8 | 9 | /// 10 | /// 解码器 11 | /// 12 | public sealed class MqttDecoder : ReplayingDecoder 13 | { 14 | private readonly bool _isServer; 15 | private readonly int _maxMessageSize; 16 | 17 | public enum ParseState { Ready, Failed } 18 | 19 | /// 20 | /// 解码器 21 | /// 22 | /// 23 | /// 24 | public MqttDecoder(bool isServer, int maxMessageSize) 25 | : base(ParseState.Ready) 26 | { 27 | _isServer = isServer; 28 | _maxMessageSize = maxMessageSize; 29 | } 30 | 31 | protected override void Decode(IChannelHandlerContext context, IByteBuffer input, List output) 32 | { 33 | try 34 | { 35 | switch (State) 36 | { 37 | case ParseState.Ready: 38 | if (!TryDecodePacket(context, input, out Packet packet)) 39 | { 40 | RequestReplay(); 41 | return; 42 | } 43 | output.Add(packet); 44 | Checkpoint(); 45 | break; 46 | case ParseState.Failed: 47 | // read out data until connection is closed 48 | input.SkipBytes(input.ReadableBytes); 49 | return; 50 | default: 51 | throw new ArgumentOutOfRangeException(); 52 | } 53 | } 54 | catch (DecoderException) 55 | { 56 | input.SkipBytes(input.ReadableBytes); 57 | Checkpoint(ParseState.Failed); 58 | throw; 59 | } 60 | } 61 | 62 | private bool TryDecodePacket(IChannelHandlerContext context, IByteBuffer buffer, out Packet packet) 63 | { 64 | if (!buffer.IsReadable(2)) 65 | { 66 | packet = default; 67 | return false; 68 | } 69 | 70 | FixedHeader fixedHeader = default; 71 | if (!fixedHeader.Decode(buffer)) 72 | { 73 | packet = null; 74 | return false; 75 | } 76 | 77 | packet = fixedHeader.PacketType switch 78 | { 79 | PacketType.CONNECT => new ConnectPacket(), 80 | PacketType.CONNACK => new ConnAckPacket(), 81 | PacketType.DISCONNECT => new DisconnectPacket(), 82 | PacketType.PINGREQ => new PingReqPacket(), 83 | PacketType.PINGRESP => new PingRespPacket(), 84 | PacketType.PUBACK => new PubAckPacket(), 85 | PacketType.PUBCOMP => new PubCompPacket(), 86 | PacketType.PUBLISH => new PublishPacket(), 87 | PacketType.PUBREC => new PubRecPacket(), 88 | PacketType.PUBREL => new PubRelPacket(), 89 | PacketType.SUBSCRIBE => new SubscribePacket(), 90 | PacketType.SUBACK => new SubAckPacket(), 91 | PacketType.UNSUBSCRIBE => new UnsubscribePacket(), 92 | PacketType.UNSUBACK => new UnsubscribePacket(), 93 | _ => throw new DecoderException("Unsupported Message Type"), 94 | }; 95 | packet.FixedHeader = fixedHeader; 96 | packet.Decode(buffer); 97 | 98 | return true; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/DotNetty.Codecs.MqttFx/MqttEncoder.cs: -------------------------------------------------------------------------------- 1 | using DotNetty.Buffers; 2 | using DotNetty.Codecs.MqttFx.Packets; 3 | using DotNetty.Common.Utilities; 4 | using DotNetty.Transport.Channels; 5 | using System.Collections.Generic; 6 | 7 | namespace DotNetty.Codecs.MqttFx; 8 | 9 | /// 10 | /// 编码器 11 | /// 12 | public sealed class MqttEncoder : MessageToMessageEncoder 13 | { 14 | public static readonly MqttEncoder Instance = new MqttEncoder(); 15 | 16 | protected override void Encode(IChannelHandlerContext context, Packet packet, List output) 17 | { 18 | DoEncode(context.Allocator, packet, output); 19 | } 20 | 21 | public static void DoEncode(IByteBufferAllocator allocator, Packet packet, List output) 22 | { 23 | var buffer = allocator.Buffer(); 24 | try 25 | { 26 | packet.Encode(buffer); 27 | output.Add(buffer); 28 | buffer = null; 29 | } 30 | finally 31 | { 32 | buffer?.SafeRelease(); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/DotNetty.Codecs.MqttFx/MqttEncoding.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace DotNetty.Codecs.MqttFx; 6 | 7 | /// 8 | /// 编码 9 | /// 10 | public class MqttEncoding : ASCIIEncoding 11 | { 12 | public override byte[] GetBytes(string s) 13 | { 14 | ValidateString(s); 15 | var stringBytes = new List 16 | { 17 | (byte)(s.Length >> 8), 18 | (byte)(s.Length & 0xFF) 19 | }; 20 | stringBytes.AddRange(ASCII.GetBytes(s)); 21 | return stringBytes.ToArray(); 22 | } 23 | 24 | public override string GetString(byte[] bytes) 25 | { 26 | return ASCII.GetString(bytes); 27 | } 28 | 29 | public override int GetCharCount(byte[] bytes) 30 | { 31 | if (bytes.Length < 2) 32 | throw new ArgumentException("Length byte array must comprise 2 bytes"); 33 | 34 | return (ushort)((bytes[0] << 8) + bytes[1]); 35 | } 36 | 37 | public override int GetByteCount(string chars) 38 | { 39 | ValidateString(chars); 40 | return ASCII.GetByteCount(chars) + 2; 41 | } 42 | 43 | private static void ValidateString(string s) 44 | { 45 | foreach (var c in s) 46 | { 47 | if (c > 0x7F) 48 | throw new ArgumentException("The input string has extended UTF characters, which are not supported"); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/DotNetty.Codecs.MqttFx/Packets/ConnAckPacket.cs: -------------------------------------------------------------------------------- 1 | namespace DotNetty.Codecs.MqttFx.Packets; 2 | 3 | /// 4 | /// 连接报文回执(CONNACK – Acknowledge connection request) 5 | /// The CONNACK Packet is the packet sent by the Server in response to a CONNECT Packet received from a Client. 6 | /// The first packet sent from the Server to the Client MUST be a CONNACK Packet [MQTT-3.2.0-1]. 7 | /// 8 | public sealed record ConnAckPacket : Packet 9 | { 10 | /// 11 | /// 连接报文回执(CONNACK) 12 | /// 13 | public ConnAckPacket() 14 | : this(new ConnAckVariableHeader()) { } 15 | 16 | /// 17 | /// 连接报文回执(CONNACK) 18 | /// 19 | /// 20 | public ConnAckPacket(ConnAckVariableHeader variableHeader) 21 | : base(variableHeader) { } 22 | } 23 | -------------------------------------------------------------------------------- /src/DotNetty.Codecs.MqttFx/Packets/ConnAckVariableHeader.cs: -------------------------------------------------------------------------------- 1 | using DotNetty.Buffers; 2 | 3 | namespace DotNetty.Codecs.MqttFx.Packets; 4 | 5 | /// 6 | /// 可变报头(CONNACK Packet variable header) 7 | /// 8 | public sealed record ConnAckVariableHeader : VariableHeader 9 | { 10 | /* 11 | * 连接确认标志(3.2.2.1 Connect Acknowledge Flags) 12 | * Byte 1 is the "Connect Acknowledge Flags". Bits 7-1 are reserved and MUST be set to 0. 13 | * Bit 0 (SP1) is the Session Present Flag. 14 | */ 15 | 16 | /// 17 | /// 当前会话 Session Present 18 | /// 19 | public bool SessionPresent { get; set; } 20 | 21 | /// 22 | /// 连接返回码(Connect Return code) 23 | /// 24 | public ConnectReturnCode ConnectReturnCode { get; set; } 25 | 26 | /// 27 | /// 编码 28 | /// 29 | /// 30 | public override void Encode(IByteBuffer buffer) 31 | { 32 | buffer.WriteByte(SessionPresent ? 0x01 : 0x00); 33 | buffer.WriteByte((byte)ConnectReturnCode); 34 | } 35 | 36 | /// 37 | /// 解码 38 | /// 39 | /// 40 | public override void Decode(IByteBuffer buffer, ref FixedHeader fixedHeader) 41 | { 42 | SessionPresent = (buffer.ReadByte(ref fixedHeader.RemainingLength) & 0x01) == 0x01; 43 | ConnectReturnCode = (ConnectReturnCode)buffer.ReadByte(ref fixedHeader.RemainingLength); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/DotNetty.Codecs.MqttFx/Packets/ConnectFlags.cs: -------------------------------------------------------------------------------- 1 | namespace DotNetty.Codecs.MqttFx.Packets; 2 | 3 | /// 4 | /// Connect Flags 5 | /// 6 | public record struct ConnectFlags 7 | { 8 | /// 9 | /// 用户名标志(User Name Flag) 10 | /// 11 | public bool UsernameFlag; 12 | 13 | /// 14 | /// 密码标志(Password Flag) 15 | /// 16 | public bool PasswordFlag; 17 | 18 | /// 19 | /// 遗嘱保留(Will Retain) 20 | /// 21 | public bool WillRetain; 22 | 23 | /// 24 | /// 遗嘱QoS(Will QoS) 25 | /// 26 | public MqttQos WillQos; 27 | 28 | /// 29 | /// 遗嘱标志(Will Flag) 30 | /// 31 | public bool WillFlag; 32 | 33 | /// 34 | /// 清理会话(Clean Session) 35 | /// 设置为 0,服务端必须基于当前会话(使用客户端标识符识别)的状态恢复与客户端的通信。 36 | /// 设置为 1,客户端和服务端必须丢弃之前的任何会话并开始一个新的会话。会话仅持续和网络连接同样长的时间。与这个会话关联的状态数据不能被任何之后的会话重用 [MQTT-3.1.2-6]。 37 | /// 38 | public bool CleanSession; 39 | } 40 | -------------------------------------------------------------------------------- /src/DotNetty.Codecs.MqttFx/Packets/ConnectPacket.cs: -------------------------------------------------------------------------------- 1 | namespace DotNetty.Codecs.MqttFx.Packets; 2 | 3 | /// 4 | /// 连接报文(CONNECT – Client requests a connection to a Server) 5 | /// 6 | public sealed record ConnectPacket : Packet 7 | { 8 | /// 9 | /// 连接报文(CONNECT) 10 | /// 11 | public ConnectPacket() 12 | : this(new ConnectVariableHeader(), new ConnectPayload()) { } 13 | 14 | /// 15 | /// 连接报文(CONNECT) 16 | /// 17 | /// 18 | /// 19 | public ConnectPacket(ConnectVariableHeader variableHeader, ConnectPayload payload) 20 | : base(variableHeader, payload) { } 21 | } 22 | -------------------------------------------------------------------------------- /src/DotNetty.Codecs.MqttFx/Packets/ConnectPayload.cs: -------------------------------------------------------------------------------- 1 | using DotNetty.Buffers; 2 | 3 | namespace DotNetty.Codecs.MqttFx.Packets; 4 | 5 | /// 6 | /// 有效载荷(Payload) 7 | /// 8 | public sealed class ConnectPayload : Payload 9 | { 10 | /// 11 | /// 客户端标识符(Client Identifier) 12 | /// Each Client connecting to the Server has a unique ClientId. The ClientId MUST be used by Clients and by Servers to identify state that they hold relating to this MQTT Session between the Client and the Server [MQTT-3.1.3-2]. 13 | /// 14 | public string ClientId { get; set; } 15 | 16 | /// 17 | /// 遗嘱主题(Will Topic) 18 | /// The Will Topic MUST be a UTF-8 encoded string as defined in Section 1.5.3 [MQTT-3.1.3-10]. 19 | /// 20 | public string WillTopic { get; set; } 21 | 22 | /// 23 | /// 遗嘱消息(Will Message) 24 | /// The Will Message defines the Application Message that is to be published to the Will Topic as described in Section 3.1.2.5. 25 | /// 26 | public byte[] WillMessage { get; set; } 27 | 28 | /// 29 | /// 用户名(User Name) 30 | /// The User Name MUST be a UTF-8 encoded string as defined in Section 1.5.3 [MQTT-3.1.3-11]. 31 | /// 32 | public string UserName { get; set; } 33 | 34 | /// 35 | /// 密码(Password) 36 | /// The Password field contains 0 to 65535 bytes of binary data prefixed with a two byte length field which indicates the number of bytes used by the binary data (it does not include the two bytes taken up by the length field itself). 37 | /// 38 | public string Password { get; set; } 39 | 40 | /// 41 | /// 编码 42 | /// 43 | /// 44 | /// 45 | public override void Encode(IByteBuffer buffer, VariableHeader variableHeader) 46 | { 47 | var connectVariableHeader = (ConnectVariableHeader)variableHeader; 48 | 49 | buffer.WriteString(ClientId); 50 | if (connectVariableHeader.ConnectFlags.WillFlag) 51 | { 52 | buffer.WriteString(WillTopic); 53 | buffer.WriteLengthBytes(WillMessage); 54 | } 55 | if (connectVariableHeader.ConnectFlags.UsernameFlag) 56 | { 57 | buffer.WriteString(UserName); 58 | } 59 | if (connectVariableHeader.ConnectFlags.PasswordFlag) 60 | { 61 | buffer.WriteString(Password); 62 | } 63 | } 64 | 65 | /// 66 | /// 解码 67 | /// 68 | /// 69 | /// 70 | /// 71 | public override void Decode(IByteBuffer buffer, VariableHeader variableHeader, ref int remainingLength) 72 | { 73 | var connectVariableHeader = (ConnectVariableHeader)variableHeader; 74 | 75 | ClientId = buffer.ReadString(ref remainingLength); 76 | if (connectVariableHeader.ConnectFlags.WillFlag) 77 | { 78 | WillTopic = buffer.ReadString(ref remainingLength); 79 | WillMessage = buffer.ReadBytesArray(ref remainingLength); 80 | } 81 | if (connectVariableHeader.ConnectFlags.UsernameFlag) 82 | { 83 | UserName = buffer.ReadString(ref remainingLength); 84 | } 85 | if (connectVariableHeader.ConnectFlags.PasswordFlag) 86 | { 87 | Password = buffer.ReadString(ref remainingLength); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/DotNetty.Codecs.MqttFx/Packets/ConnectReturnCode.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DotNetty.Codecs.MqttFx.Packets; 4 | 5 | /// 6 | /// 连接返回码(Connect Return code) 7 | /// If a server sends a CONNACK packet containing a non-zero return code it MUST then close the Network Connection [MQTT-3.2.2-5]. 8 | /// 9 | [Flags] 10 | public enum ConnectReturnCode : byte 11 | { 12 | /// 13 | /// 连接已接受(0x00 Connection Accepted) 14 | /// Connection accepted 15 | /// 16 | CONNECTION_ACCEPTED = 0x00, 17 | 18 | /// 19 | /// 连接已拒绝,不支持的协议版本(0x01 Connection Refused, unacceptable protocol version) 20 | /// The Server does not support the level of the MQTT protocol requested by the Client 21 | /// 22 | CONNECTION_REFUSED_UNACCEPTABLE_PROTOCOL_VERSION = 0x01, 23 | 24 | /// 25 | /// 接已拒绝,不合格的客户端标识符(0x02 Connection Refused, identifier rejected) 26 | /// The Client identifier is correct UTF-8 but not allowed by the Server 27 | /// 28 | CONNECTION_REFUSED_IDENTIFIER_REJECTED = 0x02, 29 | 30 | /// 31 | /// 连接已拒绝,服务端不可用(0x03 Connection Refused, Server unavailable) 32 | /// The Network Connection has been made but the MQTT service is unavailable 33 | /// 34 | CONNECTION_REFUSED_SERVER_UNAVAILABLE = 0x03, 35 | 36 | /// 37 | /// 连接已拒绝,无效的用户名或密码(0x04 Connection Refused, bad user name or password) 38 | /// The data in the user name or password is malformed 39 | /// 40 | CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD = 0x04, 41 | 42 | /// 43 | /// 连接已拒绝,未授权(0x05 Connection Refused, not authorized) 44 | /// The Client is not authorized to connect 45 | /// 46 | CONNECTION_REFUSED_NOT_AUTHORIZED = 0x05, 47 | } 48 | -------------------------------------------------------------------------------- /src/DotNetty.Codecs.MqttFx/Packets/ConnectVariableHeader.cs: -------------------------------------------------------------------------------- 1 | using DotNetty.Buffers; 2 | 3 | namespace DotNetty.Codecs.MqttFx.Packets; 4 | 5 | /// 6 | /// 可变报头(Variable header) 7 | /// The variable header for the CONNECT Packet consists of four fields in the following order: Protocol Name, Protocol Level, Connect Flags, and Keep Alive. 8 | /// 9 | public sealed record ConnectVariableHeader : VariableHeader 10 | { 11 | /// 12 | /// 协议名(UTF-8编码的字符串)(Protocol Name) 13 | /// 14 | public string ProtocolName { get; set; } = "MQTT"; 15 | 16 | /// 17 | /// 协议级别(Protocol Level) 18 | /// The 8 bit unsigned value that represents the revision level of the protocol used by the Client. 19 | /// The value of the Protocol Level field for the version 3.1.1 of the protocol is 4 (0x04). 20 | /// The Server MUST respond to the CONNECT Packet with a CONNACK return code 0x01 (unacceptable protocol level) and then disconnect the Client if the Protocol Level is not supported by the Server [MQTT-3.1.2-2]. 21 | /// 22 | public byte ProtocolLevel { get; set; } = (byte)MqttVersion.MQTT_3_1_1; 23 | 24 | /// 25 | /// The Connect Flags byte contains a number of parameters specifying the behavior of the MQTT connection. It also indicates the presence or absence of fields in the payload. 26 | /// 27 | public ConnectFlags ConnectFlags; 28 | 29 | /// 30 | /// 保持连接(Keep Alive) 31 | /// 以秒为单位的时间间隔,表示为一个16位的字,它是指在客户端传输完成一个控制报文的时刻到发送下一个报文的时刻,两者之间允许空闲的最大时间间隔。 32 | /// The Keep Alive is a time interval measured in seconds. 33 | /// Expressed as a 16-bit word, it is the maximum time interval that is permitted to elapse between the point at which the Client finishes transmitting one Control Packet and the point it starts sending the next. 34 | /// It is the responsibility of the Client to ensure that the interval between Control Packets being sent does not exceed the Keep Alive value. 35 | /// In the absence of sending any other Control Packets, the Client MUST send a PINGREQ Packet [MQTT-3.1.2-23]. 36 | /// 37 | public ushort KeepAlive { get; set; } 38 | 39 | /// 40 | /// CONNECT报文的可变报头按下列次序包含四个字段: 41 | /// 协议名(Protocol Name) 42 | /// 协议级别(Protocol Level) 43 | /// 连接标志(Connect Flags) 44 | /// 保持连接(Keep Alive) 45 | /// 46 | /// 47 | public override void Encode(IByteBuffer buffer) 48 | { 49 | // 协议名 Protocol Name 50 | buffer.WriteString(ProtocolName); 51 | 52 | // 协议级别 Protocol Level 53 | buffer.WriteByte(ProtocolLevel); 54 | 55 | // 连接标志 Connect Flags 56 | var connectFlags = ConnectFlags.UsernameFlag.ToByte() << 7; 57 | connectFlags |= ConnectFlags.PasswordFlag.ToByte() << 6; 58 | connectFlags |= ConnectFlags.WillRetain.ToByte() << 5; 59 | connectFlags |= ((byte)ConnectFlags.WillQos) << 3; 60 | connectFlags |= ConnectFlags.WillFlag.ToByte() << 2; 61 | connectFlags |= ConnectFlags.CleanSession.ToByte() << 1; 62 | buffer.WriteByte(connectFlags); 63 | 64 | // 保持连接 Keep Alive 65 | buffer.WriteShort(KeepAlive); 66 | } 67 | 68 | public override void Decode(IByteBuffer buffer, ref FixedHeader fixedHeader) 69 | { 70 | // 协议名 Protocol Name 71 | ProtocolName = buffer.ReadString(ref fixedHeader.RemainingLength); 72 | 73 | // 协议级别 Protocol Level 74 | ProtocolLevel = buffer.ReadByte(ref fixedHeader.RemainingLength); 75 | 76 | // 连接标志 Connect Flags 77 | int connectFlags = buffer.ReadByte(ref fixedHeader.RemainingLength); 78 | ConnectFlags.CleanSession = (connectFlags & 0x02) == 0x02; 79 | ConnectFlags.WillFlag = (connectFlags & 0x04) == 0x04; 80 | if (ConnectFlags.WillFlag) 81 | { 82 | ConnectFlags.WillQos = (MqttQos)((connectFlags & 0x18) >> 3); 83 | ConnectFlags.WillRetain = (connectFlags & 0x20) == 0x20; 84 | } 85 | ConnectFlags.UsernameFlag = (connectFlags & 0x80) == 0x80; 86 | ConnectFlags.PasswordFlag = (connectFlags & 0x40) == 0x40; 87 | 88 | // 保持连接 Keep Alive 89 | KeepAlive = (ushort)buffer.ReadShort(ref fixedHeader.RemainingLength); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/DotNetty.Codecs.MqttFx/Packets/DisconnectPacket.cs: -------------------------------------------------------------------------------- 1 | namespace DotNetty.Codecs.MqttFx.Packets; 2 | 3 | /// 4 | /// 断开连接(DISCONNECT – Disconnect notification) 5 | /// The DISCONNECT Packet is the final Control Packet sent from the Client to the Server. It indicates that the Client is disconnecting cleanly. 6 | /// 7 | public sealed record DisconnectPacket : Packet 8 | { 9 | public static readonly DisconnectPacket Instance = new DisconnectPacket(); 10 | } 11 | -------------------------------------------------------------------------------- /src/DotNetty.Codecs.MqttFx/Packets/FixedHeader.cs: -------------------------------------------------------------------------------- 1 | using DotNetty.Buffers; 2 | 3 | namespace DotNetty.Codecs.MqttFx.Packets; 4 | 5 | /// 6 | /// 固定报头(Fixed header) 7 | /// 8 | public record struct FixedHeader 9 | { 10 | /// 11 | /// 报文类型(MQTT Control Packet type) 12 | /// 13 | public PacketType PacketType; 14 | 15 | /// 16 | /// 每个MQTT控制报文类型特定的标志(Flags) 17 | /// 18 | public int Flags; 19 | 20 | /// 21 | /// 剩余长度(Remaining Length) 22 | /// 表示当前报文剩余部分的字节数,包括可变报头和负载的数据。 23 | /// 剩余长度不包括用于编码剩余长度字段本身的字节数。 24 | /// 25 | public int RemainingLength; 26 | 27 | /// 28 | /// 编码 29 | /// 30 | /// 31 | /// 剩余长度 32 | public void Encode(IByteBuffer buffer, int remainingLength = default) 33 | { 34 | if (remainingLength != default) 35 | RemainingLength = remainingLength; 36 | 37 | /* 38 | * MQTT控制报文的类型 39 | * 标志位 Header Flags 40 | */ 41 | var headerFlags = (byte)PacketType << 4; 42 | headerFlags |= Flags; 43 | buffer.WriteByte(headerFlags); 44 | 45 | /* 46 | * 剩余长度 Remaining Length 47 | * 剩余长度字段使用一个变长度编码方案,对小于128的值它使用单字节编码。 48 | * 更大的值按下面的方式处理。低7位有效位用于编码数据,最高有效位用于指示是否有更多的字节。 49 | * 因此每个字节可以编码128个数值和一个延续位(continuation bit)。 50 | * 剩余长度字段最大4个字节。 51 | */ 52 | do 53 | { 54 | var digit = (byte)(remainingLength % 0x80); 55 | remainingLength /= 0x80; 56 | if (remainingLength > 0) 57 | digit |= 0x80; 58 | buffer.WriteByte(digit); 59 | } while (remainingLength > 0); 60 | } 61 | 62 | /// 63 | /// 解码 64 | /// 65 | /// 66 | public bool Decode(IByteBuffer buffer) 67 | { 68 | /* 69 | * MQTT控制报文的类型 70 | * 标志位 Header Flags 71 | */ 72 | int headerFlags = buffer.ReadByte(); 73 | PacketType = (PacketType)(headerFlags >> 4); 74 | Flags = headerFlags & 0x0f; 75 | 76 | /* 77 | * 剩余长度 Remaining Length 78 | */ 79 | //int multiplier = 1; 80 | //short digit; 81 | //int loops = 0; 82 | //do 83 | //{ 84 | // digit = buffer.ReadByte(); 85 | // RemaingLength += (digit & 127) * multiplier; 86 | // multiplier *= 128; 87 | // loops++; 88 | //} while ((digit & 128) != 0 && loops < 4); 89 | 90 | //// MQTT protocol limits Remaining Length to 4 bytes 91 | //if (loops == 4 && (digit & 128) != 0) 92 | // throw new DecoderException("remaining length exceeds 4 digits (" + PacketType + ')'); 93 | 94 | if (!TryDecodeRemainingLength(buffer, out RemainingLength) || !buffer.IsReadable(RemainingLength)) 95 | return false; 96 | 97 | return true; 98 | } 99 | 100 | bool TryDecodeRemainingLength(IByteBuffer buffer, out int value) 101 | { 102 | int readable = buffer.ReadableBytes; 103 | 104 | int result = 0; 105 | int multiplier = 1; 106 | byte digit; 107 | int read = 0; 108 | do 109 | { 110 | if (readable < read + 1) 111 | { 112 | value = default(int); 113 | return false; 114 | } 115 | digit = buffer.ReadByte(); 116 | result += (digit & 0x7f) * multiplier; 117 | multiplier <<= 7; 118 | read++; 119 | } 120 | while ((digit & 0x80) != 0 && read < 4); 121 | 122 | if (read == 4 && (digit & 0x80) != 0) 123 | throw new DecoderException("Remaining length exceeds 4 bytes in length"); 124 | 125 | int completeMessageSize = result + 1 + read; 126 | //if (completeMessageSize > this.maxMessageSize) 127 | //{ 128 | // throw new DecoderException("Message is too big: " + completeMessageSize); 129 | //} 130 | 131 | value = result; 132 | return true; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/DotNetty.Codecs.MqttFx/Packets/MqttCodecUtil.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace DotNetty.Codecs.MqttFx.Packets; 5 | 6 | public static class MqttCodecUtil 7 | { 8 | public static IDictionary PacketTypes = new Dictionary 9 | { 10 | { typeof(ConnectPacket), PacketType.CONNECT }, 11 | { typeof(ConnAckPacket), PacketType.CONNACK }, 12 | { typeof(PublishPacket), PacketType.PUBLISH }, 13 | { typeof(PubAckPacket), PacketType.PUBACK }, 14 | { typeof(PubRecPacket), PacketType.PUBREC }, 15 | { typeof(PubRelPacket), PacketType.PUBREL }, 16 | { typeof(PubCompPacket), PacketType.PUBCOMP }, 17 | { typeof(SubscribePacket), PacketType.SUBSCRIBE }, 18 | { typeof(SubAckPacket), PacketType.SUBACK }, 19 | { typeof(UnsubscribePacket), PacketType.UNSUBSCRIBE }, 20 | { typeof(UnsubAckPacket), PacketType.UNSUBACK }, 21 | { typeof(PingReqPacket), PacketType.PINGREQ }, 22 | { typeof(PingRespPacket), PacketType.PINGRESP }, 23 | { typeof(DisconnectPacket), PacketType.DISCONNECT }, 24 | }; 25 | 26 | public static void ValidateTopicFilter(string topicFilter) 27 | { 28 | int length = topicFilter.Length; 29 | if (length == 0) 30 | throw new DecoderException("All Topic Names and Topic Filters MUST be at least one character long. [MQTT-4.7.3-1]"); 31 | 32 | for (int i = 0; i < length; i++) 33 | { 34 | char c = topicFilter[i]; 35 | switch (c) 36 | { 37 | case '+': 38 | if ((i > 0 && topicFilter[i - 1] != '/') || (i < length - 1 && topicFilter[i + 1] != '/')) 39 | throw new DecoderException($"[MQTT-4.7.1-3]. Invalid topic filter: {topicFilter}"); 40 | 41 | break; 42 | case '#': 43 | if (i < length - 1 || (i > 0 && topicFilter[i - 1] != '/')) 44 | throw new DecoderException($"[MQTT-4.7.1-2]. Invalid topic filter: {topicFilter}"); 45 | 46 | break; 47 | } 48 | } 49 | } 50 | 51 | public static MqttQos GetQos(this FixedHeader fixedHeader) 52 | { 53 | return (MqttQos)((fixedHeader.Flags & 0x06) >> 1); 54 | } 55 | 56 | public static bool GetDup(this PublishPacket packet) 57 | { 58 | return (packet.FixedHeader.Flags & 0x08) == 0x08; 59 | } 60 | 61 | public static MqttQos GetQos(this PublishPacket packet) 62 | { 63 | return (MqttQos)((packet.FixedHeader.Flags & 0x06) >> 1); 64 | } 65 | 66 | public static bool GetRetain(this PublishPacket packet) 67 | { 68 | return (packet.FixedHeader.Flags & 0x01) > 0; 69 | } 70 | 71 | public static void SetQos(this PublishPacket packet, MqttQos qos) 72 | { 73 | packet.FixedHeader.Flags |= (byte)qos << 1; 74 | } 75 | 76 | public static void SetDup(this PublishPacket packet, bool dup = false) 77 | { 78 | packet.FixedHeader.Flags |= dup.ToByte() << 3; 79 | } 80 | 81 | public static void SetRetain(this PublishPacket packet, bool retain = false) 82 | { 83 | packet.FixedHeader.Flags |= retain.ToByte(); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/DotNetty.Codecs.MqttFx/Packets/MqttQos.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DotNetty.Codecs.MqttFx.Packets; 4 | 5 | /// 6 | /// 服务质量等级 7 | /// 8 | [Flags] 9 | public enum MqttQos : byte 10 | { 11 | /// 12 | /// 最多分发一次 13 | /// QOS Level 0 - Message is not guaranteed delivery. No retries are made to ensure delivery is successful. 14 | /// 15 | AtMostOnce = 0x00, 16 | 17 | /// 18 | /// 至少分发一次 19 | /// QOS Level 1 - Message is guaranteed delivery. It will be delivered at least one time, but may be delivered 20 | /// more than once if network errors occur. 21 | /// 22 | AtLeastOnce = 0x01, 23 | 24 | /// 25 | /// 只分发一次 26 | /// QOS Level 2 - Message will be delivered once, and only once. Message will be retried until 27 | /// it is successfully sent.. 28 | /// 29 | ExactlyOnce = 0x02, 30 | 31 | /// 32 | /// FAILURE 33 | /// 34 | Failure = 0x80, 35 | } 36 | -------------------------------------------------------------------------------- /src/DotNetty.Codecs.MqttFx/Packets/MqttVersion.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DotNetty.Codecs.MqttFx.Packets; 4 | 5 | [Flags] 6 | public enum MqttVersion : byte 7 | { 8 | MQTT_3_1 = 0x03, 9 | MQTT_3_1_1 = 0x04, 10 | MQTT_5 = 0x05, 11 | } 12 | -------------------------------------------------------------------------------- /src/DotNetty.Codecs.MqttFx/Packets/Packet.cs: -------------------------------------------------------------------------------- 1 | using DotNetty.Buffers; 2 | using DotNetty.Common.Utilities; 3 | 4 | namespace DotNetty.Codecs.MqttFx.Packets; 5 | 6 | /// 7 | /// MQTT 控制数据包(MQTT Control Packet) 8 | /// 通过网络连接发送的信息包。MQTT 规范定义了 14 种不同类型的控制数据包。 9 | /// 10 | /// 11 | public abstract record Packet 12 | { 13 | /// 14 | /// 固定报头(Fixed header) 15 | /// 16 | public FixedHeader FixedHeader; 17 | 18 | /// 19 | /// 可变报头(Variable header) 20 | /// 21 | public VariableHeader VariableHeader; 22 | 23 | /// 24 | /// 有效载荷(Payload) 25 | /// 26 | public Payload Payload; 27 | 28 | /// 29 | /// 报文抽象类(MQTT Control Packet) 30 | /// 31 | protected Packet() 32 | { 33 | FixedHeader.PacketType = MqttCodecUtil.PacketTypes[GetType()]; 34 | } 35 | 36 | /// 37 | /// 报文抽象类(MQTT Control Packet) 38 | /// 39 | /// 可变报头(Variable header) 40 | protected Packet(VariableHeader variableHeader) 41 | : this(variableHeader, default) { } 42 | 43 | /// 44 | /// 报文抽象类(MQTT Control Packet) 45 | /// 46 | /// 可变报头(Variable header) 47 | /// 有效载荷(Payload) 48 | protected Packet(VariableHeader variableHeader, Payload payload) 49 | : this() 50 | { 51 | VariableHeader = variableHeader; 52 | Payload = payload; 53 | } 54 | 55 | /// 56 | /// 报文抽象类(MQTT Control Packet) 57 | /// 58 | /// 固定报头(Fixed header) 59 | /// 可变报头(Variable header) 60 | /// 有效载荷(Payload) 61 | protected Packet(FixedHeader fixedHeader, VariableHeader variableHeader, Payload payload) 62 | : this() 63 | { 64 | FixedHeader = fixedHeader; 65 | VariableHeader = variableHeader; 66 | Payload = payload; 67 | } 68 | 69 | /// 70 | /// 编码 71 | /// 72 | /// 73 | public virtual void Encode(IByteBuffer buffer) 74 | { 75 | var buf = Unpooled.Buffer(); 76 | try 77 | { 78 | VariableHeader?.Encode(buf, FixedHeader); 79 | Payload?.Encode(buf, VariableHeader); 80 | FixedHeader.Encode(buffer, buf.ReadableBytes); 81 | 82 | buffer.WriteBytes(buf); 83 | } 84 | finally 85 | { 86 | buf?.SafeRelease(); 87 | } 88 | } 89 | 90 | /// 91 | /// 解码 92 | /// 93 | /// 94 | public virtual void Decode(IByteBuffer buffer) 95 | { 96 | VariableHeader?.Decode(buffer, ref FixedHeader); 97 | Payload?.Decode(buffer, VariableHeader, ref FixedHeader.RemainingLength); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/DotNetty.Codecs.MqttFx/Packets/PacketIdVariableHeader.cs: -------------------------------------------------------------------------------- 1 | using DotNetty.Buffers; 2 | 3 | namespace DotNetty.Codecs.MqttFx.Packets; 4 | 5 | /// 6 | /// 可变报头(Variable header) 7 | /// 8 | public record PacketIdVariableHeader : VariableHeader 9 | { 10 | /// 11 | /// 报文标识符(Packet Identifier) 12 | /// 13 | public ushort PacketId { get; set; } 14 | 15 | /// 16 | /// 可变报头(Variable header) 17 | /// 18 | public PacketIdVariableHeader() { } 19 | 20 | /// 21 | /// 可变报头(Variable header) 22 | /// 23 | /// 报文标识符(Packet Identifier) 24 | public PacketIdVariableHeader(ushort packetId) 25 | { 26 | PacketId = packetId; 27 | } 28 | 29 | /// 30 | /// 编码 31 | /// 32 | /// 33 | /// 34 | public override void Encode(IByteBuffer buffer) 35 | { 36 | buffer.WriteUnsignedShort(PacketId); 37 | } 38 | 39 | /// 40 | /// 解码 41 | /// 42 | /// 43 | /// 44 | public override void Decode(IByteBuffer buffer, ref FixedHeader fixedHeader) 45 | { 46 | PacketId = buffer.ReadUnsignedShort(ref fixedHeader.RemainingLength); 47 | if (PacketId == 0) 48 | throw new DecoderException("SUBSCRIBE, UNSUBSCRIBE, and PUBLISH (in cases where QoS > 0) Control Packets MUST contain a non-zero 16-bit Packet Identifier. [MQTT-2.3.1-1]"); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/DotNetty.Codecs.MqttFx/Packets/PacketType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DotNetty.Codecs.MqttFx.Packets; 4 | 5 | /// 6 | /// 报文类型(MQTT Control Packet type) 7 | /// 8 | [Flags] 9 | public enum PacketType : byte 10 | { 11 | /// 12 | /// 发起连接(Client request to connect to Server) 13 | /// 14 | CONNECT = 1, 15 | 16 | /// 17 | /// 连接回执(Connect acknowledgment) 18 | /// 19 | CONNACK = 2, 20 | 21 | /// 22 | /// 发布消息(Publish message) 23 | /// 24 | PUBLISH = 3, 25 | 26 | /// 27 | /// 发布回执(Publish acknowledgment) 28 | /// 29 | PUBACK = 4, 30 | 31 | /// 32 | /// QoS2消息回执(Publish received) 33 | /// 34 | PUBREC = 5, 35 | 36 | /// 37 | /// QoS2消息释放(Publish release) 38 | /// 39 | PUBREL = 6, 40 | 41 | /// 42 | /// QoS2消息完成(Publish complet) 43 | /// 44 | PUBCOMP = 7, 45 | 46 | /// 47 | /// 订阅主题(Client subscribe request) 48 | /// 49 | SUBSCRIBE = 8, 50 | 51 | /// 52 | /// 订阅回执(Subscribe acknowledgment) 53 | /// 54 | SUBACK = 9, 55 | 56 | /// 57 | /// 取消订阅(Unsubscribe request) 58 | /// 59 | UNSUBSCRIBE = 10, 60 | 61 | /// 62 | /// 取消订阅回执(Unsubscribe acknowledgment) 63 | /// 64 | UNSUBACK = 11, 65 | 66 | /// 67 | /// PING请求(PING request) 68 | /// 69 | PINGREQ = 12, 70 | 71 | /// 72 | /// PING响应(PING response) 73 | /// 74 | PINGRESP = 13, 75 | 76 | /// 77 | /// 断开连接(Client is disconnecting) 78 | /// 79 | DISCONNECT = 14 80 | } 81 | -------------------------------------------------------------------------------- /src/DotNetty.Codecs.MqttFx/Packets/PacketWithId.cs: -------------------------------------------------------------------------------- 1 | namespace DotNetty.Codecs.MqttFx.Packets; 2 | 3 | /// 4 | /// 报文抽象类(含报文标识符)(MQTT Control Packet) 5 | /// 6 | public abstract record PacketWithId : Packet 7 | { 8 | /// 9 | /// 报文抽象类(含报文标识符)(MQTT Control Packet) 10 | /// 11 | public PacketWithId() 12 | : this(new PacketIdVariableHeader()) { } 13 | 14 | /// 15 | /// 报文抽象类(含报文标识符)(MQTT Control Packet) 16 | /// 17 | public PacketWithId(ushort packetId) 18 | : this(new PacketIdVariableHeader(packetId)) { } 19 | 20 | /// 21 | /// 报文抽象类(含报文标识符)(MQTT Control Packet) 22 | /// 23 | protected PacketWithId(PacketIdVariableHeader variableHeader) 24 | : base(variableHeader) { } 25 | 26 | /// 27 | /// 报文抽象类(MQTT Control Packet) 28 | /// 29 | /// 可变报头(Variable header) 30 | /// 有效载荷(Payload) 31 | protected PacketWithId(PacketIdVariableHeader variableHeader, Payload payload) 32 | : base(variableHeader, payload) { } 33 | 34 | /// 35 | /// 报文抽象类(MQTT Control Packet) 36 | /// 37 | /// 固定报头(Fixed header) 38 | /// 可变报头(Variable header) 39 | /// 有效载荷(Payload) 40 | public PacketWithId(FixedHeader fixedHeader, PacketIdVariableHeader variableHeader, Payload payload) 41 | : base(fixedHeader, variableHeader, payload) { } 42 | 43 | /// 44 | /// 报文标识符(Packet Identifier) 45 | /// 46 | public ushort PacketId 47 | { 48 | get => ((PacketIdVariableHeader)VariableHeader).PacketId; 49 | set => ((PacketIdVariableHeader)VariableHeader).PacketId = value; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/DotNetty.Codecs.MqttFx/Packets/Payload.cs: -------------------------------------------------------------------------------- 1 | using DotNetty.Buffers; 2 | 3 | namespace DotNetty.Codecs.MqttFx.Packets; 4 | 5 | /// 6 | /// 有效载荷(Payload) 7 | /// 8 | public abstract class Payload 9 | { 10 | /// 11 | /// 编码 12 | /// 13 | /// 14 | /// 可变报头 15 | public virtual void Encode(IByteBuffer buffer, VariableHeader variableHeader) { } 16 | 17 | /// 18 | /// 解码 19 | /// 20 | /// 21 | /// 可变报头 22 | public virtual void Decode(IByteBuffer buffer, VariableHeader variableHeader, ref int remainingLength) { } 23 | } 24 | -------------------------------------------------------------------------------- /src/DotNetty.Codecs.MqttFx/Packets/PingReqPacket.cs: -------------------------------------------------------------------------------- 1 | namespace DotNetty.Codecs.MqttFx.Packets; 2 | 3 | /// 4 | /// PING请求(PING request) 5 | /// The PINGREQ Packet is sent from a Client to the Server. It can be used to: 6 | /// 1. Indicate to the Server that the Client is alive in the absence of any other Control Packets being sent from the Client to the Server. 7 | /// 2. Request that the Server responds to confirm that it is alive. 8 | /// 3. Exercise the network to indicate that the Network Connection is active. 9 | /// 10 | public sealed record PingReqPacket : Packet 11 | { 12 | /* 13 | * PINGREQ 数据包从客户端发送到服务器。它可用于: 14 | * 1. 向服务器指示在没有从客户端向服务器发送任何其他控制数据包的情况下,客户端处于活动状态。 15 | * 2. 请求服务器响应以确认它处于活动状态。 16 | * 3. 执行网络以指示网络连接处于活动状态。 17 | */ 18 | 19 | public static readonly PingReqPacket Instance = new PingReqPacket(); 20 | } 21 | -------------------------------------------------------------------------------- /src/DotNetty.Codecs.MqttFx/Packets/PingRespPacket.cs: -------------------------------------------------------------------------------- 1 | namespace DotNetty.Codecs.MqttFx.Packets; 2 | 3 | /// 4 | /// PING响应(PING response) 5 | /// A PINGRESP Packet is sent by the Server to the Client in response to a PINGREQ Packet. It indicates that the Server is alive. 6 | /// 7 | public sealed record PingRespPacket : Packet 8 | { 9 | public static readonly PingRespPacket Instance = new PingRespPacket(); 10 | } 11 | -------------------------------------------------------------------------------- /src/DotNetty.Codecs.MqttFx/Packets/PubAckPacket.cs: -------------------------------------------------------------------------------- 1 | namespace DotNetty.Codecs.MqttFx.Packets; 2 | 3 | /// 4 | /// 发布回执 5 | /// QoS level = 1 6 | /// 7 | public sealed record PubAckPacket : PacketWithId 8 | { 9 | public PubAckPacket(ushort packetId = default) 10 | : base(packetId) { } 11 | } 12 | -------------------------------------------------------------------------------- /src/DotNetty.Codecs.MqttFx/Packets/PubCompPacket.cs: -------------------------------------------------------------------------------- 1 | namespace DotNetty.Codecs.MqttFx.Packets; 2 | 3 | /// 4 | /// QoS2消息完成 5 | /// QoS 2 publish received, part 3 6 | /// 7 | public sealed record PubCompPacket : PacketWithId 8 | { 9 | public PubCompPacket(ushort packetId = default) 10 | : base(packetId) { } 11 | } 12 | -------------------------------------------------------------------------------- /src/DotNetty.Codecs.MqttFx/Packets/PubRecPacket.cs: -------------------------------------------------------------------------------- 1 | namespace DotNetty.Codecs.MqttFx.Packets; 2 | 3 | /// 4 | /// 发布收到(QoS 2,第一步) 5 | /// 6 | public sealed record PubRecPacket : PacketWithId 7 | { 8 | public PubRecPacket(ushort packetId = default) 9 | : base(packetId) { } 10 | } 11 | -------------------------------------------------------------------------------- /src/DotNetty.Codecs.MqttFx/Packets/PubRelPacket.cs: -------------------------------------------------------------------------------- 1 | namespace DotNetty.Codecs.MqttFx.Packets; 2 | 3 | /// 4 | /// 发布释放(QoS 2,第二步) 5 | /// 6 | public sealed record PubRelPacket : PacketWithId 7 | { 8 | public PubRelPacket(ushort packetId = default) 9 | : base(packetId) { } 10 | } 11 | -------------------------------------------------------------------------------- /src/DotNetty.Codecs.MqttFx/Packets/PublishPacket.cs: -------------------------------------------------------------------------------- 1 | namespace DotNetty.Codecs.MqttFx.Packets; 2 | 3 | /// 4 | /// 发布消息(PUBLISH – Publish message) 5 | /// A PUBLISH Control Packet is sent from a Client to a Server or from Server to a Client to transport an Application Message. 6 | /// 7 | public sealed record PublishPacket : PacketWithId 8 | { 9 | /// 10 | /// 发布消息(PUBLISH – Publish message) 11 | /// 12 | public PublishPacket() 13 | : this(new PublishVariableHeader(), new PublishPayload()) { } 14 | 15 | /// 16 | /// 发布消息(PUBLISH – Publish message) 17 | /// 18 | public PublishPacket(PublishPayload payload) 19 | : base(new PublishVariableHeader(), payload) { } 20 | 21 | /// 22 | /// 发布消息(PUBLISH – Publish message) 23 | /// 24 | public PublishPacket(PublishVariableHeader variableHeader, PublishPayload payload) 25 | : base(variableHeader, payload) { } 26 | 27 | /// 28 | /// 发布消息(PUBLISH – Publish message) 29 | /// 30 | public PublishPacket(FixedHeader fixedHeader, PublishVariableHeader variableHeader, PublishPayload payload) 31 | : base(fixedHeader, variableHeader, payload) { } 32 | 33 | /// 34 | /// 重发标志(DUP) 35 | /// 如果DUP标志被设置为0,表示这是客户端或服务端第一次请求发送这个PUBLISH报文。 36 | /// 如果DUP标志被设置为1,表示这可能是一个早前报文请求的重发。 37 | /// 38 | public bool Dup 39 | { 40 | get => this.GetDup(); 41 | set => this.SetDup(value); 42 | } 43 | 44 | /// 45 | /// 服务质量等级(QoS) 46 | /// 47 | public MqttQos Qos 48 | { 49 | get => this.GetQos(); 50 | set => this.SetQos(value); 51 | } 52 | 53 | /// 54 | /// 保留标志(RETAIN) 55 | /// 56 | public bool Retain 57 | { 58 | get => this.GetRetain(); 59 | set => this.SetRetain(value); 60 | } 61 | 62 | /// 63 | /// 主题名称(UTF-8编码的字符串)(Topic Name) 64 | /// 65 | public string TopicName 66 | { 67 | get => ((PublishVariableHeader)VariableHeader).TopicName; 68 | set => ((PublishVariableHeader)VariableHeader).TopicName = value; 69 | } 70 | } -------------------------------------------------------------------------------- /src/DotNetty.Codecs.MqttFx/Packets/PublishPayload.cs: -------------------------------------------------------------------------------- 1 | using DotNetty.Buffers; 2 | using System; 3 | using System.Linq; 4 | 5 | namespace DotNetty.Codecs.MqttFx.Packets; 6 | 7 | /// 8 | /// 有效载荷(Payload) 9 | /// The Payload contains the Application Message that is being published. 10 | /// The content and format of the data is application specific. 11 | /// The length of the payload can be calculated by subtracting the length of the variable header from the Remaining Length field that is in the Fixed Header. 12 | /// It is valid for a PUBLISH Packet to contain a zero length payload. 13 | /// 14 | public class PublishPayload : Payload, IEquatable 15 | { 16 | byte[] body; 17 | 18 | public PublishPayload() { } 19 | 20 | public PublishPayload(byte[] payload) => body = payload; 21 | 22 | public override void Encode(IByteBuffer buffer, VariableHeader variableHeader) 23 | { 24 | if (body != null) 25 | buffer.WriteBytes(body); 26 | } 27 | 28 | public override void Decode(IByteBuffer buffer, VariableHeader variableHeader, ref int remainingLength) 29 | { 30 | body = buffer.ReadSliceArray(ref remainingLength); 31 | } 32 | 33 | public static implicit operator byte[](PublishPayload payload) => payload.body; 34 | 35 | public static implicit operator PublishPayload(byte[] payload) => new(payload); 36 | 37 | public static bool operator !=(PublishPayload r1, PublishPayload r2) => !(r1 == r2); 38 | 39 | public static bool operator ==(PublishPayload r1, PublishPayload r2) => r1.Equals(r2); 40 | 41 | public override int GetHashCode() => body.GetHashCode(); 42 | 43 | public override bool Equals(object obj) => Equals(obj as PublishPayload); 44 | 45 | public bool Equals(PublishPayload other) 46 | { 47 | if (other is null) 48 | return false; 49 | 50 | return body.SequenceEqual(other.body); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/DotNetty.Codecs.MqttFx/Packets/PublishVariableHeader.cs: -------------------------------------------------------------------------------- 1 | using DotNetty.Buffers; 2 | 3 | namespace DotNetty.Codecs.MqttFx.Packets; 4 | 5 | /// 6 | /// 可变报头 7 | /// 8 | public record PublishVariableHeader : PacketIdVariableHeader 9 | { 10 | /// 11 | /// 主题名称(UTF-8编码的字符串)(Topic Name) 12 | /// 附加到应用程序消息的标签,该标签与服务器已知的订阅相匹配。服务器将应用程序消息的副本发送到具有匹配订阅的每个客户端。 13 | /// The label attached to an Application Message which is matched against the Subscriptions known to the Server. The Server sends a copy of the Application Message to each Client that has a matching Subscription. 14 | /// 15 | public string TopicName { get; set; } 16 | 17 | /// 18 | /// 编码 19 | /// 20 | /// 21 | /// 22 | public override void Encode(IByteBuffer buffer, FixedHeader fixedHeader) 23 | { 24 | buffer.WriteString(TopicName); 25 | if (fixedHeader.GetQos() > MqttQos.AtMostOnce) 26 | base.Encode(buffer, fixedHeader); 27 | } 28 | 29 | /// 30 | /// 解码 31 | /// 32 | /// 33 | /// 34 | public override void Decode(IByteBuffer buffer, ref FixedHeader fixedHeader) 35 | { 36 | TopicName = buffer.ReadString(ref fixedHeader.RemainingLength); 37 | if (fixedHeader.GetQos() > MqttQos.AtMostOnce) 38 | base.Decode(buffer, ref fixedHeader); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/DotNetty.Codecs.MqttFx/Packets/SubAckPacket.cs: -------------------------------------------------------------------------------- 1 | namespace DotNetty.Codecs.MqttFx.Packets; 2 | 3 | /// 4 | /// 订阅回执(SUBACK – Subscribe acknowledgement) 5 | /// 6 | public sealed record SubAckPacket : PacketWithId 7 | { 8 | public SubAckPacket() 9 | : this(new PacketIdVariableHeader(), new SubAckPayload()) { } 10 | 11 | public SubAckPacket(ushort packetId, params MqttQos[] returnCodes) 12 | : this(new PacketIdVariableHeader(packetId), new SubAckPayload(returnCodes)) { } 13 | 14 | public SubAckPacket(PacketIdVariableHeader variableHeader, SubAckPayload payload) 15 | : base(variableHeader, payload) { } 16 | 17 | /// 18 | /// 返回代码 19 | /// 20 | public MqttQos[] ReturnCodes => ((SubAckPayload)Payload).ReturnCodes; 21 | } 22 | -------------------------------------------------------------------------------- /src/DotNetty.Codecs.MqttFx/Packets/SubAckPayload.cs: -------------------------------------------------------------------------------- 1 | using DotNetty.Buffers; 2 | using System; 3 | 4 | namespace DotNetty.Codecs.MqttFx.Packets; 5 | 6 | /// 7 | /// 有效载荷(SUBACK Packet payload) 8 | /// The payload contains a list of return codes. Each return code corresponds to a Topic Filter in the SUBSCRIBE Packet being acknowledged. The order of return codes in the SUBACK Packet MUST match the order of Topic Filters in the SUBSCRIBE Packet [MQTT-3.9.3-1]. 9 | /// 10 | public class SubAckPayload : Payload 11 | { 12 | /// 13 | /// 返回代码 14 | /// 15 | public MqttQos[] ReturnCodes { get; private set; } 16 | 17 | public SubAckPayload() { } 18 | 19 | public SubAckPayload(params MqttQos[] returnCodes) 20 | { 21 | ReturnCodes = returnCodes; 22 | } 23 | 24 | public override void Encode(IByteBuffer buffer, VariableHeader variableHeader) 25 | { 26 | foreach (var qos in ReturnCodes) 27 | { 28 | buffer.WriteByte((byte)qos); 29 | } 30 | } 31 | 32 | public override void Decode(IByteBuffer buffer, VariableHeader variableHeader, ref int remainingLength) 33 | { 34 | Span buf = stackalloc MqttQos[remainingLength]; 35 | for (int i = 0; i < buf.Length; i++) 36 | { 37 | var returnCode = (MqttQos)buffer.ReadByte(ref remainingLength); 38 | if (returnCode > MqttQos.ExactlyOnce && returnCode != MqttQos.Failure) 39 | { 40 | throw new DecoderException($"SUBACK return codes other than 0x00, 0x01, 0x02 and 0x80 are reserved and MUST NOT be used. [MQTT-3.9.3-2](Invalid return code: {returnCode})."); 41 | } 42 | buf[i] = returnCode; 43 | 44 | } 45 | ReturnCodes = buf.ToArray(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/DotNetty.Codecs.MqttFx/Packets/SubscribePacket.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace DotNetty.Codecs.MqttFx.Packets; 4 | 5 | /// 6 | /// 订阅报文(SUBSCRIBE - Subscribe to topics) 7 | /// The SUBSCRIBE Packet is sent from the Client to the Server to create one or more Subscriptions. 8 | /// Each Subscription registers a Client’s interest in one or more Topics. 9 | /// The Server sends PUBLISH Packets to the Client in order to forward Application Messages that were published to Topics that match these Subscriptions. 10 | /// The SUBSCRIBE Packet also specifies (for each Subscription) the maximum QoS with which the Server can send Application Messages to the Client. 11 | /// 12 | public sealed record SubscribePacket : PacketWithId 13 | { 14 | /// 15 | /// SUBSCRIBE - Subscribe to topics 16 | /// 17 | public SubscribePacket() 18 | : this(new PacketIdVariableHeader(), new SubscribePayload()) { } 19 | 20 | /// 21 | /// SUBSCRIBE - Subscribe to topics 22 | /// 23 | /// Packet Identifier 24 | /// Subscription request 25 | public SubscribePacket(ushort packetId, params SubscriptionRequest[] requests) 26 | : this(new PacketIdVariableHeader(packetId), new SubscribePayload(requests)) { } 27 | 28 | /// 29 | /// SUBSCRIBE - Subscribe to topics 30 | /// 31 | /// 32 | /// 33 | public SubscribePacket(PacketIdVariableHeader variableHeader, SubscribePayload payload) 34 | : base(variableHeader, payload) { } 35 | 36 | /// 37 | /// SUBSCRIBE - Subscribe to topics 38 | /// 39 | /// 40 | /// 41 | /// 42 | public SubscribePacket(FixedHeader fixedHeader, PacketIdVariableHeader variableHeader, SubscribePayload payload) 43 | : base(fixedHeader, variableHeader, payload) { } 44 | 45 | public IList SubscriptionRequests 46 | { 47 | get => ((SubscribePayload)Payload).SubscriptionRequests; 48 | } 49 | 50 | /// 51 | /// 订阅主题 52 | /// 53 | /// Topic Name 54 | /// Requested QoS 55 | public void AddSubscriptionRequest(string topicFilter, MqttQos requestedQos) 56 | { 57 | SubscriptionRequest request; 58 | request.TopicFilter = topicFilter; 59 | request.RequestedQos = requestedQos; 60 | SubscriptionRequests.Add(request); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/DotNetty.Codecs.MqttFx/Packets/SubscribePayload.cs: -------------------------------------------------------------------------------- 1 | using DotNetty.Buffers; 2 | using System.Collections.Generic; 3 | 4 | namespace DotNetty.Codecs.MqttFx.Packets; 5 | 6 | /// 7 | /// 有效载荷(SUBSCRIBE Packet payload) 8 | /// If it chooses not to support topic filters that contain wildcard characters it MUST reject any Subscription request whose filter contains them [MQTT-3.8.3-2]. 9 | /// The payload of a SUBSCRIBE packet MUST contain at least one Topic Filter / QoS pair. A SUBSCRIBE packet with no payload is a protocol violation [MQTT-3.8.3-3]. 10 | /// 11 | public class SubscribePayload : Payload 12 | { 13 | /// 14 | /// 订阅请求(Subscription request) 15 | /// 16 | public IList SubscriptionRequests { get; private set; } 17 | 18 | /// 19 | /// SUBSCRIBE Packet payload 20 | /// 21 | public SubscribePayload() { } 22 | 23 | /// 24 | /// SUBSCRIBE Packet payload 25 | /// 26 | /// Subscription request 27 | public SubscribePayload(params SubscriptionRequest[] requests) => SubscriptionRequests = requests; 28 | 29 | public override void Encode(IByteBuffer buffer, VariableHeader variableHeader) 30 | { 31 | foreach (var item in SubscriptionRequests) 32 | { 33 | buffer.WriteString(item.TopicFilter); 34 | buffer.WriteByte((byte)item.RequestedQos); 35 | } 36 | } 37 | 38 | public override void Decode(IByteBuffer buffer, VariableHeader variableHeader, ref int remainingLength) 39 | { 40 | SubscriptionRequests = new List(); 41 | while (remainingLength > 0) 42 | { 43 | string topicFilter = buffer.ReadString(ref remainingLength); 44 | MqttCodecUtil.ValidateTopicFilter(topicFilter); 45 | 46 | byte qos = buffer.ReadByte(ref remainingLength); 47 | if (qos > (byte)MqttQos.ExactlyOnce) 48 | throw new DecoderException($"The Server MUST treat a SUBSCRIBE packet as malformed and close the Network Connection if any of Reserved bits in the payload are non-zero, or QoS is not 0,1 or 2. [MQTT-3.8.3-4](Invalid QoS value: {qos}.)"); 49 | 50 | SubscriptionRequest request; 51 | request.TopicFilter = topicFilter; 52 | request.RequestedQos = (MqttQos)qos; 53 | SubscriptionRequests.Add(request); 54 | } 55 | 56 | if (SubscriptionRequests.Count == 0) 57 | throw new DecoderException("The payload of a SUBSCRIBE packet MUST contain at least one Topic Filter / QoS pair. A SUBSCRIBE packet with no payload is a protocol violation. [MQTT-3.8.3-3]"); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/DotNetty.Codecs.MqttFx/Packets/SubscriptionRequest.cs: -------------------------------------------------------------------------------- 1 | namespace DotNetty.Codecs.MqttFx.Packets; 2 | 3 | /// 4 | /// 订阅(Subscription request) 5 | /// 订阅包含主题筛选器和最大 QoS。订阅与单个会话相关联。一个会话可以包含多个订阅。会话中的每个订阅都有不同的主题筛选器。 6 | /// A Subscription comprises a Topic Filter and a maximum QoS. A Subscription is associated with a single Session. A Session can contain more than one Subscription. Each Subscription within a session has a different Topic Filter. 7 | /// 8 | public record struct SubscriptionRequest 9 | { 10 | /// 11 | /// 主题筛选器(Topic Filter) 12 | /// 订阅中包含的表达式,用于指示对一个或多个主题的兴趣。主题过滤器可以包含通配符。 13 | /// An expression contained in a Subscription, to indicate an interest in one or more topics. A Topic Filter can include wildcard characters. 14 | /// The Topic Filters in a SUBSCRIBE packet payload MUST be UTF-8 encoded strings as defined in Section 1.5.3 [MQTT-3.8.3-1]. 15 | /// 16 | public string TopicFilter; 17 | 18 | /// 19 | /// Requested QoS 20 | /// The upper 6 bits of the Requested QoS byte are not used in the current version of the protocol. They are reserved for future use. 21 | /// 22 | public MqttQos RequestedQos; 23 | } 24 | -------------------------------------------------------------------------------- /src/DotNetty.Codecs.MqttFx/Packets/UnsubAckPacket.cs: -------------------------------------------------------------------------------- 1 | namespace DotNetty.Codecs.MqttFx.Packets; 2 | 3 | /// 4 | /// 取消订阅回执(UNSUBACK – Unsubscribe acknowledgement) 5 | /// The UNSUBACK Packet is sent by the Server to the Client to confirm receipt of an UNSUBSCRIBE Packet. 6 | /// 7 | public sealed record UnsubAckPacket : PacketWithId 8 | { 9 | } 10 | -------------------------------------------------------------------------------- /src/DotNetty.Codecs.MqttFx/Packets/UnsubscribePacket.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace DotNetty.Codecs.MqttFx.Packets; 4 | 5 | /// 6 | /// 取消订阅(UNSUBSCRIBE – Unsubscribe from topics) 7 | /// An UNSUBSCRIBE Packet is sent by the Client to the Server, to unsubscribe from topics. 8 | /// 9 | public sealed record UnsubscribePacket : PacketWithId 10 | { 11 | /// 12 | /// 取消订阅(UNSUBSCRIBE – Unsubscribe from topics) 13 | /// 14 | public UnsubscribePacket() 15 | : this(new PacketIdVariableHeader(), new UnsubscribePayload()) { } 16 | 17 | /// 18 | /// 取消订阅(UNSUBSCRIBE – Unsubscribe from topics) 19 | /// 20 | /// 21 | /// 22 | public UnsubscribePacket(ushort packetId, params string[] topicFilters) 23 | : this(new PacketIdVariableHeader(packetId), new UnsubscribePayload(topicFilters)) { } 24 | 25 | /// 26 | /// 取消订阅(UNSUBSCRIBE – Unsubscribe from topics) 27 | /// 28 | /// 29 | /// 30 | public UnsubscribePacket(PacketIdVariableHeader variableHeader, UnsubscribePayload payload) 31 | : base(variableHeader, payload) { } 32 | 33 | /// 34 | /// 取消订阅(UNSUBSCRIBE – Unsubscribe from topics) 35 | /// 36 | /// 37 | /// 38 | /// 39 | public UnsubscribePacket(FixedHeader fixedHeader, PacketIdVariableHeader variableHeader, UnsubscribePayload payload) 40 | : base(fixedHeader, variableHeader, payload) { } 41 | 42 | public IList TopicFilters 43 | { 44 | get => ((UnsubscribePayload)Payload).TopicFilters; 45 | set => ((UnsubscribePayload)Payload).TopicFilters = value; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/DotNetty.Codecs.MqttFx/Packets/UnsubscribePayload.cs: -------------------------------------------------------------------------------- 1 | using DotNetty.Buffers; 2 | using System.Collections.Generic; 3 | 4 | namespace DotNetty.Codecs.MqttFx.Packets; 5 | 6 | /// 7 | /// 有效载荷(UNSUBSCRIBE Packet payload) 8 | /// The payload for the UNSUBSCRIBE Packet contains the list of Topic Filters that the Client wishes to unsubscribe from. 9 | /// The Payload of an UNSUBSCRIBE packet MUST contain at least one Topic Filter. An UNSUBSCRIBE packet with no payload is a protocol violation [MQTT-3.10.3-2]. 10 | /// 11 | public class UnsubscribePayload : Payload 12 | { 13 | /// 14 | /// Topic Filters 15 | /// The Topic Filters in an UNSUBSCRIBE packet MUST be UTF-8 encoded strings as defined in Section 1.5.3, packed contiguously [MQTT-3.10.3-1]. 16 | /// 17 | public IList TopicFilters { get; set; } 18 | 19 | /// 20 | /// UNSUBSCRIBE Packet payload 21 | /// 22 | public UnsubscribePayload() { } 23 | 24 | /// 25 | /// UNSUBSCRIBE Packet payload 26 | /// 27 | /// 28 | public UnsubscribePayload(params string[] topicFilters) => TopicFilters = topicFilters; 29 | 30 | public override void Encode(IByteBuffer buffer, VariableHeader variableHeader) 31 | { 32 | foreach (var item in TopicFilters) 33 | { 34 | buffer.WriteString(item); 35 | } 36 | } 37 | 38 | public override void Decode(IByteBuffer buffer, VariableHeader variableHeader, ref int remainingLength) 39 | { 40 | TopicFilters = new List(); 41 | 42 | while (remainingLength > 0) 43 | { 44 | string topicFilter = buffer.ReadString(ref remainingLength); 45 | MqttCodecUtil.ValidateTopicFilter(topicFilter); 46 | TopicFilters.Add(topicFilter); 47 | } 48 | 49 | if (TopicFilters.Count == 0) 50 | throw new DecoderException("The Payload of an UNSUBSCRIBE packet MUST contain at least one Topic Filter. An UNSUBSCRIBE packet with no payload is a protocol violation. [MQTT-3.10.3-2]"); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/DotNetty.Codecs.MqttFx/Packets/VariableHeader.cs: -------------------------------------------------------------------------------- 1 | using DotNetty.Buffers; 2 | 3 | namespace DotNetty.Codecs.MqttFx.Packets; 4 | 5 | /// 6 | /// 可变报头(Variable header) 7 | /// 8 | public abstract record VariableHeader 9 | { 10 | public virtual void Encode(IByteBuffer buffer) { } 11 | 12 | public virtual void Encode(IByteBuffer buffer, FixedHeader fixedHeader) => Encode(buffer); 13 | 14 | public virtual void Decode(IByteBuffer buffer, ref FixedHeader fixedHeader) { } 15 | } 16 | -------------------------------------------------------------------------------- /src/MqttDecoder.cs: -------------------------------------------------------------------------------- 1 | using DotNetty.Buffers; 2 | using DotNetty.Codecs; 3 | using DotNetty.Transport.Channels; 4 | using MqttFx.Packets; 5 | using MqttFx.Protocol; 6 | using System.Collections.Generic; 7 | 8 | namespace MqttFx 9 | { 10 | /// 11 | /// Mqtt解码器 12 | /// 13 | public sealed class MqttDecoder : ByteToMessageDecoder 14 | { 15 | readonly bool _isServer; 16 | readonly int _maxMessageSize; 17 | 18 | public MqttDecoder(bool isServer, int maxMessageSize) 19 | { 20 | _isServer = isServer; 21 | _maxMessageSize = maxMessageSize; 22 | } 23 | 24 | protected override void Decode(IChannelHandlerContext context, IByteBuffer input, List output) 25 | { 26 | try 27 | { 28 | if (!TryDecodePacket(context, input, out Packet packet)) 29 | return; 30 | 31 | output.Add(packet); 32 | } 33 | catch (DecoderException) 34 | { 35 | input.SkipBytes(input.ReadableBytes); 36 | throw; 37 | } 38 | } 39 | 40 | bool TryDecodePacket(IChannelHandlerContext context, IByteBuffer buffer, out Packet packet) 41 | { 42 | if (!buffer.IsReadable(2)) 43 | { 44 | packet = null; 45 | return false; 46 | } 47 | 48 | byte signature = buffer.ReadByte(); 49 | 50 | if (!TryDecodeRemainingLength(buffer, out int remainingLength) || !buffer.IsReadable(remainingLength)) 51 | { 52 | packet = null; 53 | return false; 54 | } 55 | 56 | var fixedHeader = new FixedHeader(signature, remainingLength); 57 | switch (fixedHeader.PacketType) 58 | { 59 | case PacketType.CONNECT: packet = new ConnectPacket(); break; 60 | case PacketType.CONNACK: packet = new ConnAckPacket(); break; 61 | case PacketType.DISCONNECT: packet = new DisconnectPacket(); break; 62 | case PacketType.PINGREQ: packet = new PingReqPacket(); break; 63 | case PacketType.PINGRESP: packet = new PingRespPacket(); break; 64 | case PacketType.PUBACK: packet = new PubAckPacket(); break; 65 | case PacketType.PUBCOMP: packet = new PubCompPacket(); break; 66 | case PacketType.PUBLISH: packet = new PublishPacket(); break; 67 | case PacketType.PUBREC: packet = new PubRecPacket(); break; 68 | case PacketType.PUBREL: packet = new PubRelPacket(); break; 69 | case PacketType.SUBSCRIBE: packet = new SubscribePacket(); break; 70 | case PacketType.SUBACK: packet = new SubAckPacket(); break; 71 | case PacketType.UNSUBSCRIBE: packet = new UnsubscribePacket(); break; 72 | case PacketType.UNSUBACK: packet = new UnsubscribePacket(); break; 73 | default: 74 | throw new DecoderException("Unsupported Message Type"); 75 | } 76 | packet.FixedHeader = fixedHeader; 77 | packet.Decode(buffer); 78 | 79 | return true; 80 | } 81 | 82 | bool TryDecodeRemainingLength(IByteBuffer buffer, out int value) 83 | { 84 | int readable = buffer.ReadableBytes; 85 | 86 | int result = 0; 87 | int multiplier = 1; 88 | byte digit; 89 | int read = 0; 90 | do 91 | { 92 | if (readable < read + 1) 93 | { 94 | value = default(int); 95 | return false; 96 | } 97 | digit = buffer.ReadByte(); 98 | result += (digit & 0x7f) * multiplier; 99 | multiplier <<= 7; 100 | read++; 101 | } 102 | while ((digit & 0x80) != 0 && read < 4); 103 | 104 | if (read == 4 && (digit & 0x80) != 0) 105 | { 106 | throw new DecoderException("Remaining length exceeds 4 bytes in length"); 107 | } 108 | 109 | int completeMessageSize = result + 1 + read; 110 | if (completeMessageSize > _maxMessageSize) 111 | { 112 | throw new DecoderException("Message is too big: " + completeMessageSize); 113 | } 114 | 115 | value = result; 116 | return true; 117 | } 118 | 119 | //static int DecodeRemainingLength(IByteBuffer buffer) 120 | //{ 121 | // byte encodedByte; 122 | // var multiplier = 1; 123 | // var remainingLength = 0; 124 | // do 125 | // { 126 | // encodedByte = buffer.ReadByte(); 127 | // remainingLength += (encodedByte & 0x7f) * multiplier; 128 | // multiplier *= 0x80; 129 | // } while ((encodedByte & 0x80) != 0); 130 | 131 | // return remainingLength; 132 | //} 133 | } 134 | } -------------------------------------------------------------------------------- /src/MqttEncoder.cs: -------------------------------------------------------------------------------- 1 | using DotNetty.Buffers; 2 | using DotNetty.Codecs; 3 | using DotNetty.Common.Utilities; 4 | using DotNetty.Transport.Channels; 5 | using MqttFx.Packets; 6 | using System.Collections.Generic; 7 | 8 | namespace MqttFx 9 | { 10 | /// 11 | /// Mqtt编码器 12 | /// 13 | public sealed class MqttEncoder : MessageToMessageEncoder 14 | { 15 | public static readonly MqttEncoder Instance = new MqttEncoder(); 16 | 17 | protected override void Encode(IChannelHandlerContext context, Packet message, List output) => DoEncode(context.Allocator, message, output); 18 | 19 | public static void DoEncode(IByteBufferAllocator bufferAllocator, Packet packet, List output) 20 | { 21 | IByteBuffer buffer = bufferAllocator.Buffer(); 22 | try 23 | { 24 | packet.Encode(buffer); 25 | output.Add(buffer); 26 | buffer = null; 27 | } 28 | finally 29 | { 30 | buffer?.SafeRelease(); 31 | } 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /src/MqttFx/ApplicationMessage.cs: -------------------------------------------------------------------------------- 1 | using DotNetty.Codecs.MqttFx.Packets; 2 | 3 | namespace MqttFx; 4 | 5 | /// 6 | /// 应用程序消息(Application Message) 7 | /// MQTT 协议通过网络为应用程序携带的数据。当应用程序消息由 MQTT 传输时,它们具有关联的服务质量和主题名称。 8 | /// The data carried by the MQTT protocol across the network for the application. When Application Messages are transported by MQTT they have an associated Quality of Service and a Topic Name. 9 | /// 10 | public class ApplicationMessage 11 | { 12 | /// 13 | /// Gets or sets the MQTT topic. 14 | /// In MQTT, the word topic refers to an UTF-8 string that the broker uses to filter messages for each connected 15 | /// client. 16 | /// The topic consists of one or more topic levels. Each topic level is separated by a forward slash (topic level 17 | /// separator). 18 | /// 19 | public string Topic { get; set; } 20 | 21 | /// 22 | /// Gets or sets the payload. 23 | /// The payload is the data bytes sent via the MQTT protocol. 24 | /// 25 | public byte[] Payload { get; set; } 26 | 27 | /// 28 | /// Gets or sets the payload format indicator. 29 | /// The payload format indicator is part of any MQTT packet that can contain a payload. The indicator is an optional 30 | /// byte value. 31 | /// A value of 0 indicates an “unspecified byte stream”. 32 | /// A value of 1 indicates a "UTF-8 encoded payload". 33 | /// If no payload format indicator is provided, the default value is 0. 34 | /// Hint: MQTT 5 feature only. 35 | /// 36 | public MqttQos Qos { get; set; } 37 | 38 | /// 39 | /// If the DUP flag is set to 0, it indicates that this is the first occasion that the Client or Server has attempted 40 | /// to send this MQTT PUBLISH Packet. 41 | /// If the DUP flag is set to 1, it indicates that this might be re-delivery of an earlier attempt to send the Packet. 42 | /// The DUP flag MUST be set to 1 by the Client or Server when it attempts to re-deliver a PUBLISH Packet 43 | /// [MQTT-3.3.1.-1]. 44 | /// The DUP flag MUST be set to 0 for all QoS 0 messages [MQTT-3.3.1-2]. 45 | /// 46 | public bool Dup { get; set; } 47 | 48 | /// 49 | /// Gets or sets a value indicating whether the message should be retained or not. 50 | /// A retained message is a normal MQTT message with the retained flag set to true. 51 | /// The broker stores the last retained message and the corresponding QoS for that topic. 52 | /// 53 | public bool Retain { get; set; } 54 | } 55 | -------------------------------------------------------------------------------- /src/MqttFx/ApplicationMessageBuilder.cs: -------------------------------------------------------------------------------- 1 | using DotNetty.Codecs.MqttFx.Packets; 2 | using System.Text; 3 | 4 | namespace MqttFx; 5 | 6 | public sealed class ApplicationMessageBuilder 7 | { 8 | string _topic; 9 | byte[] _payload; 10 | MqttQos _qos = MqttQos.AtMostOnce; 11 | bool _retain; 12 | 13 | public ApplicationMessage Build() 14 | { 15 | var message = new ApplicationMessage 16 | { 17 | Topic = _topic, 18 | Payload = _payload, 19 | Qos = _qos, 20 | Retain = _retain, 21 | }; 22 | return message; 23 | } 24 | 25 | public ApplicationMessageBuilder WithPayload(byte[] payload) 26 | { 27 | _payload = payload; 28 | return this; 29 | } 30 | 31 | public ApplicationMessageBuilder WithPayload(string payload) 32 | { 33 | if (payload == null) 34 | { 35 | _payload = null; 36 | return this; 37 | } 38 | 39 | _payload = string.IsNullOrEmpty(payload) ? null : Encoding.UTF8.GetBytes(payload); 40 | return this; 41 | } 42 | 43 | /// 44 | /// The MQTT topic. 45 | /// In MQTT, the word topic refers to an UTF-8 string that the broker uses to filter messages for each connected 46 | /// client. 47 | /// The topic consists of one or more topic levels. Each topic level is separated by a forward slash (topic level 48 | /// separator). 49 | /// 50 | public ApplicationMessageBuilder WithTopic(string topic) 51 | { 52 | _topic = topic; 53 | return this; 54 | } 55 | 56 | /// 57 | /// The quality of service level. 58 | /// The Quality of Service (QoS) level is an agreement between the sender of a message and the receiver of a message 59 | /// that defines the guarantee of delivery for a specific message. 60 | /// There are 3 QoS levels in MQTT: 61 | /// - At most once (0): Message gets delivered no time, once or multiple times. 62 | /// - At least once (1): Message gets delivered at least once (one time or more often). 63 | /// - Exactly once (2): Message gets delivered exactly once (It's ensured that the message only comes once). 64 | /// 65 | public ApplicationMessageBuilder WithQos(MqttQos qos) 66 | { 67 | _qos = qos; 68 | return this; 69 | } 70 | 71 | /// 72 | /// A value indicating whether the message should be retained or not. 73 | /// A retained message is a normal MQTT message with the retained flag set to true. 74 | /// The broker stores the last retained message and the corresponding QoS for that topic. 75 | /// 76 | public ApplicationMessageBuilder WithRetain(bool value = true) 77 | { 78 | _retain = value; 79 | return this; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/MqttFx/Client/Channels/MqttChannelHandler.cs: -------------------------------------------------------------------------------- 1 | using DotNetty.Codecs.MqttFx.Packets; 2 | using DotNetty.Transport.Channels; 3 | using MqttFx.Formatter; 4 | using MqttFx.Utils; 5 | using System.Collections.Generic; 6 | using System.Threading.Tasks; 7 | 8 | namespace MqttFx.Client.Channels; 9 | 10 | /// 11 | /// 发送和接收数据处理器 12 | /// 13 | class MqttChannelHandler : SimpleChannelInboundHandler 14 | { 15 | private readonly MqttClient client; 16 | private readonly TaskCompletionSource connectFuture; 17 | 18 | public MqttChannelHandler(MqttClient client, TaskCompletionSource connectFuture) 19 | { 20 | this.client = client; 21 | this.connectFuture = connectFuture; 22 | } 23 | 24 | /// 25 | /// 通道激活时触发,当客户端connect成功后,服务端就会接收到这个事件,从而可以把客户端的Channel记录下来 26 | /// 27 | /// 28 | public override void ChannelActive(IChannelHandlerContext context) 29 | { 30 | var packet = new ConnectPacket(); 31 | var variableHeader = (ConnectVariableHeader)packet.VariableHeader; 32 | var payload = (ConnectPayload)packet.Payload; 33 | 34 | variableHeader.ConnectFlags.CleanSession = client.Options.CleanSession; 35 | variableHeader.KeepAlive = client.Options.KeepAlive; 36 | payload.ClientId = client.Options.ClientId; 37 | if (client.Options.Credentials != null) 38 | { 39 | variableHeader.ConnectFlags.UsernameFlag = true; 40 | payload.UserName = client.Options.Credentials.Username; 41 | payload.Password = client.Options.Credentials.Username; 42 | } 43 | if (!string.IsNullOrEmpty(client.Options.WillTopic)) 44 | { 45 | variableHeader.ConnectFlags.WillFlag = true; 46 | variableHeader.ConnectFlags.WillQos = client.Options.WillQos; 47 | variableHeader.ConnectFlags.WillRetain = client.Options.WillRetain; 48 | payload.WillTopic = client.Options.WillTopic; 49 | payload.WillMessage = client.Options.WillPayload; 50 | } 51 | context.WriteAndFlushAsync(packet); 52 | } 53 | 54 | /// 55 | /// 当收到对方发来的数据后,就会触发,参数msg就是发来的信息,可以是基础类型,也可以是序列化的复杂对象 56 | /// 57 | /// 58 | /// 59 | protected override void ChannelRead0(IChannelHandlerContext ctx, Packet packet) 60 | { 61 | switch (packet) 62 | { 63 | case ConnAckPacket connAckPacket: 64 | ProcessMessage(ctx.Channel, connAckPacket); 65 | break; 66 | case PublishPacket publishPacket: 67 | ProcessMessage(ctx.Channel, publishPacket); 68 | break; 69 | case PubRecPacket pubRecPacket: 70 | ProcessMessage(ctx.Channel, pubRecPacket); 71 | break; 72 | case PubRelPacket pubRelPacket: 73 | ProcessMessage(ctx.Channel, pubRelPacket); 74 | break; 75 | case PubCompPacket pubCompPacket: 76 | ProcessMessage(ctx.Channel, pubCompPacket); 77 | break; 78 | case PubAckPacket pubAckPacket: 79 | ProcessMessage(ctx.Channel, pubAckPacket); 80 | break; 81 | case SubAckPacket subAckPacket: 82 | ProcessMessage(ctx.Channel, subAckPacket); 83 | break; 84 | case UnsubAckPacket unsubAckPacket: 85 | ProcessMessage(ctx.Channel, unsubAckPacket); 86 | break; 87 | } 88 | } 89 | 90 | async void ProcessMessage(IChannel channel, ConnAckPacket packet) 91 | { 92 | var variableHeader = (ConnAckVariableHeader)packet.VariableHeader; 93 | switch (variableHeader.ConnectReturnCode) 94 | { 95 | case ConnectReturnCode.CONNECTION_ACCEPTED: 96 | connectFuture.TrySetResult(new ConnectResult(ConnectReturnCode.CONNECTION_ACCEPTED)); 97 | await client.OnConnected(new ConnectResult(ConnectReturnCode.CONNECTION_ACCEPTED)); 98 | break; 99 | 100 | case ConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD: 101 | case ConnectReturnCode.CONNECTION_REFUSED_IDENTIFIER_REJECTED: 102 | case ConnectReturnCode.CONNECTION_REFUSED_SERVER_UNAVAILABLE: 103 | case ConnectReturnCode.CONNECTION_REFUSED_UNACCEPTABLE_PROTOCOL_VERSION: 104 | connectFuture.TrySetResult(new ConnectResult(variableHeader.ConnectReturnCode)); 105 | await channel.CloseAsync(); 106 | break; 107 | } 108 | } 109 | 110 | void ProcessMessage(IChannel channel, PublishPacket packet) 111 | { 112 | switch (packet.Qos) 113 | { 114 | case MqttQos.AtMostOnce: 115 | InvokeProcessForIncomingPublish(packet); 116 | break; 117 | 118 | case MqttQos.AtLeastOnce: 119 | InvokeProcessForIncomingPublish(packet); 120 | if (packet.PacketId > 0) 121 | channel.WriteAndFlushAsync(new PubAckPacket(packet.PacketId)); 122 | break; 123 | 124 | case MqttQos.ExactlyOnce: 125 | if (packet.PacketId > 0) 126 | channel.WriteAndFlushAsync(new PubRecPacket(packet.PacketId)); 127 | break; 128 | } 129 | } 130 | 131 | void ProcessMessage(IChannel channel, PubRecPacket packet) 132 | { 133 | if (client.PendingPublishs.TryGetValue(packet.PacketId, out PendingPublish pending)) 134 | { 135 | pending.OnPubAckReceived(); 136 | 137 | PubRelPacket pubRelPacket = new(packet.PacketId); 138 | channel.WriteAndFlushAsync(pubRelPacket); 139 | 140 | pending.SetPubRelMessage(pubRelPacket); 141 | pending.StartPubrelRetransmissionTimer(client.EventLoop.GetNext(), client.SendAsync); 142 | } 143 | } 144 | 145 | void ProcessMessage(IChannel channel, PubRelPacket packet) 146 | { 147 | channel.WriteAndFlushAsync(new PubCompPacket(packet.PacketId)); 148 | } 149 | 150 | void ProcessMessage(IChannel channel, PubCompPacket packet) 151 | { 152 | if (client.PendingPublishs.TryRemove(packet.PacketId, out PendingPublish pending)) 153 | { 154 | pending.Future.TrySetResult(new PublishResult(packet.PacketId)); 155 | pending.OnPubCompReceived(); 156 | } 157 | } 158 | 159 | void ProcessMessage(IChannel channel, PubAckPacket packet) 160 | { 161 | if (client.PendingPublishs.TryRemove(packet.PacketId, out PendingPublish pending)) 162 | { 163 | pending.Future.TrySetResult(new PublishResult(packet.PacketId)); 164 | pending.OnPubAckReceived(); 165 | } 166 | } 167 | 168 | void ProcessMessage(IChannel channel, SubAckPacket packet) 169 | { 170 | if (client.PendingSubscriptions.TryRemove(packet.PacketId, out PendingSubscription pending)) 171 | { 172 | pending.OnSubackReceived(); 173 | 174 | var items = new List(); 175 | 176 | for (int i = 0; i < pending.SubscribePacket.SubscriptionRequests.Count; i++) 177 | { 178 | items.Add(new SubscribeResultItem 179 | { 180 | TopicFilter = pending.SubscribePacket.SubscriptionRequests[i].TopicFilter, 181 | ResultCode = packet.ReturnCodes[i] 182 | }); 183 | } 184 | 185 | pending.Future.TrySetResult(new SubscribeResult(items)); 186 | } 187 | } 188 | 189 | void ProcessMessage(IChannel channel, UnsubAckPacket packet) 190 | { 191 | if (client.PendingUnSubscriptions.TryRemove(packet.PacketId, out PendingUnSubscription pending)) 192 | { 193 | pending.Future.TrySetResult(new UnSubscribeResult(packet.PacketId)); 194 | pending.OnUnsubackReceived(); 195 | } 196 | } 197 | 198 | void InvokeProcessForIncomingPublish(PublishPacket packet) 199 | { 200 | client.OnApplicationMessageReceived(packet.ToApplicationMessage()); 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/MqttFx/Client/Channels/MqttPingHandler.cs: -------------------------------------------------------------------------------- 1 | using DotNetty.Codecs.MqttFx.Packets; 2 | using DotNetty.Common.Concurrency; 3 | using DotNetty.Common.Utilities; 4 | using DotNetty.Handlers.Timeout; 5 | using DotNetty.Transport.Channels; 6 | using System; 7 | 8 | namespace MqttFx.Client.Channels; 9 | 10 | /// 11 | /// ping 处理器 12 | /// 客户端有责任确保发送的控制数据包之间的间隔不超过"保持活动状态"值。在不发送任何其他控制数据包的情况下,客户端必须发送 PINGREQ 数据包 [MQTT-3.1.2-23]。 13 | /// 如果"保持活动状态"值不为零,并且服务器在"保持活动状态"时间段的一倍半内未收到来自客户端的控制数据包,则它必须断开与客户端的网络连接,就好像网络出现故障一样 [MQTT-3.1.2-24]。 14 | /// 15 | class MqttPingHandler : SimpleChannelInboundHandler 16 | { 17 | private readonly ushort keepAlive; 18 | private IScheduledTask pingRespTimeout; 19 | 20 | public MqttPingHandler(ushort keepAlive) 21 | { 22 | this.keepAlive = keepAlive; 23 | } 24 | 25 | protected override void ChannelRead0(IChannelHandlerContext ctx, object packet) 26 | { 27 | switch (packet) 28 | { 29 | case PingReqPacket: 30 | HandlePingReq(ctx.Channel); 31 | break; 32 | case PingRespPacket: 33 | HandlePingResp(); 34 | break; 35 | default: 36 | ctx.FireChannelRead(ReferenceCountUtil.Retain(packet)); 37 | break; 38 | } 39 | } 40 | 41 | public override void UserEventTriggered(IChannelHandlerContext ctx, object evt) 42 | { 43 | base.UserEventTriggered(ctx, evt); 44 | 45 | if (evt is IdleStateEvent evt2) 46 | { 47 | switch (evt2.State) 48 | { 49 | case IdleState.ReaderIdle: 50 | break; 51 | case IdleState.WriterIdle: 52 | SendPingReq(ctx.Channel); 53 | break; 54 | } 55 | } 56 | } 57 | 58 | /// 59 | /// This Packet is used in Keep Alive processing 60 | /// 发送心跳包至server端,并建立心跳超时断开连接任务 61 | /// 此处,先行创建心跳超时任务,后续再发送心跳包(避免收到心跳响应时,心跳超时任务未建立完成) 62 | /// 63 | void SendPingReq(IChannel channel) 64 | { 65 | // 创建心跳超时,断开连接任务 66 | // 如果客户端在发送 PINGREQ 后的合理时间内未收到 PINGRESP 数据包,则应关闭与服务器的网络连接。 67 | // 服务端在1.5个时长内未收到PINGREQ,就断开连接。 68 | // 客户端在1个时长内未收到PINGRES,断开连接。 69 | if (pingRespTimeout == null) 70 | { 71 | pingRespTimeout = channel.EventLoop.Schedule(() => 72 | { 73 | channel.WriteAndFlushAsync(DisconnectPacket.Instance); 74 | }, TimeSpan.FromSeconds(keepAlive)); 75 | } 76 | 77 | channel.WriteAndFlushAsync(PingReqPacket.Instance); 78 | } 79 | 80 | /// 81 | /// 服务器必须发送 PINGRESP 数据包以响应 PINGREQ 数据包 [MQTT-3.12.4-1]。 82 | /// The Server MUST send a PINGRESP Packet in response to a PINGREQ Packet [MQTT-3.12.4-1]. 83 | /// 84 | void HandlePingReq(IChannel channel) 85 | { 86 | channel.WriteAndFlushAsync(PingRespPacket.Instance); 87 | } 88 | 89 | /// 90 | /// 处理ping resp,取消ping超时任务(断开连接) 91 | /// 92 | void HandlePingResp() 93 | { 94 | if (pingRespTimeout != null) 95 | { 96 | pingRespTimeout.Cancel(); 97 | pingRespTimeout = null; 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/MqttFx/Client/Extensions/MqttClientCredentials.cs: -------------------------------------------------------------------------------- 1 | namespace MqttFx; 2 | 3 | /// 4 | /// 凭证 5 | /// 6 | public class MqttClientCredentials 7 | { 8 | /// 9 | /// 用户名 10 | /// 11 | public string Username { get; set; } 12 | 13 | /// 14 | /// 密码 15 | /// 16 | public string Password { get; set; } 17 | } 18 | -------------------------------------------------------------------------------- /src/MqttFx/Client/Extensions/MqttClientOptions.cs: -------------------------------------------------------------------------------- 1 | using DotNetty.Codecs.MqttFx.Packets; 2 | using MqttFx.Client; 3 | using System; 4 | 5 | namespace MqttFx; 6 | 7 | /// 8 | /// Options for 9 | /// 10 | public class MqttClientOptions 11 | { 12 | /// 13 | /// 客户端标识 14 | /// 15 | public string ClientId { get; set; } = $"MqttFx_{Guid.NewGuid().GetHashCode() & ushort.MaxValue}"; 16 | 17 | /// 18 | /// 清除标识 19 | /// 20 | public bool CleanSession { get; set; } = true; 21 | 22 | /// 23 | /// Keep Alive(秒) 24 | /// Gets or sets the keep alive period. 25 | /// The connection is normally left open by the client so that is can send and receive data at any time. 26 | /// If no data flows over an open connection for a certain time period then the client will generate a PINGREQ and 27 | /// expect to receive a PINGRESP from the broker. 28 | /// This message exchange confirms that the connection is open and working. 29 | /// This period is known as the keep alive period. 30 | /// 31 | public ushort KeepAlive { get; set; } = 15; 32 | 33 | /// 34 | /// 凭证 35 | /// 36 | public MqttClientCredentials Credentials { get; set; } 37 | 38 | /// 39 | /// Gets or sets the retain flag of the will message. 40 | /// 41 | public bool WillRetain { get; set; } 42 | 43 | /// 44 | /// Gets or sets the QoS level of the will message. 45 | /// 46 | public MqttQos WillQos { get; set; } 47 | 48 | /// 49 | /// Gets or sets the topic of the will message. 50 | /// 51 | public string WillTopic { get; set; } 52 | 53 | /// 54 | /// Gets or sets the payload of the will message. 55 | /// 56 | public byte[] WillPayload { get; set; } 57 | 58 | /// 59 | /// 连接超时时长(秒) 60 | /// Gets or sets the timeout which will be applied at socket level and internal operations. 61 | /// The default value is the same as for sockets in .NET in general. 62 | /// 63 | public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(10); 64 | 65 | /// 66 | /// HostNameOrAddress 67 | /// 68 | public string Host { get; set; } = "localhost"; 69 | 70 | /// 71 | /// 端口 72 | /// 73 | public int Port { get; set; } = 1883; 74 | } 75 | -------------------------------------------------------------------------------- /src/MqttFx/Client/Extensions/MqttClientOptionsBuilder.cs: -------------------------------------------------------------------------------- 1 | namespace MqttFx; 2 | 3 | public class MqttClientOptionsBuilder 4 | { 5 | private readonly MqttClientOptions _options = new(); 6 | 7 | public MqttClientOptionsBuilder WithCleanSession(bool value = true) 8 | { 9 | _options.CleanSession = value; 10 | return this; 11 | } 12 | 13 | public MqttClientOptionsBuilder WithClientId(string clientId) 14 | { 15 | _options.ClientId = clientId; 16 | return this; 17 | } 18 | 19 | public MqttClientOptionsBuilder WithCredentials(string username, string password = default) 20 | { 21 | _options.Credentials = new MqttClientCredentials 22 | { 23 | Username = username, 24 | Password = password 25 | }; 26 | return this; 27 | } 28 | 29 | public MqttClientOptionsBuilder WithTcpServer(string server, int port = 1883) 30 | { 31 | _options.Host = server; 32 | _options.Port = port; 33 | return this; 34 | } 35 | 36 | public MqttClientOptions Build() 37 | { 38 | return _options; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/MqttFx/Client/Extensions/MqttClientTlsOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net.Security; 4 | using System.Security.Cryptography.X509Certificates; 5 | 6 | namespace MqttFx; 7 | 8 | public class MqttClientTlsOptions 9 | { 10 | public bool UseTls { get; set; } 11 | 12 | public bool IgnoreCertificateRevocationErrors { get; set; } 13 | 14 | public bool IgnoreCertificateChainErrors { get; set; } 15 | 16 | public bool AllowUntrustedCertificates { get; set; } 17 | 18 | public List Certificates { get; set; } 19 | 20 | public Func CertificateValidationCallback { get; set; } 21 | } -------------------------------------------------------------------------------- /src/MqttFx/Client/Extensions/MqttFxClientServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using MqttFx; 2 | using MqttFx.Client; 3 | using System; 4 | 5 | namespace Microsoft.Extensions.DependencyInjection; 6 | 7 | public static class MqttFxClientServiceCollectionExtensions 8 | { 9 | public static IServiceCollection AddMqttFxClient(this IServiceCollection services, Action optionsAction) 10 | { 11 | if (optionsAction == null) 12 | throw new ArgumentNullException(nameof(optionsAction)); 13 | 14 | services.AddLogging(); 15 | services.AddOptions(); 16 | services.Configure(optionsAction); 17 | services.AddSingleton(); 18 | return services; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/MqttFx/Client/MqttClient.cs: -------------------------------------------------------------------------------- 1 | using DotNetty.Codecs.MqttFx; 2 | using DotNetty.Codecs.MqttFx.Packets; 3 | using DotNetty.Handlers.Logging; 4 | using DotNetty.Handlers.Timeout; 5 | using DotNetty.Transport.Bootstrapping; 6 | using DotNetty.Transport.Channels; 7 | using DotNetty.Transport.Channels.Sockets; 8 | using Microsoft.Extensions.Logging; 9 | using Microsoft.Extensions.Logging.Abstractions; 10 | using Microsoft.Extensions.Options; 11 | using MqttFx.Client.Channels; 12 | using MqttFx.Formatter; 13 | using MqttFx.Utils; 14 | using System; 15 | using System.Collections.Concurrent; 16 | using System.Threading; 17 | using System.Threading.Tasks; 18 | 19 | namespace MqttFx.Client; 20 | 21 | /// 22 | /// Mqtt客户端 23 | /// 使用 MQTT 的程序或设备。客户端始终建立与服务器的网络连接。 24 | /// A program or device that uses MQTT. A Client always establishes the Network Connection to the Server. 25 | /// # 发布其他客户端可能感兴趣的应用程序消息。( Publish Application Messages that other Clients might be interested in.) 26 | /// # 发布其他客户端可能感兴趣的应用程序消息。( Subscribe to request Application Messages that it is interested in receiving.) 27 | /// # 取消订阅以删除对应用程序消息的请求。(Unsubscribe to remove a request for Application Messages.) 28 | /// # 断开与服务器的连接。(Disconnect from the Server.) 29 | /// 30 | public class MqttClient 31 | { 32 | private readonly ILogger logger; 33 | public IEventLoopGroup EventLoop { get; private set; } 34 | private volatile IChannel channel; 35 | private readonly PacketIdProvider packetIdProvider = new(); 36 | 37 | internal ConcurrentDictionary PendingPublishs = new(); 38 | internal ConcurrentDictionary PendingSubscriptions = new(); 39 | internal ConcurrentDictionary PendingUnSubscriptions = new(); 40 | 41 | public bool IsConnected { get; private set; } 42 | 43 | public MqttClientOptions Options { get; } 44 | 45 | public event Func ConnectedAsync; 46 | 47 | public event Func DisconnectedAsync; 48 | 49 | public event Func ApplicationMessageReceivedAsync; 50 | 51 | public MqttClient(ILogger logger, IOptions options) 52 | { 53 | this.logger = logger ?? NullLogger.Instance; 54 | Options = options.Value; 55 | } 56 | 57 | /// 58 | /// 连接 59 | /// 60 | /// 61 | public async ValueTask ConnectAsync(CancellationToken cancellationToken = default) 62 | { 63 | cancellationToken.ThrowIfCancellationRequested(); 64 | 65 | if (EventLoop == null) 66 | EventLoop = new MultithreadEventLoopGroup(); 67 | 68 | var connectFuture = new TaskCompletionSource(); 69 | var bootstrap = new Bootstrap(); 70 | bootstrap 71 | .Group(EventLoop) 72 | .Channel() 73 | .Option(ChannelOption.TcpNodelay, true) 74 | .RemoteAddress(Options.Host, Options.Port) 75 | .Handler(new ActionChannelInitializer(ch => 76 | { 77 | ch.Pipeline.AddLast(new LoggingHandler()); 78 | ch.Pipeline.AddLast(MqttEncoder.Instance, new MqttDecoder(false, 256 * 1024)); 79 | ch.Pipeline.AddLast(new IdleStateHandler(Options.Timeout, Options.Timeout, TimeSpan.Zero), new MqttPingHandler(Options.KeepAlive)); 80 | ch.Pipeline.AddLast(new MqttChannelHandler(this, connectFuture)); 81 | })); 82 | 83 | try 84 | { 85 | channel = await bootstrap.ConnectAsync(); 86 | if (channel.Open) 87 | { 88 | packetIdProvider.Reset(); 89 | PendingSubscriptions.Clear(); 90 | IsConnected = true; 91 | } 92 | return await connectFuture.Task; 93 | } 94 | catch (Exception ex) 95 | { 96 | logger.LogError(ex, ex.Message); 97 | throw new MqttException("BrokerUnavailable: " + ex.Message); 98 | } 99 | } 100 | 101 | /// 102 | /// 发布消息 103 | /// 104 | /// 105 | /// 106 | /// 107 | public Task PublishAsync(ApplicationMessage applicationMessage, CancellationToken cancellationToken = default) 108 | { 109 | cancellationToken.ThrowIfCancellationRequested(); 110 | 111 | var packet = PublishPacketFactory.Create(applicationMessage); 112 | if (packet.Qos > MqttQos.AtMostOnce) 113 | packet.PacketId = packetIdProvider.NewPacketId(); 114 | 115 | SendAsync(packet); 116 | 117 | if (packet.Qos == MqttQos.AtMostOnce) 118 | return Task.FromResult(new PublishResult()); 119 | else 120 | { 121 | PendingPublish pending = new(packet); 122 | PendingPublishs.TryAdd(packet.PacketId, pending); 123 | pending.StartPublishRetransmissionTimer(EventLoop.GetNext(), SendAsync); 124 | return pending.Future.Task; 125 | } 126 | } 127 | 128 | /// 129 | /// 订阅消息 130 | /// 131 | /// 132 | /// 133 | /// 134 | public Task SubscribeAsync(SubscriptionRequests subscriptionRequests, CancellationToken cancellationToken = default) 135 | { 136 | cancellationToken.ThrowIfCancellationRequested(); 137 | 138 | var packet = new SubscribePacket(packetIdProvider.NewPacketId(), subscriptionRequests.Requests.ToArray()); 139 | SendAsync(packet); 140 | 141 | PendingSubscription pending = new(packet); 142 | PendingSubscriptions.TryAdd(packet.PacketId, pending); 143 | pending.StartRetransmitTimer(EventLoop.GetNext(), SendAsync); 144 | return pending.Future.Task; 145 | } 146 | 147 | /// 148 | /// 取消订阅 149 | /// 150 | /// 主题 151 | public Task UnsubscribeAsync(params string[] topicFilters) 152 | { 153 | var packet = new UnsubscribePacket(packetIdProvider.NewPacketId(), topicFilters); 154 | SendAsync(packet); 155 | 156 | PendingUnSubscription pending = new(packet); 157 | PendingUnSubscriptions.TryAdd(packet.PacketId, pending); 158 | pending.StartRetransmitTimer(EventLoop.GetNext(), SendAsync); 159 | return pending.Future.Task; 160 | } 161 | 162 | /// 163 | /// 断开连接 164 | /// 165 | /// 166 | public async Task DisconnectAsync(CancellationToken cancellationToken = default) 167 | { 168 | cancellationToken.ThrowIfCancellationRequested(); 169 | 170 | IsConnected = false; 171 | if (channel != null) 172 | { 173 | await SendAsync(new DisconnectPacket()); 174 | await channel.CloseAsync(); 175 | } 176 | await EventLoop.ShutdownGracefullyAsync(TimeSpan.FromMilliseconds(100), TimeSpan.FromSeconds(1)); 177 | await OnDisconnected(); 178 | } 179 | 180 | /// 181 | /// 发送包 182 | /// 183 | /// 184 | /// 185 | public Task SendAsync(Packet packet) 186 | { 187 | if (channel == null) 188 | return Task.CompletedTask; 189 | 190 | if (channel.Active) 191 | return channel.WriteAndFlushAsync(packet); 192 | 193 | return Task.CompletedTask; 194 | } 195 | 196 | internal Task OnConnected(ConnectResult result) 197 | { 198 | if (ConnectedAsync == null) 199 | return Task.CompletedTask; 200 | 201 | return ConnectedAsync(result); 202 | } 203 | 204 | internal Task OnDisconnected() 205 | { 206 | if (DisconnectedAsync == null) 207 | return Task.CompletedTask; 208 | 209 | return DisconnectedAsync(); 210 | } 211 | 212 | internal Task OnApplicationMessageReceived(ApplicationMessage message) 213 | { 214 | if (ApplicationMessageReceivedAsync == null) 215 | return Task.CompletedTask; 216 | 217 | return ApplicationMessageReceivedAsync(message); 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /src/MqttFx/Client/MqttClientExtensions.cs: -------------------------------------------------------------------------------- 1 | using DotNetty.Codecs.MqttFx.Packets; 2 | using System; 3 | using System.Text; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace MqttFx.Client; 8 | 9 | public static class MqttClientExtensions 10 | { 11 | public static Task PublishAsync(this MqttClient mqttClient, string topic, string payload = default, MqttQos qos = MqttQos.AtMostOnce, bool retain = false, CancellationToken cancellationToken = default) 12 | { 13 | var payloadBuffer = Encoding.UTF8.GetBytes(payload ?? string.Empty); 14 | return PublishAsync(mqttClient, topic, payloadBuffer, qos, retain, cancellationToken); 15 | } 16 | 17 | public static Task PublishAsync(this MqttClient mqttClient, string topic, byte[] payload = default, MqttQos qos = MqttQos.AtMostOnce, bool retain = false, CancellationToken cancellationToken = default) 18 | { 19 | if (mqttClient == null) 20 | throw new ArgumentNullException(nameof(mqttClient)); 21 | 22 | if (topic == null) 23 | throw new ArgumentNullException(nameof(topic)); 24 | 25 | var applicationMessage = new ApplicationMessageBuilder() 26 | .WithTopic(topic) 27 | .WithPayload(payload) 28 | .WithQos(qos) 29 | .WithRetain(retain) 30 | .Build(); 31 | 32 | return mqttClient.PublishAsync(applicationMessage, cancellationToken); 33 | } 34 | 35 | public static Task SubscribeAsync(this MqttClient mqttClient, TopicFilter topicFilter, CancellationToken cancellationToken = default) 36 | { 37 | if (mqttClient == null) 38 | throw new ArgumentNullException(nameof(mqttClient)); 39 | 40 | if (topicFilter == null) 41 | throw new ArgumentNullException(nameof(topicFilter)); 42 | 43 | var subscribeOptions = new SubscriptionRequestsBuilder() 44 | .WithTopicFilter(topicFilter) 45 | .Build(); 46 | 47 | return mqttClient.SubscribeAsync(subscribeOptions, cancellationToken); 48 | } 49 | 50 | public static Task SubscribeAsync(this MqttClient mqttClient, string topic, MqttQos qos = MqttQos.AtMostOnce, CancellationToken cancellationToken = default) 51 | { 52 | if (mqttClient == null) 53 | throw new ArgumentNullException(nameof(mqttClient)); 54 | 55 | if (topic == null) 56 | throw new ArgumentNullException(nameof(topic)); 57 | 58 | var subscriptionRequests = new SubscriptionRequestsBuilder() 59 | .WithTopicFilter(topic, qos) 60 | .Build(); 61 | 62 | return mqttClient.SubscribeAsync(subscriptionRequests, cancellationToken); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/MqttFx/ConnectResult.cs: -------------------------------------------------------------------------------- 1 | using DotNetty.Codecs.MqttFx.Packets; 2 | 3 | namespace MqttFx; 4 | 5 | public class ConnectResult 6 | { 7 | public ConnectResult(ConnectReturnCode connectReturn) 8 | { 9 | ConnectReturn = connectReturn; 10 | } 11 | 12 | public bool Succeeded => ConnectReturn == ConnectReturnCode.CONNECTION_ACCEPTED; 13 | 14 | public ConnectReturnCode ConnectReturn { get; set; } = ConnectReturnCode.CONNECTION_REFUSED_SERVER_UNAVAILABLE; 15 | } 16 | -------------------------------------------------------------------------------- /src/MqttFx/Formatter/PublishPacketFactory.cs: -------------------------------------------------------------------------------- 1 | using DotNetty.Codecs.MqttFx.Packets; 2 | using System; 3 | 4 | namespace MqttFx.Formatter; 5 | 6 | class PublishPacketFactory 7 | { 8 | public static PublishPacket Create(ApplicationMessage applicationMessage) 9 | { 10 | if (applicationMessage == null) 11 | throw new ArgumentNullException(nameof(applicationMessage)); 12 | 13 | var packet = new PublishPacket(applicationMessage.Payload) 14 | { 15 | TopicName = applicationMessage.Topic, 16 | Qos = applicationMessage.Qos, 17 | Dup = applicationMessage.Dup, 18 | Retain = applicationMessage.Retain 19 | }; 20 | return packet; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/MqttFx/Formatter/PublishPacketFactoryExtensions.cs: -------------------------------------------------------------------------------- 1 | using DotNetty.Codecs.MqttFx.Packets; 2 | 3 | namespace MqttFx.Formatter; 4 | 5 | public static class PublishPacketFactoryExtensions 6 | { 7 | public static ApplicationMessage ToApplicationMessage(this PublishPacket packet) 8 | { 9 | return new ApplicationMessage 10 | { 11 | Qos = packet.Qos, 12 | Retain = packet.Retain, 13 | Topic = packet.TopicName, 14 | Payload = (PublishPayload)packet.Payload 15 | }; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/MqttFx/MqttException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MqttFx; 4 | 5 | public class MqttException : Exception 6 | { 7 | public MqttException() { } 8 | 9 | public MqttException(string message) : base(message) { } 10 | 11 | public MqttException(string message, Exception innerException) : base(message, innerException) { } 12 | } 13 | 14 | public class MqttTimeoutException : Exception 15 | { 16 | public MqttTimeoutException(Exception ex) { } 17 | } 18 | -------------------------------------------------------------------------------- /src/MqttFx/MqttFx - Backup.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.1 5 | latest 6 | MqttFx 7 | MqttFx 8 | 3.1.4 9 | linfx 10 | Mqtt Client, using Dotnetty 11 | https://github.com/linfx/MqttFx 12 | MqttFx 13 | MqttFx 14 | 1701;1702;1591; 15 | true 16 | latest 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/MqttFx/MqttFx.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.1 5 | latest 6 | MqttFx 7 | MqttFx 8 | 3.1.4 9 | DTXLink 10 | Mqtt Client, using Dotnetty 11 | https://github.com/linfx/MqttFx 12 | MqttFx 13 | 1701;1702;1591; 14 | true 15 | latest 16 | True 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/MqttFx/MqttFx.csproj.user: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | false 5 | 6 | -------------------------------------------------------------------------------- /src/MqttFx/PublishResult.cs: -------------------------------------------------------------------------------- 1 | namespace MqttFx; 2 | 3 | public class PublishResult 4 | { 5 | /// 6 | /// Gets the packet identifier which was used for this publish. 7 | /// 8 | public ushort PacketId { get; set; } 9 | 10 | public PublishResult() { } 11 | 12 | public PublishResult(ushort packetId) 13 | { 14 | PacketId = packetId; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/MqttFx/SubscribeResult.cs: -------------------------------------------------------------------------------- 1 | using DotNetty.Codecs.MqttFx.Packets; 2 | using System.Collections.Generic; 3 | 4 | namespace MqttFx; 5 | 6 | public class SubscribeResult 7 | { 8 | public IReadOnlyCollection Items { get; internal set; } 9 | 10 | public SubscribeResult(IReadOnlyCollection items) 11 | { 12 | Items = items; 13 | } 14 | } 15 | 16 | public struct SubscribeResultItem 17 | { 18 | public string TopicFilter; 19 | 20 | public MqttQos ResultCode { get; internal set; } 21 | } 22 | -------------------------------------------------------------------------------- /src/MqttFx/SubscriptionRequests.cs: -------------------------------------------------------------------------------- 1 | using DotNetty.Codecs.MqttFx.Packets; 2 | using System.Collections.Generic; 3 | 4 | namespace MqttFx; 5 | 6 | public class SubscriptionRequests 7 | { 8 | /// 9 | /// Gets or sets a list of topic filters the client wants to subscribe to. 10 | /// Topic filters can include regular topics or wild cards. 11 | /// 12 | public List Requests { get; set; } = new List(); 13 | } 14 | -------------------------------------------------------------------------------- /src/MqttFx/SubscriptionRequestsBuilder.cs: -------------------------------------------------------------------------------- 1 | using DotNetty.Codecs.MqttFx.Packets; 2 | using System; 3 | 4 | namespace MqttFx; 5 | 6 | public class SubscriptionRequestsBuilder 7 | { 8 | readonly SubscriptionRequests _requests = new(); 9 | 10 | public SubscriptionRequestsBuilder WithTopicFilter(string topic, MqttQos qos = MqttQos.AtMostOnce) 11 | { 12 | return WithTopicFilter(new TopicFilter 13 | { 14 | Topic = topic, 15 | Qos = qos, 16 | }); 17 | } 18 | 19 | public SubscriptionRequestsBuilder WithTopicFilter(TopicFilter topicFilter) 20 | { 21 | if (topicFilter == null) 22 | throw new ArgumentNullException(nameof(topicFilter)); 23 | 24 | SubscriptionRequest request; 25 | request.TopicFilter = topicFilter.Topic; 26 | request.RequestedQos = topicFilter.Qos; 27 | _requests.Requests.Add(request); 28 | 29 | return this; 30 | } 31 | 32 | public SubscriptionRequestsBuilder WithTopicFilter(Action topicFilterBuilder) 33 | { 34 | if (topicFilterBuilder == null) 35 | throw new ArgumentNullException(nameof(topicFilterBuilder)); 36 | 37 | var internalTopicFilterBuilder = new TopicFilterBuilder(); 38 | topicFilterBuilder(internalTopicFilterBuilder); 39 | 40 | return WithTopicFilter(internalTopicFilterBuilder); 41 | } 42 | 43 | public SubscriptionRequestsBuilder WithTopicFilter(TopicFilterBuilder topicFilterBuilder) 44 | { 45 | if (topicFilterBuilder == null) 46 | throw new ArgumentNullException(nameof(topicFilterBuilder)); 47 | 48 | return WithTopicFilter(topicFilterBuilder.Build()); 49 | } 50 | 51 | public SubscriptionRequests Build() 52 | { 53 | return _requests; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/MqttFx/TopicFilter.cs: -------------------------------------------------------------------------------- 1 | using DotNetty.Codecs.MqttFx.Packets; 2 | 3 | namespace MqttFx; 4 | 5 | public class TopicFilter 6 | { 7 | /// 8 | /// Gets or sets the MQTT topic. 9 | /// In MQTT, the word topic refers to an UTF-8 string that the broker uses to filter messages for each connected 10 | /// client. 11 | /// The topic consists of one or more topic levels. Each topic level is separated by a forward slash (topic level 12 | /// separator). 13 | /// 14 | public string Topic { get; set; } 15 | 16 | /// 17 | /// Gets or sets the payload format indicator. 18 | /// The payload format indicator is part of any MQTT packet that can contain a payload. The indicator is an optional 19 | /// byte value. 20 | /// A value of 0 indicates an “unspecified byte stream”. 21 | /// A value of 1 indicates a "UTF-8 encoded payload". 22 | /// If no payload format indicator is provided, the default value is 0. 23 | /// Hint: MQTT 5 feature only. 24 | /// 25 | public MqttQos Qos { get; set; } 26 | } 27 | -------------------------------------------------------------------------------- /src/MqttFx/TopicFilterBuilder.cs: -------------------------------------------------------------------------------- 1 | using DotNetty.Codecs.MqttFx.Packets; 2 | 3 | namespace MqttFx; 4 | 5 | public sealed class TopicFilterBuilder 6 | { 7 | /// 8 | /// The MQTT topic. 9 | /// In MQTT, the word topic refers to an UTF-8 string that the broker uses to filter messages for each connected client. 10 | /// The topic consists of one or more topic levels. Each topic level is separated by a forward slash (topic level separator). 11 | /// 12 | string _topic; 13 | 14 | /// 15 | /// The quality of service level. 16 | /// The Quality of Service (QoS) level is an agreement between the sender of a message and the receiver of a message that defines the guarantee of delivery for a specific message. 17 | /// There are 3 QoS levels in MQTT: 18 | /// - At most once (0): Message gets delivered no time, once or multiple times. 19 | /// - At least once (1): Message gets delivered at least once (one time or more often). 20 | /// - Exactly once (2): Message gets delivered exactly once (It's ensured that the message only comes once). 21 | /// 22 | MqttQos _qos = MqttQos.AtMostOnce; 23 | 24 | public TopicFilterBuilder WithTopic(string topic) 25 | { 26 | _topic = topic; 27 | return this; 28 | } 29 | 30 | public TopicFilterBuilder WithQos(MqttQos qos) 31 | { 32 | _qos = qos; 33 | return this; 34 | } 35 | 36 | public TopicFilterBuilder WithAtLeastOnceQoS() 37 | { 38 | _qos = MqttQos.AtLeastOnce; 39 | return this; 40 | } 41 | 42 | public TopicFilterBuilder WithAtMostOnceQoS() 43 | { 44 | _qos = MqttQos.AtMostOnce; 45 | return this; 46 | } 47 | 48 | public TopicFilterBuilder WithExactlyOnceQoS() 49 | { 50 | _qos = MqttQos.ExactlyOnce; 51 | return this; 52 | } 53 | 54 | public TopicFilter Build() 55 | { 56 | if (string.IsNullOrEmpty(_topic)) 57 | throw new MqttException("Topic is not set."); 58 | 59 | return new TopicFilter 60 | { 61 | Topic = _topic, 62 | Qos = _qos, 63 | }; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/MqttFx/UnsubscribeResult.cs: -------------------------------------------------------------------------------- 1 | namespace MqttFx; 2 | 3 | public class UnSubscribeResult 4 | { 5 | public ushort PacketId { get; set; } 6 | 7 | public UnSubscribeResult(ushort packetId) 8 | { 9 | PacketId = packetId; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/MqttFx/Utils/PacketIdProvider.cs: -------------------------------------------------------------------------------- 1 | namespace MqttFx.Utils; 2 | 3 | class PacketIdProvider 4 | { 5 | private readonly object _syncRoot = new(); 6 | private ushort _value; 7 | 8 | public void Reset() 9 | { 10 | lock (_syncRoot) 11 | _value = 0; 12 | } 13 | 14 | public ushort NewPacketId() 15 | { 16 | lock (_syncRoot) 17 | { 18 | if (_value == ushort.MaxValue) 19 | _value = 0; 20 | 21 | _value++; 22 | 23 | return _value; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/MqttFx/Utils/PendingPublish.cs: -------------------------------------------------------------------------------- 1 | using DotNetty.Codecs.MqttFx.Packets; 2 | using DotNetty.Transport.Channels; 3 | using System; 4 | using System.Threading.Tasks; 5 | 6 | namespace MqttFx.Utils; 7 | 8 | class PendingPublish 9 | { 10 | private readonly RetransmissionHandler publishRetransmissionHandler = new(); 11 | private readonly RetransmissionHandler pubRelRetransmissionHandler = new(); 12 | 13 | public TaskCompletionSource Future { get; set; } = new(); 14 | 15 | public PendingPublish(PublishPacket packet) 16 | { 17 | publishRetransmissionHandler.OriginalMessage = packet; 18 | } 19 | 20 | public void SetPubRelMessage(PubRelPacket packet) => pubRelRetransmissionHandler.OriginalMessage = packet; 21 | 22 | public void StartPublishRetransmissionTimer(IEventLoop eventLoop, Func send) 23 | { 24 | publishRetransmissionHandler.Handler = originalMessage => send(originalMessage with { Dup = true }); 25 | publishRetransmissionHandler.Start(eventLoop); 26 | } 27 | 28 | public void StartPubrelRetransmissionTimer(IEventLoop eventLoop, Func send) 29 | { 30 | pubRelRetransmissionHandler.Handler = originalMessage => send(originalMessage with { }); 31 | pubRelRetransmissionHandler.Start(eventLoop); 32 | } 33 | 34 | public void OnPubAckReceived() 35 | { 36 | publishRetransmissionHandler.Stop(); 37 | } 38 | 39 | public void OnPubCompReceived() 40 | { 41 | pubRelRetransmissionHandler.Stop(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/MqttFx/Utils/PendingSubscription.cs: -------------------------------------------------------------------------------- 1 | using DotNetty.Codecs.MqttFx.Packets; 2 | using DotNetty.Transport.Channels; 3 | using System; 4 | using System.Threading.Tasks; 5 | 6 | namespace MqttFx.Utils; 7 | 8 | class PendingSubscription 9 | { 10 | private readonly RetransmissionHandler retransmissionHandler = new(); 11 | 12 | public TaskCompletionSource Future { get; set; } = new(); 13 | 14 | public SubscribePacket SubscribePacket { get; set; } 15 | 16 | public PendingSubscription(SubscribePacket packet) 17 | { 18 | SubscribePacket = packet; 19 | retransmissionHandler.OriginalMessage = packet; 20 | } 21 | 22 | public void StartRetransmitTimer(IEventLoop eventLoop, Func send) 23 | { 24 | retransmissionHandler.Handler = originalMessage => send(originalMessage with { }); 25 | retransmissionHandler.Start(eventLoop); 26 | } 27 | 28 | public void OnSubackReceived() 29 | { 30 | retransmissionHandler.Stop(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/MqttFx/Utils/PendingUnsubscription.cs: -------------------------------------------------------------------------------- 1 | using DotNetty.Codecs.MqttFx.Packets; 2 | using DotNetty.Transport.Channels; 3 | using System; 4 | using System.Threading.Tasks; 5 | 6 | namespace MqttFx.Utils; 7 | 8 | class PendingUnSubscription 9 | { 10 | private readonly RetransmissionHandler retransmissionHandler = new(); 11 | 12 | public TaskCompletionSource Future { get; set; } = new(); 13 | 14 | public PendingUnSubscription(UnsubscribePacket packet) 15 | { 16 | retransmissionHandler.OriginalMessage = packet; 17 | } 18 | 19 | public void StartRetransmitTimer(IEventLoop eventLoop, Func send) 20 | { 21 | retransmissionHandler.Handler = originalMessage => send(originalMessage with { }); 22 | retransmissionHandler.Start(eventLoop); 23 | } 24 | 25 | public void OnUnsubackReceived() 26 | { 27 | retransmissionHandler.Stop(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/MqttFx/Utils/RetransmissionHandler.cs: -------------------------------------------------------------------------------- 1 | using DotNetty.Codecs.MqttFx.Packets; 2 | using DotNetty.Common.Concurrency; 3 | using DotNetty.Transport.Channels; 4 | using System; 5 | 6 | namespace MqttFx.Utils; 7 | 8 | /// 9 | /// 消息重发 10 | /// 11 | /// 12 | class RetransmissionHandler where T : Packet 13 | { 14 | private volatile bool stopped; 15 | //private PendingOperation pendingOperation; 16 | private IScheduledTask timer; 17 | private int timeout; 18 | public Action Handler { get; set; } 19 | public T OriginalMessage { get; set; } 20 | 21 | public void Start(IEventLoop eventLoop) 22 | { 23 | if (eventLoop is null) 24 | throw new ArgumentNullException(nameof(eventLoop)); 25 | 26 | timeout = 10; 27 | StartTimer(eventLoop); 28 | } 29 | 30 | void StartTimer(IEventLoop eventLoop) 31 | { 32 | //if (stopped || pendingOperation.isCanceled()) 33 | // return; 34 | 35 | if (stopped) 36 | return; 37 | 38 | timer = eventLoop.Schedule(() => 39 | { 40 | //if (stopped || pendingOperation.isCanceled()) 41 | // return; 42 | 43 | if (stopped) 44 | return; 45 | 46 | timeout += 5; 47 | Handler(OriginalMessage); 48 | StartTimer(eventLoop); 49 | }, TimeSpan.FromSeconds(timeout)); 50 | } 51 | 52 | public void Stop() 53 | { 54 | stopped = true; 55 | if (timer != null) 56 | timer.Cancel(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /test/MqttFx.Test/MqttCodecTests.cs: -------------------------------------------------------------------------------- 1 | using DotNetty.Buffers; 2 | using DotNetty.Codecs.MqttFx; 3 | using DotNetty.Codecs.MqttFx.Packets; 4 | using DotNetty.Transport.Channels; 5 | using Moq; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using Xunit; 9 | 10 | namespace MqttFx.Test 11 | { 12 | public class MqttCodecTests 13 | { 14 | private static readonly IByteBufferAllocator Allocator = new UnpooledByteBufferAllocator(); 15 | 16 | private readonly MqttDecoder serverDecoder; 17 | private readonly MqttDecoder clientDecoder; 18 | private readonly Mock contextMock; 19 | 20 | public MqttCodecTests() 21 | { 22 | serverDecoder = new MqttDecoder(true, 256 * 1024); 23 | clientDecoder = new MqttDecoder(false, 256 * 1024); 24 | contextMock = new Mock(MockBehavior.Strict); 25 | contextMock.Setup(x => x.Removed).Returns(false); 26 | contextMock.Setup(x => x.Allocator).Returns(UnpooledByteBufferAllocator.Default); 27 | } 28 | 29 | [Theory] 30 | [InlineData("a", true, 0, null, null, "will/topic/name", new byte[] { 5, 3, 255, 6, 5 }, MqttQos.ExactlyOnce, true)] 31 | [InlineData("11a_2", false, 1, "user1", null, "will", new byte[0], MqttQos.AtLeastOnce, false)] 32 | [InlineData("abc/ж", false, 10, "", "pwd", null, null, null, false)] 33 | [InlineData("", true, 1000, "имя", "密碼", null, null, null, false)] 34 | public void ConnectMessageTest(string clientId, bool cleanSession, ushort keepAlive, string userName, string password, string willTopicName, byte[] willMessage, MqttQos? willQos, bool willRetain) 35 | { 36 | var packet = new ConnectPacket(); 37 | var packet_variableHeader = (ConnectVariableHeader)packet.VariableHeader; 38 | var packet_payload = (ConnectPayload)packet.Payload; 39 | 40 | packet_payload.ClientId = clientId; 41 | packet_variableHeader.ConnectFlags.CleanSession = cleanSession; 42 | packet_variableHeader.KeepAlive = keepAlive; 43 | if (userName != null) 44 | { 45 | packet_variableHeader.ConnectFlags.UsernameFlag = true; 46 | packet_payload.UserName = userName; 47 | } 48 | if (password != null) 49 | { 50 | packet_variableHeader.ConnectFlags.PasswordFlag = true; 51 | packet_payload.Password = password; 52 | } 53 | if (willTopicName != null) 54 | { 55 | packet_variableHeader.ConnectFlags.WillFlag = true; 56 | packet_variableHeader.ConnectFlags.WillQos = willQos ?? MqttQos.AtMostOnce; 57 | packet_variableHeader.ConnectFlags.WillRetain = willRetain; 58 | packet_payload.WillTopic = willTopicName; 59 | packet_payload.WillMessage = willMessage; 60 | } 61 | 62 | var recoded = RecodePacket(packet, true, false); 63 | var recoded_variableHeader = (ConnectVariableHeader)recoded.VariableHeader; 64 | var recoded_payload = (ConnectPayload)recoded.Payload; 65 | 66 | contextMock.Verify(x => x.FireChannelRead(It.IsAny()), Times.Once); 67 | Assert.Equal(packet_payload.ClientId, recoded_payload.ClientId); 68 | Assert.Equal(packet_variableHeader.KeepAlive, recoded_variableHeader.KeepAlive); 69 | Assert.Equal(packet_variableHeader.ConnectFlags.CleanSession, recoded_variableHeader.ConnectFlags.CleanSession); 70 | Assert.Equal(packet_variableHeader.ConnectFlags.UsernameFlag, recoded_variableHeader.ConnectFlags.UsernameFlag); 71 | if (packet_variableHeader.ConnectFlags.UsernameFlag) 72 | { 73 | Assert.Equal(recoded_payload.UserName, recoded_payload.UserName); 74 | } 75 | Assert.Equal(packet_variableHeader.ConnectFlags.PasswordFlag, packet_variableHeader.ConnectFlags.PasswordFlag); 76 | if (packet_variableHeader.ConnectFlags.PasswordFlag) 77 | { 78 | Assert.Equal(recoded_payload.Password, recoded_payload.Password); 79 | } 80 | if (packet_variableHeader.ConnectFlags.WillFlag) 81 | { 82 | Assert.Equal(packet_variableHeader.ConnectFlags.WillQos, recoded_variableHeader.ConnectFlags.WillQos); 83 | Assert.Equal(packet_variableHeader.ConnectFlags.WillRetain, recoded_variableHeader.ConnectFlags.WillRetain); 84 | Assert.Equal(recoded_payload.WillTopic, recoded_payload.WillTopic); 85 | //Assert.True(Equals(Unpooled.WrappedBuffer(packet_payload.WillMessage), recoded_payload.WillMessage)); 86 | } 87 | } 88 | 89 | [Theory] 90 | [InlineData(false, ConnectReturnCode.CONNECTION_ACCEPTED)] 91 | [InlineData(true, ConnectReturnCode.CONNECTION_ACCEPTED)] 92 | [InlineData(false, ConnectReturnCode.CONNECTION_REFUSED_UNACCEPTABLE_PROTOCOL_VERSION)] 93 | [InlineData(false, ConnectReturnCode.CONNECTION_REFUSED_IDENTIFIER_REJECTED)] 94 | [InlineData(false, ConnectReturnCode.CONNECTION_REFUSED_SERVER_UNAVAILABLE)] 95 | [InlineData(false, ConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD)] 96 | public void ConnAckMessageTest(bool sessionPresent, ConnectReturnCode returnCode) 97 | { 98 | var packet = new ConnAckPacket(); 99 | var packet_variableHeader = (ConnAckVariableHeader)packet.VariableHeader; 100 | packet_variableHeader.SessionPresent = sessionPresent; 101 | packet_variableHeader.ConnectReturnCode = returnCode; 102 | 103 | var recoded = RecodePacket(packet, false, false); 104 | var recoded_variableHeader = (ConnAckVariableHeader)packet.VariableHeader; 105 | 106 | contextMock.Verify(x => x.FireChannelRead(It.IsAny()), Times.Once); 107 | Assert.Equal(packet_variableHeader.SessionPresent, recoded_variableHeader.SessionPresent); 108 | Assert.Equal(packet_variableHeader.ConnectReturnCode, recoded_variableHeader.ConnectReturnCode); 109 | } 110 | 111 | [Theory] 112 | [InlineData(1, new[] { "+", "+/+", "//", "/#", "+//+" }, new[] { MqttQos.ExactlyOnce, MqttQos.AtLeastOnce, MqttQos.AtMostOnce, MqttQos.ExactlyOnce, MqttQos.AtMostOnce })] 113 | [InlineData(ushort.MaxValue, new[] { "a" }, new[] { MqttQos.AtLeastOnce })] 114 | public void SubscribeMessageTest(ushort packetId, string[] topicFilters, MqttQos[] requestedQosValues) 115 | { 116 | var packet = new SubscribePacket(packetId, topicFilters.Zip(requestedQosValues, (topic, qos) => 117 | { 118 | SubscriptionRequest request; 119 | request.TopicFilter = topic; 120 | request.RequestedQos = qos; 121 | return request; 122 | }).ToArray()); 123 | 124 | var recoded = RecodePacket(packet, true, true); 125 | 126 | contextMock.Verify(x => x.FireChannelRead(It.IsAny()), Times.Once); 127 | Assert.Equal(packet.SubscriptionRequests, recoded.SubscriptionRequests, EqualityComparer.Default); 128 | Assert.Equal(packet.PacketId, recoded.PacketId); 129 | } 130 | 131 | [Theory] 132 | [InlineData(1, new[] { MqttQos.ExactlyOnce, MqttQos.AtLeastOnce, MqttQos.AtMostOnce, MqttQos.Failure })] 133 | [InlineData(ushort.MaxValue, new[] { MqttQos.AtLeastOnce })] 134 | public void SubAckMessageTest(ushort packetId, MqttQos[] qosValues) 135 | { 136 | var packet = new SubAckPacket(packetId, qosValues); 137 | 138 | var recoded = RecodePacket(packet, false, true); 139 | 140 | contextMock.Verify(x => x.FireChannelRead(It.IsAny()), Times.Once); 141 | Assert.Equal(packet.ReturnCodes, recoded.ReturnCodes); 142 | Assert.Equal(packet.PacketId, recoded.PacketId); 143 | } 144 | 145 | [Theory] 146 | [InlineData(1, new[] { "+", "+/+", "//", "/#", "+//+" })] 147 | [InlineData(ushort.MaxValue, new[] { "a" })] 148 | public void UnsubscribeMessageTest(ushort packetId, string[] topicFilters) 149 | { 150 | var packet = new UnsubscribePacket(packetId, topicFilters); 151 | 152 | var recoded = RecodePacket(packet, true, true); 153 | 154 | contextMock.Verify(x => x.FireChannelRead(It.IsAny()), Times.Once); 155 | Assert.Equal(packet.TopicFilters, recoded.TopicFilters); 156 | Assert.Equal(packet.PacketId, recoded.PacketId); 157 | } 158 | 159 | [Theory] 160 | [InlineData(MqttQos.AtMostOnce, false, false, 1, "a", new byte[0])] 161 | [InlineData(MqttQos.ExactlyOnce, true, false, ushort.MaxValue, "/", new byte[0])] 162 | [InlineData(MqttQos.AtLeastOnce, false, true, 129, "a/b", new byte[] { 1, 2, 3 })] 163 | [InlineData(MqttQos.ExactlyOnce, true, true, ushort.MaxValue - 1, "topic/name/that/is/longer/than/256/characters/topic/name/that/is/longer/than/256/characters/topic/name/that/is/longer/than/256/characters/topic/name/that/is/longer/than/256/characters/topic/name/that/is/longer/than/256/characters/topic/name/that/is/longer/than/256/characters/", new byte[] { 1 })] 164 | public void PublishMessageTest(MqttQos qos, bool dup, bool retain, ushort packetId, string topicName, byte[] payload) 165 | { 166 | var packet = new PublishPacket(payload) 167 | { 168 | TopicName = topicName, 169 | Dup = dup, 170 | Retain = retain, 171 | }; 172 | 173 | if (qos > MqttQos.AtMostOnce) 174 | { 175 | packet.PacketId = packetId; 176 | } 177 | 178 | var recoded = RecodePacket(packet, false, true); 179 | 180 | contextMock.Verify(x => x.FireChannelRead(It.IsAny()), Times.Once); 181 | Assert.Equal(packet.TopicName, recoded.TopicName); 182 | if (packet.Qos > MqttQos.AtMostOnce) 183 | { 184 | Assert.Equal(packet.PacketId, recoded.PacketId); 185 | } 186 | Assert.Equal((PublishPayload)packet.Payload, (PublishPayload)recoded.Payload); 187 | } 188 | 189 | //[Theory] 190 | //[InlineData(1)] 191 | ////[InlineData(127)] 192 | ////[InlineData(128)] 193 | ////[InlineData(256)] 194 | ////[InlineData(257)] 195 | ////[InlineData(ushort.MaxValue)] 196 | //public void PacketIdOnlyResponseMessagesTest(ushort packetId) 197 | //{ 198 | // //this.PublishResponseMessageTest(packetId, true); 199 | // //this.PublishResponseMessageTest(packetId, false); 200 | // //this.PublishResponseMessageTest(packetId, true); 201 | // //this.PublishResponseMessageTest(packetId, false); 202 | // //this.PublishResponseMessageTest(packetId, true); 203 | // //this.PublishResponseMessageTest(packetId, false); 204 | // //this.PublishResponseMessageTest(packetId, true); 205 | // //this.PublishResponseMessageTest(packetId, false); 206 | // PublishResponseMessageTest(packetId, false); 207 | //} 208 | 209 | //void PublishResponseMessageTest(ushort packetId, bool useServer) 210 | // where T : PacketWithId, new() 211 | //{ 212 | // var packet = new T 213 | // { 214 | // PacketId = packetId 215 | // }; 216 | 217 | // var recoded = RecodePacket(packet, useServer, true); 218 | 219 | // contextMock.Verify(x => x.FireChannelRead(It.IsAny()), Times.Once); 220 | // contextMock.ResetCalls(); 221 | // Assert.Equal(packet.PacketId, recoded.PacketId); 222 | //} 223 | 224 | [Fact] 225 | public void EmptyPacketMessagesTest() 226 | { 227 | EmptyPacketMessageTest(PingReqPacket.Instance, true); 228 | EmptyPacketMessageTest(PingRespPacket.Instance, false); 229 | EmptyPacketMessageTest(DisconnectPacket.Instance, true); 230 | } 231 | 232 | private void EmptyPacketMessageTest(T packet, bool useServer) where T : Packet 233 | { 234 | T recoded = RecodePacket(packet, useServer, false); 235 | contextMock.Verify(x => x.FireChannelRead(It.IsAny()), Times.Once); 236 | } 237 | 238 | private T RecodePacket(T packet, bool useServer, bool explodeForDecode) where T : Packet 239 | { 240 | var output = new List(); 241 | MqttEncoder.DoEncode(Allocator, packet, output); 242 | 243 | T observedPacket = null; 244 | contextMock.Setup(x => x.FireChannelRead(It.IsAny())) 245 | .Callback((object message) => observedPacket = Assert.IsAssignableFrom(message)) 246 | .Returns(contextMock.Object); 247 | 248 | foreach (IByteBuffer message in output) 249 | { 250 | MqttDecoder mqttDecoder = useServer ? serverDecoder : clientDecoder; 251 | if (explodeForDecode) 252 | { 253 | while (message.IsReadable()) 254 | { 255 | IByteBuffer finalBuffer = message.ReadBytes(1); 256 | mqttDecoder.ChannelRead(contextMock.Object, finalBuffer); 257 | } 258 | } 259 | else 260 | { 261 | mqttDecoder.ChannelRead(contextMock.Object, message); 262 | } 263 | } 264 | return observedPacket; 265 | } 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /test/MqttFx.Test/MqttFx.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | false 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | all 15 | runtime; build; native; contentfiles; analyzers; buildtransitive 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | --------------------------------------------------------------------------------