SpringBoot 开发之 MQTT 协议的主题消息发布与订阅

本篇文章主要讲解MQTT 消息的发布和订阅

MQTT协议介绍:https://mcxiaoke.gitbooks.io/mqtt-cn/content/mqtt/01-Introduction.html
MQTT使用的是EMQ,官网地址:https://www.emqx.io/cn/products/broker
MQTT协议官方测试工具:http://tools.emqx.io

maven 导入 MQTT依赖

<!--mqtt-->
<dependency>
   <groupId>org.eclipse.paho</groupId>
   <artifactId>org.eclipse.paho.client.mqttv3</artifactId>
   <version>1.2.1</version>
</dependency>

1. 客户端 Client 与 服务端 Server

在MQTT协议中,并不是发送消息的是客户端,接收消息的是服务端,客户端和服务端都是可以发送消息和接收消息的。

1.1 客户端

客户端总是通过网络连接到服务端
  • 发布应用消息给其它相关的客户端。
  • 订阅以请求接受相关的应用消息。
  • 取消订阅以移除接受应用消息的请求。
  • 从服务端断开连接

我们正常用来发送消息的和接收消息的都是属于客户端。

1.2 服务端

作为发送消息的客户端和请求订阅的客户端之间的中介
  • 接受来自客户端的网络连接。
  • 接受客户端发布的应用消息。
  • 处理客户端的订阅和取消订阅请求。
  • 转发应用消息给符合条件的已订阅客户端。

服务端我使用的是EMQ Linux 下 EMQ X 服务器部署

2、创建客户端

发布者和订阅者都是属于客户端,服务器是 EMQ Broker。

发布者把消息发送给 Broker,Broker 接收到消息后再把消息发送给订阅者,发布者和订阅者之间实际上是没有直接关系的。

 	// 全局创建 client 引用
    public static MqttClient client = null;
	/**
     * 创建客户端
     */
    public static void createClient(){
        if(client != null){
            logger.info("MQTT 客户端已存在,不可再重复创建");
            return;
        }
        // 创建客户端, MemoryPersistence 设置 client 的保存方式默认以内存保存
        // clientId 是客户端的唯一标识, 相同的 clientId 客户端连接时会导致已连接的客户端断开连接
        String clientId = AddressUtils.getLocalMac() + IPPortUtil.getLocalPort(); // 为保证每次连接都是用同一个客户端Id, 我这边是获取机器的MAC地址作为客户端的唯一标识
        try {
            client = new MqttClient(HOST,clientId,new MemoryPersistence());
        } catch (MqttException e) {
            e.printStackTrace();
        }
        // 设置回调函数
        client.setCallback(new MqttCallbackExtended(){
            @Override
            public void connectComplete(boolean reconnect, String serverURI) {
                logger.info("[MQTT] 连接已完成...");
                isConnect = true;
                // 连接完成可以做一些初始化操作(我在这边发送客户端上线通知)
                mqttServiceStatic.callBackBusinessProcessing(reconnect,serverURI);

            }

            @Override
            public void connectionLost(Throwable cause) {
                logger.info("[MQTT] 连接已断开...");
                isConnect = false;
                // 不设置断线自动连接时,可在这边自定义重连
                mqttServiceStatic.connectionLost(cause);
            }


            @Override
            public void messageArrived(String topic, MqttMessage message) throws Exception {
//                    logger.info("[MQTT] 数据已到达...");
                // 接收订阅的消息后执行的方法
                String content = new String(message.getPayload(),"UTF-8");
                MqttData mqttData = new MqttData(new String(message.getPayload(),"UTF-8"));
                if (!passList.contains(mqttData.getServiceId())) {
                    logger.info("【MQTT】订阅主题:" + topic);
                    logger.info("【MQTT】订阅质量:" + message.getQos());
                    logger.info("【MQTT】订阅内容:" + content);
                }
                if(StringUtils.isNotBlank(mqttData.getServiceId())){
                    mqttServiceStatic.callBackBusinessProcessing(mqttData.getServiceId(), mqttData);
                }
            }
            @Override
            public void deliveryComplete(IMqttDeliveryToken token) {
                if(token.getMessageId() == 0){
                    // qos 为0
                } else {
                    // qos 为1或者2 则会有 MessageId
                    logger.info("[MQTT] 数据推送成功,MessageId:"+token.getMessageId());
                }
            }
        });
    }

服务器不允许同一个 clientId 的客户端多次接入,当相同 clientId 多次接入时会导致客户端反复的断线-连接

