简介
本文主要介绍怎么用java客户端paho连接emqx并实现共享订阅,所谓共享订阅就是在开多个节点的客户端消费时,保证一条消息有且仅有一个节点消费,不会造成重复消费。若对您有帮助请帮忙点个star。
本文讲解怎么使用java客户端paho连接emqx并实现共享订阅,emqx安装部署请移步我的下一篇文章linux环境安装emqx单机版和集群版。如对您有帮助请收藏评论,不胜感激。
文章源码地址:https://github.com/itwwj/iot-project.git 中的iot-Shore项目 其他项目可以忽略
1.依赖导入
<properties>
<springboot.version>2.3.6.RELEASE</springboot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${springboot.version}</version>
</dependency>
<!-- test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${springboot.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.paho</groupId>
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
<version>1.2.2</version>
</dependency>
</dependencies>
2.写入配置文件和配置类
配置文件:
在配置文件:application.yml 添加以下配置
emqx:
broker: tcp://192.168.1.177:1883 #broker地址
userName: root #授权账号 一定要授权的
password: root #密码
cleanSession: true #是否清除会话
reconnect: true #是否断线重连
timeout: 20 #连接超时时间
keepAlive: 10 #心跳间隔
配置类:
/**
* 配置类
* @author jie
*/
@Data
@Component
@ConfigurationProperties(prefix = PREFIX)
public class EmqProperties {
public static final String PREFIX="emqx";
/**
* emq服务器地址
*/
private String broker;
/**
* 用户名
*/
private String userName;
/**
* 密码
*/
private String password;
/**
* 设置是否清空session,这里如果设置为false表示服务器会保留客户端的连接记录,这里设置为true表示每次连接到服务器都以新的身份连接
*/
private Boolean cleanSession;
/**
* 是否断线重连
*/
private Boolean reconnect;
/**
* 连接超时时间
*/
private Integer timeout;
/**
* 心跳间隔
*/
private Integer keepAlive;
}
3.自定义主题消费类注解
此注解只有一个作用,将topic和mqtt报文消费类绑定,注意此注解我加了个@Component默认加此注解的类都会被spring管理。可以直接注入bean。
/**
* 自定义标记注解
* @author jie
*/
@Component
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Topic {
/**
* topic
* @return
*/
String topic() default "";
/**
* qos
* @return
*/
int qos() default 0;
/**
* 订阅模式
* @return
*/
Pattern patten() default Pattern.NONE;
/**
* 共享订阅组
* @return
*/
String group() default "group1";
}
订阅模式枚举:
/**
* 订阅模式
* @author jie
*/
public enum Pattern {
/**
* 普通订阅
*/
NONE,
/**
* 不带群组的共享订阅
*/
QUEUE,
/**
* 带群组的共享订阅
*/
SHARE;
}
4.连接broker核心类
自定义topic映射类:
/**
* @author jie
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SubscriptTopic {
/**
* 原主题
*/
private String topic;
/**
* 订阅主题
*/
private String subTopic;
/**
* 订阅模式
*/
private Pattern pattern;
/**
* 消息等级
*/
private int qos;
/**
* 消费类
*/
private IMqttMessageListener messageListener;
}
回调类:
@Slf4j
public class MqttCallback implements MqttCallbackExtended {
private List<SubscriptTopic> topicMap;
public MqttCallback(List<SubscriptTopic> topicMap) {
this.topicMap = topicMap;
}
/**
* 客户端断开后触发
*
* @param throwable 异常
*/
@SneakyThrows
@Override
public void connectionLost(Throwable throwable) {
MqttClient client = ApplicationContextUtil.getBean(MqttClient.class);
MqttConnectOptions option = ApplicationContextUtil.getBean(MqttConnectOptions.class);
while (!client.isConnected()) {
log.info("emqx重新连接....................................................");
client.connect(option);
Thread.sleep(1000);
}
}
/**
* 客户端收到消息触发
*
* @param topic 主题
* @param message 消息
*/
@Override
public void messageArrived(String topic, MqttMessage message) throws Exception {
for (SubscriptTopic subscriptTopic : topicMap) {
if (subscriptTopic.getPattern() != Pattern.NONE && isMatched(subscriptTopic.getTopic(), topic)) {
subscriptTopic.getMessageListener().messageArrived(topic, message);
break;
}
}
}
/**
* 检测一个主题是否为一个通配符表示的子主题
*
* @param topicFilter 通配符主题
* @param topic 子主题
* @return 是否为通配符主题的子主题
*/
private boolean isMatched(String topicFilter, String topic) {
return MqttTopic.isMatched(topicFilter, topic);
}
/**
* 发布消息成功
*
* @param token token
*/
@SneakyThrows
@Override
public void deliveryComplete(IMqttDeliveryToken token) {
String[] topics = token.getTopics();
for (String topic : topics) {
log.info("向主题:" + topic + "发送数据"
}
}
/**
* 连接emq服务器后触发
*
* @param b
* @param s
*/
@SneakyThrows
@Override
public void connectComplete(boolean b, String s) {
MqttClient client = ApplicationContextUtil.getBean(MqttClient.class);
if (client.isConnected()) {
for (SubscriptTopic sub : topicMap) {
client.subscribe(sub.getSubTopic(), sub.getQos(), sub.getMessageListener());
log.info("订阅主题:" + sub.getSubTopic());
}
log.info("共订阅: " + topicMap.size() + " 个主题!");
}
}
}
mqtt broker Bean连接类:
@Slf4j
@Configuration
@AutoConfigureAfter(EmqProperties.class)
public class EmqConfig {
@Value("${server.port}")
private int port;
/**
* MQTT的连接设置
*
* @param emqProperties
* @return
*/
@Bean
public MqttConnectOptions getOption(EmqProperties emqProperties) {
MqttConnectOptions options = new MqttConnectOptions();
options.setUserName(emqProperties.getUserName());
options.setPassword(emqProperties.getPassword().toCharArray());
// 设置是否清空session,这里如果设置为false表示服务器会保留客户端的连接记录,这里设置为true表示每次连接到服务器都以新的身份连接
options.setCleanSession(emqProperties.getCleanSession());
//断线重连
options.setAutomaticReconnect(emqProperties.getReconnect());
// 设置超时时间 单位为秒
options.setConnectionTimeout(emqProperties.getTimeout());
// 设置会话心跳时间 单位为秒 服务器会每隔1.5*10秒的时间向客户端发送个消息判断客户端是否在线,但这个方法并没有重连的机制
options.setKeepAliveInterval(emqProperties.getKeepAlive());
return options;
}
@Bean
public MqttClient getClient(MqttConnectOptions options, EmqProperties emqProperties, ApplicationContext applicationContext) throws Exception {
List<SubscriptTopic> topicMap = new ArrayList<SubscriptTopic>();
MqttClient client = new MqttClient(emqProperties.getBroker(), Inet4Address.getLocalHost().getHostAddress() + ":" + port, new MemoryPersistence());
//得到所有使用@Topic注解的类
Map<String, Object> beansWithAnnotation = applicationContext.getBeansWithAnnotation(Topic.class);
for (String className : beansWithAnnotation.keySet()) {
Class<?> classByteCode = beansWithAnnotation.get(className).getClass();
//获取类的注解属性
Topic annotation = AnnotationUtils.findAnnotation(classByteCode, Topic.class);
String topic = annotation.topic();
int qos = annotation.qos();
Pattern patten = annotation.patten();
String group = annotation.group();
String subTopic = topic;
if (patten == Pattern.SHARE) {
subTopic = "$share/" + group + "/" + topic;
} else if (patten == Pattern.QUEUE) {
subTopic = "$queue/" + topic;
}
topicMap.add(new SubscriptTopic(topic, subTopic, patten, qos, (IMqttMessageListener) applicationContext.getBean(classByteCode)));
}
client.setCallback(new MqttCallback(topicMap));
client.connect(options);
return client;
}
}
设计接收数据基类:
/**
* @author jie
*/
public interface MsgDecoder<T> {
/**
* 下位机消息解码器
* @param msg
* @return
*/
T decoder(MqttMessage msg);
}
/**
* @author jie
*/
public interface MsgEncoder<T> {
/**
* 数据库消息编码为string
* @param t
* @return
*/
String encoder(T t);
}
/**
* 封装的主题消费父类
*
* @author jie
*/
@Slf4j
public abstract class SuperConsumer<T> implements IMqttMessageListener, MsgDecoder<T> {
public static ExecutorService executorService = Executors.newFixedThreadPool(10);
@Override
public void messageArrived(String topic, MqttMessage mqttMessage) {
log.info("\r\n 收到主题 :\r\n" + topic + " 的消息:\r\n" + new String(mqttMessage.getPayload()));
executorService.submit(() -> {
try {
T decoder = decoder(mqttMessage);
msgHandler(topic, decoder);
} catch (Exception ex) {
//解决业务处理错误导致断线问题
log.error(ex.toString());
}
});
}
/**
* 业务操作
*
* @param topic
* @param entity
*/
protected abstract void msgHandler(String topic, T entity);
}
5.使用自定义注解绑定消费类
这里我们以系统的客户端上线消息为例
注意,第一个案例为系统主题,若订阅失败或报错请配置系统主题的acl,关于系统主题的acl及集群搭建请参考:
https://blog.csdn.net/weixin_44032502/article/details/107972171.
/**
* @author jie
*/
@Topic(topic = "$SYS/brokers/+/clients/+/connected", patten = Pattern.SHARE)
public class ConnectMsg extends SuperConsumer<Connect> {
@Override
protected void msgHandler(String topic, Connect entity) {
//接下来就是你自己的操作了
//TODO 业务操作
}
@Override
public Connect decoder(MqttMessage msg) {
return JSON.parseObject(new String(msg.getPayload()), Connect.class);
}
}
新增测试类:
/**
* @author jie
*/
@Topic(topic = "device/+/test",patten = Pattern.SHARE)
public class MqttTest extends SuperConsumer<String> {
@Override
protected void msgHandler(String topic, String entity) {
}
@Override
public String decoder(MqttMessage msg) {
return "";
}
}
6.使用助手调试
助手下载地址:
https://mqttx.app/cn/
输入账号信息和clientId,切记clientId不能重复!!
程序收到消息:
调试成功!
有问题可加微信私聊: