java mqtt客户端连接emqx实现共享订阅

简介

本文主要介绍怎么用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不能重复!!
在这里插入图片描述
程序收到消息:

在这里插入图片描述

调试成功!

有问题可加微信私聊:
在这里插入图片描述

评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值