Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,14 @@
import org.apache.commons.lang3.StringUtils;
import org.apache.shenyu.common.utils.Singleton;
import org.apache.shenyu.protocol.mqtt.repositories.ChannelRepository;
import org.apache.shenyu.protocol.mqtt.repositories.MqttSession;
import org.apache.shenyu.protocol.mqtt.repositories.SessionRepository;
import org.apache.shenyu.protocol.mqtt.repositories.SubscribeRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Objects;

import static io.netty.channel.ChannelFutureListener.CLOSE_ON_FAILURE;
import static io.netty.handler.codec.mqtt.MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD;
import static io.netty.handler.codec.mqtt.MqttConnectReturnCode.CONNECTION_REFUSED_IDENTIFIER_REJECTED;
Expand Down Expand Up @@ -74,9 +79,30 @@ public void connect(final ChannelHandlerContext ctx, final MqttConnectMessage ms

// record connect
Singleton.INST.get(ChannelRepository.class).add(ctx.channel(), clientId);

boolean cleanSession = msg.variableHeader().isCleanSession();
SessionRepository sessionRepository = Singleton.INST.get(SessionRepository.class);
MqttSession session = sessionRepository.get(clientId);
// MQTT-3.2.2-6/7: session present is only true when the client connects
// with cleanSession=0 and the server holds a stored session for the clientId.
boolean sessionPresent = !cleanSession && Objects.nonNull(session);
if (cleanSession) {
// A clean-session connect must discard any previously stored session.
session = new MqttSession(clientId, true);
sessionRepository.add(clientId, session);
} else if (Objects.isNull(session)) {
session = new MqttSession(clientId, false);
sessionRepository.add(clientId, session);
}
if (sessionPresent && !session.getTopics().isEmpty()) {
// Resume the stored subscriptions (with their QoS) for the new channel.
Singleton.INST.get(SubscribeRepository.class)
.add(ctx.channel(), session.getTopicSubscriptions());
}

MqttConnAckMessage ackMessage = MqttMessageBuilders.connAck()
.returnCode(MqttConnectReturnCode.CONNECTION_ACCEPTED)
.sessionPresent(true)
.sessionPresent(sessionPresent)
.build();
ctx.writeAndFlush(ackMessage);
setConnected(ctx.channel(), true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@
import io.netty.channel.ChannelHandlerContext;
import org.apache.shenyu.common.utils.Singleton;
import org.apache.shenyu.protocol.mqtt.repositories.ChannelRepository;
import org.apache.shenyu.protocol.mqtt.repositories.MqttSession;
import org.apache.shenyu.protocol.mqtt.repositories.SessionRepository;
import org.apache.shenyu.protocol.mqtt.repositories.SubscribeRepository;

import java.util.Objects;

/**
* The DISCONNECT message is sent from the client to the server to indicate
Expand All @@ -37,7 +42,15 @@ public class Disconnect extends MessageType {
@Override
public void disconnect(final ChannelHandlerContext ctx) {
//// todo Last words
//// todo Clean session
String clientId = Singleton.INST.get(ChannelRepository.class).get(ctx.channel());
if (Objects.nonNull(clientId)) {
MqttSession session = Singleton.INST.get(SessionRepository.class).get(clientId);
if (Objects.nonNull(session) && session.isCleanSession()) {
// A clean-session disconnect discards all stored session state.
Singleton.INST.get(SessionRepository.class).remove(clientId);
}
}
Singleton.INST.get(SubscribeRepository.class).removeChannel(ctx.channel());
cleanChannel(ctx.channel());
ctx.close();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,10 @@ public void connect() {
messageType.pingReq(ctx);
break;
case PUBACK:
break;
case DISCONNECT:
messageType.disconnect(ctx);
break;
default:
break;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,15 @@
import io.netty.util.CharsetUtil;
import org.apache.commons.lang3.StringUtils;
import org.apache.shenyu.common.utils.Singleton;
import org.apache.shenyu.protocol.mqtt.repositories.ChannelRepository;
import org.apache.shenyu.protocol.mqtt.repositories.MqttSession;
import org.apache.shenyu.protocol.mqtt.repositories.SessionRepository;
import org.apache.shenyu.protocol.mqtt.repositories.SubscribeRepository;
import org.apache.shenyu.protocol.mqtt.repositories.TopicRepository;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

import static io.netty.channel.ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE;
Expand Down Expand Up @@ -69,6 +73,12 @@ public void subscribe(final ChannelHandlerContext ctx, final MqttSubscribeMessag

Singleton.INST.get(SubscribeRepository.class).add(ctx.channel(), mqttTopicSubscriptions);

String clientId = Singleton.INST.get(ChannelRepository.class).get(ctx.channel());
MqttSession session = Singleton.INST.get(SessionRepository.class).get(clientId);
if (Objects.nonNull(session)) {
mqttTopicSubscriptions.forEach(subscription -> session.addTopic(subscription.topicName(), subscription.qualityOfService()));
}

for (String ackTopic : ackTopics) {
String message = Singleton.INST.get(TopicRepository.class).get(ackTopic);
if (StringUtils.isNotEmpty(message)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,13 @@
import io.netty.handler.codec.mqtt.MqttUnsubscribeMessage;
import io.netty.handler.codec.mqtt.MqttUnsubAckMessage;
import org.apache.shenyu.common.utils.Singleton;
import org.apache.shenyu.protocol.mqtt.repositories.ChannelRepository;
import org.apache.shenyu.protocol.mqtt.repositories.MqttSession;
import org.apache.shenyu.protocol.mqtt.repositories.SessionRepository;
import org.apache.shenyu.protocol.mqtt.repositories.SubscribeRepository;

import java.util.List;
import java.util.Objects;

import static io.netty.channel.ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE;
import static io.netty.handler.codec.mqtt.MqttMessageIdVariableHeader.from;
Expand All @@ -46,6 +50,11 @@ public void unsubscribe(final ChannelHandlerContext ctx, final MqttUnsubscribeMe
List<String> topics = msg.payload().topics();
Channel channel = ctx.channel();
Singleton.INST.get(SubscribeRepository.class).remove(topics, channel);
String clientId = Singleton.INST.get(ChannelRepository.class).get(channel);
MqttSession session = Singleton.INST.get(SessionRepository.class).get(clientId);
if (Objects.nonNull(session)) {
topics.forEach(session::removeTopic);
}
int packetId = msg.variableHeader().messageId();
MqttFixedHeader mqttFixedHeader = new MqttFixedHeader(MqttMessageType.UNSUBACK, false, MqttQoS.AT_MOST_ONCE, false, 0);
MqttUnsubAckMessage mqttUnsubAckMessage = new MqttUnsubAckMessage(mqttFixedHeader, from(packetId));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.shenyu.protocol.mqtt.repositories;

import io.netty.handler.codec.mqtt.MqttQoS;
import io.netty.handler.codec.mqtt.MqttTopicSubscription;

import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

/**
* MQTT session state stored for a client.
*/
public class MqttSession {

private final String clientId;

private final boolean cleanSession;

private final Map<String, MqttQoS> topics = new ConcurrentHashMap<>();

/**
* MqttSession constructor.
* @param clientId clientId
* @param cleanSession cleanSession
*/
public MqttSession(final String clientId, final boolean cleanSession) {
this.clientId = clientId;
this.cleanSession = cleanSession;
}

/**
* get clientId.
* @return clientId
*/
public String getClientId() {
return clientId;
}

/**
* get cleanSession.
* @return cleanSession
*/
public boolean isCleanSession() {
return cleanSession;
}

/**
* add topic with its QoS, replacing any existing subscription for the same topic.
* @param topic topic
* @param qos qos
* @return true if the topic was newly added
*/
public boolean addTopic(final String topic, final MqttQoS qos) {
return Objects.isNull(topics.put(topic, qos));
}

/**
* remove topic.
* @param topic topic
* @return true if the topic was removed
*/
public boolean removeTopic(final String topic) {
return Objects.nonNull(topics.remove(topic));
}

/**
* get topics.
* @return topics
*/
public Set<String> getTopics() {
return topics.keySet();
}

/**
* get topic subscriptions with their QoS for session resume.
* @return topic subscriptions
*/
public List<MqttTopicSubscription> getTopicSubscriptions() {
return topics.entrySet().stream()
.map(entry -> new MqttTopicSubscription(entry.getKey(), entry.getValue()))
.collect(Collectors.toList());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.shenyu.protocol.mqtt.repositories;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
* Session repository, keyed by clientId.
*/
public class SessionRepository implements BaseRepository<String, MqttSession> {

private static final Map<String, MqttSession> SESSION_FACTORY = new ConcurrentHashMap<>();

@Override
public void add(final String clientId, final MqttSession session) {
SESSION_FACTORY.put(clientId, session);
}

@Override
public void remove(final String clientId) {
SESSION_FACTORY.remove(clientId);
}

@Override
public MqttSession get(final String clientId) {
return SESSION_FACTORY.get(clientId);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,8 @@ public class SubscribeRepository implements BaseRepository<List<String>, List<Ch

@Override
public void add(final List<String> topics, final List<Channel> channels) {
CompletableFuture.runAsync(() -> topics.parallelStream().forEach(s -> {
List<Channel> list = get(s);
list.addAll(channels);
TOPIC_CHANNEL_FACTORY.put(s, list);
}));
CompletableFuture.runAsync(() -> topics.parallelStream().forEach(s ->
TOPIC_CHANNEL_FACTORY.computeIfAbsent(s, key -> new CopyOnWriteArrayList<>()).addAll(channels)));
}

/**
Expand All @@ -55,11 +52,8 @@ public void add(final List<String> topics, final List<Channel> channels) {
* @param mqttTopicSubscription mqtt subscription info
*/
public void add(final Channel channel, final List<MqttTopicSubscription> mqttTopicSubscription) {
CompletableFuture.runAsync(() -> mqttTopicSubscription.parallelStream().forEach(s -> {
List<Channel> channels = get(s.topicName());
channels.add(channel);
TOPIC_CHANNEL_FACTORY.put(s.topicName(), channels);
}));
CompletableFuture.runAsync(() -> mqttTopicSubscription.parallelStream().forEach(s ->
TOPIC_CHANNEL_FACTORY.computeIfAbsent(s.topicName(), key -> new CopyOnWriteArrayList<>()).add(channel)));
}

@Override
Expand All @@ -81,6 +75,14 @@ public void remove(final List<String> topics, final Channel channel) {
}));
}

/**
* remove channel from all topics.
* @param channel channel
*/
public void removeChannel(final Channel channel) {
CompletableFuture.runAsync(() -> TOPIC_CHANNEL_FACTORY.values().forEach(channels -> channels.remove(channel)));
}

@Override
public List<Channel> get(final List<String> topics) {
Set<Channel> channels = new CopyOnWriteArraySet<>();
Expand Down
Loading
Loading