├── .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 | 6 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | 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 | 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 { 471 | 472 | @Override 473 | public void channelActive(ChannelHandlerContext ctx) throws Exception { 474 | super.channelActive(ctx); 475 | Log.i(""); 476 | } 477 | 478 | @Override 479 | public void channelInactive(ChannelHandlerContext ctx) throws Exception { 480 | super.channelInactive(ctx); 481 | Log.i(""); 482 | } 483 | 484 | @Override 485 | public void channelRegistered(ChannelHandlerContext ctx) throws Exception { 486 | super.channelRegistered(ctx); 487 | Log.i(""); 488 | } 489 | 490 | @Override 491 | public void channelUnregistered(ChannelHandlerContext ctx) throws Exception { 492 | super.channelUnregistered(ctx); 493 | Log.i(""); 494 | } 495 | 496 | 497 | @Override 498 | protected void channelRead0(ChannelHandlerContext ctx, Object msgx) throws Exception { 499 | if (msgx == null) { 500 | return; 501 | } 502 | 503 | MqttMessage msg = (MqttMessage) msgx; 504 | MqttFixedHeader mqttFixedHeader = msg.fixedHeader(); 505 | if (mqttFixedHeader.messageType() == MqttMessageType.PINGRESP) { 506 | Log.i("[ping]-->channelRead0 : " + msgx); 507 | } else { 508 | Log.i("-->channelRead0 : " + msgx); 509 | } 510 | switch (mqttFixedHeader.messageType()) { 511 | case CONNACK: 512 | if (connectProcessor != null) 513 | connectProcessor.processAck(ctx.channel(), (MqttConnAckMessage) msg); 514 | break; 515 | case SUBACK: 516 | if (subscribeProcessorList.size() > 0) { 517 | for (SubscribeProcessor subscribeProcessor : subscribeProcessorList) { 518 | subscribeProcessor.processAck(ctx.channel(), (MqttSubAckMessage) msg); 519 | } 520 | } 521 | break; 522 | case UNSUBACK: 523 | if (unsubscribeProcessorList.size() > 0) { 524 | for (UnsubscribeProcessor unsubscribeProcessor : unsubscribeProcessorList) { 525 | unsubscribeProcessor.processAck(ctx.channel(), (MqttUnsubAckMessage) msg); 526 | } 527 | } 528 | break; 529 | case PUBLISH: 530 | MqttPublishMessage publishMessage = (MqttPublishMessage) msg; 531 | MqttPublishVariableHeader mqttPublishVariableHeader = publishMessage.variableHeader(); 532 | String topicName = mqttPublishVariableHeader.topicName(); 533 | ByteBuf payload = publishMessage.payload(); 534 | String content = payload.toString(StandardCharsets.UTF_8); 535 | onMessageArrived(topicName, content); 536 | 537 | if (mqttFixedHeader.qosLevel() == MqttQoS.AT_LEAST_ONCE 538 | || mqttFixedHeader.qosLevel() == MqttQoS.EXACTLY_ONCE) { 539 | // qos为1、2级别的消息需要发送回执 540 | // 注:需要完成数据的完全读取后才能发送回执 541 | MqttPubAckMessage mqttPubAckMessage = ProtocolUtils.pubAckMessage(mqttPublishVariableHeader.messageId()); 542 | Log.i("-->发送消息回执:" + mqttPubAckMessage); 543 | ctx.channel().writeAndFlush(mqttPubAckMessage); 544 | } 545 | break; 546 | case PUBACK: 547 | // qos = 1的发布才有该响应 548 | if (publishProcessorList.size() > 0) { 549 | for (PublishProcessor publishProcessor : publishProcessorList) { 550 | publishProcessor.processAck(ctx.channel(), (MqttPubAckMessage) msg); 551 | } 552 | } 553 | break; 554 | case PUBREC: 555 | // qos = 2的发布才参与 556 | break; 557 | case PUBREL: 558 | // qos = 2的发布才参与 559 | break; 560 | case PUBCOMP: 561 | // qos = 2的发布才参与 562 | break; 563 | case PINGRESP: 564 | // 心跳请求响应 565 | if (pingProcessor != null) { 566 | pingProcessor.processAck(ctx.channel(), msg); 567 | } 568 | break; 569 | default: 570 | break; 571 | } 572 | } 573 | 574 | @Override 575 | public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { 576 | super.channelReadComplete(ctx); 577 | // Log.i(""); 578 | } 579 | 580 | @Override 581 | public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { 582 | super.userEventTriggered(ctx, evt); 583 | Log.i(""); 584 | } 585 | 586 | @Override 587 | public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception { 588 | super.channelWritabilityChanged(ctx); 589 | Log.i(""); 590 | } 591 | 592 | @Override 593 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { 594 | super.exceptionCaught(ctx, cause); 595 | Log.i("-->exceptionCaught : " + cause); 596 | } 597 | } 598 | 599 | class PingCallback implements PingProcessor.Callback { 600 | 601 | @Override 602 | public void onConnectLost(Throwable t) { 603 | Log.i("-->发生异常:" + t); 604 | MqttClient.this.onConnectLost(t); 605 | } 606 | } 607 | 608 | public interface Callback { 609 | 610 | void onConnected(); 611 | 612 | void onConnectFailed(Throwable e); 613 | 614 | void onConnectLost(Throwable e); 615 | 616 | /** 617 | * @param cur 第几次重连 618 | */ 619 | void onReconnectStart(int cur); 620 | 621 | void onMessageArrived(String topic, String s); 622 | } 623 | 624 | } 625 | 626 | 627 | 628 | -------------------------------------------------------------------------------- /src/main/java/io/x2ge/mqtt/MqttConnectOptions.java: -------------------------------------------------------------------------------- 1 | package io.x2ge.mqtt; 2 | 3 | import io.netty.handler.codec.mqtt.MqttVersion; 4 | import io.x2ge.mqtt.utils.StringUtils; 5 | 6 | public class MqttConnectOptions { 7 | private String host; 8 | private int port; 9 | 10 | // 可变报头部分 11 | private MqttVersion mqttVersion = MqttVersion.MQTT_3_1_1; 12 | private boolean isWillRetain = false; 13 | private int willQos = 0; 14 | private boolean isWillFlag = false; 15 | private boolean isCleanSession = false; 16 | private int keepAliveTime = 60; 17 | 18 | // 有效载荷 :客户端标识符,遗嘱主题,遗嘱消息,用户名,密码 19 | private String clientIdentifier = ""; 20 | private String willTopic = ""; 21 | private byte[] willMessage; 22 | private String userName = ""; 23 | private byte[] password; 24 | 25 | 26 | public String getHost() { 27 | return host; 28 | } 29 | 30 | public void setHost(String host) { 31 | this.host = host; 32 | } 33 | 34 | public int getPort() { 35 | return port; 36 | } 37 | 38 | public void setPort(int port) { 39 | this.port = port; 40 | } 41 | 42 | public MqttVersion getMqttVersion() { 43 | return mqttVersion; 44 | } 45 | 46 | public boolean isHasUserName() { 47 | return !StringUtils.isEmpty(userName); 48 | } 49 | 50 | public boolean isHasPassword() { 51 | return password != null && password.length > 0; 52 | } 53 | 54 | public boolean isWillRetain() { 55 | return isWillRetain; 56 | } 57 | 58 | public void setWillRetain(boolean willRetain) { 59 | this.isWillRetain = willRetain; 60 | } 61 | 62 | public int getWillQos() { 63 | return willQos; 64 | } 65 | 66 | public void setWillQos(int willQos) { 67 | this.willQos = willQos; 68 | } 69 | 70 | public boolean isWillFlag() { 71 | return isWillFlag; 72 | } 73 | 74 | public void setWillFlag(boolean willFlag) { 75 | this.isWillFlag = willFlag; 76 | } 77 | 78 | public boolean isCleanSession() { 79 | return isCleanSession; 80 | } 81 | 82 | /** 83 | * 如果清理会话(CleanSession)标志被设置为true, 84 | * 客户端和服务端在重连后,会丢弃之前的任何会话相关内容及配置 85 | * 86 | * @param cleanSession true 重连后丢弃相关数据 87 | */ 88 | public void setCleanSession(boolean cleanSession) { 89 | this.isCleanSession = cleanSession; 90 | } 91 | 92 | public int getKeepAliveTime() { 93 | return keepAliveTime; 94 | } 95 | 96 | /** 97 | * @param keepAliveTime 维持连接时间,秒 98 | */ 99 | public void setKeepAliveTime(int keepAliveTime) { 100 | this.keepAliveTime = keepAliveTime; 101 | } 102 | 103 | public String getClientIdentifier() { 104 | return clientIdentifier; 105 | } 106 | 107 | public void setClientIdentifier(String clientIdentifier) { 108 | this.clientIdentifier = clientIdentifier; 109 | } 110 | 111 | public String getWillTopic() { 112 | return willTopic; 113 | } 114 | 115 | public void setWillTopic(String willTopic) { 116 | this.willTopic = willTopic; 117 | } 118 | 119 | public byte[] getWillMessage() { 120 | return willMessage; 121 | } 122 | 123 | public void setWillMessage(byte[] willMessage) { 124 | this.willMessage = willMessage; 125 | } 126 | 127 | public String getUserName() { 128 | return userName; 129 | } 130 | 131 | public void setUserName(String userName) { 132 | this.userName = userName; 133 | } 134 | 135 | public byte[] getPassword() { 136 | return password; 137 | } 138 | 139 | public void setPassword(byte[] password) { 140 | this.password = password; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/main/java/io/x2ge/mqtt/core/ConnectProcessor.java: -------------------------------------------------------------------------------- 1 | package io.x2ge.mqtt.core; 2 | 3 | import io.netty.channel.Channel; 4 | import io.netty.handler.codec.mqtt.MqttConnAckMessage; 5 | import io.netty.handler.codec.mqtt.MqttConnAckVariableHeader; 6 | import io.netty.handler.codec.mqtt.MqttConnectMessage; 7 | import io.x2ge.mqtt.MqttConnectOptions; 8 | import io.x2ge.mqtt.utils.AsyncTask; 9 | import io.x2ge.mqtt.utils.Log; 10 | 11 | import java.io.IOException; 12 | import java.util.concurrent.TimeUnit; 13 | import java.util.concurrent.atomic.AtomicBoolean; 14 | 15 | public class ConnectProcessor extends AsyncTask { 16 | 17 | private long timeout; 18 | private final AtomicBoolean receivedAck = new AtomicBoolean(false); 19 | private Exception e; 20 | 21 | @Override 22 | public String call() throws Exception { 23 | if (!isCancelled() && !receivedAck.get() && e == null) { 24 | synchronized (receivedAck) { 25 | receivedAck.wait(timeout); 26 | } 27 | } 28 | 29 | if (e != null) { 30 | throw e; 31 | } 32 | 33 | return receivedAck.get() ? ProcessorResult.RESULT_SUCCESS : ProcessorResult.RESULT_FAIL; 34 | } 35 | 36 | public String connect(Channel channel, MqttConnectOptions options, long timeout) throws Exception { 37 | this.timeout = timeout; 38 | 39 | MqttConnectMessage msg = ProtocolUtils.connectMessage(options); 40 | Log.i("-->发起连接:" + msg); 41 | channel.writeAndFlush(msg); 42 | return execute().get(timeout, TimeUnit.MILLISECONDS); 43 | } 44 | 45 | public void processAck(Channel channel, MqttConnAckMessage msg) { 46 | MqttConnAckVariableHeader mqttConnAckVariableHeader = msg.variableHeader(); 47 | String errormsg = ""; 48 | switch (mqttConnAckVariableHeader.connectReturnCode()) { 49 | case CONNECTION_ACCEPTED: 50 | synchronized (receivedAck) { 51 | receivedAck.set(true); 52 | receivedAck.notify(); 53 | } 54 | return; 55 | case CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD: 56 | errormsg = "用户名密码错误"; 57 | break; 58 | case CONNECTION_REFUSED_IDENTIFIER_REJECTED: 59 | errormsg = "clientId不允许链接"; 60 | break; 61 | case CONNECTION_REFUSED_SERVER_UNAVAILABLE: 62 | errormsg = "服务不可用"; 63 | break; 64 | case CONNECTION_REFUSED_UNACCEPTABLE_PROTOCOL_VERSION: 65 | errormsg = "mqtt 版本不可用"; 66 | break; 67 | case CONNECTION_REFUSED_NOT_AUTHORIZED: 68 | errormsg = "未授权登录"; 69 | break; 70 | default: 71 | errormsg = "未知问题"; 72 | break; 73 | } 74 | 75 | synchronized (receivedAck) { 76 | e = new IOException(errormsg); 77 | receivedAck.notify(); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/io/x2ge/mqtt/core/MessageData.java: -------------------------------------------------------------------------------- 1 | package io.x2ge.mqtt.core; 2 | 3 | import java.io.Serializable; 4 | 5 | public class MessageData implements Serializable { 6 | private static final long serialVersionUID = 1L; 7 | private String topic; 8 | private byte[] payload; 9 | 10 | private int qos; 11 | private boolean retained; 12 | private boolean dup; 13 | private int messageId; 14 | 15 | private long timestamp = System.currentTimeMillis(); 16 | 17 | private volatile MessageStatus status; 18 | 19 | public String getStringId() { 20 | return String.valueOf(messageId); 21 | } 22 | 23 | public String getTopic() { 24 | return topic; 25 | } 26 | 27 | public void setTopic(String topic) { 28 | this.topic = topic; 29 | } 30 | 31 | public byte[] getPayload() { 32 | return payload; 33 | } 34 | 35 | public void setPayload(byte[] payload) { 36 | this.payload = payload; 37 | } 38 | 39 | public int getQos() { 40 | return qos; 41 | } 42 | 43 | public void setQos(int qos) { 44 | this.qos = qos; 45 | } 46 | 47 | public boolean isRetained() { 48 | return retained; 49 | } 50 | 51 | public void setRetained(boolean retained) { 52 | this.retained = retained; 53 | } 54 | 55 | public boolean isDup() { 56 | return dup; 57 | } 58 | 59 | public void setDup(boolean dup) { 60 | this.dup = dup; 61 | } 62 | 63 | public int getMessageId() { 64 | return messageId; 65 | } 66 | 67 | public void setMessageId(int messageId) { 68 | this.messageId = messageId; 69 | } 70 | 71 | public long getTimestamp() { 72 | return timestamp; 73 | } 74 | 75 | public MessageStatus getStatus() { 76 | return status; 77 | } 78 | 79 | public void setStatus(MessageStatus status) { 80 | this.status = status; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/io/x2ge/mqtt/core/MessageIdFactory.java: -------------------------------------------------------------------------------- 1 | package io.x2ge.mqtt.core; 2 | 3 | import java.util.Hashtable; 4 | 5 | public class MessageIdFactory { 6 | 7 | private static final Hashtable using = new Hashtable<>(); 8 | private static int lastId = 0; 9 | 10 | public static int get() throws Exception { 11 | synchronized (using) { 12 | int id = lastId; 13 | for (int i = 1; i <= 65535; i++) { 14 | // id范围1~65535 15 | ++id; 16 | if (id < 1 || id > 65535) 17 | id = 1; 18 | 19 | if (!using.contains(id)) { 20 | using.put(id, id); 21 | lastId = id; 22 | return id; 23 | } 24 | } 25 | throw new Exception("The message id has been used up!"); 26 | } 27 | } 28 | 29 | public static void release(int id) { 30 | if (id > 0) 31 | synchronized (using) { 32 | using.remove(id); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/io/x2ge/mqtt/core/MessageStatus.java: -------------------------------------------------------------------------------- 1 | package io.x2ge.mqtt.core; 2 | 3 | public enum MessageStatus { 4 | /** 5 | * none 6 | */ 7 | None, 8 | /** 9 | * Qos1 10 | */ 11 | PUB, 12 | /** 13 | * Qos2 14 | */ 15 | PUBREC, 16 | /** 17 | * Qos2 18 | */ 19 | PUBREL, 20 | /** 21 | * finish 22 | */ 23 | COMPLETE, 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/io/x2ge/mqtt/core/PingProcessor.java: -------------------------------------------------------------------------------- 1 | package io.x2ge.mqtt.core; 2 | 3 | import io.netty.channel.Channel; 4 | import io.netty.handler.codec.mqtt.MqttMessage; 5 | import io.x2ge.mqtt.utils.AsyncTask; 6 | import io.x2ge.mqtt.utils.Log; 7 | 8 | import java.util.concurrent.TimeUnit; 9 | import java.util.concurrent.TimeoutException; 10 | import java.util.concurrent.atomic.AtomicBoolean; 11 | 12 | public class PingProcessor extends AsyncTask { 13 | 14 | public Channel channel; 15 | public int keepAlive = 60; 16 | public Callback cb; 17 | 18 | private final AtomicBoolean receivedAck = new AtomicBoolean(false); 19 | 20 | @Override 21 | public String call() throws Exception { 22 | while (!isCancelled()) { 23 | 24 | receivedAck.set(false); 25 | 26 | ping(channel); 27 | 28 | if (!isCancelled() && !receivedAck.get()) { 29 | synchronized (receivedAck) { 30 | receivedAck.wait(TimeUnit.SECONDS.toMillis(keepAlive) / 2); 31 | } 32 | } 33 | 34 | if (!isCancelled()) { 35 | if (!receivedAck.get()) { 36 | TimeoutException te = new TimeoutException("Did not receive a response for a long time : " + keepAlive + "s"); 37 | if (cb != null) { 38 | cb.onConnectLost(te); 39 | } 40 | throw te; 41 | } 42 | 43 | Thread.sleep(TimeUnit.SECONDS.toMillis(keepAlive)); 44 | } 45 | } 46 | return null; 47 | } 48 | 49 | public void start(Channel channel, int keepAlive, Callback callback) { 50 | this.channel = channel; 51 | this.keepAlive = keepAlive; 52 | this.cb = callback; 53 | execute(); 54 | } 55 | 56 | public void ping(Channel channel) throws Exception { 57 | MqttMessage msg = ProtocolUtils.pingReqMessage(); 58 | Log.i("[ping]-->发起ping:" + msg); 59 | channel.writeAndFlush(msg); 60 | } 61 | 62 | public void processAck(Channel channel, MqttMessage msg) { 63 | synchronized (receivedAck) { 64 | receivedAck.set(true); 65 | receivedAck.notify(); 66 | } 67 | } 68 | 69 | public interface Callback { 70 | void onConnectLost(Throwable t); 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/io/x2ge/mqtt/core/ProcessorResult.java: -------------------------------------------------------------------------------- 1 | package io.x2ge.mqtt.core; 2 | 3 | public class ProcessorResult { 4 | 5 | public static final String RESULT_SUCCESS = "success"; 6 | public static final String RESULT_FAIL = "fail"; 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/io/x2ge/mqtt/core/ProtocolUtils.java: -------------------------------------------------------------------------------- 1 | package io.x2ge.mqtt.core; 2 | 3 | import io.netty.buffer.Unpooled; 4 | import io.netty.handler.codec.mqtt.*; 5 | import io.x2ge.mqtt.MqttConnectOptions; 6 | 7 | import java.util.ArrayList; 8 | import java.util.LinkedList; 9 | import java.util.List; 10 | 11 | public class ProtocolUtils { 12 | public static MqttConnectMessage connectMessage(MqttConnectOptions options) { 13 | MqttFixedHeader fixedHeader = new MqttFixedHeader( 14 | MqttMessageType.CONNECT, 15 | false, 16 | MqttQoS.AT_MOST_ONCE, 17 | false, 18 | 10); 19 | MqttConnectVariableHeader variableHeader = new MqttConnectVariableHeader( 20 | options.getMqttVersion().protocolName(), 21 | options.getMqttVersion().protocolLevel(), 22 | options.isHasUserName(), 23 | options.isHasPassword(), 24 | options.isWillRetain(), 25 | options.getWillQos(), 26 | options.isWillFlag(), 27 | options.isCleanSession(), 28 | options.getKeepAliveTime()); 29 | MqttConnectPayload payload = new MqttConnectPayload( 30 | options.getClientIdentifier(), 31 | options.getWillTopic(), 32 | options.getWillMessage(), 33 | options.getUserName(), 34 | options.getPassword()); 35 | return new MqttConnectMessage(fixedHeader, variableHeader, payload); 36 | } 37 | 38 | public static MqttConnAckMessage connAckMessage(MqttConnectReturnCode returnCode, boolean sessionPresent) { 39 | MqttFixedHeader fixedHeader = new MqttFixedHeader( 40 | MqttMessageType.CONNACK, 41 | false, 42 | MqttQoS.AT_MOST_ONCE, 43 | false, 44 | 0); 45 | MqttConnAckVariableHeader variableHeader = new MqttConnAckVariableHeader( 46 | returnCode, 47 | sessionPresent); 48 | return (MqttConnAckMessage) MqttMessageFactory.newMessage( 49 | fixedHeader, 50 | variableHeader, 51 | null); 52 | } 53 | 54 | public static MqttConnectReturnCode connectReturnCodeForException(Throwable cause) { 55 | MqttConnectReturnCode code; 56 | if (cause instanceof MqttUnacceptableProtocolVersionException) { 57 | // 不支持的协议版本 58 | code = MqttConnectReturnCode.CONNECTION_REFUSED_UNACCEPTABLE_PROTOCOL_VERSION; 59 | } else if (cause instanceof MqttIdentifierRejectedException) { 60 | // 不合格的clientId 61 | code = MqttConnectReturnCode.CONNECTION_REFUSED_IDENTIFIER_REJECTED; 62 | } else { 63 | code = MqttConnectReturnCode.CONNECTION_REFUSED_SERVER_UNAVAILABLE; 64 | } 65 | return code; 66 | } 67 | 68 | public static MqttMessage disConnectMessage() { 69 | MqttFixedHeader mqttFixedHeader = new MqttFixedHeader(MqttMessageType.DISCONNECT, false, MqttQoS.AT_MOST_ONCE, 70 | false, 0x02); 71 | return new MqttMessage(mqttFixedHeader); 72 | } 73 | 74 | public static List getTopics(SubscriptionTopic[] subscriptionTopics) { 75 | if (subscriptionTopics != null) { 76 | List topics = new LinkedList<>(); 77 | for (SubscriptionTopic sb : subscriptionTopics) { 78 | topics.add(sb.getTopic()); 79 | } 80 | return topics; 81 | } else { 82 | return null; 83 | } 84 | } 85 | 86 | public static List getTopicSubscriptions(SubscriptionTopic[] subscriptionTopics) { 87 | if (subscriptionTopics != null && subscriptionTopics.length > 0) { 88 | List list = new LinkedList<>(); 89 | for (SubscriptionTopic sm : subscriptionTopics) { 90 | list.add(new MqttTopicSubscription(sm.getTopic(), MqttQoS.valueOf(sm.getQos()))); 91 | } 92 | return list; 93 | } 94 | return null; 95 | } 96 | 97 | public static MqttSubscribeMessage subscribeMessage(int messageId, String... topics) { 98 | return subscribeMessage(messageId, 0, topics); 99 | } 100 | 101 | public static MqttSubscribeMessage subscribeMessage(int messageId, int qos, String... topics) { 102 | List list = new ArrayList<>(); 103 | for (String topic : topics) { 104 | SubscriptionTopic sb = new SubscriptionTopic(); 105 | sb.setQos(qos); 106 | sb.setTopic(topic); 107 | list.add(sb); 108 | } 109 | return subscribeMessage(messageId, list.toArray(new SubscriptionTopic[0])); 110 | } 111 | 112 | public static MqttSubscribeMessage subscribeMessage(int messageId, SubscriptionTopic... subscriptionTopics) { 113 | return subscribeMessage(messageId, getTopicSubscriptions(subscriptionTopics)); 114 | } 115 | 116 | public static MqttSubscribeMessage subscribeMessage(int messageId, List mqttTopicSubscriptions) { 117 | MqttFixedHeader mqttFixedHeader = new MqttFixedHeader(MqttMessageType.SUBSCRIBE, false, MqttQoS.AT_LEAST_ONCE, 118 | false, 0); 119 | MqttMessageIdVariableHeader mqttMessageIdVariableHeader = MqttMessageIdVariableHeader.from(messageId); 120 | MqttSubscribePayload mqttSubscribePayload = new MqttSubscribePayload(mqttTopicSubscriptions); 121 | return new MqttSubscribeMessage(mqttFixedHeader, mqttMessageIdVariableHeader, mqttSubscribePayload); 122 | } 123 | 124 | public static MqttSubAckMessage subAckMessage(int messageId, List mqttQoSList) { 125 | return (MqttSubAckMessage) MqttMessageFactory.newMessage( 126 | new MqttFixedHeader(MqttMessageType.SUBACK, false, MqttQoS.AT_MOST_ONCE, false, 0), 127 | MqttMessageIdVariableHeader.from(messageId), 128 | new MqttSubAckPayload(mqttQoSList)); 129 | } 130 | 131 | public static MqttUnsubscribeMessage unsubscribeMessage(int messageId, List topicList) { 132 | MqttFixedHeader mqttFixedHeader = new MqttFixedHeader(MqttMessageType.UNSUBSCRIBE, false, MqttQoS.AT_MOST_ONCE, 133 | false, 0x02); 134 | MqttMessageIdVariableHeader variableHeader = MqttMessageIdVariableHeader.from(messageId); 135 | MqttUnsubscribePayload mqttUnsubscribeMessage = new MqttUnsubscribePayload(topicList); 136 | return new MqttUnsubscribeMessage(mqttFixedHeader, variableHeader, mqttUnsubscribeMessage); 137 | } 138 | 139 | public static MqttUnsubAckMessage unsubAckMessage(int messageId) { 140 | return (MqttUnsubAckMessage) MqttMessageFactory.newMessage( 141 | new MqttFixedHeader(MqttMessageType.UNSUBACK, false, MqttQoS.AT_MOST_ONCE, false, 0), 142 | MqttMessageIdVariableHeader.from(messageId), 143 | null); 144 | } 145 | 146 | public static MqttMessage pingReqMessage() { 147 | return MqttMessageFactory.newMessage( 148 | new MqttFixedHeader(MqttMessageType.PINGREQ, false, MqttQoS.AT_MOST_ONCE, false, 0), 149 | null, 150 | null); 151 | } 152 | 153 | public static MqttMessage pingRespMessage() { 154 | return MqttMessageFactory.newMessage( 155 | new MqttFixedHeader(MqttMessageType.PINGRESP, false, MqttQoS.AT_MOST_ONCE, false, 0), 156 | null, 157 | null); 158 | } 159 | 160 | public static MqttPublishMessage publishMessage(MessageData mqttMessage) { 161 | return publishMessage(mqttMessage.getTopic(), mqttMessage.getPayload(), mqttMessage.getQos(), 162 | mqttMessage.isRetained(), mqttMessage.getMessageId(), mqttMessage.isDup()); 163 | } 164 | 165 | public static MqttPublishMessage publishMessage(String topic, byte[] payload, int qosValue, int messageId, boolean isRetain) { 166 | return publishMessage(topic, payload, qosValue, isRetain, messageId, false); 167 | } 168 | 169 | public static MqttPublishMessage publishMessage(String topic, byte[] payload, int qosValue, boolean isRetain, int messageId, boolean isDup) { 170 | return (MqttPublishMessage) MqttMessageFactory.newMessage( 171 | new MqttFixedHeader(MqttMessageType.PUBLISH, isDup, MqttQoS.valueOf(qosValue), isRetain, 0), 172 | new MqttPublishVariableHeader(topic, messageId), 173 | Unpooled.buffer().writeBytes(payload)); 174 | } 175 | 176 | public static MqttMessage pubCompMessage(int messageId) { 177 | return MqttMessageFactory.newMessage( 178 | new MqttFixedHeader(MqttMessageType.PUBCOMP, false, MqttQoS.AT_MOST_ONCE, false, 0), 179 | MqttMessageIdVariableHeader.from(messageId), 180 | null); 181 | } 182 | 183 | public static MqttPubAckMessage pubAckMessage(int messageId) { 184 | return (MqttPubAckMessage) MqttMessageFactory.newMessage( 185 | new MqttFixedHeader(MqttMessageType.PUBACK, false, MqttQoS.AT_MOST_ONCE, false, 0), 186 | MqttMessageIdVariableHeader.from(messageId), 187 | null); 188 | } 189 | 190 | public static MqttMessage pubRecMessage(int messageId) { 191 | return MqttMessageFactory.newMessage( 192 | new MqttFixedHeader(MqttMessageType.PUBREC, false, MqttQoS.AT_MOST_ONCE, false, 0), 193 | MqttMessageIdVariableHeader.from(messageId), 194 | null); 195 | } 196 | 197 | public static MqttMessage pubRelMessage(int messageId, boolean isDup) { 198 | return MqttMessageFactory.newMessage( 199 | new MqttFixedHeader(MqttMessageType.PUBREL, isDup, MqttQoS.AT_LEAST_ONCE, false, 0), 200 | MqttMessageIdVariableHeader.from(messageId), 201 | null); 202 | } 203 | } -------------------------------------------------------------------------------- /src/main/java/io/x2ge/mqtt/core/PublishProcessor.java: -------------------------------------------------------------------------------- 1 | package io.x2ge.mqtt.core; 2 | 3 | import io.netty.channel.Channel; 4 | import io.netty.handler.codec.mqtt.MqttMessageIdVariableHeader; 5 | import io.netty.handler.codec.mqtt.MqttPubAckMessage; 6 | import io.netty.handler.codec.mqtt.MqttPublishMessage; 7 | import io.x2ge.mqtt.utils.AsyncTask; 8 | import io.x2ge.mqtt.utils.Log; 9 | 10 | import java.nio.charset.StandardCharsets; 11 | import java.util.concurrent.TimeUnit; 12 | import java.util.concurrent.atomic.AtomicBoolean; 13 | 14 | public class PublishProcessor extends AsyncTask { 15 | 16 | private long timeout; 17 | private int msgId; 18 | private final AtomicBoolean receivedAck = new AtomicBoolean(false); 19 | 20 | @Override 21 | public String call() throws Exception { 22 | if (!isCancelled() && !receivedAck.get()) { 23 | synchronized (receivedAck) { 24 | receivedAck.wait(timeout); 25 | } 26 | } 27 | return receivedAck.get() ? ProcessorResult.RESULT_SUCCESS : ProcessorResult.RESULT_FAIL; 28 | } 29 | 30 | public String publish(Channel channel, String topic, String content, long timeout) throws Exception { 31 | this.timeout = timeout; 32 | 33 | int id = 0; 34 | String s; 35 | try { 36 | id = MessageIdFactory.get(); 37 | 38 | this.msgId = id; 39 | 40 | MqttPublishMessage msg = ProtocolUtils.publishMessage(topic, 41 | content.getBytes(StandardCharsets.UTF_8), 42 | 1, 43 | id, 44 | false 45 | ); 46 | Log.i("-->发送消息:" + msg); 47 | channel.writeAndFlush(msg); 48 | s = execute().get(timeout, TimeUnit.MILLISECONDS); 49 | } finally { 50 | MessageIdFactory.release(id); 51 | } 52 | return s; 53 | } 54 | 55 | public void processAck(Channel channel, MqttPubAckMessage msg) { 56 | MqttMessageIdVariableHeader variableHeader = msg.variableHeader(); 57 | if (variableHeader.messageId() == msgId) { 58 | synchronized (receivedAck) { 59 | receivedAck.set(true); 60 | receivedAck.notify(); 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/io/x2ge/mqtt/core/SubscribeProcessor.java: -------------------------------------------------------------------------------- 1 | package io.x2ge.mqtt.core; 2 | 3 | import io.netty.channel.Channel; 4 | import io.netty.handler.codec.mqtt.MqttMessageIdAndPropertiesVariableHeader; 5 | import io.netty.handler.codec.mqtt.MqttSubAckMessage; 6 | import io.netty.handler.codec.mqtt.MqttSubscribeMessage; 7 | import io.x2ge.mqtt.utils.AsyncTask; 8 | import io.x2ge.mqtt.utils.Log; 9 | 10 | import java.util.concurrent.TimeUnit; 11 | import java.util.concurrent.atomic.AtomicBoolean; 12 | 13 | public class SubscribeProcessor extends AsyncTask { 14 | 15 | private long timeout; 16 | private int msgId; 17 | private final AtomicBoolean receivedAck = new AtomicBoolean(false); 18 | 19 | @Override 20 | public String call() throws Exception { 21 | if (!isCancelled() && !receivedAck.get()) { 22 | synchronized (receivedAck) { 23 | receivedAck.wait(timeout); 24 | } 25 | } 26 | return receivedAck.get() ? ProcessorResult.RESULT_SUCCESS : ProcessorResult.RESULT_FAIL; 27 | } 28 | 29 | public String subscribe(Channel channel, String[] topics, long timeout) throws Exception { 30 | return subscribe(channel, 0, topics, timeout); 31 | } 32 | 33 | public String subscribe(Channel channel, int qos, String[] topics, long timeout) throws Exception { 34 | this.timeout = timeout; 35 | int id = 0; 36 | String s; 37 | try { 38 | id = MessageIdFactory.get(); 39 | 40 | this.msgId = id; 41 | 42 | MqttSubscribeMessage msg = ProtocolUtils.subscribeMessage(id, qos, topics); 43 | Log.i("-->发起订阅:" + msg); 44 | channel.writeAndFlush(msg); 45 | s = execute().get(timeout, TimeUnit.MILLISECONDS); 46 | } finally { 47 | MessageIdFactory.release(id); 48 | } 49 | return s; 50 | } 51 | 52 | public void processAck(Channel channel, MqttSubAckMessage msg) { 53 | MqttMessageIdAndPropertiesVariableHeader variableHeader = msg.idAndPropertiesVariableHeader(); 54 | if (variableHeader.messageId() == msgId) { 55 | synchronized (receivedAck) { 56 | receivedAck.set(true); 57 | receivedAck.notify(); 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/io/x2ge/mqtt/core/SubscriptionTopic.java: -------------------------------------------------------------------------------- 1 | package io.x2ge.mqtt.core; 2 | 3 | public class SubscriptionTopic { 4 | private String topic; 5 | private int qos; 6 | 7 | public String getTopic() { 8 | return topic; 9 | } 10 | 11 | public void setTopic(String topic) { 12 | this.topic = topic; 13 | } 14 | 15 | public int getQos() { 16 | return qos; 17 | } 18 | 19 | public void setQos(int qos) { 20 | this.qos = qos; 21 | } 22 | } -------------------------------------------------------------------------------- /src/main/java/io/x2ge/mqtt/core/UnsubscribeProcessor.java: -------------------------------------------------------------------------------- 1 | package io.x2ge.mqtt.core; 2 | 3 | import io.netty.channel.Channel; 4 | import io.netty.handler.codec.mqtt.MqttMessageIdAndPropertiesVariableHeader; 5 | import io.netty.handler.codec.mqtt.MqttUnsubAckMessage; 6 | import io.netty.handler.codec.mqtt.MqttUnsubscribeMessage; 7 | import io.x2ge.mqtt.utils.AsyncTask; 8 | import io.x2ge.mqtt.utils.Log; 9 | 10 | import java.util.Arrays; 11 | import java.util.concurrent.TimeUnit; 12 | import java.util.concurrent.atomic.AtomicBoolean; 13 | 14 | public class UnsubscribeProcessor extends AsyncTask { 15 | 16 | private long timeout; 17 | private int msgId; 18 | private final AtomicBoolean receivedAck = new AtomicBoolean(false); 19 | 20 | @Override 21 | public String call() throws Exception { 22 | if (!isCancelled() && !receivedAck.get()) { 23 | synchronized (receivedAck) { 24 | receivedAck.wait(timeout); 25 | } 26 | } 27 | return receivedAck.get() ? ProcessorResult.RESULT_SUCCESS : ProcessorResult.RESULT_FAIL; 28 | } 29 | 30 | public String unsubscribe(Channel channel, String[] topics, long timeout) throws Exception { 31 | this.timeout = timeout; 32 | 33 | int id = 0; 34 | String s; 35 | try { 36 | id = MessageIdFactory.get(); 37 | 38 | this.msgId = id; 39 | 40 | MqttUnsubscribeMessage msg = ProtocolUtils.unsubscribeMessage(id, Arrays.asList(topics)); 41 | Log.i("-->发起取消订阅:" + msg); 42 | channel.writeAndFlush(msg); 43 | s = execute().get(timeout, TimeUnit.MILLISECONDS); 44 | } finally { 45 | MessageIdFactory.release(id); 46 | } 47 | return s; 48 | } 49 | 50 | public void processAck(Channel channel, MqttUnsubAckMessage msg) { 51 | MqttMessageIdAndPropertiesVariableHeader variableHeader = msg.idAndPropertiesVariableHeader(); 52 | if (variableHeader.messageId() == msgId) { 53 | synchronized (receivedAck) { 54 | receivedAck.set(true); 55 | receivedAck.notify(); 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/io/x2ge/mqtt/utils/AsyncTask.java: -------------------------------------------------------------------------------- 1 | package io.x2ge.mqtt.utils; 2 | 3 | import java.util.concurrent.*; 4 | 5 | public abstract class AsyncTask implements RunnableFuture, Callable { 6 | 7 | private static volatile Executor sDefaultExecutor = Executors.newCachedThreadPool(); 8 | 9 | private FutureTask futureTask; 10 | 11 | public AsyncTask() { 12 | this.futureTask = new FutureTask(this) { 13 | @Override 14 | protected void done() { 15 | AsyncTask.this.done(); 16 | if (!isCancelled() && callback != null) { 17 | callback.onDone(); 18 | } 19 | } 20 | }; 21 | } 22 | 23 | @Override 24 | public boolean cancel(boolean mayInterruptIfRunning) { 25 | return futureTask.cancel(mayInterruptIfRunning); 26 | } 27 | 28 | @Override 29 | public boolean isCancelled() { 30 | return futureTask.isCancelled(); 31 | } 32 | 33 | @Override 34 | public boolean isDone() { 35 | return futureTask.isDone(); 36 | } 37 | 38 | @Override 39 | public T get() throws InterruptedException, ExecutionException { 40 | try { 41 | return futureTask.get(); 42 | } catch (Exception e) { 43 | // 发生异常,尝试停止任务 44 | cancel(true); 45 | throw e; 46 | } 47 | } 48 | 49 | @Override 50 | public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { 51 | try { 52 | return futureTask.get(timeout, unit); 53 | } catch (Exception e) { 54 | // 发生异常,尝试停止任务 55 | cancel(true); 56 | throw e; 57 | } 58 | } 59 | 60 | @Override 61 | public void run() { 62 | futureTask.run(); 63 | } 64 | 65 | protected void done() { 66 | 67 | } 68 | 69 | public AsyncTask execute() { 70 | try { 71 | sDefaultExecutor.execute(this); 72 | } catch (Exception e) { 73 | e.printStackTrace(); 74 | } 75 | return this; 76 | } 77 | 78 | public AsyncTask executeOnExecutor(Executor executor) { 79 | try { 80 | executor.execute(this); 81 | } catch (Exception e) { 82 | e.printStackTrace(); 83 | } 84 | return this; 85 | } 86 | 87 | private Callback callback; 88 | 89 | public AsyncTask setCallback(Callback callback) { 90 | this.callback = callback; 91 | return this; 92 | } 93 | 94 | public interface Callback { 95 | void onDone(); 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /src/main/java/io/x2ge/mqtt/utils/Log.java: -------------------------------------------------------------------------------- 1 | package io.x2ge.mqtt.utils; 2 | 3 | import java.lang.management.ManagementFactory; 4 | import java.text.SimpleDateFormat; 5 | import java.util.Date; 6 | import java.util.Locale; 7 | 8 | public class Log { 9 | 10 | static SimpleDateFormat f = new SimpleDateFormat("MM-dd HH:mm:ss.SSS"); 11 | 12 | private static boolean isEnable = true; 13 | private static boolean isEnablePing = false; 14 | 15 | public static void enable(boolean b) { 16 | isEnable = b; 17 | } 18 | 19 | public static void enablePing(boolean b) { 20 | isEnablePing = b; 21 | } 22 | 23 | public static void i(String msg) { 24 | if (!isEnable) 25 | return; 26 | 27 | if (!isEnablePing && msg.contains("[ping]")) 28 | return; 29 | 30 | System.out.println(f.format(new Date()) + " " + getPid() + "-" + Thread.currentThread().getId() + " I/" + 31 | format(msg, 3)); 32 | } 33 | 34 | public static String format(String message, int stackTraceIndex) { 35 | StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); 36 | String fullClassName = Thread.currentThread().getStackTrace()[3].getClassName(); 37 | String className = fullClassName.substring(fullClassName.lastIndexOf(".") + 1); 38 | String methodName = Thread.currentThread().getStackTrace()[3].getMethodName(); 39 | String fileName = Thread.currentThread().getStackTrace()[3].getFileName(); 40 | int lineNumber = Thread.currentThread().getStackTrace()[3].getLineNumber(); 41 | 42 | int depth = Math.min(stackTrace.length - 1, stackTraceIndex); 43 | StackTraceElement ele = stackTrace[depth]; 44 | return String.format(Locale.getDefault(), "%s.%s(%s:%d): \n%s" + (message.length() > 0 ? "\n" : ""), 45 | className, methodName, fileName, lineNumber, message); 46 | // return String.format(Locale.getDefault(), "(%d.%d):%s", Process.myPid(), Process.myTid(), message); 47 | } 48 | 49 | public static long getPid() { 50 | try { 51 | String name = ManagementFactory.getRuntimeMXBean().getName(); 52 | String pid = name.split("@")[0]; 53 | return Long.parseLong(pid); 54 | } catch (Exception e) { 55 | return 0; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/io/x2ge/mqtt/utils/StringUtils.java: -------------------------------------------------------------------------------- 1 | package io.x2ge.mqtt.utils; 2 | 3 | public class StringUtils { 4 | 5 | public static boolean isEmpty(String s) { 6 | return s == null || s.length() == 0; 7 | } 8 | } 9 | --------------------------------------------------------------------------------