├── .gitignore ├── README.md ├── pom.xml └── src ├── main └── java │ └── org │ └── jetlinks │ └── mqtt │ └── client │ ├── ChannelClosedException.java │ ├── MqttChannelHandler.java │ ├── MqttClient.java │ ├── MqttClientCallback.java │ ├── MqttClientConfig.java │ ├── MqttClientImpl.java │ ├── MqttConnectResult.java │ ├── MqttHandler.java │ ├── MqttIncomingQos2Publish.java │ ├── MqttLastWill.java │ ├── MqttPendingPublish.java │ ├── MqttPendingSubscription.java │ ├── MqttPendingUnsubscription.java │ ├── MqttPingHandler.java │ ├── MqttSubscription.java │ └── RetransmissionHandler.java └── test └── java └── org └── jetlinks └── mqtt └── client └── MqttClientImplTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.ipr 3 | *.iws 4 | *.ids 5 | *.iml 6 | logs 7 | target 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # netty-mqtt-client 2 | 3 | [![Maven Central](https://img.shields.io/maven-central/v/org.jetlinks/netty-mqtt-client.svg)](http://search.maven.org/#search%7Cga%7C1%7Cnetty-mqtt-client) 4 | 5 | 6 | ```java 7 | EventLoopGroup loop = new NioEventLoopGroup(); 8 | MqttClient mqttClient = new MqttClientImpl(((topic, payload) -> { 9 | System.out.println(topic + "=>" + payload.toString(StandardCharsets.UTF_8)); 10 | })); 11 | mqttClient.setEventLoop(loop); 12 | mqttClient.getClientConfig().setClientId("{clientId}"); 13 | mqttClient.getClientConfig().setUsername("{username}"); 14 | mqttClient.getClientConfig().setPassword("{password}"); 15 | mqttClient.getClientConfig().setProtocolVersion(MqttVersion.MQTT_3_1_1); 16 | mqttClient.getClientConfig().setReconnect(true); 17 | mqttClient.setCallback(new MqttClientCallback() { 18 | @Override 19 | public void connectionLost(Throwable cause) { 20 | cause.printStackTrace(); 21 | } 22 | 23 | @Override 24 | public void onSuccessfulReconnect() { 25 | 26 | } 27 | }); 28 | MqttConnectResult result = mqttClient.connect("127.0.0.1", 1883).await().get(); 29 | if (result.getReturnCode() != MqttConnectReturnCode.CONNECTION_ACCEPTED) { 30 | System.out.println("error:" + result.getReturnCode()); 31 | mqttClient.disconnect(); 32 | } else { 33 | System.out.println("success"); 34 | // mqttClient.publish("test", Unpooled.copiedBuffer("{\"type\":\"event\"}", StandardCharsets.UTF_8)); 35 | } 36 | ``` -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | org.jetlinks 5 | netty-mqtt-client 6 | 1.0.0 7 | jar 8 | 9 | Netty MQTT Client 10 | https://github.com/jetlinks/netty-mqtt-client 11 | Netty MQTT Client 12 | 13 | 14 | UTF-8 15 | zh_CN 16 | 1.8 17 | ${java.version} 18 | 19 | 20 | 21 | 22 | The Apache License, Version 2.0 23 | http://www.apache.org/licenses/LICENSE-2.0.txt 24 | 25 | 26 | 27 | 28 | scm:git:https://github.com/jetlinks/netty-mqtt-client.git 29 | scm:git:https://github.com/jetlinks/netty-mqtt-client.git 30 | https://github.com/jetlinks/netty-mqtt-client 31 | ${project.version} 32 | 33 | 34 | 35 | 36 | zhouhao 37 | i@hsweb.me 38 | 39 | Owner 40 | 41 | +8 42 | https://github.com/zhou-hao 43 | 44 | 45 | 46 | 47 | 48 | release 49 | 50 | 51 | 52 | org.sonatype.plugins 53 | nexus-staging-maven-plugin 54 | 1.6.3 55 | true 56 | 57 | sonatype-releases 58 | https://oss.sonatype.org/ 59 | true 60 | 120 61 | 62 | 63 | 64 | org.apache.maven.plugins 65 | maven-release-plugin 66 | 67 | true 68 | false 69 | release 70 | deploy 71 | 72 | 73 | 74 | org.apache.maven.plugins 75 | maven-gpg-plugin 76 | 1.5 77 | 78 | 79 | sign-artifacts 80 | verify 81 | 82 | sign 83 | 84 | 85 | 86 | 87 | 88 | org.apache.maven.plugins 89 | maven-javadoc-plugin 90 | 2.9.1 91 | 92 | -Xdoclint:none 93 | 94 | 95 | 96 | attach-javadocs 97 | 98 | jar 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | sonatype-releases 108 | sonatype repository 109 | https://oss.sonatype.org/service/local/staging/deploy/maven2 110 | 111 | 112 | sonatype-snapshots 113 | Nexus Snapshot Repository 114 | https://oss.sonatype.org/content/repositories/snapshots 115 | 116 | 117 | 118 | 119 | 120 | 121 | ${project.artifactId} 122 | 123 | 124 | src/main/resources 125 | true 126 | 127 | 128 | 129 | 130 | 131 | org.jacoco 132 | jacoco-maven-plugin 133 | 0.8.0 134 | 135 | 136 | 137 | prepare-agent 138 | 139 | 140 | 141 | report 142 | test 143 | 144 | report 145 | 146 | 147 | 148 | 149 | 150 | 151 | org.apache.maven.plugins 152 | maven-source-plugin 153 | 2.4 154 | 155 | 156 | attach-sources 157 | 158 | jar-no-fork 159 | 160 | 161 | 162 | 163 | 164 | 165 | org.apache.maven.plugins 166 | maven-compiler-plugin 167 | 3.1 168 | 169 | ${project.build.jdk} 170 | ${project.build.jdk} 171 | ${project.build.sourceEncoding} 172 | 173 | 174 | 175 | 176 | org.codehaus.gmavenplus 177 | gmavenplus-plugin 178 | 1.6.1 179 | 180 | 181 | 182 | addTestSources 183 | compile 184 | compileTests 185 | 186 | 187 | 188 | 189 | 190 | org.codehaus.groovy 191 | groovy-all 192 | 2.4.15 193 | 194 | 195 | 196 | 197 | 198 | org.apache.maven.plugins 199 | maven-surefire-plugin 200 | 2.17 201 | 202 | 203 | **/*Test.java 204 | **/*Test.groovy 205 | **/*Tests.java 206 | **/*Test.groovy 207 | **/*Spec.java 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | junit 218 | junit 219 | 4.13.1 220 | test 221 | 222 | 223 | io.netty 224 | netty-codec-mqtt 225 | 4.1.32.Final 226 | 227 | 228 | io.netty 229 | netty-handler 230 | 4.1.46.Final 231 | 232 | 233 | io.netty 234 | netty-transport-native-epoll 235 | linux-x86_64 236 | 4.1.32.Final 237 | 238 | 239 | com.google.guava 240 | guava 241 | 21.0 242 | 243 | 244 | 245 | 246 | 247 | aliyun-nexus 248 | aliyun 249 | http://maven.aliyun.com/nexus/content/groups/public/ 250 | 251 | 252 | 253 | 254 | 255 | releases 256 | Nexus Release Repository 257 | http://nexus.hsweb.me/content/repositories/releases/ 258 | 259 | 260 | snapshots 261 | Nexus Snapshot Repository 262 | http://nexus.hsweb.me/content/repositories/snapshots/ 263 | 264 | 265 | 266 | -------------------------------------------------------------------------------- /src/main/java/org/jetlinks/mqtt/client/ChannelClosedException.java: -------------------------------------------------------------------------------- 1 | package org.jetlinks.mqtt.client; 2 | 3 | /** 4 | * Created by Valerii Sosliuk on 12/26/2017. 5 | */ 6 | public class ChannelClosedException extends RuntimeException { 7 | 8 | private static final long serialVersionUID = 6266638352424706909L; 9 | 10 | public ChannelClosedException() { 11 | } 12 | 13 | public ChannelClosedException(String message) { 14 | super(message); 15 | } 16 | 17 | public ChannelClosedException(String message, Throwable cause) { 18 | super(message, cause); 19 | } 20 | 21 | public ChannelClosedException(Throwable cause) { 22 | super(cause); 23 | } 24 | 25 | public ChannelClosedException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { 26 | super(message, cause, enableSuppression, writableStackTrace); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/org/jetlinks/mqtt/client/MqttChannelHandler.java: -------------------------------------------------------------------------------- 1 | package org.jetlinks.mqtt.client; 2 | 3 | import com.google.common.collect.ImmutableSet; 4 | import io.netty.channel.Channel; 5 | import io.netty.channel.ChannelHandlerContext; 6 | import io.netty.channel.SimpleChannelInboundHandler; 7 | import io.netty.handler.codec.mqtt.*; 8 | import io.netty.util.CharsetUtil; 9 | import io.netty.util.concurrent.Promise; 10 | 11 | final class MqttChannelHandler extends SimpleChannelInboundHandler { 12 | 13 | private final MqttClientImpl client; 14 | private final Promise connectFuture; 15 | 16 | MqttChannelHandler(MqttClientImpl client, Promise connectFuture) { 17 | this.client = client; 18 | this.connectFuture = connectFuture; 19 | } 20 | 21 | @Override 22 | protected void channelRead0(ChannelHandlerContext ctx, MqttMessage msg) throws Exception { 23 | switch (msg.fixedHeader().messageType()) { 24 | case CONNACK: 25 | handleConack(ctx.channel(), (MqttConnAckMessage) msg); 26 | break; 27 | case SUBACK: 28 | handleSubAck((MqttSubAckMessage) msg); 29 | break; 30 | case PUBLISH: 31 | handlePublish(ctx.channel(), (MqttPublishMessage) msg); 32 | break; 33 | case UNSUBACK: 34 | handleUnsuback((MqttUnsubAckMessage) msg); 35 | break; 36 | case PUBACK: 37 | handlePuback((MqttPubAckMessage) msg); 38 | break; 39 | case PUBREC: 40 | handlePubrec(ctx.channel(), msg); 41 | break; 42 | case PUBREL: 43 | handlePubrel(ctx.channel(), msg); 44 | break; 45 | case PUBCOMP: 46 | handlePubcomp(msg); 47 | break; 48 | } 49 | } 50 | 51 | @Override 52 | public void channelActive(ChannelHandlerContext ctx) throws Exception { 53 | super.channelActive(ctx); 54 | 55 | MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.CONNECT, false, MqttQoS.AT_MOST_ONCE, false, 0); 56 | MqttConnectVariableHeader variableHeader = new MqttConnectVariableHeader( 57 | this.client.getClientConfig().getProtocolVersion().protocolName(), // Protocol Name 58 | this.client.getClientConfig().getProtocolVersion().protocolLevel(), // Protocol Level 59 | this.client.getClientConfig().getUsername() != null, // Has Username 60 | this.client.getClientConfig().getPassword() != null, // Has Password 61 | this.client.getClientConfig().getLastWill() != null // Will Retain 62 | && this.client.getClientConfig().getLastWill().isRetain(), 63 | this.client.getClientConfig().getLastWill() != null // Will QOS 64 | ? this.client.getClientConfig().getLastWill().getQos().value() 65 | : 0, 66 | this.client.getClientConfig().getLastWill() != null, // Has Will 67 | this.client.getClientConfig().isCleanSession(), // Clean Session 68 | this.client.getClientConfig().getTimeoutSeconds() // Timeout 69 | ); 70 | MqttConnectPayload payload = new MqttConnectPayload( 71 | this.client.getClientConfig().getClientId(), 72 | this.client.getClientConfig().getLastWill() != null ? this.client.getClientConfig().getLastWill().getTopic() : null, 73 | this.client.getClientConfig().getLastWill() != null ? this.client.getClientConfig().getLastWill().getMessage().getBytes(CharsetUtil.UTF_8) : null, 74 | this.client.getClientConfig().getUsername(), 75 | this.client.getClientConfig().getPassword() != null ? this.client.getClientConfig().getPassword().getBytes(CharsetUtil.UTF_8) : null 76 | ); 77 | ctx.channel().writeAndFlush(new MqttConnectMessage(fixedHeader, variableHeader, payload)); 78 | } 79 | 80 | @Override 81 | public void channelInactive(ChannelHandlerContext ctx) throws Exception { 82 | super.channelInactive(ctx); 83 | } 84 | 85 | private void invokeHandlersForIncomingPublish(MqttPublishMessage message) { 86 | boolean handlerInvoked = false; 87 | for (MqttSubscription subscription : ImmutableSet.copyOf(this.client.getSubscriptions().values())) { 88 | if (subscription.matches(message.variableHeader().topicName())) { 89 | if (subscription.isOnce() && subscription.isCalled()) { 90 | continue; 91 | } 92 | message.payload().markReaderIndex(); 93 | subscription.setCalled(true); 94 | subscription.getHandler().onMessage(message.variableHeader().topicName(), message.payload()); 95 | if (subscription.isOnce()) { 96 | this.client.off(subscription.getTopic(), subscription.getHandler()); 97 | } 98 | message.payload().resetReaderIndex(); 99 | handlerInvoked = true; 100 | } 101 | } 102 | if (!handlerInvoked && client.getDefaultHandler() != null) { 103 | client.getDefaultHandler().onMessage(message.variableHeader().topicName(), message.payload()); 104 | } 105 | message.payload().release(); 106 | } 107 | 108 | private void handleConack(Channel channel, MqttConnAckMessage message) { 109 | switch (message.variableHeader().connectReturnCode()) { 110 | case CONNECTION_ACCEPTED: 111 | this.connectFuture.setSuccess(new MqttConnectResult(true, MqttConnectReturnCode.CONNECTION_ACCEPTED, channel.closeFuture())); 112 | 113 | this.client.getPendingSubscriptions().entrySet().stream().filter((e) -> !e.getValue().isSent()).forEach((e) -> { 114 | channel.write(e.getValue().getSubscribeMessage()); 115 | e.getValue().setSent(true); 116 | }); 117 | 118 | this.client.getPendingPublishes().forEach((id, publish) -> { 119 | if (publish.isSent()) return; 120 | channel.write(publish.getMessage()); 121 | publish.setSent(true); 122 | if (publish.getQos() == MqttQoS.AT_MOST_ONCE) { 123 | publish.getFuture().setSuccess(null); //We don't get an ACK for QOS 0 124 | this.client.getPendingPublishes().remove(publish.getMessageId()); 125 | } 126 | }); 127 | channel.flush(); 128 | if (this.client.isReconnect()) { 129 | this.client.onSuccessfulReconnect(); 130 | } 131 | break; 132 | 133 | case CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD: 134 | case CONNECTION_REFUSED_IDENTIFIER_REJECTED: 135 | case CONNECTION_REFUSED_NOT_AUTHORIZED: 136 | case CONNECTION_REFUSED_SERVER_UNAVAILABLE: 137 | case CONNECTION_REFUSED_UNACCEPTABLE_PROTOCOL_VERSION: 138 | this.connectFuture.setSuccess(new MqttConnectResult(false, message.variableHeader().connectReturnCode(), channel.closeFuture())); 139 | channel.close(); 140 | // Don't start reconnect logic here 141 | break; 142 | } 143 | } 144 | 145 | private void handleSubAck(MqttSubAckMessage message) { 146 | MqttPendingSubscription pendingSubscription = this.client.getPendingSubscriptions().remove(message.variableHeader().messageId()); 147 | if (pendingSubscription == null) { 148 | return; 149 | } 150 | pendingSubscription.onSubackReceived(); 151 | for (MqttPendingSubscription.MqttPendingHandler handler : pendingSubscription.getHandlers()) { 152 | MqttSubscription subscription = new MqttSubscription(pendingSubscription.getTopic(), handler.getHandler(), handler.isOnce()); 153 | this.client.getSubscriptions().put(pendingSubscription.getTopic(), subscription); 154 | this.client.getHandlerToSubscribtion().put(handler.getHandler(), subscription); 155 | } 156 | this.client.getPendingSubscribeTopics().remove(pendingSubscription.getTopic()); 157 | 158 | this.client.getServerSubscriptions().add(pendingSubscription.getTopic()); 159 | 160 | if (!pendingSubscription.getFuture().isDone()) { 161 | pendingSubscription.getFuture().setSuccess(null); 162 | } 163 | } 164 | 165 | private void handlePublish(Channel channel, MqttPublishMessage message) { 166 | switch (message.fixedHeader().qosLevel()) { 167 | case AT_MOST_ONCE: 168 | invokeHandlersForIncomingPublish(message); 169 | break; 170 | 171 | case AT_LEAST_ONCE: 172 | invokeHandlersForIncomingPublish(message); 173 | if (message.variableHeader().messageId() != -1) { 174 | MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBACK, false, MqttQoS.AT_MOST_ONCE, false, 0); 175 | MqttMessageIdVariableHeader variableHeader = MqttMessageIdVariableHeader.from(message.variableHeader().messageId()); 176 | channel.writeAndFlush(new MqttPubAckMessage(fixedHeader, variableHeader)); 177 | } 178 | break; 179 | 180 | case EXACTLY_ONCE: 181 | if (message.variableHeader().messageId() != -1) { 182 | MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBREC, false, MqttQoS.AT_MOST_ONCE, false, 0); 183 | MqttMessageIdVariableHeader variableHeader = MqttMessageIdVariableHeader.from(message.variableHeader().messageId()); 184 | MqttMessage pubrecMessage = new MqttMessage(fixedHeader, variableHeader); 185 | 186 | MqttIncomingQos2Publish incomingQos2Publish = new MqttIncomingQos2Publish(message, pubrecMessage); 187 | this.client.getQos2PendingIncomingPublishes().put(message.variableHeader().messageId(), incomingQos2Publish); 188 | message.payload().retain(); 189 | incomingQos2Publish.startPubrecRetransmitTimer(this.client.getEventLoop().next(), this.client::sendAndFlushPacket); 190 | 191 | channel.writeAndFlush(pubrecMessage); 192 | } 193 | break; 194 | } 195 | } 196 | 197 | private void handleUnsuback(MqttUnsubAckMessage message) { 198 | MqttPendingUnsubscription unsubscription = this.client.getPendingServerUnsubscribes().get(message.variableHeader().messageId()); 199 | if (unsubscription == null) { 200 | return; 201 | } 202 | unsubscription.onUnsubackReceived(); 203 | this.client.getServerSubscriptions().remove(unsubscription.getTopic()); 204 | unsubscription.getFuture().setSuccess(null); 205 | this.client.getPendingServerUnsubscribes().remove(message.variableHeader().messageId()); 206 | } 207 | 208 | private void handlePuback(MqttPubAckMessage message) { 209 | MqttPendingPublish pendingPublish = this.client.getPendingPublishes().get(message.variableHeader().messageId()); 210 | pendingPublish.getFuture().setSuccess(null); 211 | pendingPublish.onPubackReceived(); 212 | this.client.getPendingPublishes().remove(message.variableHeader().messageId()); 213 | pendingPublish.getPayload().release(); 214 | } 215 | 216 | private void handlePubrec(Channel channel, MqttMessage message) { 217 | MqttPendingPublish pendingPublish = this.client.getPendingPublishes().get(((MqttMessageIdVariableHeader) message.variableHeader()).messageId()); 218 | pendingPublish.onPubackReceived(); 219 | 220 | MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBREL, false, MqttQoS.AT_LEAST_ONCE, false, 0); 221 | MqttMessageIdVariableHeader variableHeader = (MqttMessageIdVariableHeader) message.variableHeader(); 222 | MqttMessage pubrelMessage = new MqttMessage(fixedHeader, variableHeader); 223 | channel.writeAndFlush(pubrelMessage); 224 | 225 | pendingPublish.setPubrelMessage(pubrelMessage); 226 | pendingPublish.startPubrelRetransmissionTimer(this.client.getEventLoop().next(), this.client::sendAndFlushPacket); 227 | } 228 | 229 | private void handlePubrel(Channel channel, MqttMessage message) { 230 | if (this.client.getQos2PendingIncomingPublishes().containsKey(((MqttMessageIdVariableHeader) message.variableHeader()).messageId())) { 231 | MqttIncomingQos2Publish incomingQos2Publish = this.client.getQos2PendingIncomingPublishes().get(((MqttMessageIdVariableHeader) message.variableHeader()).messageId()); 232 | this.invokeHandlersForIncomingPublish(incomingQos2Publish.getIncomingPublish()); 233 | incomingQos2Publish.onPubrelReceived(); 234 | this.client.getQos2PendingIncomingPublishes().remove(incomingQos2Publish.getIncomingPublish().variableHeader().messageId()); 235 | } 236 | MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBCOMP, false, MqttQoS.AT_MOST_ONCE, false, 0); 237 | MqttMessageIdVariableHeader variableHeader = MqttMessageIdVariableHeader.from(((MqttMessageIdVariableHeader) message.variableHeader()).messageId()); 238 | channel.writeAndFlush(new MqttMessage(fixedHeader, variableHeader)); 239 | } 240 | 241 | private void handlePubcomp(MqttMessage message) { 242 | MqttMessageIdVariableHeader variableHeader = (MqttMessageIdVariableHeader) message.variableHeader(); 243 | MqttPendingPublish pendingPublish = this.client.getPendingPublishes().get(variableHeader.messageId()); 244 | pendingPublish.getFuture().setSuccess(null); 245 | this.client.getPendingPublishes().remove(variableHeader.messageId()); 246 | pendingPublish.getPayload().release(); 247 | pendingPublish.onPubcompReceived(); 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /src/main/java/org/jetlinks/mqtt/client/MqttClient.java: -------------------------------------------------------------------------------- 1 | package org.jetlinks.mqtt.client; 2 | 3 | import io.netty.buffer.ByteBuf; 4 | import io.netty.channel.Channel; 5 | import io.netty.channel.EventLoopGroup; 6 | import io.netty.channel.nio.NioEventLoopGroup; 7 | import io.netty.handler.codec.mqtt.MqttQoS; 8 | import io.netty.util.concurrent.Future; 9 | 10 | public interface MqttClient { 11 | 12 | /** 13 | * Connect to the specified hostname/ip. By default uses port 1883. 14 | * If you want to change the port number, see {@link #connect(String, int)} 15 | * 16 | * @param host The ip address or host to connect to 17 | * @return A future which will be completed when the connection is opened and we received an CONNACK 18 | */ 19 | Future connect(String host); 20 | 21 | /** 22 | * Connect to the specified hostname/ip using the specified port 23 | * 24 | * @param host The ip address or host to connect to 25 | * @param port The tcp port to connect to 26 | * @return A future which will be completed when the connection is opened and we received an CONNACK 27 | */ 28 | Future connect(String host, int port); 29 | 30 | /** 31 | * 32 | * @return boolean value indicating if channel is active 33 | */ 34 | boolean isConnected(); 35 | 36 | /** 37 | * Attempt reconnect to the host that was attempted with {@link #connect(String, int)} method before 38 | * 39 | * @return A future which will be completed when the connection is opened and we received an CONNACK 40 | * @throws IllegalStateException if no previous {@link #connect(String, int)} calls were attempted 41 | */ 42 | Future reconnect(); 43 | 44 | /** 45 | * Retrieve the netty {@link EventLoopGroup} we are using 46 | * @return The netty {@link EventLoopGroup} we use for the connection 47 | */ 48 | EventLoopGroup getEventLoop(); 49 | 50 | /** 51 | * By default we use the netty {@link NioEventLoopGroup}. 52 | * If you change the EventLoopGroup to another type, make sure to change the {@link Channel} class using {@link MqttClientConfig#setChannelClass(Class)} 53 | * If you want to force the MqttClient to use another {@link EventLoopGroup}, call this function before calling {@link #connect(String, int)} 54 | * 55 | * @param eventLoop The new eventloop to use 56 | */ 57 | void setEventLoop(EventLoopGroup eventLoop); 58 | 59 | /** 60 | * Subscribe on the given topic. When a message is received, MqttClient will invoke the {@link MqttHandler#onMessage(String, ByteBuf)} function of the given handler 61 | * 62 | * @param topic The topic filter to subscribe to 63 | * @param handler The handler to invoke when we receive a message 64 | * @return A future which will be completed when the server acknowledges our subscribe request 65 | */ 66 | Future on(String topic, MqttHandler handler); 67 | 68 | /** 69 | * Subscribe on the given topic, with the given qos. When a message is received, MqttClient will invoke the {@link MqttHandler#onMessage(String, ByteBuf)} function of the given handler 70 | * 71 | * @param topic The topic filter to subscribe to 72 | * @param handler The handler to invoke when we receive a message 73 | * @param qos The qos to request to the server 74 | * @return A future which will be completed when the server acknowledges our subscribe request 75 | */ 76 | Future on(String topic, MqttHandler handler, MqttQoS qos); 77 | 78 | /** 79 | * Subscribe on the given topic. When a message is received, MqttClient will invoke the {@link MqttHandler#onMessage(String, ByteBuf)} function of the given handler 80 | * This subscription is only once. If the MqttClient has received 1 message, the subscription will be removed 81 | * 82 | * @param topic The topic filter to subscribe to 83 | * @param handler The handler to invoke when we receive a message 84 | * @return A future which will be completed when the server acknowledges our subscribe request 85 | */ 86 | Future once(String topic, MqttHandler handler); 87 | 88 | /** 89 | * Subscribe on the given topic, with the given qos. When a message is received, MqttClient will invoke the {@link MqttHandler#onMessage(String, ByteBuf)} function of the given handler 90 | * This subscription is only once. If the MqttClient has received 1 message, the subscription will be removed 91 | * 92 | * @param topic The topic filter to subscribe to 93 | * @param handler The handler to invoke when we receive a message 94 | * @param qos The qos to request to the server 95 | * @return A future which will be completed when the server acknowledges our subscribe request 96 | */ 97 | Future once(String topic, MqttHandler handler, MqttQoS qos); 98 | 99 | /** 100 | * Remove the subscription for the given topic and handler 101 | * If you want to unsubscribe from all handlers known for this topic, use {@link #off(String)} 102 | * 103 | * @param topic The topic to unsubscribe for 104 | * @param handler The handler to unsubscribe 105 | * @return A future which will be completed when the server acknowledges our unsubscribe request 106 | */ 107 | Future off(String topic, MqttHandler handler); 108 | 109 | /** 110 | * Remove all subscriptions for the given topic. 111 | * If you want to specify which handler to unsubscribe, use {@link #off(String, MqttHandler)} 112 | * 113 | * @param topic The topic to unsubscribe for 114 | * @return A future which will be completed when the server acknowledges our unsubscribe request 115 | */ 116 | Future off(String topic); 117 | 118 | /** 119 | * Publish a message to the given payload 120 | * @param topic The topic to publish to 121 | * @param payload The payload to send 122 | * @return A future which will be completed when the message is sent out of the MqttClient 123 | */ 124 | Future publish(String topic, ByteBuf payload); 125 | 126 | /** 127 | * Publish a message to the given payload, using the given qos 128 | * @param topic The topic to publish to 129 | * @param payload The payload to send 130 | * @param qos The qos to use while publishing 131 | * @return A future which will be completed when the message is delivered to the server 132 | */ 133 | Future publish(String topic, ByteBuf payload, MqttQoS qos); 134 | 135 | /** 136 | * Publish a message to the given payload, using optional retain 137 | * @param topic The topic to publish to 138 | * @param payload The payload to send 139 | * @param retain true if you want to retain the message on the server, false otherwise 140 | * @return A future which will be completed when the message is sent out of the MqttClient 141 | */ 142 | Future publish(String topic, ByteBuf payload, boolean retain); 143 | 144 | /** 145 | * Publish a message to the given payload, using the given qos and optional retain 146 | * @param topic The topic to publish to 147 | * @param payload The payload to send 148 | * @param qos The qos to use while publishing 149 | * @param retain true if you want to retain the message on the server, false otherwise 150 | * @return A future which will be completed when the message is delivered to the server 151 | */ 152 | Future publish(String topic, ByteBuf payload, MqttQoS qos, boolean retain); 153 | 154 | /** 155 | * Retrieve the MqttClient configuration 156 | * @return The {@link MqttClientConfig} instance we use 157 | */ 158 | MqttClientConfig getClientConfig(); 159 | 160 | 161 | /** 162 | * Construct the MqttClientImpl with additional config. 163 | * This config can also be changed using the {@link #getClientConfig()} function 164 | * 165 | * @param config The config object to use while looking for settings 166 | * @param defaultHandler The handler for incoming messages that do not match any topic subscriptions 167 | */ 168 | static MqttClient create(MqttClientConfig config, MqttHandler defaultHandler){ 169 | return new MqttClientImpl(config, defaultHandler); 170 | } 171 | 172 | /** 173 | * Send disconnect and close channel 174 | * 175 | */ 176 | void disconnect(); 177 | 178 | /** 179 | * Sets the {@see #MqttClientCallback} object for this MqttClient 180 | * @param callback The callback to be set 181 | */ 182 | void setCallback(MqttClientCallback callback); 183 | 184 | } 185 | -------------------------------------------------------------------------------- /src/main/java/org/jetlinks/mqtt/client/MqttClientCallback.java: -------------------------------------------------------------------------------- 1 | package org.jetlinks.mqtt.client; 2 | 3 | /** 4 | * Created by Valerii Sosliuk on 12/30/2017. 5 | */ 6 | public interface MqttClientCallback { 7 | 8 | /** 9 | * This method is called when the connection to the server is lost. 10 | * 11 | * @param cause the reason behind the loss of connection. 12 | */ 13 | void connectionLost(Throwable cause); 14 | 15 | /** 16 | * This method is called when the connection to the server is recovered. 17 | * 18 | */ 19 | void onSuccessfulReconnect(); 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/org/jetlinks/mqtt/client/MqttClientConfig.java: -------------------------------------------------------------------------------- 1 | package org.jetlinks.mqtt.client; 2 | 3 | import io.netty.channel.Channel; 4 | import io.netty.channel.socket.nio.NioSocketChannel; 5 | import io.netty.handler.codec.mqtt.MqttVersion; 6 | import io.netty.handler.ssl.SslContext; 7 | 8 | import javax.net.ssl.SSLEngine; 9 | import java.net.SocketAddress; 10 | import java.util.Random; 11 | import java.util.function.Consumer; 12 | 13 | @SuppressWarnings({"WeakerAccess", "unused"}) 14 | public final class MqttClientConfig { 15 | 16 | private SslContext sslContext; 17 | private final String randomClientId; 18 | 19 | private String clientId; 20 | private int timeoutSeconds = 60; 21 | private MqttVersion protocolVersion = MqttVersion.MQTT_3_1; 22 | private String username = null; 23 | private String password = null; 24 | private boolean cleanSession = true; 25 | private MqttLastWill lastWill; 26 | 27 | private Class channelClass = NioSocketChannel.class; 28 | 29 | private SocketAddress bindAddress; 30 | 31 | private Consumer sslEngineConsumer=(engine)->{}; 32 | 33 | private boolean reconnect = true; 34 | private long retryInterval = 1L; 35 | 36 | public MqttClientConfig() { 37 | this(null); 38 | } 39 | 40 | public MqttClientConfig(SslContext sslContext) { 41 | this.sslContext = sslContext; 42 | Random random = new Random(); 43 | String id = "netty-mqtt/"; 44 | String[] options = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".split(""); 45 | for (int i = 0; i < 8; i++) { 46 | id += options[random.nextInt(options.length)]; 47 | } 48 | this.clientId = id; 49 | this.randomClientId = id; 50 | } 51 | 52 | 53 | public String getClientId() { 54 | return clientId; 55 | } 56 | 57 | public void setClientId(String clientId) { 58 | if (clientId == null || clientId.isEmpty()) { 59 | this.clientId = randomClientId; 60 | } else { 61 | this.clientId = clientId; 62 | } 63 | } 64 | 65 | public int getTimeoutSeconds() { 66 | return timeoutSeconds; 67 | } 68 | 69 | public void setTimeoutSeconds(int timeoutSeconds) { 70 | if (timeoutSeconds != -1 && timeoutSeconds <= 0) { 71 | throw new IllegalArgumentException("timeoutSeconds must be > 0 or -1"); 72 | } 73 | this.timeoutSeconds = timeoutSeconds; 74 | } 75 | 76 | public MqttVersion getProtocolVersion() { 77 | return protocolVersion; 78 | } 79 | 80 | public void setProtocolVersion(MqttVersion protocolVersion) { 81 | if (protocolVersion == null) { 82 | throw new NullPointerException("protocolVersion"); 83 | } 84 | this.protocolVersion = protocolVersion; 85 | } 86 | 87 | 88 | public String getUsername() { 89 | return username; 90 | } 91 | 92 | public void setUsername(String username) { 93 | this.username = username; 94 | } 95 | 96 | 97 | public String getPassword() { 98 | return password; 99 | } 100 | 101 | public void setPassword(String password) { 102 | this.password = password; 103 | } 104 | 105 | public boolean isCleanSession() { 106 | return cleanSession; 107 | } 108 | 109 | public void setCleanSession(boolean cleanSession) { 110 | this.cleanSession = cleanSession; 111 | } 112 | 113 | 114 | public MqttLastWill getLastWill() { 115 | return lastWill; 116 | } 117 | 118 | public void setLastWill(MqttLastWill lastWill) { 119 | this.lastWill = lastWill; 120 | } 121 | 122 | public Class getChannelClass() { 123 | return channelClass; 124 | } 125 | 126 | public void setChannelClass(Class channelClass) { 127 | this.channelClass = channelClass; 128 | } 129 | 130 | public SslContext getSslContext() { 131 | return sslContext; 132 | } 133 | 134 | public void setSslContext(SslContext sslContext) { 135 | this.sslContext = sslContext; 136 | } 137 | 138 | public boolean isReconnect() { 139 | return reconnect; 140 | } 141 | 142 | public void setReconnect(boolean reconnect) { 143 | this.reconnect = reconnect; 144 | } 145 | 146 | public long getRetryInterval() { 147 | return retryInterval; 148 | } 149 | 150 | public void setRetryInterval(long retryInterval) { 151 | this.retryInterval = retryInterval; 152 | } 153 | 154 | public SocketAddress getBindAddress() { 155 | return bindAddress; 156 | } 157 | 158 | public void setBindAddress(SocketAddress bindAddress) { 159 | this.bindAddress = bindAddress; 160 | } 161 | 162 | public Consumer getSslEngineConsumer() { 163 | return sslEngineConsumer; 164 | } 165 | 166 | public void setSslEngineConsumer(Consumer sslEngineConsumer) { 167 | this.sslEngineConsumer = sslEngineConsumer; 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/main/java/org/jetlinks/mqtt/client/MqttClientImpl.java: -------------------------------------------------------------------------------- 1 | package org.jetlinks.mqtt.client; 2 | 3 | import com.google.common.collect.HashMultimap; 4 | import com.google.common.collect.ImmutableSet; 5 | import io.netty.bootstrap.Bootstrap; 6 | import io.netty.buffer.ByteBuf; 7 | import io.netty.channel.*; 8 | import io.netty.channel.nio.NioEventLoopGroup; 9 | import io.netty.channel.socket.SocketChannel; 10 | import io.netty.handler.codec.mqtt.*; 11 | import io.netty.handler.ssl.SslContext; 12 | import io.netty.handler.ssl.SslHandler; 13 | import io.netty.handler.timeout.IdleStateHandler; 14 | import io.netty.util.collection.IntObjectHashMap; 15 | import io.netty.util.concurrent.DefaultPromise; 16 | import io.netty.util.concurrent.Future; 17 | import io.netty.util.concurrent.Promise; 18 | 19 | import javax.net.ssl.SSLEngine; 20 | import java.util.*; 21 | import java.util.concurrent.TimeUnit; 22 | import java.util.concurrent.atomic.AtomicInteger; 23 | import java.util.function.Consumer; 24 | 25 | /** 26 | * Represents an MqttClientImpl connected to a single MQTT server. Will try to keep the connection going at all times 27 | */ 28 | @SuppressWarnings({"WeakerAccess", "unused"}) 29 | final class MqttClientImpl implements MqttClient { 30 | 31 | private final Set serverSubscriptions = new HashSet<>(); 32 | private final IntObjectHashMap pendingServerUnsubscribes = new IntObjectHashMap<>(); 33 | private final IntObjectHashMap qos2PendingIncomingPublishes = new IntObjectHashMap<>(); 34 | private final IntObjectHashMap pendingPublishes = new IntObjectHashMap<>(); 35 | private final HashMultimap subscriptions = HashMultimap.create(); 36 | private final IntObjectHashMap pendingSubscriptions = new IntObjectHashMap<>(); 37 | private final Set pendingSubscribeTopics = new HashSet<>(); 38 | private final HashMultimap handlerToSubscribtion = HashMultimap.create(); 39 | private final AtomicInteger nextMessageId = new AtomicInteger(1); 40 | 41 | private final MqttClientConfig clientConfig; 42 | 43 | private final MqttHandler defaultHandler; 44 | 45 | private EventLoopGroup eventLoop; 46 | 47 | private volatile Channel channel; 48 | 49 | private volatile boolean disconnected = false; 50 | private volatile boolean reconnect = false; 51 | private String host; 52 | private int port; 53 | private MqttClientCallback callback; 54 | 55 | 56 | /** 57 | * Construct the MqttClientImpl with default config 58 | */ 59 | public MqttClientImpl(MqttHandler defaultHandler) { 60 | this.clientConfig = new MqttClientConfig(); 61 | this.defaultHandler = defaultHandler; 62 | } 63 | 64 | /** 65 | * Construct the MqttClientImpl with additional config. 66 | * This config can also be changed using the {@link #getClientConfig()} function 67 | * 68 | * @param clientConfig The config object to use while looking for settings 69 | */ 70 | public MqttClientImpl(MqttClientConfig clientConfig, MqttHandler defaultHandler) { 71 | this.clientConfig = clientConfig; 72 | this.defaultHandler = defaultHandler; 73 | } 74 | 75 | /** 76 | * Connect to the specified hostname/ip. By default uses port 1883. 77 | * If you want to change the port number, see {@link #connect(String, int)} 78 | * 79 | * @param host The ip address or host to connect to 80 | * @return A future which will be completed when the connection is opened and we received an CONNACK 81 | */ 82 | @Override 83 | public Future connect(String host) { 84 | return connect(host, 1883); 85 | } 86 | 87 | /** 88 | * Connect to the specified hostname/ip using the specified port 89 | * 90 | * @param host The ip address or host to connect to 91 | * @param port The tcp port to connect to 92 | * @return A future which will be completed when the connection is opened and we received an CONNACK 93 | */ 94 | @Override 95 | public Future connect(String host, int port) { 96 | return connect(host, port, false); 97 | } 98 | 99 | private Future connect(String host, int port, boolean reconnect) { 100 | if (this.eventLoop == null) { 101 | this.eventLoop = new NioEventLoopGroup(); 102 | } 103 | this.host = host; 104 | this.port = port; 105 | Promise connectFuture = new DefaultPromise<>(this.eventLoop.next()); 106 | Bootstrap bootstrap = new Bootstrap(); 107 | bootstrap.group(this.eventLoop); 108 | bootstrap.channel(clientConfig.getChannelClass()); 109 | bootstrap.option(ChannelOption.SO_REUSEADDR, true); 110 | bootstrap.remoteAddress(host, port); 111 | if (clientConfig.getBindAddress() != null) { 112 | bootstrap.localAddress(clientConfig.getBindAddress()); 113 | } 114 | bootstrap.handler(new MqttChannelInitializer(connectFuture, host, port, clientConfig.getSslContext(),clientConfig.getSslEngineConsumer())); 115 | 116 | ChannelFuture future = bootstrap.connect(); 117 | future.addListener((ChannelFutureListener) f -> { 118 | if (f.isSuccess()) { 119 | MqttClientImpl.this.channel = f.channel(); 120 | MqttClientImpl.this.channel.closeFuture().addListener((ChannelFutureListener) channelFuture -> { 121 | if (isConnected()) { 122 | return; 123 | } 124 | if (callback != null) { 125 | MqttConnectResult result = connectFuture.getNow(); 126 | if (result != null) { 127 | if (result.getReturnCode() != MqttConnectReturnCode.CONNECTION_ACCEPTED) { 128 | ChannelClosedException e = new ChannelClosedException(result.getReturnCode().name()); 129 | callback.connectionLost(e); 130 | connectFuture.tryFailure(e); 131 | } 132 | } else { 133 | ChannelClosedException e = new ChannelClosedException("Channel is closed!", channelFuture.cause()); 134 | callback.connectionLost(e); 135 | connectFuture.tryFailure(e); 136 | } 137 | } 138 | pendingSubscriptions.clear(); 139 | serverSubscriptions.clear(); 140 | subscriptions.clear(); 141 | pendingServerUnsubscribes.clear(); 142 | qos2PendingIncomingPublishes.clear(); 143 | pendingPublishes.clear(); 144 | pendingSubscribeTopics.clear(); 145 | handlerToSubscribtion.clear(); 146 | scheduleConnectIfRequired(host, port, true); 147 | }); 148 | } else { 149 | if (callback != null) { 150 | callback.connectionLost(f.cause()); 151 | } 152 | connectFuture.tryFailure(f.cause()); 153 | scheduleConnectIfRequired(host, port, reconnect); 154 | } 155 | }); 156 | return connectFuture; 157 | } 158 | 159 | private void scheduleConnectIfRequired(String host, int port, boolean reconnect) { 160 | if (clientConfig.isReconnect() && !disconnected) { 161 | if (reconnect) { 162 | this.reconnect = true; 163 | } 164 | eventLoop.schedule((Runnable) () -> connect(host, port, reconnect), clientConfig.getRetryInterval(), TimeUnit.SECONDS); 165 | } 166 | } 167 | 168 | @Override 169 | public boolean isConnected() { 170 | return !disconnected && channel != null && channel.isActive(); 171 | } 172 | 173 | @Override 174 | public Future reconnect() { 175 | if (host == null) { 176 | throw new IllegalStateException("Cannot reconnect. Call connect() first"); 177 | } 178 | return connect(host, port); 179 | } 180 | 181 | /** 182 | * Retrieve the netty {@link EventLoopGroup} we are using 183 | * 184 | * @return The netty {@link EventLoopGroup} we use for the connection 185 | */ 186 | @Override 187 | public EventLoopGroup getEventLoop() { 188 | return eventLoop; 189 | } 190 | 191 | /** 192 | * By default we use the netty {@link NioEventLoopGroup}. 193 | * If you change the EventLoopGroup to another type, make sure to change the {@link Channel} class using {@link MqttClientConfig#setChannelClass(Class)} 194 | * If you want to force the MqttClient to use another {@link EventLoopGroup}, call this function before calling {@link #connect(String, int)} 195 | * 196 | * @param eventLoop The new eventloop to use 197 | */ 198 | @Override 199 | public void setEventLoop(EventLoopGroup eventLoop) { 200 | this.eventLoop = eventLoop; 201 | } 202 | 203 | /** 204 | * Subscribe on the given topic. When a message is received, MqttClient will invoke the {@link MqttHandler#onMessage(String, ByteBuf)} function of the given handler 205 | * 206 | * @param topic The topic filter to subscribe to 207 | * @param handler The handler to invoke when we receive a message 208 | * @return A future which will be completed when the server acknowledges our subscribe request 209 | */ 210 | @Override 211 | public Future on(String topic, MqttHandler handler) { 212 | return on(topic, handler, MqttQoS.AT_MOST_ONCE); 213 | } 214 | 215 | /** 216 | * Subscribe on the given topic, with the given qos. When a message is received, MqttClient will invoke the {@link MqttHandler#onMessage(String, ByteBuf)} function of the given handler 217 | * 218 | * @param topic The topic filter to subscribe to 219 | * @param handler The handler to invoke when we receive a message 220 | * @param qos The qos to request to the server 221 | * @return A future which will be completed when the server acknowledges our subscribe request 222 | */ 223 | @Override 224 | public Future on(String topic, MqttHandler handler, MqttQoS qos) { 225 | return createSubscription(topic, handler, false, qos); 226 | } 227 | 228 | /** 229 | * Subscribe on the given topic. When a message is received, MqttClient will invoke the {@link MqttHandler#onMessage(String, ByteBuf)} function of the given handler 230 | * This subscription is only once. If the MqttClient has received 1 message, the subscription will be removed 231 | * 232 | * @param topic The topic filter to subscribe to 233 | * @param handler The handler to invoke when we receive a message 234 | * @return A future which will be completed when the server acknowledges our subscribe request 235 | */ 236 | @Override 237 | public Future once(String topic, MqttHandler handler) { 238 | return once(topic, handler, MqttQoS.AT_MOST_ONCE); 239 | } 240 | 241 | /** 242 | * Subscribe on the given topic, with the given qos. When a message is received, MqttClient will invoke the {@link MqttHandler#onMessage(String, ByteBuf)} function of the given handler 243 | * This subscription is only once. If the MqttClient has received 1 message, the subscription will be removed 244 | * 245 | * @param topic The topic filter to subscribe to 246 | * @param handler The handler to invoke when we receive a message 247 | * @param qos The qos to request to the server 248 | * @return A future which will be completed when the server acknowledges our subscribe request 249 | */ 250 | @Override 251 | public Future once(String topic, MqttHandler handler, MqttQoS qos) { 252 | return createSubscription(topic, handler, true, qos); 253 | } 254 | 255 | /** 256 | * Remove the subscription for the given topic and handler 257 | * If you want to unsubscribe from all handlers known for this topic, use {@link #off(String)} 258 | * 259 | * @param topic The topic to unsubscribe for 260 | * @param handler The handler to unsubscribe 261 | * @return A future which will be completed when the server acknowledges our unsubscribe request 262 | */ 263 | @Override 264 | public Future off(String topic, MqttHandler handler) { 265 | Promise future = new DefaultPromise<>(this.eventLoop.next()); 266 | for (MqttSubscription subscription : this.handlerToSubscribtion.get(handler)) { 267 | this.subscriptions.remove(topic, subscription); 268 | } 269 | this.handlerToSubscribtion.removeAll(handler); 270 | this.checkSubscribtions(topic, future); 271 | return future; 272 | } 273 | 274 | /** 275 | * Remove all subscriptions for the given topic. 276 | * If you want to specify which handler to unsubscribe, use {@link #off(String, MqttHandler)} 277 | * 278 | * @param topic The topic to unsubscribe for 279 | * @return A future which will be completed when the server acknowledges our unsubscribe request 280 | */ 281 | @Override 282 | public Future off(String topic) { 283 | Promise future = new DefaultPromise<>(this.eventLoop.next()); 284 | ImmutableSet subscriptions = ImmutableSet.copyOf(this.subscriptions.get(topic)); 285 | for (MqttSubscription subscription : subscriptions) { 286 | for (MqttSubscription handSub : this.handlerToSubscribtion.get(subscription.getHandler())) { 287 | this.subscriptions.remove(topic, handSub); 288 | } 289 | this.handlerToSubscribtion.remove(subscription.getHandler(), subscription); 290 | } 291 | this.checkSubscribtions(topic, future); 292 | return future; 293 | } 294 | 295 | /** 296 | * Publish a message to the given payload 297 | * 298 | * @param topic The topic to publish to 299 | * @param payload The payload to send 300 | * @return A future which will be completed when the message is sent out of the MqttClient 301 | */ 302 | @Override 303 | public Future publish(String topic, ByteBuf payload) { 304 | return publish(topic, payload, MqttQoS.AT_MOST_ONCE, false); 305 | } 306 | 307 | /** 308 | * Publish a message to the given payload, using the given qos 309 | * 310 | * @param topic The topic to publish to 311 | * @param payload The payload to send 312 | * @param qos The qos to use while publishing 313 | * @return A future which will be completed when the message is delivered to the server 314 | */ 315 | @Override 316 | public Future publish(String topic, ByteBuf payload, MqttQoS qos) { 317 | return publish(topic, payload, qos, false); 318 | } 319 | 320 | /** 321 | * Publish a message to the given payload, using optional retain 322 | * 323 | * @param topic The topic to publish to 324 | * @param payload The payload to send 325 | * @param retain true if you want to retain the message on the server, false otherwise 326 | * @return A future which will be completed when the message is sent out of the MqttClient 327 | */ 328 | @Override 329 | public Future publish(String topic, ByteBuf payload, boolean retain) { 330 | return publish(topic, payload, MqttQoS.AT_MOST_ONCE, retain); 331 | } 332 | 333 | /** 334 | * Publish a message to the given payload, using the given qos and optional retain 335 | * 336 | * @param topic The topic to publish to 337 | * @param payload The payload to send 338 | * @param qos The qos to use while publishing 339 | * @param retain true if you want to retain the message on the server, false otherwise 340 | * @return A future which will be completed when the message is delivered to the server 341 | */ 342 | @Override 343 | public Future publish(String topic, ByteBuf payload, MqttQoS qos, boolean retain) { 344 | Promise future = new DefaultPromise<>(this.eventLoop.next()); 345 | MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBLISH, false, qos, retain, 0); 346 | MqttPublishVariableHeader variableHeader = new MqttPublishVariableHeader(topic, getNewMessageId().messageId()); 347 | MqttPublishMessage message = new MqttPublishMessage(fixedHeader, variableHeader, payload); 348 | MqttPendingPublish pendingPublish = new MqttPendingPublish(variableHeader.messageId(), future, payload.retain(), message, qos); 349 | ChannelFuture channelFuture = this.sendAndFlushPacket(message); 350 | 351 | if (channelFuture != null) { 352 | pendingPublish.setSent(true); 353 | if (channelFuture.cause() != null) { 354 | future.setFailure(channelFuture.cause()); 355 | return future; 356 | } 357 | } 358 | if (pendingPublish.isSent() && pendingPublish.getQos() == MqttQoS.AT_MOST_ONCE) { 359 | pendingPublish.getFuture().setSuccess(null); //We don't get an ACK for QOS 0 360 | } else if (pendingPublish.isSent()) { 361 | this.pendingPublishes.put(pendingPublish.getMessageId(), pendingPublish); 362 | pendingPublish.startPublishRetransmissionTimer(this.eventLoop.next(), this::sendAndFlushPacket); 363 | } 364 | return future; 365 | } 366 | 367 | /** 368 | * Retrieve the MqttClient configuration 369 | * 370 | * @return The {@link MqttClientConfig} instance we use 371 | */ 372 | @Override 373 | public MqttClientConfig getClientConfig() { 374 | return clientConfig; 375 | } 376 | 377 | @Override 378 | public void disconnect() { 379 | disconnected = true; 380 | if (this.channel != null) { 381 | MqttMessage message = new MqttMessage(new MqttFixedHeader(MqttMessageType.DISCONNECT, false, MqttQoS.AT_MOST_ONCE, false, 0)); 382 | this.sendAndFlushPacket(message).addListener(future1 -> channel.close()); 383 | } 384 | } 385 | 386 | @Override 387 | public void setCallback(MqttClientCallback callback) { 388 | this.callback = callback; 389 | } 390 | 391 | 392 | ///////////////////////////////////////////// PRIVATE API ///////////////////////////////////////////// 393 | 394 | public boolean isReconnect() { 395 | return reconnect; 396 | } 397 | 398 | public void onSuccessfulReconnect() { 399 | callback.onSuccessfulReconnect(); 400 | } 401 | 402 | 403 | ChannelFuture sendAndFlushPacket(Object message) { 404 | if (this.channel == null) { 405 | return null; 406 | } 407 | if (this.channel.isActive()) { 408 | return this.channel.writeAndFlush(message); 409 | } 410 | return this.channel.newFailedFuture(new ChannelClosedException("Channel is closed!")); 411 | } 412 | 413 | private MqttMessageIdVariableHeader getNewMessageId() { 414 | this.nextMessageId.compareAndSet(0xffff, 1); 415 | return MqttMessageIdVariableHeader.from(this.nextMessageId.getAndIncrement()); 416 | } 417 | 418 | private Future createSubscription(String topic, MqttHandler handler, boolean once, MqttQoS qos) { 419 | if (this.pendingSubscribeTopics.contains(topic)) { 420 | Optional> subscriptionEntry = this.pendingSubscriptions.entrySet().stream().filter((e) -> e.getValue().getTopic().equals(topic)).findAny(); 421 | if (subscriptionEntry.isPresent()) { 422 | subscriptionEntry.get().getValue().addHandler(handler, once); 423 | return subscriptionEntry.get().getValue().getFuture(); 424 | } 425 | } 426 | if (this.serverSubscriptions.contains(topic)) { 427 | MqttSubscription subscription = new MqttSubscription(topic, handler, once); 428 | this.subscriptions.put(topic, subscription); 429 | this.handlerToSubscribtion.put(handler, subscription); 430 | return this.channel.newSucceededFuture(); 431 | } 432 | 433 | Promise future = new DefaultPromise<>(this.eventLoop.next()); 434 | MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.SUBSCRIBE, false, MqttQoS.AT_LEAST_ONCE, false, 0); 435 | MqttTopicSubscription subscription = new MqttTopicSubscription(topic, qos); 436 | MqttMessageIdVariableHeader variableHeader = getNewMessageId(); 437 | MqttSubscribePayload payload = new MqttSubscribePayload(Collections.singletonList(subscription)); 438 | MqttSubscribeMessage message = new MqttSubscribeMessage(fixedHeader, variableHeader, payload); 439 | 440 | final MqttPendingSubscription pendingSubscription = new MqttPendingSubscription(future, topic, message); 441 | pendingSubscription.addHandler(handler, once); 442 | this.pendingSubscriptions.put(variableHeader.messageId(), pendingSubscription); 443 | this.pendingSubscribeTopics.add(topic); 444 | pendingSubscription.setSent(this.sendAndFlushPacket(message) != null); //If not sent, we will send it when the connection is opened 445 | 446 | pendingSubscription.startRetransmitTimer(this.eventLoop.next(), this::sendAndFlushPacket); 447 | 448 | return future; 449 | } 450 | 451 | private void checkSubscribtions(String topic, Promise promise) { 452 | if (!(this.subscriptions.containsKey(topic) && this.subscriptions.get(topic).size() != 0) && this.serverSubscriptions.contains(topic)) { 453 | MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.UNSUBSCRIBE, false, MqttQoS.AT_LEAST_ONCE, false, 0); 454 | MqttMessageIdVariableHeader variableHeader = getNewMessageId(); 455 | MqttUnsubscribePayload payload = new MqttUnsubscribePayload(Collections.singletonList(topic)); 456 | MqttUnsubscribeMessage message = new MqttUnsubscribeMessage(fixedHeader, variableHeader, payload); 457 | 458 | MqttPendingUnsubscription pendingUnsubscription = new MqttPendingUnsubscription(promise, topic, message); 459 | this.pendingServerUnsubscribes.put(variableHeader.messageId(), pendingUnsubscription); 460 | pendingUnsubscription.startRetransmissionTimer(this.eventLoop.next(), this::sendAndFlushPacket); 461 | 462 | this.sendAndFlushPacket(message); 463 | } else { 464 | promise.setSuccess(null); 465 | } 466 | } 467 | 468 | IntObjectHashMap getPendingSubscriptions() { 469 | return pendingSubscriptions; 470 | } 471 | 472 | HashMultimap getSubscriptions() { 473 | return subscriptions; 474 | } 475 | 476 | Set getPendingSubscribeTopics() { 477 | return pendingSubscribeTopics; 478 | } 479 | 480 | HashMultimap getHandlerToSubscribtion() { 481 | return handlerToSubscribtion; 482 | } 483 | 484 | Set getServerSubscriptions() { 485 | return serverSubscriptions; 486 | } 487 | 488 | IntObjectHashMap getPendingServerUnsubscribes() { 489 | return pendingServerUnsubscribes; 490 | } 491 | 492 | IntObjectHashMap getPendingPublishes() { 493 | return pendingPublishes; 494 | } 495 | 496 | IntObjectHashMap getQos2PendingIncomingPublishes() { 497 | return qos2PendingIncomingPublishes; 498 | } 499 | 500 | private class MqttChannelInitializer extends ChannelInitializer { 501 | 502 | private final Promise connectFuture; 503 | private final String host; 504 | private final int port; 505 | private final SslContext sslContext; 506 | private final Consumer sslEngineConsumer; 507 | 508 | 509 | public MqttChannelInitializer(Promise connectFuture, 510 | String host, 511 | int port, 512 | SslContext sslContext, 513 | Consumer engineConsumer) { 514 | this.connectFuture = connectFuture; 515 | this.host = host; 516 | this.port = port; 517 | this.sslContext = sslContext; 518 | this.sslEngineConsumer = engineConsumer; 519 | } 520 | 521 | @Override 522 | protected void initChannel(SocketChannel ch) throws Exception { 523 | if (sslContext != null) { 524 | 525 | SSLEngine engine = sslContext.newEngine(ch.alloc(), host, port); 526 | sslEngineConsumer.accept(engine); 527 | ch.pipeline().addFirst("ssl", new SslHandler(engine)); 528 | } 529 | 530 | ch.pipeline().addLast("mqttDecoder", new MqttDecoder()); 531 | ch.pipeline().addLast("mqttEncoder", MqttEncoder.INSTANCE); 532 | ch.pipeline().addLast("idleStateHandler", new IdleStateHandler(MqttClientImpl.this.clientConfig.getTimeoutSeconds(), MqttClientImpl.this.clientConfig.getTimeoutSeconds(), 0)); 533 | ch.pipeline().addLast("mqttPingHandler", new MqttPingHandler(MqttClientImpl.this.clientConfig.getTimeoutSeconds())); 534 | ch.pipeline().addLast("mqttHandler", new MqttChannelHandler(MqttClientImpl.this, connectFuture)); 535 | } 536 | } 537 | 538 | MqttHandler getDefaultHandler() { 539 | return defaultHandler; 540 | } 541 | 542 | } 543 | -------------------------------------------------------------------------------- /src/main/java/org/jetlinks/mqtt/client/MqttConnectResult.java: -------------------------------------------------------------------------------- 1 | package org.jetlinks.mqtt.client; 2 | 3 | import io.netty.channel.ChannelFuture; 4 | import io.netty.handler.codec.mqtt.MqttConnectReturnCode; 5 | 6 | @SuppressWarnings({"WeakerAccess", "unused"}) 7 | public final class MqttConnectResult { 8 | 9 | private final boolean success; 10 | private final MqttConnectReturnCode returnCode; 11 | private final ChannelFuture closeFuture; 12 | 13 | MqttConnectResult(boolean success, MqttConnectReturnCode returnCode, ChannelFuture closeFuture) { 14 | this.success = success; 15 | this.returnCode = returnCode; 16 | this.closeFuture = closeFuture; 17 | } 18 | 19 | public boolean isSuccess() { 20 | return success; 21 | } 22 | 23 | public MqttConnectReturnCode getReturnCode() { 24 | return returnCode; 25 | } 26 | 27 | public ChannelFuture getCloseFuture() { 28 | return closeFuture; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/org/jetlinks/mqtt/client/MqttHandler.java: -------------------------------------------------------------------------------- 1 | package org.jetlinks.mqtt.client; 2 | 3 | import io.netty.buffer.ByteBuf; 4 | 5 | public interface MqttHandler { 6 | 7 | void onMessage(String topic, ByteBuf payload); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/org/jetlinks/mqtt/client/MqttIncomingQos2Publish.java: -------------------------------------------------------------------------------- 1 | package org.jetlinks.mqtt.client; 2 | 3 | import io.netty.channel.EventLoop; 4 | import io.netty.handler.codec.mqtt.*; 5 | 6 | import java.util.function.Consumer; 7 | 8 | final class MqttIncomingQos2Publish { 9 | 10 | private final MqttPublishMessage incomingPublish; 11 | 12 | private final RetransmissionHandler retransmissionHandler = new RetransmissionHandler<>(); 13 | 14 | MqttIncomingQos2Publish(MqttPublishMessage incomingPublish, MqttMessage originalMessage) { 15 | this.incomingPublish = incomingPublish; 16 | 17 | this.retransmissionHandler.setOriginalMessage(originalMessage); 18 | } 19 | 20 | MqttPublishMessage getIncomingPublish() { 21 | return incomingPublish; 22 | } 23 | 24 | void startPubrecRetransmitTimer(EventLoop eventLoop, Consumer sendPacket) { 25 | this.retransmissionHandler.setHandle((fixedHeader, originalMessage) -> 26 | sendPacket.accept(new MqttMessage(fixedHeader, originalMessage.variableHeader()))); 27 | this.retransmissionHandler.start(eventLoop); 28 | } 29 | 30 | void onPubrelReceived() { 31 | this.retransmissionHandler.stop(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/org/jetlinks/mqtt/client/MqttLastWill.java: -------------------------------------------------------------------------------- 1 | package org.jetlinks.mqtt.client; 2 | 3 | import io.netty.handler.codec.mqtt.MqttQoS; 4 | 5 | @SuppressWarnings({"WeakerAccess", "unused", "SimplifiableIfStatement", "StringBufferReplaceableByString"}) 6 | public final class MqttLastWill { 7 | 8 | private final String topic; 9 | private final String message; 10 | private final boolean retain; 11 | private final MqttQoS qos; 12 | 13 | public MqttLastWill(String topic, String message, boolean retain, MqttQoS qos) { 14 | if(topic == null){ 15 | throw new NullPointerException("topic"); 16 | } 17 | if(message == null){ 18 | throw new NullPointerException("message"); 19 | } 20 | if(qos == null){ 21 | throw new NullPointerException("qos"); 22 | } 23 | this.topic = topic; 24 | this.message = message; 25 | this.retain = retain; 26 | this.qos = qos; 27 | } 28 | 29 | public String getTopic() { 30 | return topic; 31 | } 32 | 33 | public String getMessage() { 34 | return message; 35 | } 36 | 37 | public boolean isRetain() { 38 | return retain; 39 | } 40 | 41 | public MqttQoS getQos() { 42 | return qos; 43 | } 44 | 45 | public static MqttLastWill.Builder builder(){ 46 | return new MqttLastWill.Builder(); 47 | } 48 | 49 | public static final class Builder { 50 | 51 | private String topic; 52 | private String message; 53 | private boolean retain; 54 | private MqttQoS qos; 55 | 56 | public String getTopic() { 57 | return topic; 58 | } 59 | 60 | public Builder setTopic(String topic) { 61 | if(topic == null){ 62 | throw new NullPointerException("topic"); 63 | } 64 | this.topic = topic; 65 | return this; 66 | } 67 | 68 | public String getMessage() { 69 | return message; 70 | } 71 | 72 | public Builder setMessage(String message) { 73 | if(message == null){ 74 | throw new NullPointerException("message"); 75 | } 76 | this.message = message; 77 | return this; 78 | } 79 | 80 | public boolean isRetain() { 81 | return retain; 82 | } 83 | 84 | public Builder setRetain(boolean retain) { 85 | this.retain = retain; 86 | return this; 87 | } 88 | 89 | public MqttQoS getQos() { 90 | return qos; 91 | } 92 | 93 | public Builder setQos(MqttQoS qos) { 94 | if(qos == null){ 95 | throw new NullPointerException("qos"); 96 | } 97 | this.qos = qos; 98 | return this; 99 | } 100 | 101 | public MqttLastWill build(){ 102 | return new MqttLastWill(topic, message, retain, qos); 103 | } 104 | } 105 | 106 | @Override 107 | public boolean equals(Object o) { 108 | if (this == o) return true; 109 | if (o == null || getClass() != o.getClass()) return false; 110 | 111 | MqttLastWill that = (MqttLastWill) o; 112 | 113 | if (retain != that.retain) return false; 114 | if (!topic.equals(that.topic)) return false; 115 | if (!message.equals(that.message)) return false; 116 | return qos == that.qos; 117 | 118 | } 119 | 120 | @Override 121 | public int hashCode() { 122 | int result = topic.hashCode(); 123 | result = 31 * result + message.hashCode(); 124 | result = 31 * result + (retain ? 1 : 0); 125 | result = 31 * result + qos.hashCode(); 126 | return result; 127 | } 128 | 129 | @Override 130 | public String toString() { 131 | final StringBuilder sb = new StringBuilder("MqttLastWill{"); 132 | sb.append("topic='").append(topic).append('\''); 133 | sb.append(", message='").append(message).append('\''); 134 | sb.append(", retain=").append(retain); 135 | sb.append(", qos=").append(qos.name()); 136 | sb.append('}'); 137 | return sb.toString(); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/main/java/org/jetlinks/mqtt/client/MqttPendingPublish.java: -------------------------------------------------------------------------------- 1 | package org.jetlinks.mqtt.client; 2 | 3 | import io.netty.buffer.ByteBuf; 4 | import io.netty.channel.EventLoop; 5 | import io.netty.handler.codec.mqtt.MqttMessage; 6 | import io.netty.handler.codec.mqtt.MqttPublishMessage; 7 | import io.netty.handler.codec.mqtt.MqttQoS; 8 | import io.netty.util.concurrent.Promise; 9 | 10 | import java.util.function.Consumer; 11 | 12 | final class MqttPendingPublish { 13 | 14 | private final int messageId; 15 | private final Promise future; 16 | private final ByteBuf payload; 17 | private final MqttPublishMessage message; 18 | private final MqttQoS qos; 19 | 20 | private final RetransmissionHandler publishRetransmissionHandler = new RetransmissionHandler<>(); 21 | private final RetransmissionHandler pubrelRetransmissionHandler = new RetransmissionHandler<>(); 22 | 23 | private boolean sent = false; 24 | 25 | MqttPendingPublish(int messageId, Promise future, ByteBuf payload, MqttPublishMessage message, MqttQoS qos) { 26 | this.messageId = messageId; 27 | this.future = future; 28 | this.payload = payload; 29 | this.message = message; 30 | this.qos = qos; 31 | 32 | this.publishRetransmissionHandler.setOriginalMessage(message); 33 | } 34 | 35 | int getMessageId() { 36 | return messageId; 37 | } 38 | 39 | Promise getFuture() { 40 | return future; 41 | } 42 | 43 | ByteBuf getPayload() { 44 | return payload; 45 | } 46 | 47 | boolean isSent() { 48 | return sent; 49 | } 50 | 51 | void setSent(boolean sent) { 52 | this.sent = sent; 53 | } 54 | 55 | MqttPublishMessage getMessage() { 56 | return message; 57 | } 58 | 59 | MqttQoS getQos() { 60 | return qos; 61 | } 62 | 63 | void startPublishRetransmissionTimer(EventLoop eventLoop, Consumer sendPacket) { 64 | this.publishRetransmissionHandler.setHandle(((fixedHeader, originalMessage) -> 65 | sendPacket.accept(new MqttPublishMessage(fixedHeader, originalMessage.variableHeader(), this.payload.retain())))); 66 | this.publishRetransmissionHandler.start(eventLoop); 67 | } 68 | 69 | void onPubackReceived() { 70 | this.publishRetransmissionHandler.stop(); 71 | } 72 | 73 | void setPubrelMessage(MqttMessage pubrelMessage) { 74 | this.pubrelRetransmissionHandler.setOriginalMessage(pubrelMessage); 75 | } 76 | 77 | void startPubrelRetransmissionTimer(EventLoop eventLoop, Consumer sendPacket) { 78 | this.pubrelRetransmissionHandler.setHandle((fixedHeader, originalMessage) -> 79 | sendPacket.accept(new MqttMessage(fixedHeader, originalMessage.variableHeader()))); 80 | this.pubrelRetransmissionHandler.start(eventLoop); 81 | } 82 | 83 | void onPubcompReceived() { 84 | this.pubrelRetransmissionHandler.stop(); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/main/java/org/jetlinks/mqtt/client/MqttPendingSubscription.java: -------------------------------------------------------------------------------- 1 | package org.jetlinks.mqtt.client; 2 | 3 | import io.netty.channel.EventLoop; 4 | import io.netty.handler.codec.mqtt.MqttSubscribeMessage; 5 | import io.netty.util.concurrent.Promise; 6 | 7 | import java.util.HashSet; 8 | import java.util.Set; 9 | import java.util.function.Consumer; 10 | 11 | final class MqttPendingSubscription { 12 | 13 | private final Promise future; 14 | private final String topic; 15 | private final Set handlers = new HashSet<>(); 16 | private final MqttSubscribeMessage subscribeMessage; 17 | 18 | private final RetransmissionHandler retransmissionHandler = new RetransmissionHandler<>(); 19 | 20 | private boolean sent = false; 21 | 22 | MqttPendingSubscription(Promise future, String topic, MqttSubscribeMessage message) { 23 | this.future = future; 24 | this.topic = topic; 25 | this.subscribeMessage = message; 26 | 27 | this.retransmissionHandler.setOriginalMessage(message); 28 | } 29 | 30 | Promise getFuture() { 31 | return future; 32 | } 33 | 34 | String getTopic() { 35 | return topic; 36 | } 37 | 38 | boolean isSent() { 39 | return sent; 40 | } 41 | 42 | void setSent(boolean sent) { 43 | this.sent = sent; 44 | } 45 | 46 | MqttSubscribeMessage getSubscribeMessage() { 47 | return subscribeMessage; 48 | } 49 | 50 | void addHandler(MqttHandler handler, boolean once){ 51 | this.handlers.add(new MqttPendingHandler(handler, once)); 52 | } 53 | 54 | Set getHandlers() { 55 | return handlers; 56 | } 57 | 58 | void startRetransmitTimer(EventLoop eventLoop, Consumer sendPacket) { 59 | if(this.sent){ //If the packet is sent, we can start the retransmit timer 60 | this.retransmissionHandler.setHandle((fixedHeader, originalMessage) -> 61 | sendPacket.accept(new MqttSubscribeMessage(fixedHeader, originalMessage.variableHeader(), originalMessage.payload()))); 62 | this.retransmissionHandler.start(eventLoop); 63 | } 64 | } 65 | 66 | void onSubackReceived(){ 67 | this.retransmissionHandler.stop(); 68 | } 69 | 70 | final class MqttPendingHandler { 71 | private final MqttHandler handler; 72 | private final boolean once; 73 | 74 | MqttPendingHandler(MqttHandler handler, boolean once) { 75 | this.handler = handler; 76 | this.once = once; 77 | } 78 | 79 | MqttHandler getHandler() { 80 | return handler; 81 | } 82 | 83 | boolean isOnce() { 84 | return once; 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/org/jetlinks/mqtt/client/MqttPendingUnsubscription.java: -------------------------------------------------------------------------------- 1 | package org.jetlinks.mqtt.client; 2 | 3 | import io.netty.channel.EventLoop; 4 | import io.netty.handler.codec.mqtt.MqttUnsubscribeMessage; 5 | import io.netty.util.concurrent.Promise; 6 | 7 | import java.util.function.Consumer; 8 | 9 | final class MqttPendingUnsubscription { 10 | 11 | private final Promise future; 12 | private final String topic; 13 | 14 | private final RetransmissionHandler retransmissionHandler = new RetransmissionHandler<>(); 15 | 16 | MqttPendingUnsubscription(Promise future, String topic, MqttUnsubscribeMessage unsubscribeMessage) { 17 | this.future = future; 18 | this.topic = topic; 19 | 20 | this.retransmissionHandler.setOriginalMessage(unsubscribeMessage); 21 | } 22 | 23 | Promise getFuture() { 24 | return future; 25 | } 26 | 27 | String getTopic() { 28 | return topic; 29 | } 30 | 31 | void startRetransmissionTimer(EventLoop eventLoop, Consumer sendPacket) { 32 | this.retransmissionHandler.setHandle((fixedHeader, originalMessage) -> 33 | sendPacket.accept(new MqttUnsubscribeMessage(fixedHeader, originalMessage.variableHeader(), originalMessage.payload()))); 34 | this.retransmissionHandler.start(eventLoop); 35 | } 36 | 37 | void onUnsubackReceived(){ 38 | this.retransmissionHandler.stop(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/org/jetlinks/mqtt/client/MqttPingHandler.java: -------------------------------------------------------------------------------- 1 | package org.jetlinks.mqtt.client; 2 | 3 | import io.netty.channel.Channel; 4 | import io.netty.channel.ChannelFutureListener; 5 | import io.netty.channel.ChannelHandlerContext; 6 | import io.netty.channel.ChannelInboundHandlerAdapter; 7 | import io.netty.handler.codec.mqtt.MqttFixedHeader; 8 | import io.netty.handler.codec.mqtt.MqttMessage; 9 | import io.netty.handler.codec.mqtt.MqttMessageType; 10 | import io.netty.handler.codec.mqtt.MqttQoS; 11 | import io.netty.handler.timeout.IdleStateEvent; 12 | import io.netty.util.ReferenceCountUtil; 13 | import io.netty.util.concurrent.ScheduledFuture; 14 | 15 | import java.util.concurrent.TimeUnit; 16 | 17 | final class MqttPingHandler extends ChannelInboundHandlerAdapter { 18 | 19 | private final int keepaliveSeconds; 20 | 21 | private ScheduledFuture pingRespTimeout; 22 | 23 | MqttPingHandler(int keepaliveSeconds) { 24 | this.keepaliveSeconds = keepaliveSeconds; 25 | } 26 | 27 | @Override 28 | public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { 29 | if (!(msg instanceof MqttMessage)) { 30 | ctx.fireChannelRead(msg); 31 | return; 32 | } 33 | MqttMessage message = (MqttMessage) msg; 34 | if(message.fixedHeader().messageType() == MqttMessageType.PINGREQ){ 35 | this.handlePingReq(ctx.channel()); 36 | } else if(message.fixedHeader().messageType() == MqttMessageType.PINGRESP){ 37 | this.handlePingResp(); 38 | }else{ 39 | ctx.fireChannelRead(ReferenceCountUtil.retain(msg)); 40 | } 41 | } 42 | 43 | @Override 44 | public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { 45 | super.userEventTriggered(ctx, evt); 46 | 47 | if(evt instanceof IdleStateEvent){ 48 | IdleStateEvent event = (IdleStateEvent) evt; 49 | switch(event.state()){ 50 | case READER_IDLE: 51 | break; 52 | case WRITER_IDLE: 53 | this.sendPingReq(ctx.channel()); 54 | break; 55 | } 56 | } 57 | } 58 | 59 | private void sendPingReq(Channel channel){ 60 | MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PINGREQ, false, MqttQoS.AT_MOST_ONCE, false, 0); 61 | channel.writeAndFlush(new MqttMessage(fixedHeader)); 62 | 63 | if(this.pingRespTimeout != null){ 64 | this.pingRespTimeout = channel.eventLoop().schedule(() -> { 65 | MqttFixedHeader fixedHeader2 = new MqttFixedHeader(MqttMessageType.DISCONNECT, false, MqttQoS.AT_MOST_ONCE, false, 0); 66 | channel.writeAndFlush(new MqttMessage(fixedHeader2)).addListener(ChannelFutureListener.CLOSE); 67 | //TODO: what do when the connection is closed ? 68 | }, this.keepaliveSeconds, TimeUnit.SECONDS); 69 | } 70 | } 71 | 72 | private void handlePingReq(Channel channel){ 73 | MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PINGRESP, false, MqttQoS.AT_MOST_ONCE, false, 0); 74 | channel.writeAndFlush(new MqttMessage(fixedHeader)); 75 | } 76 | 77 | private void handlePingResp(){ 78 | if(this.pingRespTimeout != null && !this.pingRespTimeout.isCancelled() && !this.pingRespTimeout.isDone()){ 79 | this.pingRespTimeout.cancel(true); 80 | this.pingRespTimeout = null; 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/org/jetlinks/mqtt/client/MqttSubscription.java: -------------------------------------------------------------------------------- 1 | package org.jetlinks.mqtt.client; 2 | 3 | import java.util.regex.Pattern; 4 | 5 | final class MqttSubscription { 6 | 7 | private final String topic; 8 | private final Pattern topicRegex; 9 | private final MqttHandler handler; 10 | 11 | private final boolean once; 12 | 13 | private boolean called; 14 | 15 | MqttSubscription(String topic, MqttHandler handler, boolean once) { 16 | if(topic == null){ 17 | throw new NullPointerException("topic"); 18 | } 19 | if(handler == null){ 20 | throw new NullPointerException("handler"); 21 | } 22 | this.topic = topic; 23 | this.handler = handler; 24 | this.once = once; 25 | this.topicRegex = Pattern.compile(topic.replace("+", "[^/]+").replace("#", ".+") + "$"); 26 | } 27 | 28 | String getTopic() { 29 | return topic; 30 | } 31 | 32 | public MqttHandler getHandler() { 33 | return handler; 34 | } 35 | 36 | boolean isOnce() { 37 | return once; 38 | } 39 | 40 | boolean isCalled() { 41 | return called; 42 | } 43 | 44 | boolean matches(String topic){ 45 | return this.topicRegex.matcher(topic).matches(); 46 | } 47 | 48 | @Override 49 | public boolean equals(Object o) { 50 | if (this == o) return true; 51 | if (o == null || getClass() != o.getClass()) return false; 52 | 53 | MqttSubscription that = (MqttSubscription) o; 54 | 55 | return once == that.once && topic.equals(that.topic) && handler.equals(that.handler); 56 | } 57 | 58 | @Override 59 | public int hashCode() { 60 | int result = topic.hashCode(); 61 | result = 31 * result + handler.hashCode(); 62 | result = 31 * result + (once ? 1 : 0); 63 | return result; 64 | } 65 | 66 | void setCalled(boolean called) { 67 | this.called = called; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/org/jetlinks/mqtt/client/RetransmissionHandler.java: -------------------------------------------------------------------------------- 1 | package org.jetlinks.mqtt.client; 2 | 3 | import io.netty.channel.EventLoop; 4 | import io.netty.handler.codec.mqtt.MqttFixedHeader; 5 | import io.netty.handler.codec.mqtt.MqttMessage; 6 | import io.netty.util.concurrent.ScheduledFuture; 7 | 8 | import java.util.concurrent.TimeUnit; 9 | import java.util.function.BiConsumer; 10 | 11 | final class RetransmissionHandler { 12 | 13 | private ScheduledFuture timer; 14 | private int timeout = 10; 15 | private BiConsumer handler; 16 | private T originalMessage; 17 | 18 | void start(EventLoop eventLoop){ 19 | if(eventLoop == null){ 20 | throw new NullPointerException("eventLoop"); 21 | } 22 | if(this.handler == null){ 23 | throw new NullPointerException("handler"); 24 | } 25 | this.timeout = 10; 26 | this.startTimer(eventLoop); 27 | } 28 | 29 | private void startTimer(EventLoop eventLoop){ 30 | this.timer = eventLoop.schedule(() -> { 31 | this.timeout += 5; 32 | MqttFixedHeader fixedHeader = new MqttFixedHeader(this.originalMessage.fixedHeader().messageType(), true, this.originalMessage.fixedHeader().qosLevel(), this.originalMessage.fixedHeader().isRetain(), this.originalMessage.fixedHeader().remainingLength()); 33 | handler.accept(fixedHeader, originalMessage); 34 | startTimer(eventLoop); 35 | }, timeout, TimeUnit.SECONDS); 36 | } 37 | 38 | void stop(){ 39 | if(this.timer != null){ 40 | this.timer.cancel(true); 41 | } 42 | } 43 | 44 | void setHandle(BiConsumer runnable) { 45 | this.handler = runnable; 46 | } 47 | 48 | void setOriginalMessage(T originalMessage) { 49 | this.originalMessage = originalMessage; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/test/java/org/jetlinks/mqtt/client/MqttClientImplTest.java: -------------------------------------------------------------------------------- 1 | package org.jetlinks.mqtt.client; 2 | 3 | import io.netty.channel.EventLoopGroup; 4 | import io.netty.channel.nio.NioEventLoopGroup; 5 | import io.netty.channel.socket.nio.NioSocketChannel; 6 | import io.netty.handler.codec.mqtt.MqttConnectReturnCode; 7 | import io.netty.handler.codec.mqtt.MqttVersion; 8 | 9 | import java.nio.charset.StandardCharsets; 10 | import java.util.concurrent.TimeUnit; 11 | 12 | 13 | /** 14 | * @author zhouhao 15 | * @since 16 | */ 17 | public class MqttClientImplTest { 18 | 19 | 20 | public static void main(String[] args) throws Exception { 21 | EventLoopGroup loop = new NioEventLoopGroup(Runtime.getRuntime().availableProcessors() * 2); 22 | 23 | MqttClient mqttClient = new MqttClientImpl(((topic, payload) -> { 24 | System.out.println(topic + "=>" + payload.toString(StandardCharsets.UTF_8)); 25 | })); 26 | 27 | mqttClient.setEventLoop(loop); 28 | mqttClient.getClientConfig().setChannelClass(NioSocketChannel.class); 29 | mqttClient.getClientConfig().setClientId("test1"); 30 | mqttClient.getClientConfig().setUsername("test1"); 31 | mqttClient.getClientConfig().setPassword("test"); 32 | mqttClient.getClientConfig().setProtocolVersion(MqttVersion.MQTT_3_1_1); 33 | mqttClient.getClientConfig().setReconnect(false); 34 | mqttClient.setCallback(new MqttClientCallback() { 35 | @Override 36 | public void connectionLost(Throwable cause) { 37 | 38 | cause.printStackTrace(); 39 | } 40 | 41 | @Override 42 | public void onSuccessfulReconnect() { 43 | 44 | } 45 | }); 46 | mqttClient.connect("127.0.0.1", 1883) 47 | .addListener(future -> { 48 | try { 49 | MqttConnectResult result = (MqttConnectResult) future.get(15, TimeUnit.SECONDS); 50 | if (result.getReturnCode() != MqttConnectReturnCode.CONNECTION_ACCEPTED) { 51 | System.out.println("error:" + result.getReturnCode() + "--"); 52 | mqttClient.disconnect(); 53 | } else { 54 | System.out.println("success:"); 55 | // mqttClient.publish("test", Unpooled.copiedBuffer("{\"type\":\"read-property\"}", StandardCharsets.UTF_8)); 56 | } 57 | } catch (Exception e) { 58 | e.printStackTrace(); 59 | } 60 | }).await(5, TimeUnit.SECONDS); 61 | 62 | } 63 | 64 | } --------------------------------------------------------------------------------