├── .gitignore
├── .idea
├── .gitignore
├── compiler.xml
├── encodings.xml
├── gradle.xml
├── inspectionProfiles
│ └── Project_Default.xml
├── jarRepositories.xml
├── misc.xml
├── modules.xml
├── netty-mqtt-client.iml
├── runConfigurations.xml
├── uiDesigner.xml
└── vcs.xml
├── README.md
├── build.gradle
├── example
├── build.gradle
└── src
│ └── main
│ └── java
│ └── io
│ └── x2ge
│ └── example
│ └── App.java
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── maven-publish.gradle
├── settings.gradle
└── src
└── main
└── java
└── io
└── x2ge
├── example
└── App.java
└── mqtt
├── MqttClient.java
├── MqttConnectOptions.java
├── core
├── ConnectProcessor.java
├── MessageData.java
├── MessageIdFactory.java
├── MessageStatus.java
├── PingProcessor.java
├── ProcessorResult.java
├── ProtocolUtils.java
├── PublishProcessor.java
├── SubscribeProcessor.java
├── SubscriptionTopic.java
└── UnsubscribeProcessor.java
└── utils
├── AsyncTask.java
├── Log.java
└── StringUtils.java
/.gitignore:
--------------------------------------------------------------------------------
1 | build
2 | .gradle
3 |
4 | /gradle.properties
5 | /*.gpg
6 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/encodings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
17 |
18 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/jarRepositories.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/netty-mqtt-client.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/uiDesigner.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | -
6 |
7 |
8 | -
9 |
10 |
11 | -
12 |
13 |
14 | -
15 |
16 |
17 | -
18 |
19 |
20 |
21 |
22 |
23 | -
24 |
25 |
26 |
27 |
28 |
29 | -
30 |
31 |
32 |
33 |
34 |
35 | -
36 |
37 |
38 |
39 |
40 |
41 | -
42 |
43 |
44 |
45 |
46 | -
47 |
48 |
49 |
50 |
51 | -
52 |
53 |
54 |
55 |
56 | -
57 |
58 |
59 |
60 |
61 | -
62 |
63 |
64 |
65 |
66 | -
67 |
68 |
69 |
70 |
71 | -
72 |
73 |
74 | -
75 |
76 |
77 |
78 |
79 | -
80 |
81 |
82 |
83 |
84 | -
85 |
86 |
87 |
88 |
89 | -
90 |
91 |
92 |
93 |
94 | -
95 |
96 |
97 |
98 |
99 | -
100 |
101 |
102 | -
103 |
104 |
105 | -
106 |
107 |
108 | -
109 |
110 |
111 | -
112 |
113 |
114 |
115 |
116 | -
117 |
118 |
119 | -
120 |
121 |
122 |
123 |
124 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # netty-mqtt-client
2 |
3 | ## 关于
4 |
5 | 基于netty实现的mqtt客户端,可用于Java、Android环境。持续开发中,现已完成基本框架及功能,目前仅支持qos1级别通讯,后期根据需要开发qos2级别。
6 |
7 | ## 如何使用
8 |
9 | #### Gradle:
10 |
11 | repositories {
12 | mavenCentral()
13 | }
14 |
15 | dependencies {
16 | implementation 'io.github.x2ge:netty-mqtt-client:2.0.3'
17 | }
18 |
19 | #### 连接
20 |
21 | MqttClient mqttClient = new MqttClient();
22 | MqttConnectOptions options = new MqttConnectOptions();
23 | options.setHost("localhost");
24 | options.setPort(1883);
25 | options.setUserName("testuser");
26 | options.setPassword("123456".getBytes(StandardCharsets.UTF_8));
27 | options.setClientIdentifier("netty_mqtt_c1");
28 | options.setKeepAliveTime(10);
29 | options.setCleanSession(true);
30 | // 配置动作超时时间
31 | mqttClient.setActionTimeout(3000);
32 | // 配置掉线重连
33 | mqttClient.setReconnectOnLost(5, 10000);
34 | mqttClient.connect(options);
35 |
36 | #### 监听
37 |
38 | mqttClient.setCallback(new MqttClient.Callback() {
39 | @Override
40 | public void onConnected() {
41 | // test
42 | try {
43 | // 订阅主题
44 | mqttClient.subscribe("topic");
45 | // 订阅主题 topic 中可使用 /# ,表示模糊匹配该主题
46 | // 示例:订阅主题 topic1/# ,可接收 topic1、
47 | // topic1/aaa、topic1/bbb等主题下消息
48 | mqttClient.subscribe("topic1/#");
49 | // 发布一个消息到主题topic1/aaa
50 | mqttClient.publish("topic1/aaa", "hello, netty mqtt!");
51 | // 取消订阅
52 | mqttClient.unsubscribe("topic");
53 | } catch (Exception e) {
54 | e.printStackTrace();
55 | }
56 | }
57 |
58 | @Override
59 | public void onConnectFailed(Throwable e) {
60 |
61 | }
62 |
63 | @Override
64 | public void onConnectLost(Throwable e) {
65 |
66 | }
67 |
68 | @Override
69 | public void onReconnectStart(int cur) {
70 |
71 | }
72 |
73 | @Override
74 | public void onMessageArrived(String topic, String s) {
75 |
76 | }
77 | });
78 |
79 | #### 订阅
80 |
81 | // 订阅主题
82 | mqttClient.subscribe("testtopic");
83 | // 订阅主题 topic 中可使用 /# ,表示模糊匹配该主题
84 | // 示例:订阅主题 parenttopic/# ,可接收 parenttopic、
85 | // parenttopic/c1、parenttopic/c2等主题下消息
86 | mqttClient.subscribe("parenttopic/#");
87 |
88 | #### 取消订阅
89 |
90 | mqttClient.unsubscribe("testtopic");
91 |
92 | #### 发布消息
93 |
94 | // 发布一个消息到主题parenttopic/c2
95 | mqttClient.publish("parenttopic/c2", "hello, netty mqtt!");
96 |
97 | #### 关闭连接
98 |
99 | mqttClient.close();
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'java'
3 | id 'maven-publish'
4 | id 'signing'
5 | }
6 |
7 | group 'io.github.x2ge'
8 | version '2.0.3'
9 | //version '2.0.1-SNAPSHOT'
10 |
11 | compileJava {
12 | sourceCompatibility = 1.8
13 | targetCompatibility = 1.8
14 | [compileJava]*.options*.encoding = 'UTF-8'
15 | }
16 |
17 | repositories {
18 | // maven {
19 | // allowInsecureProtocol = true
20 | // url "http://192.168.46.167:9191/repository/maven-public/"
21 | // }
22 | google()
23 | // jcenter()
24 | mavenCentral()
25 | }
26 |
27 | dependencies {
28 | // implementation 'io.netty:netty-all:4.1.68.Final'
29 | implementation 'io.netty:netty-codec-mqtt:4.1.68.Final'
30 | }
31 |
32 | apply from: 'maven-publish.gradle'
33 |
34 |
--------------------------------------------------------------------------------
/example/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'java'
3 | }
4 |
5 | group 'com.x2ge.example'
6 | version '1.0.0-SNAPSHOT'
7 |
8 | repositories {
9 | // maven {
10 | // allowInsecureProtocol = true
11 | // url "http://192.168.46.167:9191/repository/maven-public/"
12 | // }
13 | google()
14 | // jcenter()
15 | maven { url "https://s01.oss.sonatype.org/content/repositories/snapshots/" }
16 | maven { url "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/" }
17 | mavenCentral()
18 | }
19 |
20 | dependencies {
21 | implementation 'io.github.x2ge:netty-mqtt-client:2.0.3'
22 | }
23 |
24 |
--------------------------------------------------------------------------------
/example/src/main/java/io/x2ge/example/App.java:
--------------------------------------------------------------------------------
1 | package io.x2ge.example;
2 |
3 | import io.x2ge.mqtt.MqttClient;
4 | import io.x2ge.mqtt.MqttConnectOptions;
5 | import io.x2ge.mqtt.utils.Log;
6 |
7 | public class App {
8 |
9 | public static void main(String[] args) {
10 | Log.enablePing(true);
11 | MqttClient mqttClient = new MqttClient();
12 | MqttConnectOptions options = new MqttConnectOptions();
13 |
14 | // emqx
15 | options.setHost("broker-cn.emqx.io");
16 | options.setPort(1883);
17 |
18 | // apollo
19 | // options.setHost("localhost");
20 | // options.setPort(61613);
21 | // options.setUserName("admin");
22 | // options.setPassword("password".getBytes(StandardCharsets.UTF_8));
23 |
24 | // anoah
25 | // options.setHost("localhost");
26 | // options.setPort(30380);
27 | // options.setUserName("anoah");
28 | // options.setPassword("uclass2019".getBytes(StandardCharsets.UTF_8));
29 | options.setClientIdentifier("netty_mqtt_c1");
30 | options.setKeepAliveTime(5);
31 | options.setCleanSession(true);
32 | // 配置动作超时时间
33 | mqttClient.setActionTimeout(3000);
34 | // 配置掉线重连
35 | mqttClient.setReconnectOnLost(5, 10000);
36 |
37 | mqttClient.setCallback(new MqttClient.Callback() {
38 | @Override
39 | public void onConnected() {
40 | // test
41 | try {
42 | // 订阅主题
43 | mqttClient.subscribe(1, "topic111");
44 | mqttClient.publish("topic111", "hello, netty mqtt!");
45 | // 订阅主题 topic 中可使用 /# ,表示模糊匹配该主题
46 | // 示例:订阅主题 topic1/# ,可接收 topic1、
47 | // topic1/aaa、topic1/bbb等主题下消息
48 | // mqttClient.subscribe("topic1/#");
49 | // 发布一个消息到主题topic1/aaa
50 | // mqttClient.publish("topic1/aaa", "hello, netty mqtt!-2-");
51 | // 取消订阅
52 | mqttClient.unsubscribe("topic111");
53 | // mqttClient.close();
54 | } catch (Exception e) {
55 | e.printStackTrace();
56 | }
57 | }
58 |
59 | @Override
60 | public void onConnectFailed(Throwable e) {
61 |
62 | }
63 |
64 | @Override
65 | public void onConnectLost(Throwable e) {
66 | Log.i("-->onConnectLost : " + e);
67 | }
68 |
69 | @Override
70 | public void onReconnectStart(int cur) {
71 |
72 | }
73 |
74 | @Override
75 | public void onMessageArrived(String topic, String s) {
76 |
77 | }
78 | });
79 |
80 | try {
81 | mqttClient.connect(options);
82 | } catch (Exception e) {
83 | // e.printStackTrace();
84 | // Log.i("--->连接失败了:" + e);
85 | }
86 |
87 | for (; ; ) ;
88 |
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/x2ge/netty-mqtt-client/8c0ea5bcf5b7348c23a74301b4e3830ac9269ab8/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MSYS* | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/maven-publish.gradle:
--------------------------------------------------------------------------------
1 | task sourceJar(type: Jar, dependsOn: classes) {
2 | classifier = 'sources'
3 | from sourceSets.main.allSource
4 | }
5 |
6 | tasks.withType(Javadoc) {
7 | options.encoding = "UTF-8"
8 | }
9 |
10 | task javadocJar(type: Jar, dependsOn: javadoc) {
11 | classifier = 'javadoc'
12 | from javadoc.destinationDir
13 | }
14 |
15 | publishing {
16 | publications {
17 | myLib(MavenPublication) {
18 | groupId project.group
19 | artifactId project.name
20 | version project.version
21 |
22 | from components.java
23 | artifact sourceJar
24 | artifact javadocJar
25 |
26 | pom {
27 | name = project.name
28 | description = "mqtt client for java or android"
29 | url = "https://github.com/x2ge/netty-mqtt-client"
30 | licenses {
31 | license {
32 | name = "The Apache License, Version 2.0"
33 | url = "http://www.apache.org/licenses/LICENSE-2.0.txt"
34 | }
35 | }
36 | developers {
37 | developer {
38 | id = developerId
39 | name = developerName
40 | email = developerEmail
41 | }
42 | }
43 | scm {
44 | connection = "scm:git:https://github.com/x2ge/netty-mqtt-client.git"
45 | developerConnection = "scm:git:https://github.com/x2ge/netty-mqtt-client.git"
46 | url = "https://github.com/x2ge/netty-mqtt-client"
47 | }
48 | }
49 | }
50 | }
51 | repositories {
52 | // releases 仓库
53 | maven {
54 | name 'Sonatype'
55 | url 'https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/'
56 | credentials {
57 | username = ossrhUsername // 之前在 sonatype 注册的账户名
58 | password = ossrhPassword // 对应的密码
59 | }
60 | }
61 | // snapshots 仓库
62 | maven {
63 | name = 'SonatypeSnapshot'
64 | url = 'https://s01.oss.sonatype.org/content/repositories/snapshots/'
65 | credentials {
66 | username = ossrhUsername
67 | password = ossrhPassword
68 | }
69 | }
70 | // 本地 snapshots 仓库
71 | maven {
72 | allowInsecureProtocol true
73 | name = 'LocalSnapshot'
74 | url = 'http://192.168.46.167:9191/repository/maven-snapshots/'
75 | credentials {
76 | username = "admin"
77 | password = "admin123"
78 | }
79 | }
80 | }
81 | }
82 |
83 | signing {
84 | sign publishing.publications.myLib
85 | }
86 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = 'netty-mqtt-client'
2 | include 'example'
3 |
4 |
--------------------------------------------------------------------------------
/src/main/java/io/x2ge/example/App.java:
--------------------------------------------------------------------------------
1 | package io.x2ge.example;
2 |
3 | import io.x2ge.mqtt.MqttClient;
4 | import io.x2ge.mqtt.MqttConnectOptions;
5 | import io.x2ge.mqtt.utils.Log;
6 |
7 | public class App {
8 |
9 | public static void main(String[] args) {
10 | Log.enablePing(true);
11 | MqttClient mqttClient = new MqttClient();
12 | MqttConnectOptions options = new MqttConnectOptions();
13 |
14 | // emqx
15 | options.setHost("broker-cn.emqx.io");
16 | options.setPort(1883);
17 |
18 | // apollo
19 | // options.setHost("localhost");
20 | // options.setPort(61613);
21 | // options.setUserName("admin");
22 | // options.setPassword("password".getBytes(StandardCharsets.UTF_8));
23 |
24 | // anoah
25 | // options.setHost("localhost");
26 | // options.setPort(30380);
27 | // options.setUserName("anoah");
28 | // options.setPassword("uclass2019".getBytes(StandardCharsets.UTF_8));
29 | options.setClientIdentifier("netty_mqtt_c1");
30 | options.setKeepAliveTime(5);
31 | options.setCleanSession(true);
32 | // 配置动作超时时间
33 | mqttClient.setActionTimeout(3000);
34 | // 配置掉线重连
35 | mqttClient.setReconnectOnLost(1, 10000);
36 |
37 | mqttClient.setCallback(new MqttClient.Callback() {
38 | @Override
39 | public void onConnected() {
40 | // test
41 | try {
42 | // 订阅主题
43 | mqttClient.subscribe(1, "topic111");
44 | mqttClient.publish("topic111", "hello, netty mqtt!");
45 | // 订阅主题 topic 中可使用 /# ,表示模糊匹配该主题
46 | // 示例:订阅主题 topic1/# ,可接收 topic1、
47 | // topic1/aaa、topic1/bbb等主题下消息
48 | // mqttClient.subscribe("topic1/#");
49 | // 发布一个消息到主题topic1/aaa
50 | // mqttClient.publish("topic1/aaa", "hello, netty mqtt!-2-");
51 | // 取消订阅
52 | mqttClient.unsubscribe("topic111");
53 | // mqttClient.close();
54 | } catch (Exception e) {
55 | e.printStackTrace();
56 | }
57 | }
58 |
59 | @Override
60 | public void onConnectFailed(Throwable e) {
61 |
62 | }
63 |
64 | @Override
65 | public void onConnectLost(Throwable e) {
66 | Log.i("-->onConnectLost : " + e);
67 | }
68 |
69 | @Override
70 | public void onReconnectStart(int cur) {
71 |
72 | }
73 |
74 | @Override
75 | public void onMessageArrived(String topic, String s) {
76 |
77 | }
78 | });
79 |
80 | try {
81 | mqttClient.connect(options);
82 | } catch (Exception e) {
83 | // e.printStackTrace();
84 | // Log.i("--->连接失败了:" + e);
85 | }
86 |
87 | for (; ; ) ;
88 |
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/main/java/io/x2ge/mqtt/MqttClient.java:
--------------------------------------------------------------------------------
1 | package io.x2ge.mqtt;
2 |
3 | import io.netty.bootstrap.Bootstrap;
4 | import io.netty.buffer.ByteBuf;
5 | import io.netty.channel.*;
6 | import io.netty.channel.nio.NioEventLoopGroup;
7 | import io.netty.channel.socket.SocketChannel;
8 | import io.netty.channel.socket.nio.NioSocketChannel;
9 | import io.netty.handler.codec.mqtt.*;
10 | import io.x2ge.mqtt.core.*;
11 | import io.x2ge.mqtt.utils.AsyncTask;
12 | import io.x2ge.mqtt.utils.Log;
13 |
14 | import java.net.ConnectException;
15 | import java.nio.charset.StandardCharsets;
16 | import java.util.ArrayList;
17 | import java.util.Arrays;
18 | import java.util.List;
19 | import java.util.concurrent.CancellationException;
20 | import java.util.concurrent.TimeUnit;
21 |
22 | public class MqttClient {
23 |
24 | private MqttConnectOptions connectOptions;
25 | private long actionTimeout = 5000;
26 | private long connectTimeout = 5000;
27 |
28 | private int maxReconnectTimesOnLost = 0;
29 | private long reconnectTimeoutOnLost = 0;
30 | private final static long MIN_RECONNECT_INTERVAL = 1800L;
31 |
32 | private AsyncTask connectTask;
33 | private AsyncTask reconnectTask;
34 | private Channel channel;
35 |
36 | private ConnectProcessor connectProcessor;
37 | private PingProcessor pingProcessor;
38 | private List subscribeProcessorList = new ArrayList<>();
39 | private List unsubscribeProcessorList = new ArrayList<>();
40 | private List publishProcessorList = new ArrayList<>();
41 |
42 | private boolean isConnected = false;
43 | private boolean isClosed = false;
44 |
45 | private Callback callback;
46 |
47 | public void setCallback(Callback c) {
48 | this.callback = c;
49 | }
50 |
51 | /**
52 | * 设置连接、订阅、取消订阅、发布消息、ping等动作的超时时间
53 | *
54 | * @param actionTimeout 等待动作完成的超时时间
55 | */
56 | public void setActionTimeout(long actionTimeout) {
57 | this.actionTimeout = actionTimeout;
58 | }
59 |
60 | /**
61 | * 当maxTimes大于0时,如果发生掉线,则自动尝试重连,重连成功则回调onConnected方法,
62 | * 重连次数用完则回调onConnectLost方法。
63 | * 当timeout大于0时,如果整个重连过程消耗时间超过timeout,此时无论重连次数是否用完都
64 | * 停止重试,并回调onConnectLost方法。
65 | *
66 | * @param maxTimes 重试最大次数
67 | * @param timeout 重试超时时间
68 | */
69 | public void setReconnectOnLost(int maxTimes, long timeout) {
70 | this.maxReconnectTimesOnLost = maxTimes;
71 | this.reconnectTimeoutOnLost = timeout;
72 | }
73 |
74 | synchronized public void connect(MqttConnectOptions options) throws Exception {
75 | connect(options, actionTimeout);
76 | }
77 |
78 | synchronized public void connect(MqttConnectOptions options, long timeout) throws Exception {
79 | if (this.connectOptions != null) {
80 | return;
81 | }
82 | this.connectOptions = options;
83 | this.connectTimeout = timeout;
84 |
85 | try {
86 | doConnect(options, timeout);
87 | onConnected();
88 | } catch (Exception e) {
89 | // e.printStackTrace();
90 | onConnectFailed(e);
91 | throw e;
92 | }
93 | }
94 |
95 | private void doConnect(MqttConnectOptions options, long timeout) throws Exception {
96 | // 创建长连接
97 | EventLoopGroup group = new NioEventLoopGroup();
98 | connectTask = new AsyncTask() {
99 | @Override
100 | public String call() throws Exception {
101 | Bootstrap b = new Bootstrap()
102 | .group(group)
103 | .channel(NioSocketChannel.class)
104 | .handler(new ChannelInitializer() {
105 | @Override
106 | protected void initChannel(SocketChannel channel) throws Exception {
107 | channel.pipeline()
108 | .addLast("decoder", new MqttDecoder())//解码
109 | .addLast("encoder", MqttEncoder.INSTANCE)//编码
110 | .addLast("handler", new MqttHandler());
111 | }
112 | });
113 | ChannelFuture ch = b.connect(options.getHost(), options.getPort()).sync();
114 | channel = ch.channel();
115 | Log.i("--已连接->" + channel.localAddress().toString());
116 | return null;
117 | }
118 | }.execute();
119 | try {
120 | connectTask.get(timeout, TimeUnit.MILLISECONDS);
121 | } catch (Exception e) {
122 | // e.printStackTrace();
123 | Log.i("-->连接异常:" + e);
124 | group.shutdownGracefully();
125 | throw e;
126 | }
127 |
128 | if (channel == null)
129 | return;
130 |
131 | // 发送mqtt协议连接报文
132 | doConnect0(channel, options, timeout);
133 |
134 | // 等待连接关闭的任务
135 | connectTask = new AsyncTask() {
136 | @Override
137 | public String call() throws Exception {
138 | try {
139 | channel.closeFuture().sync();
140 | } catch (Exception e) {
141 | // e.printStackTrace();
142 | Log.i("-->连接断开异常:" + e);
143 | } finally {
144 | group.shutdownGracefully();
145 | if (!isClosed()) {
146 | // 非主动断开,可能源于服务器原因
147 | Exception e = new ConnectException("Connection closed unexpectedly");
148 | Log.i("-->连接断开:" + e);
149 | onConnectLost(e);
150 | } else {
151 | Log.i("-->连接断开:主动");
152 | }
153 | }
154 | return null;
155 | }
156 | }.execute();
157 | }
158 |
159 | private void doConnect0(Channel channel, MqttConnectOptions options, long timeout) throws Exception {
160 | if (channel == null)
161 | return;
162 |
163 | try {
164 | connectProcessor = new ConnectProcessor();
165 | String s = connectProcessor.connect(channel, options, timeout);
166 | if (ProcessorResult.RESULT_SUCCESS.equals(s)) {
167 | Log.i("-->连接成功");
168 | } else {
169 | throw new CancellationException();
170 | }
171 | } catch (Exception e) {
172 | // e.printStackTrace();
173 | if (e instanceof CancellationException) {
174 | Log.i("-->连接取消");
175 | } else {
176 | Log.i("-->连接异常:" + e);
177 | throw e;
178 | }
179 | }
180 | }
181 |
182 | private void doReconnect(MqttConnectOptions options, final int maxTimes, final long timeout, Throwable t) {
183 | reconnectTask = new AsyncTask() {
184 | @Override
185 | public String call() throws Exception {
186 | long interval = MIN_RECONNECT_INTERVAL;
187 | if (timeout > 0) {
188 | interval = timeout / maxTimes;
189 | if (interval < MIN_RECONNECT_INTERVAL)
190 | interval = MIN_RECONNECT_INTERVAL;
191 | }
192 |
193 | boolean bSuccess = false;
194 | int num = 0;
195 | long start = System.nanoTime();
196 | do {
197 | ++num;
198 | Log.i("-->重连开始:" + num);
199 | onReconnectStart(num);
200 |
201 | long begin = System.nanoTime();
202 | try {
203 | doConnect(options, interval);
204 | Log.i("<--重连成功:" + num);
205 | bSuccess = true;
206 | break;
207 | } catch (Exception e) {
208 | // e.printStackTrace();
209 | Log.i("<--重连失败:" + num);
210 | }
211 |
212 | if (maxTimes <= num) { // 重试次数已经消耗殆尽
213 | break;
214 | }
215 |
216 | // 判断是否timeout
217 | if (timeout > 0) { // 只在配置了重连超时时间情况下才进行相关判断
218 | // 重连总消耗时间
219 | long spendTotal = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
220 | if (timeout <= spendTotal) {// 超时时间已经消耗殆尽
221 | break;
222 | }
223 | }
224 |
225 | // 单次连接消耗时间
226 | long spend = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - begin);
227 | long sleepTime = interval - spend;
228 | if (sleepTime > 0) {
229 | try {
230 | Thread.sleep(sleepTime);
231 | } catch (InterruptedException e) {
232 | // e.printStackTrace();
233 | break;
234 | }
235 | }
236 | } while (!isCancelled());
237 |
238 | if (!isCancelled()) {
239 | if (bSuccess) {
240 | onConnected();
241 | } else {
242 | onReconnectFailed(t);
243 | }
244 | }
245 | return null;
246 | }
247 | }.execute();
248 | }
249 |
250 |
251 | private void startPingTask() {
252 | if (channel == null)
253 | return;
254 |
255 | if (pingProcessor == null
256 | || pingProcessor.isCancelled()
257 | || pingProcessor.isDone())
258 | pingProcessor = new PingProcessor();
259 | pingProcessor.start(channel, connectOptions.getKeepAliveTime(), new PingCallback());
260 | }
261 |
262 | public void subscribe(String... topics) throws Exception {
263 | subscribe(0, topics);
264 | }
265 |
266 | /**
267 | * 订阅主题
268 | *
269 | * @param qos 0-至多发1次
270 | * 1-至少送达1次
271 | * 2-完全送达并回应
272 | * @param topics 主题集
273 | * @throws Exception 失败异常
274 | */
275 | public void subscribe(int qos, String... topics) throws Exception {
276 | if (channel == null)
277 | return;
278 |
279 | SubscribeProcessor sp = new SubscribeProcessor();
280 | subscribeProcessorList.add(sp);
281 | try {
282 | String result = sp.subscribe(channel, qos, topics, actionTimeout);
283 | if (ProcessorResult.RESULT_SUCCESS.equals(result)) {
284 | Log.i("-->订阅成功:" + Arrays.toString(topics));
285 | } else {
286 | throw new CancellationException();
287 | }
288 | } catch (Exception e) {
289 | // e.printStackTrace();
290 | if (e instanceof CancellationException) {
291 | Log.i("-->订阅取消:" + Arrays.toString(topics));
292 | } else {
293 | Log.i("-->订阅异常:" + Arrays.toString(topics) + " " + e);
294 | throw e;
295 | }
296 | } finally {
297 | subscribeProcessorList.remove(sp);
298 | }
299 | }
300 |
301 | public void unsubscribe(String... topics) throws Exception {
302 | if (channel == null)
303 | return;
304 |
305 | UnsubscribeProcessor usp = new UnsubscribeProcessor();
306 | unsubscribeProcessorList.add(usp);
307 | try {
308 | String result = usp.unsubscribe(channel, topics, actionTimeout);
309 | if (ProcessorResult.RESULT_SUCCESS.equals(result)) {
310 | Log.i("-->取消订阅成功:" + Arrays.toString(topics));
311 | } else {
312 | throw new CancellationException();
313 | }
314 | } catch (Exception e) {
315 | // e.printStackTrace();
316 | if (e instanceof CancellationException) {
317 | Log.i("-->取消订阅取消:" + Arrays.toString(topics));
318 | } else {
319 | Log.i("-->取消订阅异常:" + Arrays.toString(topics) + " " + e);
320 | throw e;
321 | }
322 | } finally {
323 | unsubscribeProcessorList.remove(usp);
324 | }
325 | }
326 |
327 | public void publish(String topic, String content) throws Exception {
328 | if (channel == null)
329 | return;
330 |
331 | PublishProcessor pp = new PublishProcessor();
332 | publishProcessorList.add(pp);
333 | try {
334 | String result = pp.publish(channel, topic, content, actionTimeout);
335 | if (ProcessorResult.RESULT_SUCCESS.equals(result)) {
336 | Log.i("-->发布成功:" + content);
337 | } else {
338 | throw new CancellationException();
339 | }
340 | } catch (Exception e) {
341 | // e.printStackTrace();
342 | if (e instanceof CancellationException) {
343 | Log.i("-->发布取消:" + content);
344 | } else {
345 | Log.i("-->发布异常:" + content + " " + e);
346 | throw e;
347 | }
348 | } finally {
349 | publishProcessorList.remove(pp);
350 | }
351 | }
352 |
353 | public void disConnect() throws Exception {
354 | if (channel == null)
355 | return;
356 |
357 | MqttMessage msg = ProtocolUtils.disConnectMessage();
358 | Log.i("-->发起断开连接:" + msg);
359 | channel.writeAndFlush(msg);
360 | }
361 |
362 | public void close() {
363 | setConnected(false);
364 | setClosed(true);
365 |
366 | if (reconnectTask != null)
367 | reconnectTask.cancel(true);
368 |
369 | if (connectProcessor != null) {
370 | connectProcessor.cancel(true);
371 | }
372 |
373 | if (pingProcessor != null) {
374 | pingProcessor.cancel(true);
375 | }
376 |
377 | if (subscribeProcessorList.size() > 0) {
378 | for (SubscribeProcessor sp : subscribeProcessorList) {
379 | sp.cancel(true);
380 | }
381 | }
382 |
383 | if (unsubscribeProcessorList.size() > 0) {
384 | for (UnsubscribeProcessor usp : unsubscribeProcessorList) {
385 | usp.cancel(true);
386 | }
387 | }
388 |
389 | if (publishProcessorList.size() > 0) {
390 | for (PublishProcessor pp : publishProcessorList) {
391 | pp.cancel(true);
392 | }
393 | }
394 |
395 | if (channel != null) {
396 | try {
397 | disConnect();
398 | } catch (Exception e) {
399 | e.printStackTrace();
400 | }
401 | try {
402 | channel.close();
403 | } catch (Exception e) {
404 | e.printStackTrace();
405 | }
406 | channel = null;
407 | }
408 | }
409 |
410 | public boolean isConnected() {
411 | return isConnected;
412 | }
413 |
414 | private void setConnected(boolean b) {
415 | isConnected = b;
416 | }
417 |
418 | public boolean isClosed() {
419 | return isClosed;
420 | }
421 |
422 | private void setClosed(boolean b) {
423 | isClosed = b;
424 | }
425 |
426 | private void onConnected() {
427 | setConnected(true);
428 | setClosed(false);
429 | startPingTask();
430 | if (callback != null)
431 | callback.onConnected();
432 | }
433 |
434 | private void onConnectFailed(Throwable t) {
435 | close();
436 | if (callback != null)
437 | callback.onConnectFailed(t);
438 | }
439 |
440 | private void onConnectLost(Throwable t) {
441 | close();
442 |
443 | if (maxReconnectTimesOnLost > 0) {
444 | doReconnect(connectOptions, maxReconnectTimesOnLost, reconnectTimeoutOnLost, t);
445 | } else {
446 | if (callback != null) {
447 | callback.onConnectLost(t);
448 | }
449 | }
450 | }
451 |
452 | private void onReconnectStart(int num) {
453 | if (callback != null)
454 | callback.onReconnectStart(num);
455 | }
456 |
457 | private void onReconnectFailed(Throwable t) {
458 | close();
459 | if (callback != null)
460 | callback.onConnectLost(t);
461 | }
462 |
463 | private void onMessageArrived(String topic, String s) {
464 | Log.i("-->收到消息:" + topic + " | " + s);
465 | if (callback != null) {
466 | callback.onMessageArrived(topic, s);
467 | }
468 | }
469 |
470 | class MqttHandler extends SimpleChannelInboundHandler