├── .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 | [](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 extends Channel> 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 extends Channel> getChannelClass() {
123 | return channelClass;
124 | }
125 |
126 | public void setChannelClass(Class extends Channel> 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