写在前面的话:
计划梳理MQTT集成至Java、Vue的系列文档,详见收录专栏。
该示例文章,已将相关方法封装至工具类,已实现断线重连,已将相关参数提取至配置文件。
-- 真积力久则入
目录
一、前情提要
二、环境说明
开发平台:windows
开发工具:IDEA
联测工具:MQTTX
SpringBoot版本:2.2.3.RELEASE
三、Pom文件集成
<!-- MQTT --> <dependency> <groupId>org.springframework.integration</groupId> <artifactId>spring-integration-stream</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-integration</artifactId> </dependency> <dependency> <groupId>org.springframework.integration</groupId> <artifactId>spring-integration-mqtt</artifactId> </dependency>
四、代码集成
1、在配置文件中,预设参数值
# Mqtt Config # 以下配置,详见 前情提要 # client_id 是设置一个前缀,在应用的时候需要加上时间戳,以此保证唯一性 spring.mqtt.url=tcp://192.168.3.30:1882 spring.mqtt.username=admin spring.mqtt.password=public spring.mqtt.client.id=code_dev
/**************************************************** * * Mqtt Config * 备注:以下刻意将 defaultClientId、clientId 区分使用。 * 防止断线重连的时候,clientId 被重复拼接时间戳 * * * @author Francis * @date 2022/4/27 22:05 * @version 1.0 **************************************************/ @Getter @Configuration public class MqttProperties { @ApiModelProperty("服务端地址") @Value("${spring.mqtt.url}") private String hostUrl; @ApiModelProperty("用户名") @Value("${spring.mqtt.username}") private String username; @ApiModelProperty("密码") @Value("${spring.mqtt.password}") private String password; @ApiModelProperty("初始化的客户端id") @Value("${spring.mqtt.client.id}") private String defaultClientId; @ApiModelProperty("客户端id") private String clientId; public String getClientId() { clientId = defaultClientId + Constants.DEFAULT_SPLIT_SYMBOL + System.currentTimeMillis(); return clientId; } }
2、新建枚举类,用于配置默认订阅话题
/**************************************************** * * 默认订阅的话题 -- 枚举类 * 备注:此类不可为空,若无默认订阅话题,MQTT在连接上后,会立即断开。 * * * @author Francis * @date 2022/7/27 10:14 * @version 1.0 **************************************************/ @Getter @AllArgsConstructor @ApiModel("默认订阅的话题 -- 枚举类") public enum DefineSubTopicEnum { // 设备输出订阅 TEST("test", 0); private final String topic; private final int qos; /** * 获取所有话题名 * * @return topicArr */ public static String[] queryAllTopic() { List<String> topicList = new ArrayList<>(); for (DefineSubTopicEnum item : DefineSubTopicEnum.values()) { topicList.add(item.getTopic()); } String[] topicArr = new String[topicList.size()]; topicArr = topicList.toArray(topicArr); return topicArr; } /** * 获取所有qos * * @return qosArr */ public static int[] queryAllQos() { List<Integer> qosList = new ArrayList<>(); for (DefineSubTopicEnum item : DefineSubTopicEnum.values()) { qosList.add(item.getQos()); } int[] qosArr = new int[qosList.size()]; qosArr = qosList.stream().mapToInt(Integer::intValue).toArray(); return qosArr; } }
3、MQTT配置类
/**************************************************** * * Mqtt 工厂类 * * * @author Francis * @date 2022/4/28 21:55 * @version 1.0 **************************************************/ @Slf4j @Component public class MqttFactory { @Autowired private MqttProperties config; private static MqttFactory factory; private static MqttClient client; @PostConstruct public void init() { factory = this; factory.config = this.config; } /** * 获取客户端实例 * 单例模式:存在即返回,不存在则初始化 * * @return client * @throws MqttException 此处刻意抛出异常,否则无法执行断线重连 */ public static MqttClient getInstance() throws MqttException { if (client == null) { connect(); } return client; } /** * 清空客户端实例 * 当 mqtt 断开连接时,需清空 clientId,再执行断线重连 */ public static void clear() { client = null; } /** * 断线重连方法 */ public static void reconnect() { int count = 0; while (true) { clear(); ++ count; try { log.info("----------------[MQTT]即将执行自动重连----------------"); getInstance(); log.info("----------------[MQTT]自动重连成功----------------"); break; } catch (MqttException e) { log.error("----------------[MQTT]自动重连失败,当前为第 {} 次尝试----------------", count); try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException ex) { log.error("----------------[MQTT]自动重连,休眠失败!----------------", e); } } } } /** * 客户端连接服务端 * * @throws MqttException 此处刻意抛出异常,否则无法执行断线重连 */ private static void connect() throws MqttException { // 创建MQTT客户端对象 client = new MqttClient(factory.config.getHostUrl(), factory.config.getClientId(), new MemoryPersistence()); // 连接设置 MqttConnectOptions options = new MqttConnectOptions(); // 是否清空session,设置false表示服务器会保留客户端的连接记录(订阅主题,qos),客户端重连之后能获取到服务器在客户端断开连接期间推送的消息 // 设置为true表示每次连接服务器都是以新的身份 options.setCleanSession(true); // 设置连接用户名 options.setUserName(factory.config.getUsername()); // 设置连接密码 options.setPassword(factory.config.getPassword().toCharArray()); // 设置超时时间,单位为秒 options.setConnectionTimeout(100); // 设置心跳时间 单位为秒,表示服务器每隔 20 秒的时间向客户端发送心跳判断客户端是否在线 options.setKeepAliveInterval(20); // 设置遗嘱消息的话题,若客户端和服务器之间的连接意外断开,服务器将发布客户端的遗嘱信息 options.setWill("willTopic", (factory.config.getClientId() + "与服务器断开连接").getBytes(), 0, false); // 设置回调 client.setCallback(new MqttCallBack()); client.connect(options); // 设置默认订阅主题 // 消息等级,与主题数组一一对应 int[] qos = DefineSubTopicEnum.queryAllQos(); // 主题 String[] topics = DefineSubTopicEnum.queryAllTopic(); // 订阅主题 client.subscribe(topics, qos); } }
4、MQTT回调类
/**************************************************** * * Mqtt 回调 * * * @author Francis * @since 2022/4/27 22:11 * @version 1.0 **************************************************/ @Slf4j @Configuration public class MqttCallBack implements MqttCallback { /** * 与服务器断开的回调 */ @Override public void connectionLost(Throwable throwable) { log.info("[MQTT]断开了与服务端的连接。考虑是否服务端掉线 or 回调参数解析报错 or 无默认sub"); // 执行自动重连 MqttFactory.reconnect(); } /** * 消息到达的回调 * * @param topic 话题 * @param mqttMessage 消息内容 */ @Override public void messageArrived(String topic, MqttMessage mqttMessage) throws Exception { String msg = new String(mqttMessage.getPayload()); log.info("[MQTT]已获取返回数据,当前数据为:{}", msg); // 自己的业务处理 } /** * 消息发布成功的回调 * * @param token token */ @Override public void deliveryComplete(IMqttDeliveryToken token) { IMqttAsyncClient client = token.getClient(); log.info("[MQTT]{}:消息发布成功!", client.getClientId()); } }
5、MQTT工具类封装
/**************************************************** * * Mqtt工具类 * * * @author Francis * @date 2022/4/28 21:54 * @version 1.0 **************************************************/ public class MqttUtils { /** * 发布消息 * * @param qos 0-至多1次、1-至少1次、2-一次 * @param retained 是否保留:true-sub重新连接mqtt服务端时,总能拿到该主题的最新消息、false-sub重新连接mqtt服务端时,只能拿到连接后发布的消息 * @param topic 话题 * @param message 消息内容 */ public static void pub(int qos, boolean retained, String topic, String message) throws MqttException { // 获取客户端实例 MqttClient client = MqttFactory.getInstance(); MqttMessage mqttMessage = new MqttMessage(); mqttMessage.setQos(qos); mqttMessage.setRetained(retained); // 此处必须指明编码方式,否则会出现订阅端中文乱码的情况 mqttMessage.setPayload(message.getBytes(StandardCharsets.UTF_8)); // 主题的目的地,用于发布/订阅信息 MqttTopic mqttTopic = client.getTopic(topic); // 提供一种机制来跟踪消息的传递进度 // 用于在以非阻塞方式(在后台运行)执行发布是跟踪消息的传递进度 MqttDeliveryToken token; try { // 将指定消息发布到主题,但不等待消息传递完成,返回的token可用于跟踪消息的传递状态 // 一旦此方法干净地返回,消息就已被客户端接受发布,当连接可用,将在后台完成消息传递。 token = mqttTopic.publish(mqttMessage); token.waitForCompletion(); } catch (MqttException e) { e.printStackTrace(); } } /** * 订阅话题 * * @param topic 话题 * @param qos 0-至多1次、1-至少1次、2-一次 */ public static void sub(String topic, int qos) throws MqttException { // 获取客户端实例 MqttClient client = MqttFactory.getInstance(); client.subscribe(topic, qos); } /** * 断开连接 */ public static void disConnect() { try { // 获取客户端实例 MqttClient client = MqttFactory.getInstance(); client.disconnect(); } catch (MqttException e) { e.printStackTrace(); } } }
6、配置MQTT初始化
注:此处按需取舍。图示只粘贴了关于MQTT初始化的核心代码。
/**************************************************** * * 初始化执行 * * * @author Francis * @date 2022/10/12 10:37 * @version 1.0 **************************************************/ @Slf4j @Order(1) @Component public class MqttBeforePoint implements ApplicationRunner { @Override public void run(ApplicationArguments args) throws Exception { // 初始化Mqtt连接 try { MqttFactory.getInstance(); log.info("[MQTT]初始化成功"); } catch (MqttException e) { log.info("[MQTT]初始化失败。考虑是否服务端掉线"); MqttFactory.reconnect(); } } }
7、新建测试Controller
/**************************************************** * * Mqtt测试 -- controller * * * @author Francis * @date 2022/4/28 22:28 * @version 1.0 **************************************************/ @Slf4j @RestController @RequestMapping("/mqtt") public class MqttTestController { @PostMapping("/pub") public JsonResult<String> pub(int qos, boolean retained, String topic, String msg) { try { MqttUtils.pub(qos, retained, topic, msg); return JsonResult.success("发送成功!"); } catch (Exception e) { log.error("消息发送失败!", e); return JsonResult.success("发送失败!"); } } @PostMapping("/sub") public JsonResult<String> sub(String topic, int qos) throws MqttException { MqttUtils.sub(topic, qos); return JsonResult.success("动态订阅成功!"); } }
五、基于MQTTX,联调测试
1、测试Java项目sub
2、测试Java项目Pub
六、关于断线重连趟过的一些坑
1、可能出现断线的几种情况
1、clientId冲突,后来的会把先来的干掉。此处通过固定clientId前缀 + 时间戳避免该问题,实现clientId的唯一。 2、Mqtt回调解析,若解析过程中有代码报错,且没有try...catch...,会出现掉线情况。 3、Mqtt服务端断开,会出现掉线的情况。 4、Mqtt连接后,没有默认订阅话题,会在连接好后,立即断开。
2、断线重连的方案
1、在断线后,需先清空MqttFactory的client,否则再次连接时,client依旧存在,不会重新走connect方法。 2、断线重连的重试的方案选择: a. 可通过配置文件,设置断线重连的重试次数。因项目业务需求,故本文中未采用该方案。但在count处,已预留相关拓展口。 b. 通过休眠固定时间,永久请求重连,直到连接成功。本文中采用此方案。另:休眠时间可提取至配置文件。