5 changed files with 396 additions and 0 deletions
@ -0,0 +1,27 @@ |
|||
/** |
|||
* Copyright © 2016-2022 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.mqtt.integration; |
|||
|
|||
import org.junit.extensions.cpsuite.ClasspathSuite; |
|||
import org.junit.runner.RunWith; |
|||
|
|||
@RunWith(ClasspathSuite.class) |
|||
@ClasspathSuite.ClassnameFilters({ |
|||
"org.thingsboard.mqtt.integration.*Test", |
|||
}) |
|||
public class IntegrationTestSuite { |
|||
|
|||
} |
|||
@ -0,0 +1,139 @@ |
|||
/** |
|||
* Copyright © 2016-2022 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.mqtt.integration; |
|||
|
|||
import io.netty.buffer.ByteBuf; |
|||
import io.netty.buffer.ByteBufAllocator; |
|||
import io.netty.buffer.UnpooledByteBufAllocator; |
|||
import io.netty.channel.EventLoopGroup; |
|||
import io.netty.channel.nio.NioEventLoopGroup; |
|||
import io.netty.handler.codec.mqtt.MqttMessageType; |
|||
import io.netty.handler.codec.mqtt.MqttQoS; |
|||
import io.netty.util.concurrent.Future; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.junit.After; |
|||
import org.junit.Assert; |
|||
import org.junit.Before; |
|||
import org.junit.Test; |
|||
import org.thingsboard.mqtt.MqttClient; |
|||
import org.thingsboard.mqtt.MqttClientConfig; |
|||
import org.thingsboard.mqtt.MqttConnectResult; |
|||
import org.thingsboard.mqtt.integration.server.MqttServer; |
|||
|
|||
import java.nio.charset.StandardCharsets; |
|||
import java.util.List; |
|||
import java.util.concurrent.CountDownLatch; |
|||
import java.util.concurrent.TimeUnit; |
|||
import java.util.concurrent.TimeoutException; |
|||
|
|||
@Slf4j |
|||
public class MqttIntegrationTest { |
|||
|
|||
static final String MQTT_HOST = "localhost"; |
|||
static final int KEEPALIVE_TIMEOUT_SECONDS = 2; |
|||
static final ByteBufAllocator ALLOCATOR = new UnpooledByteBufAllocator(false); |
|||
|
|||
EventLoopGroup eventLoopGroup; |
|||
MqttServer mqttServer; |
|||
|
|||
MqttClient mqttClient; |
|||
|
|||
@Before |
|||
public void init() throws Exception { |
|||
this.eventLoopGroup = new NioEventLoopGroup(); |
|||
|
|||
this.mqttServer = new MqttServer(); |
|||
this.mqttServer.init(); |
|||
} |
|||
|
|||
@After |
|||
public void destroy() throws InterruptedException { |
|||
if (this.mqttClient != null) { |
|||
this.mqttClient.disconnect(); |
|||
} |
|||
if (this.mqttServer != null) { |
|||
this.mqttServer.shutdown(); |
|||
} |
|||
if (this.eventLoopGroup != null) { |
|||
this.eventLoopGroup.shutdownGracefully(0, 5, TimeUnit.SECONDS); |
|||
} |
|||
} |
|||
|
|||
@Test |
|||
public void givenActiveMqttClient_whenNoActivityForKeepAliveTimeout_thenDisconnectClient() throws Throwable { |
|||
//given
|
|||
this.mqttClient = initClient(); |
|||
|
|||
log.warn("Sending publish messages..."); |
|||
CountDownLatch latch = new CountDownLatch(3); |
|||
for (int i = 0; i < 3; i++) { |
|||
Future<Void> pubFuture = publishMsg(); |
|||
pubFuture.addListener(future -> latch.countDown()); |
|||
} |
|||
|
|||
log.warn("Waiting for messages acknowledgments..."); |
|||
boolean awaitResult = latch.await(10, TimeUnit.SECONDS); |
|||
Assert.assertTrue(awaitResult); |
|||
|
|||
//when
|
|||
CountDownLatch keepAliveLatch = new CountDownLatch(1); |
|||
|
|||
log.warn("Starting idle period..."); |
|||
boolean keepaliveAwaitResult = keepAliveLatch.await(5, TimeUnit.SECONDS); |
|||
Assert.assertFalse(keepaliveAwaitResult); |
|||
|
|||
//then
|
|||
List<MqttMessageType> allReceivedEvents = this.mqttServer.getEventsFromClient(); |
|||
long pubCount = allReceivedEvents.stream().filter(mqttMessageType -> mqttMessageType == MqttMessageType.PUBLISH).count(); |
|||
long disconnectCount = allReceivedEvents.stream().filter(type -> type == MqttMessageType.DISCONNECT).count(); |
|||
|
|||
Assert.assertEquals(3, pubCount); |
|||
Assert.assertEquals(1, disconnectCount); |
|||
} |
|||
|
|||
private Future<Void> publishMsg() { |
|||
ByteBuf byteBuf = ALLOCATOR.buffer(); |
|||
byteBuf.writeBytes("payload".getBytes(StandardCharsets.UTF_8)); |
|||
return this.mqttClient.publish( |
|||
"test/topic", |
|||
byteBuf, |
|||
MqttQoS.AT_LEAST_ONCE); |
|||
} |
|||
|
|||
private MqttClient initClient() throws Exception { |
|||
MqttClientConfig config = new MqttClientConfig(); |
|||
config.setTimeoutSeconds(KEEPALIVE_TIMEOUT_SECONDS); |
|||
MqttClient client = MqttClient.create(config, null); |
|||
client.setEventLoop(this.eventLoopGroup); |
|||
Future<MqttConnectResult> connectFuture = client.connect(MQTT_HOST, this.mqttServer.getMqttPort()); |
|||
|
|||
String hostPort = MQTT_HOST + ":" + this.mqttServer.getMqttPort(); |
|||
MqttConnectResult result; |
|||
try { |
|||
result = connectFuture.get(10, TimeUnit.SECONDS); |
|||
} catch (TimeoutException ex) { |
|||
connectFuture.cancel(true); |
|||
client.disconnect(); |
|||
throw new RuntimeException(String.format("Failed to connect to MQTT server at %s.", hostPort)); |
|||
} |
|||
if (!result.isSuccess()) { |
|||
connectFuture.cancel(true); |
|||
client.disconnect(); |
|||
throw new RuntimeException(String.format("Failed to connect to MQTT server at %s. Result code is: %s", hostPort, result.getReturnCode())); |
|||
} |
|||
return client; |
|||
} |
|||
} |
|||
@ -0,0 +1,84 @@ |
|||
/** |
|||
* Copyright © 2016-2022 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.mqtt.integration.server; |
|||
|
|||
import io.netty.bootstrap.ServerBootstrap; |
|||
import io.netty.channel.Channel; |
|||
import io.netty.channel.ChannelInitializer; |
|||
import io.netty.channel.ChannelOption; |
|||
import io.netty.channel.ChannelPipeline; |
|||
import io.netty.channel.EventLoopGroup; |
|||
import io.netty.channel.nio.NioEventLoopGroup; |
|||
import io.netty.channel.socket.SocketChannel; |
|||
import io.netty.channel.socket.nio.NioServerSocketChannel; |
|||
import io.netty.handler.codec.mqtt.MqttDecoder; |
|||
import io.netty.handler.codec.mqtt.MqttEncoder; |
|||
import io.netty.handler.codec.mqtt.MqttMessageType; |
|||
import lombok.Getter; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
|
|||
import java.util.List; |
|||
import java.util.concurrent.CopyOnWriteArrayList; |
|||
|
|||
@Slf4j |
|||
public class MqttServer { |
|||
|
|||
@Getter |
|||
private final List<MqttMessageType> eventsFromClient = new CopyOnWriteArrayList<>(); |
|||
@Getter |
|||
private final int mqttPort = 8885; |
|||
|
|||
private Channel serverChannel; |
|||
private EventLoopGroup bossGroup; |
|||
private EventLoopGroup workerGroup; |
|||
|
|||
public void init() throws Exception { |
|||
log.info("Starting MQTT server..."); |
|||
bossGroup = new NioEventLoopGroup(); |
|||
workerGroup = new NioEventLoopGroup(); |
|||
ServerBootstrap b = new ServerBootstrap(); |
|||
b.group(bossGroup, workerGroup) |
|||
.channel(NioServerSocketChannel.class) |
|||
.childHandler(new ChannelInitializer<SocketChannel>() { |
|||
@Override |
|||
protected void initChannel(SocketChannel ch) throws Exception { |
|||
ChannelPipeline pipeline = ch.pipeline(); |
|||
pipeline.addLast("decoder", new MqttDecoder(65536)); |
|||
pipeline.addLast("encoder", MqttEncoder.INSTANCE); |
|||
|
|||
MqttTransportHandler handler = new MqttTransportHandler(eventsFromClient); |
|||
|
|||
pipeline.addLast(handler); |
|||
ch.closeFuture().addListener(handler); |
|||
} |
|||
}) |
|||
.childOption(ChannelOption.SO_KEEPALIVE, true); |
|||
|
|||
serverChannel = b.bind(mqttPort).sync().channel(); |
|||
log.info("Mqtt transport started!"); |
|||
} |
|||
|
|||
public void shutdown() throws InterruptedException { |
|||
log.info("Stopping MQTT transport!"); |
|||
try { |
|||
serverChannel.close().sync(); |
|||
} finally { |
|||
workerGroup.shutdownGracefully(); |
|||
bossGroup.shutdownGracefully(); |
|||
} |
|||
log.info("MQTT transport stopped!"); |
|||
} |
|||
} |
|||
@ -0,0 +1,141 @@ |
|||
/** |
|||
* Copyright © 2016-2022 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.mqtt.integration.server; |
|||
|
|||
import io.netty.channel.ChannelHandlerContext; |
|||
import io.netty.channel.ChannelInboundHandlerAdapter; |
|||
import io.netty.handler.codec.mqtt.MqttConnAckMessage; |
|||
import io.netty.handler.codec.mqtt.MqttConnAckVariableHeader; |
|||
import io.netty.handler.codec.mqtt.MqttConnectMessage; |
|||
import io.netty.handler.codec.mqtt.MqttConnectReturnCode; |
|||
import io.netty.handler.codec.mqtt.MqttFixedHeader; |
|||
import io.netty.handler.codec.mqtt.MqttMessage; |
|||
import io.netty.handler.codec.mqtt.MqttMessageIdVariableHeader; |
|||
import io.netty.handler.codec.mqtt.MqttMessageType; |
|||
import io.netty.handler.codec.mqtt.MqttPubAckMessage; |
|||
import io.netty.handler.codec.mqtt.MqttPublishMessage; |
|||
import io.netty.util.ReferenceCountUtil; |
|||
import io.netty.util.concurrent.Future; |
|||
import io.netty.util.concurrent.GenericFutureListener; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
|
|||
import java.util.List; |
|||
import java.util.UUID; |
|||
|
|||
import static io.netty.handler.codec.mqtt.MqttMessageType.CONNACK; |
|||
import static io.netty.handler.codec.mqtt.MqttMessageType.CONNECT; |
|||
import static io.netty.handler.codec.mqtt.MqttMessageType.DISCONNECT; |
|||
import static io.netty.handler.codec.mqtt.MqttMessageType.PINGREQ; |
|||
import static io.netty.handler.codec.mqtt.MqttMessageType.PUBACK; |
|||
import static io.netty.handler.codec.mqtt.MqttMessageType.PUBLISH; |
|||
import static io.netty.handler.codec.mqtt.MqttQoS.AT_MOST_ONCE; |
|||
|
|||
@Slf4j |
|||
public class MqttTransportHandler extends ChannelInboundHandlerAdapter implements GenericFutureListener<Future<? super Void>> { |
|||
|
|||
private final List<MqttMessageType> eventsFromClient; |
|||
private final UUID sessionId; |
|||
|
|||
MqttTransportHandler(List<MqttMessageType> eventsFromClient) { |
|||
this.sessionId = UUID.randomUUID(); |
|||
this.eventsFromClient = eventsFromClient; |
|||
} |
|||
|
|||
@Override |
|||
public void channelRead(ChannelHandlerContext ctx, Object msg) { |
|||
log.trace("[{}] Processing msg: {}", sessionId, msg); |
|||
try { |
|||
if (msg instanceof MqttMessage) { |
|||
MqttMessage message = (MqttMessage) msg; |
|||
if (message.decoderResult().isSuccess()) { |
|||
processMqttMsg(ctx, message); |
|||
} else { |
|||
log.error("[{}] Message decoding failed: {}", sessionId, message.decoderResult().cause().getMessage()); |
|||
ctx.close(); |
|||
} |
|||
} else { |
|||
log.debug("[{}] Received non mqtt message: {}", sessionId, msg.getClass().getSimpleName()); |
|||
ctx.close(); |
|||
} |
|||
} finally { |
|||
ReferenceCountUtil.safeRelease(msg); |
|||
} |
|||
} |
|||
|
|||
void processMqttMsg(ChannelHandlerContext ctx, MqttMessage msg) { |
|||
if (msg.fixedHeader() == null) { |
|||
ctx.close(); |
|||
return; |
|||
} |
|||
switch (msg.fixedHeader().messageType()) { |
|||
case CONNECT: |
|||
eventsFromClient.add(CONNECT); |
|||
processConnect(ctx, (MqttConnectMessage) msg); |
|||
break; |
|||
case DISCONNECT: |
|||
eventsFromClient.add(DISCONNECT); |
|||
ctx.close(); |
|||
break; |
|||
case PUBLISH: |
|||
// QoS 0 and 1 supported only here
|
|||
eventsFromClient.add(PUBLISH); |
|||
MqttPublishMessage mqttPubMsg = (MqttPublishMessage) msg; |
|||
ack(ctx, mqttPubMsg.variableHeader().packetId()); |
|||
break; |
|||
case PINGREQ: |
|||
// We will not handle PINGREQ and will not send any PINGRESP to simulate the MQTT server is down
|
|||
eventsFromClient.add(PINGREQ); |
|||
break; |
|||
default: |
|||
break; |
|||
} |
|||
} |
|||
|
|||
void processConnect(ChannelHandlerContext ctx, MqttConnectMessage msg) { |
|||
String userName = msg.payload().userName(); |
|||
String clientId = msg.payload().clientIdentifier(); |
|||
|
|||
log.warn("[{}][{}] Processing connect msg for client: {}!", sessionId, userName, clientId); |
|||
ctx.writeAndFlush(createMqttConnAckMsg(msg)); |
|||
} |
|||
|
|||
private MqttConnAckMessage createMqttConnAckMsg(MqttConnectMessage msg) { |
|||
MqttFixedHeader mqttFixedHeader = |
|||
new MqttFixedHeader(CONNACK, false, AT_MOST_ONCE, false, 0); |
|||
MqttConnAckVariableHeader mqttConnAckVariableHeader = |
|||
new MqttConnAckVariableHeader(MqttConnectReturnCode.CONNECTION_ACCEPTED, !msg.variableHeader().isCleanSession()); |
|||
return new MqttConnAckMessage(mqttFixedHeader, mqttConnAckVariableHeader); |
|||
} |
|||
|
|||
private void ack(ChannelHandlerContext ctx, int msgId) { |
|||
if (msgId > 0) { |
|||
ctx.writeAndFlush(createMqttPubAckMsg(msgId)); |
|||
} |
|||
} |
|||
|
|||
public static MqttPubAckMessage createMqttPubAckMsg(int requestId) { |
|||
MqttFixedHeader mqttFixedHeader = |
|||
new MqttFixedHeader(PUBACK, false, AT_MOST_ONCE, false, 0); |
|||
MqttMessageIdVariableHeader mqttMsgIdVariableHeader = |
|||
MqttMessageIdVariableHeader.from(requestId); |
|||
return new MqttPubAckMessage(mqttFixedHeader, mqttMsgIdVariableHeader); |
|||
} |
|||
|
|||
@Override |
|||
public void operationComplete(Future<? super Void> future) { |
|||
log.trace("[{}] Channel closed!", sessionId); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue