基于MQTT消息队列遥测传输协议实战解析

内容提要

  1. MQTT概述
  2. 案例解析

一、MQTT概述

1.1 MQTT的简介与作用

MQTT(Message Queuing Telemetry Transport)是一种基于发布/订阅模式的轻量级通讯协议‌,旨在为资源受限的设备提供高效、可靠的消息传输服务,特别适合于低带宽、高延迟或不可靠的网络环境‌。

1、MQTT协议的主要特点

  • ‌轻量级‌:MQTT协议设计得非常轻量级,占用极少的代码和带宽,适用于资源受限的设备‌。
  • ‌发布/订阅模式‌:MQTT通过发布/订阅模式实现消息的传递,支持一对多的消息发布和接收,有效减少了网络流量和资源使用‌。
  • ‌服务质量(QoS)‌:MQTT协议支持不同的服务质量等级,确保消息的可靠传递‌。
  • ‌心跳机制‌:通过心跳机制监测连接状态,减少网络重新连接的开销‌。
  • ‌安全性‌:支持认证和加密技术,确保通信过程中的数据安全‌。

2、MQTT协议广泛应用于各种场景

  • ‌物联网(IoT)‌:在智能家居工业自动化远程监控等领域,MQTT使得设备之间能够实时、可靠地交换信息‌。
  • ‌移动应用和小型设备‌:MQTT的低带宽消耗和高效资源利用,使其在移动设备和传感器网络中广泛应用‌。
  • 车联网通信‌:在汽车、制造、石油、天然气等行业市场,MQTT协议被用于车辆通信和数据采集‌。

3、MQTT协议的基本组件

  • ‌发布者(Publisher)‌:将消息发布到特定的主题(Topic)。
  • ‌代理(Broker)‌:作为中介,接收发布者的消息并将其传递给已订阅该主题的订阅者。
  • ‌订阅者(Subscriber)‌:订阅特定的主题以接收相关消息‌。

说明:MQTT的安装有完全独立完装与基本RabbitMQ的安装,接下来我们将在RabbitMQ中开启MQTT协议。

1.2 RabbitMQ中MQTT在windows中安装与配置

在Windows中安装RabbitMQ和MQTT插件的步骤如下。

1、首先在Windows中安装RabbitMQ

参考本站:消息队列RabbitMQ在Windows中安装与配置完全解析_rabbitmq windows-CSDN博客

2、安装MQTT插件

(1)打开RabbitMQ的sbin目录

(2)地址栏输入cmd,切换到Windows的命令行

(3)运行以下命令来安装MQTT插件

rabbitmq-plugins enable rabbitmq_mqtt

这将同时会启用RabbitMQ的MQTT插件,提供与后端服务交互使用,对应端口1883。

完成以上操作后,我们就可以在Windows系统中使用RabbitMQ和MQTT了。如果需要进一步配置MQTT,可以查看RabbitMQ官方文档中关于MQTT插件的配置部分。

二、案例解析

2.1 RabbitMQ下开启MQTT

默认情况下RabbitMQ是不开启MQTT 协议的,所以需要我们手动的开启相关的插件,而RabbitMQ的MQTT 协议分为两种。

(1)第一种 rabbitmq_mqtt 提供与后端服务交互使用

对应端口1883(上边已安装过)

rabbitmq-plugins enable rabbitmq_mqtt

(2)第二种 rabbitmq_web_mqtt 提供与前端交互使用

对应端口15675

rabbitmq-plugins enable rabbitmq_web_mqtt

2.2 Java案例代码

2.2.1 引入Maven依赖

创建Springboot项目,在pom.xml中引入依赖spring-integration-mqtt

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.7.RELEASE</version>
    <relativePath/>
  </parent>
  <groupId>com.yhc</groupId>
  <artifactId>mqtt-push</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>mqtt</name>

  <description>demo project</description>

  <properties>
    <java.version>1.8</java.version>
    <paho.client.mqttv3.version>1.2.5</paho.client.mqttv3.version>
  </properties>
 <!--导入依赖包-->
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
    <!--mqtt依赖包-->
    <dependency>
      <groupId>org.springframework.integration</groupId>
      <artifactId>spring-integration-mqtt</artifactId>
    </dependency>
    <!-- lombok工具包 -->
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <optional>true</optional>
    </dependency>

  </dependencies>

</project>

2.2.2 MQTT公共的配置信息

包括clientId和serverClientId,默认的topic信息, 以及连接rabbitmq的用户名和密码。

1、clientId的唯一性设置

这里可以使用随机数或者分布式id来生成clientId。

此处clientId直接在代码中写死了,服务为单实例部署,运行没有什么问题。

如果是分布式项目,则需另行处理。

public class MQTTProducer{
    /**
     * MQ4IOT clientId,由业务系统分配,需要保证每个tcp连接都不一样,保证全局唯一,如果不同的客户端对象(tcp 连接)使用了相同的 clientId 会导致连接异常断开。
     * clientId 由两部分组成,格式为 GroupID@@@DeviceId,其中 groupId在MQ4IOT控制台申请,DeviceId 由业务方自己设置,clientId 总长度不得超过64个字符。
     */
    private String clientId = "GID_XXXXX@@@XXXXX";
}

2、yaml配置文件application.yaml

server:
  port: 8090
mqtt-push:
    # clientId的前缀
    clientId: mqtt_client_
    # 通过通配符 "+", 订阅主题, 就可以接收所有传感器发送的温度数据了
    # 多个的话通过,号分割
    defaultTopic: sensor/+/temperature
    # clientId的前缀
    serverClientId: mqtt_server_
    # 这边替换为自己的ip(或者域名)
    # 多个的话通过,号分割
    servers: tcp://127.0.0.1:1883
    # 访问rabbitmq的用户名
    username: guest
    # 访问rabbitmq的密码
    password: guest
3、MQTT通用配置信息
package com.yhc.config;

import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.integration.annotation.IntegrationComponentScan;
import org.springframework.integration.mqtt.core.DefaultMqttPahoClientFactory;
import org.springframework.integration.mqtt.core.MqttPahoClientFactory;
import org.springframework.stereotype.Component;

/**
 * mqtt连接配置
 */
@Data
@Component
@IntegrationComponentScan
@ConfigurationProperties(prefix = "mqtt-push")
public class MqttConfig {
    // 服务地址
   private String servers;
    //客户端id
   private String clientId;
    //服务端id
   private String serverClientId;
    //默认主题
   private String defaultTopic;
    //用户名
   private String username;
    //密码
   private String password;

    /**
     * 创建MqttPahoClientFactory,设置MQTT Broker连接属性
     * 如果使用SSL验证,也在这里设置
     * @return MqttPahoClientFactory
     */
    @Bean(name = "mqttClientFactory")
    public MqttPahoClientFactory mqttClientFactory() {
        DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory();
        factory.setConnectionOptions(mqttConnectOptions());
        return factory;
    }

    @Bean(name = "mqttConnectOptions")
    public MqttConnectOptions mqttConnectOptions() {
        MqttConnectOptions options = new MqttConnectOptions();
        //断开后,是否自动连接
        options.setAutomaticReconnect(true);
        // 设置是否清空session,这里如果设置为false表示服务器会保留客户端的连接记录;
        // 把配置里的 cleanSession 设为false,客户端掉线后 服务器端不会清除session;
        // 当重连后可以接收之前订阅主题的消息。当客户端上线后会接受到它离线的这段时间的消息
        options.setCleanSession(false);
        // 设置连接的用户名
        options.setUserName(username);
        // 设置连接的密码
        options.setPassword(password.toCharArray());
        options.setServerURIs(servers.split(","));
        // 设置超时时间 单位为秒
        options.setConnectionTimeout(10);
        // 设置会话心跳时间 单位为秒 服务器会每隔1.5*20秒的时间向客户端发送心跳判断客户端是否在线,但这个方法并没有重连的机制
        options.setKeepAliveInterval(20);
        // 设置‘遗嘱’消息的话题,若客户端与服务器之间的连接意外中断,服务器将发布客户端的“遗嘱”消息。
        // options.setWill("willTopic", WILL_DATA, 2, false);
        return options;
    }
}

2.2.3 发布消息配置

package com.yhc.config;

import com.mqttpush.constant.MqttConstant;
import com.mqttpush.util.RandomUtil;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.mqtt.outbound.MqttPahoMessageHandler;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;

import javax.annotation.Resource;

/**
 * mqtt 消息发布配置
 */
@Configuration
public class MqttProducerConfig {

    @Resource
    private MqttConfig mqttConfig;

    @Bean
    public MessageChannel mqttOutboundChannel() {
        return new DirectChannel();
    }

    /**
     * ServiceActivator注解表明:当前方法用于处理MQTT消息,inputChannel参数指定了用于消费消息的channel。
     * @return
     */
    @Bean
    @ServiceActivator(inputChannel = MqttConstant.CHANNEL_NAME_OUT)
    public MessageHandler mqttOutbound() {
        // clientId相同导致客户端间相互竞争消费
        // 同一时间内只能有一个客户端能拿到消息, 其他客户端不但不能消费消息,而且还在不断的掉线重连:Lost connection: 已断开连接; retrying...。
        MqttPahoMessageHandler messageHandler
        = new MqttPahoMessageHandler(mqttConfig.getServerClientId() + MqttConstant.MESSAGE_HANDLER_CLIENT_ID_PRODUCER + RandomUtil.getRandomStr(),
                                     mqttConfig.mqttClientFactory());
        messageHandler.setAsync(true);
        // MQTT 提供了三种服务质量(QoS),在不同网络环境下保证消息的可靠性。
        // QoS 0:消息最多传送一次。如果当前客户端不可用,它将丢失这条消息。
        // QoS 1:消息至少传送一次。
        // QoS 2:消息只传送一次。
        messageHandler.setDefaultQos(1);
        messageHandler.setDefaultTopic(mqttConfig.getDefaultTopic());
        return messageHandler;
    }
}

2.2.4 订阅消息配置

通信是双向的,服务端既可以发布消息,也可以订阅消息。

package com.yhc.config;

import com.mqttpush.constant.MqttConstant;
import com.mqttpush.util.RandomUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.core.MessageProducer;
import org.springframework.integration.mqtt.inbound.MqttPahoMessageDrivenChannelAdapter;
import org.springframework.integration.mqtt.support.DefaultPahoMessageConverter;
import org.springframework.integration.mqtt.support.MqttHeaders;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.MessagingException;

import javax.annotation.Resource;

/**
 * mqtt 消息订阅配置
 */
@Slf4j
@Configuration
public class MqttSubscriberConfig {

    @Resource
    private MqttConfig mqttConfig;

    @Bean
    public MessageChannel mqttInputChannel() {
        return new DirectChannel();
    }

    @Bean
    public MessageProducer inbound() {
        // clientId相同导致客户端间相互竞争消费
        // 同一时间内只能有一个客户端能拿到消息, 其他客户端不但不能消费消息,而且还在不断的掉线重连:Lost connection: 已断开连接; retrying...。
        MqttPahoMessageDrivenChannelAdapter adapter
        = new MqttPahoMessageDrivenChannelAdapter(mqttConfig.getClientId() + MqttConstant.MESSAGE_HANDLER_CLIENT_ID_CONSUMER + RandomUtil.getRandomStr(),
                                                  mqttConfig.mqttClientFactory(),
                                                  mqttConfig.getDefaultTopic());
        adapter.setCompletionTimeout(5000);
        adapter.setConverter(new DefaultPahoMessageConverter());
        adapter.setQos(2);
        adapter.setOutputChannel(mqttInputChannel());
        return adapter;
    }

    /**
     * 消息订阅, 处理收到的消息
     */
    @Bean
    @ServiceActivator(inputChannel = MqttConstant.CHANNEL_NAME_IN)
    public MessageHandler mqttInMessageHandler() {
        return message -> {
            try {
                // 消息体
                String payload = message.getPayload().toString();
                log.info("接收到消息, 内容: {}", payload);

                // byte[] bytes = (byte[]) message.getPayload(); // 收到的消息是字节格式
                // 消息的topic
                String topic = message.getHeaders().get(MqttHeaders.RECEIVED_TOPIC).toString();
                log.info("接收到消息, topic: {}", topic);

                // 根据主题分别进行消息处理。
                if (topic.matches(".+/sensor")) { // 匹配:1/sensor
                    String sensorSn = topic.split("/")[0];
                    log.info("传感器" + sensorSn + ": 的消息: " + payload);
                } else if (topic.equals("collector")) {
                    log.info("采集器的消息:" + payload);
                } else {
                    log.info("丢弃消息:主题[" + topic  + "],负载:" + payload);
                }
            } catch (MessagingException ex) {
                //logger.info(ex.getMessage());
            }
        };
    }
}

2.2.5 常量类

package com.yhc.constant;

public class MqttConstant {
     //mqtt发布者信道名称
    public static final String CHANNEL_NAME_OUT = "mqttOutboundChannel";
     // mqtt接收信道名称
    public static final String CHANNEL_NAME_IN = "mqttInputChannel";
     // mqtt消息发布者, serverClientId的前缀
    public static final String MESSAGE_HANDLER_CLIENT_ID_PRODUCER = "producer";
     // mqtt消息接收者, clientId的前缀
    public static final String MESSAGE_HANDLER_CLIENT_ID_CONSUMER = "consumer";
}

2.2.6 发布消息

使用@MessagingGateway注解发送消息

package com.yhc.producer;

import com.mqttpush.constant.MqttConstant;
import org.springframework.integration.annotation.MessagingGateway;
import org.springframework.integration.mqtt.support.MqttHeaders;
import org.springframework.messaging.handler.annotation.Header;

/**
 * rabbitmq mqtt协议网关接口
 */
@MessagingGateway(defaultRequestChannel = MqttConstant.CHANNEL_NAME_OUT)
public interface MqttGateway {

    void sendMessage2Mqtt(String data);

    void sendMessage2Mqtt(String data, @Header(MqttHeaders.TOPIC) String topic);

    void sendMessage2Mqtt(@Header(MqttHeaders.TOPIC) String topic, @Header(MqttHeaders.QOS) int qos, String payload);
}

2.2.7 控制器和测试类

控制器类:

package com.yhc.controller;

import com.mqttpush.dto.ReqSendMsgDTO;
import com.mqttpush.producer.MqttGateway;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;

/**
 * mqtt发送消息控制器
 */
@RestController
@RequestMapping("/mqtt")
public class MqttController {

    @Resource
    private MqttGateway mqttGateway;

    @PostMapping("/sendMessage")
    public String sendMqtt(@RequestBody ReqSendMsgDTO reqSendMsgDTO) {
        mqttGateway.sendMessage2Mqtt(reqSendMsgDTO.getTopic(), reqSendMsgDTO.getPayload());
        return "SUCCESS";
    }
}

测试类:

package com.yhc.test;

import com.mqttpush.producer.MqttGateway;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;

@SpringBootTest
class MqttPushApplicationTests {

    @Resource
    private MqttGateway mqttGateway;

    @Test
    void contextLoads() {
    }

    @Test
    public void sendMqtt() {
        mqttGateway.sendMessage2Mqtt("mqtt/test", "这里是topic的内容");
    }

}

2.3 案例实测

2.3.1 MQTTBOX连接配置

2.3.2 MQTTBOX 发送和订阅配置

左边为发布消息,右边为订阅消息

2.3.3 发送消息

可以使用Postman发送消息或MQTTBOX

1、使用Postman发送消息

(1) 服务器订阅消息

(2) MQTTBOX订阅消息

2、MQTTBOX发送消息

 (1)通过代码订阅消息

(2)MQTTBOX订阅消息

更多精彩内容请持续关注本站!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值