2、连接客户端


    public static Boolean isConnect = false; // 是否连接
	/**
     * 连接客户端
     */
    public static void connectClient(String topic){
        // 未开启 MQTT 服务
        if(!ServiceOpen){
            logger.info("【未开启 MQTT 服务】");
            return;
        }
        // 且网络正常才连接MQTT
        if (!AddressUtils.isConnect(linuxServiceUrl)) {
            logger.info("【MQTT 网络异常,终止连接客户端!!】");
            return;
        }
        // 客户端已连接则结束
        if(isConnect){
            logger.info("【MQTT 客户端已连接!!】");
            return;
        }
        // 客户端不存在则先创建客户端
        if(client == null){
            logger.info("【MQTT 客户端未创建】");
            createClient();
        }
        MqttConnectOptions connectOptions = new MqttConnectOptions();
        // 设置是否关闭连接会话
        // 设置为 true 时,客户端每次都会以新的身份连接
        // 设置为 false 时,客户端再次连接会继续之前的会话,可以收到断线后发布的消息,并且再次连接后无需再次订阅即可收到之前已订阅过的主题消息。
        // 设置为 false 有个前置条件就是 clientId 不变
        connectOptions.setCleanSession(CleanSession);
        connectOptions.setUserName(USERNAME);
        connectOptions.setPassword(PASSWORD.toCharArray());
        // 断线后是否自动重连
        connectOptions.setAutomaticReconnect(true);
        connectOptions.setConnectionTimeout(3); // 连接报错:已在进行连接,尝试设置连接超时时间为3s
       try {
                // 设置遗嘱消息
                MqttContentDataDto contentDataDto = new MqttContentDataDto();
                contentDataDto.setServiceId("sync.client.offline");
                contentDataDto.setMsg("客户端下线通知");
                String content = JSONObject.toJSONString(contentDataDto);
                connectOptions.setWill(topic,content.getBytes("UTF-8"),1,false);
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
        try {
            logger.info("【MQTT 开始连接客户端!】");
            // 客户端重复连接会报错:已连接客户机(32100)
            client.connect(connectOptions);
        } catch (MqttException e) {
            logger.error("[MQTT] 客户端连接失败!"+ e.getMessage());
        }

    }

如果需要在客户端断线重连之后还能接收到掉线期间所发布的消息,clientId 必须保持不变设置 cleanSession 为 false。

相关文档
客户端断线后自动重连与保留断线时发布的主题消息篇
遗嘱消息及消息保留的应用篇

3、发布消息

	/**
     * 推送消息
     *
     * @param qos 消息质量
     * @param topic          主题
     * @param contentDataDto 内容
     */
    public static void PublishSample(int qos, String topic, boolean retained, MqttContentDataDto contentDataDto) throws ApiProcessException {
        if(!isConnect){
            logger.info("【MQTT 客户端未连接,先连接客户端】");
            connectClient(topic);
            try {
                Thread.sleep(10000);
                if(!isConnect){
                    return;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        String content = "";
        if (contentDataDto == null) {
            Map<String, Object> dataMap = new HashMap<>();
            dataMap.put("time", DateUtils.getDateTime());
            content = JSONObject.toJSONString(dataMap);
        } else {
            content = JSONObject.toJSONString(contentDataDto);
            if (!passList.contains(contentDataDto.getServiceId())) {
                logger.info("【MQTT】推送主题:" + topic);
                logger.info("【MQTT】推送内容:" + content + "\n");
            }
        }
        try {
            // 创建消息
            MqttMessage message = new MqttMessage();
            try {
                message.setPayload(content.getBytes("utf-8"));
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
            // 设置消息的服务质量
            message.setQos(qos);
            // 设置消息保留
            // 设置为 true 则 Broker 会保留对该主题发生的最后一条消息,每有客户端订阅时都会推送一次
            // 设置为 true 后推送的那条消息会被一直保留,需要取消只有在设置为 true 下发送一条空的 payload 消息
            message.setRetained(retained);

            // 发布消息
            client.publish(topic, message);
        } catch (MqttException me) {
            logger.info("MQTT 消息推送失败 " + me);
            throw ErrorCode.MQTT_SERVICE_ERROR;
        }
    }

消息的服务质量详情可查看 消息质量等级分析篇

4、消息订阅

	/**
     * 订阅主题
     *
     * @param topic    主题
     */
    public static JSONObject SubscribeSample(String topic){
        JSONObject result = new JSONObject();
        if(!isConnect || client == null){
            logger.info("【MQTT 客户端未连接,无法订阅】");
            return result;
        }
        try {
            client.subscribe(topic);
        } catch (MqttException e) {
            e.printStackTrace();
        }
        result.put("client",client);
        result.put("topic",topic);
        result.put("QOS",QOS);
        return result;
    }

    /**
     * 取消订阅主题
     *
     * @param topic    主题
     */
    public static JSONObject UnSubscribeSample(String topic){
        JSONObject result = new JSONObject();
        if(!isConnect || client == null){
            logger.info("【MQTT 客户端未连接,无法取消订阅】");
            return result;
        }
        try {
            client.unsubscribe(topic);
        } catch (MqttException e) {
            e.printStackTrace();
        }
        result.put("client",client);
        result.put("topic",topic);
        result.put("QOS",QOS);
        return result;
    }

在发布和订阅过程中,如果出现异常而没有处理会导致客户端反复的断线-连接

5、关闭客户端

	/**
     *  关闭客户端
     */
    public static void closeClient(){
        if(client == null){
            isConnect = false;
            return;
        }
        try {
            // 断开连接
            client.disconnect();
            // 关闭客户端
            client.close();
            client = null;
            isConnect = false;
        } catch (MqttException e) {
            e.printStackTrace();
        }
    }

主题(Topic)通过’/‘分割层级,支持’+’, '#'通配符:
‘+’: 表示通配一个层级,例如a/+,匹配a/x, a/y
‘#’: 表示通配多个层级,例如a/#,匹配a/x, a/b/c/d
ps:订阅者可以订阅含通配符主题,但发布者不允许向含通配符主题发布消息。

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值