Springboot 集成MQTT
概念
MQTT
MQTT是一种基于发布/订阅模式的轻量级消息传输协议,专门用于低带宽和不稳定的网络环境的物联网应用而设计,可以用极少的代码为联网设备提供实时可靠的消息服务。
以下为MQTT其中两个特性:
QoS
MQTT 协议中规定了消息服务质量(Quality of Service),它保证了在不同的网络环境下消息传递的可靠性。
QoS 等级 | 描述 | 应用 |
---|---|---|
QoS 0 | 最多交付一次,可能会丢失消息 | 传输一些高频且不那么重要的数据,比如传感器数据,周期性更新,即使遗漏几个周期的数据也可以接受 |
QoS 1 | 至少交付一次,保证信息可达,但是消息可能会重复 | 适合传输一些较为重要的数据,比如下达关键指令、更新重要的有实时性要求的状态等(注意重复消费) |
QoS 2 | 只交付一次,可以保证消息到达,也可以保证消息不会重复,但传输成本最高 | 不愿意自行实现去重方案,并且能够接受 QoS 2 带来的额外开。金融、航空场景应用较多 |
Qos消息保证不是端到端的,而是客户端与服务之间的,订阅者接收到的消息等级取决于发布消息Qos与订阅主题的Qos。 举个例子: 有个订阅者订阅的消息主题是Qos 1,如果发布者对该主题发布一条消息Qos为2,那么订阅者接收到该消息的QoS为2,如果发布是一条QoS 0的消息,则订阅者接收的消息QoS等级也为Qos 0。
订阅者接收的消息QoS level <= 订阅消息主题的QoS level
消息主题QoS | 发布者QoS | 订阅者QoS |
---|---|---|
0 | 0 | 0 |
0 | 1 | 0 |
0 | 2 | 0 |
1 | 0 | 0 |
1 | 1 | 1 |
1 | 2 | 1 |
2 | 0 | 0 |
2 | 1 | 1 |
2 | 2 | 2 |
保留消息
MQTT客户端向服务器发布(publish)消息时,可以设置保留消息(Retained Messages)标志在当前设置一条持久消息,消息被保存在服务器上,新的订阅者订阅主题时将接受到该信息。 每个主题下只能存在一份保留信息,因此如果已经存在相同主题的保留信息,则该保留消息会被替换
保留消息虽然存储在服务端中,但它不属于会话的一部分。所以即使发布消息的会话结束,该保留消息也不会被删除,删除方式有以下几种:
- 客户端向有保留消息的主题发布一个空消息。
- 超过EMQX设置的最大保留信息数。
- 通过EMQX保留消息REST API删除。
- 设置了消息过期间隔,到期后保留消息将被删除(MQTT5.0)
EMQX
EMQX (opens new window)是一款大规模可弹性伸缩的云原生分布式物联网 MQTT (opens new window)消息服务器。
作为全球最具扩展性的 MQTT 消息服务器,EMQX 提供了高效可靠海量物联网设备连接,能够高性能实时移动与处理消息和事件流数据,快速构建关键业务的物联网平台与应用。
部署
docker run -itd \
--restart always \
--name emqx \
-p 1883:1883 -p 8083:8083 \
-p 8084:8084 -p 8883:8883 \
-p 18083:18083 emqx/emqx:latest
windows:Windows | EMQX 5.0 文档
Springboot使用
pom.xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.13</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</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>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.26</version>
</dependency>
</dependencies>
spring-integration-mqtt
集成了org.eclipse.paho.client.mqttv3
和 org.eclipse.paho.mqttv5.client
版本都是1.2.5
版本
application.yml
server:
port: 8899
mqtt:
broker: tcp://localhost:1883
username: admin
password: public
client-id: blessing-client
integration-client-id: gravel-client
qos-list:
- 2
- 2
- 2
topics:
- blessing
- star
- gravel
公共配置文件
package com.silvergravel.share;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* Description:
*
* @author DawnStar
* Date: 2023/7/8
*/
@ConfigurationProperties(prefix = "mqtt")
@Component
@Data
public class MqttProperties {
private String broker;
private String username;
private String password;
private String clientId;
private String integrationClientId;
private String[] topics;
private Integer[] qosList;
}
方式一:使用Spring Integration
项目结构
config
package com.silvergravel.intergration.config;
import com.silvergravel.share.MqttProperties;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.annotation.IntegrationComponentScan;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.core.MessageProducer;
import org.springframework.integration.mqtt.core.DefaultMqttPahoClientFactory;
import org.springframework.integration.mqtt.core.MqttPahoClientFactory;
import org.springframework.integration.mqtt.inbound.MqttPahoMessageDrivenChannelAdapter;
import org.springframework.integration.mqtt.outbound.MqttPahoMessageHandler;
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 javax.annotation.Resource;
/**
* Description:
*
* @author DawnStar
* Date: 2023/7/1
*/
@Configuration
@IntegrationComponentScan
public class IntegrationMqttConfiguration {
@Resource
private MqttProperties mqttProperties;
/**
* 设置默认连接参数
*/
@Bean
public MqttPahoClientFactory mqttClientFactory() {
DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory();
MqttConnectOptions options = new MqttConnectOptions();
options.setServerURIs(new String[] { "tcp://localhost:1883"});
// 删除会话,默认true
options.setCleanSession(false);
options.setUserName(mqttProperties.getUsername());
options.setPassword(mqttProperties.getPassword().toCharArray());
factory.setConnectionOptions(options);
return factory;
}
@Bean
@ServiceActivator(inputChannel = "mqttOutboundChannel")
public MessageHandler mqttOutbound() {
MqttPahoMessageHandler messageHandler =
new MqttPahoMessageHandler("silver-gravel", mqttClientFactory());
messageHandler.setAsync(true);
// 设置推送消息的默认主题,topic不能为null
messageHandler.setDefaultTopic(mqttProperties.getTopics()[0]);
// 设置推送QoS默认等级
messageHandler.setDefaultQos(1);
return messageHandler;
}
@Bean
public MessageChannel mqttOutboundChannel() {
return new DirectChannel();
}
/**
* 多个MessageProducer实例可以共用一个管道
* 也可以使用
* @return 一个消息管道
*/
@Bean
public MessageChannel mqttInboundChannel() {
return new DirectChannel();
}
@Bean
public MessageChannel mqttInboundChannel1() {
return new DirectChannel();
}
@Bean
public MessageProducer client1() {
MqttPahoMessageDrivenChannelAdapter adapter =
new MqttPahoMessageDrivenChannelAdapter(mqttProperties.getBroker(),
mqttProperties.getIntegrationClientId(),
mqttProperties.getTopics());
adapter.setCompletionTimeout(5000);
adapter.setConverter(new DefaultPahoMessageConverter());
// 订阅主题Qos为1
adapter.setQos(1);
adapter.setOutputChannel(mqttInboundChannel());
return adapter;
}
@Bean
public MessageProducer client2() {
MqttPahoMessageDrivenChannelAdapter adapter =
new MqttPahoMessageDrivenChannelAdapter(mqttProperties.getBroker(),
mqttProperties.getIntegrationClientId()+1,
mqttProperties.getTopics()[0]);
adapter.setCompletionTimeout(5000);
adapter.setConverter(new DefaultPahoMessageConverter());
// 订阅主题为0
adapter.setQos(0);
adapter.setOutputChannel(mqttInboundChannel());
return adapter;
}
@Bean
@ServiceActivator(inputChannel = "mqttInboundChannel")
public MessageHandler handler() {
// 处理topic 以及相应payload内容
return (message -> {
String prefix = "**************";
String content = "\n集成Client:" +
"\ntopic:"+message.getHeaders().get(MqttHeaders.RECEIVED_TOPIC)
+"\nqos:"+message.getHeaders().get(MqttHeaders.RECEIVED_QOS)
+"\npayload:"+message.getPayload()
+"\nretained:"+message.getHeaders().get(MqttHeaders.RECEIVED_RETAINED)
+"\n";
System.out.println(prefix+content+prefix);
});
}
// @Bean
// @ServiceActivator(inputChannel = "mqttInboundChannel1")
// public MessageHandler handler1() {
// return message -> System.out.println("handler1:"+message.getHeaders()+":"+message.getPayload());
// }
}
controller
package com.silvergravel.intergration.controller;
import com.silvergravel.intergration.service.MqttGateway;
import com.silvergravel.share.MqttProperties;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* Description:
*
* @author DawnStar
* Date: 2023/7/1
*/
@RestController
public class MessageController {
@Resource
private MqttGateway mqttGateway;
@Resource
private MqttProperties mqttProperties;
@GetMapping("/integration/qos0")
public String qos0() {
String content = "这是integration接口的一条 QoS 0消息";
mqttGateway.sendData(content,mqttProperties.getTopics()[0],0);
return content;
}
@GetMapping("/integration/qos1")
public String qos1() {
String content = "这是integration接口的一条 QoS 1消息";
mqttGateway.sendData(content,mqttProperties.getTopics()[0],1);
return content;
}
@GetMapping("/integration/qos2")
public String qos2() {
String content = "这是integration接口的一条 QoS 2消息";
mqttGateway.sendData(content,mqttProperties.getTopics()[0],2);
return content;
}
@GetMapping("/integration/qos2/retained")
public String retained() {
String content = "这是integration接口的一条 QoS 2消息并且开启保留策略";
mqttGateway.sendData(content,mqttProperties.getTopics()[0],2,true);
return content;
}
@GetMapping("/integration/empty")
public String empty() {
mqttGateway.sendData("",mqttProperties.getTopics()[0],1,true);
return "发送一条空消息删除保留信息,空信息是带有保留策略的空信息,无关QoS等级";
}
}
生产者
package com.silvergravel.intergration.service;
import org.springframework.integration.annotation.MessagingGateway;
import org.springframework.integration.mqtt.outbound.MqttPahoMessageHandler;
import org.springframework.integration.mqtt.support.MqttHeaders;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.stereotype.Service;
/**
* Description:
*
* @author DawnStar
* Date: 2023/7/1
*/
@MessagingGateway(defaultRequestChannel = "mqttOutboundChannel")
@Service
public interface MqttGateway {
/**
* 发送数据,没有指定Topic 默认是MqttPahoMessageHandler默认值
* 不设置默认Qos 0
*
* @param data 数据
* @see MqttPahoMessageHandler#setDefaultTopic(String)
*/
void sendData(String data);
/**
* 发送数据
*
* @param topic 指定topic
* @param data 数据
*/
void sendData(String data, @Header(MqttHeaders.TOPIC) String topic);
/**
* 发送数据
*
* @param topic 指定topic
* @param qos 消息质量等级
* @param data 数据
*/
void sendData(String data, @Header(MqttHeaders.TOPIC) String topic, @Header(MqttHeaders.QOS) int qos);
/**
* 发送数据
*
* @param data 数据
* @param topic 指定主题
* @param qos 消息质量等级
* @param retained 消息是否保留 (如果这条消息没有被消费,
* 那么就会保留在代理服务器上)
*/
void sendData(String data, @Header(MqttHeaders.TOPIC) String topic
, @Header(MqttHeaders.QOS) int qos,
@Header(MqttHeaders.RETAINED) boolean retained);
}
方式二:使用普通方式
项目结构
config
package com.silvergravel.base.config;
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.MqttCallbackExtended;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import java.util.Arrays;
/**
* Description:
*
* @author DawnStar
* Date: 2023/7/7
*/
public class MqttCallbackConfig implements MqttCallbackExtended {
/**
* @param reconnect If true, the connection was the result of automatic reconnect.
* @param serverUri The server URI that the connection was made to.
*/
@Override
public void connectComplete(boolean reconnect, String serverUri) {
System.out.println(serverUri);
// reconnect进行自动重连
}
/**
* 导致失去连接的原因
*
* @param cause the reason behind the loss of connection.
*/
@Override
public void connectionLost(Throwable cause) {
cause.printStackTrace();
}
@Override
public void messageArrived(String topic, MqttMessage message) throws Exception {
// 这里处理接收的消息
String prefix = "**************";
String content = "\n普通Client:\n" + "topic: " + topic
+ "\nqos:" + message.getQos()
+ "\npayload:" + new String(message.getPayload())
+ "\nretained:"+message.isRetained()+"\n";
System.out.println(prefix + content + prefix);
}
@Override
public void deliveryComplete(IMqttDeliveryToken token) {
System.out.println(Arrays.toString(token.getTopics()));
}
}
package com.silvergravel.base.config;
import com.silvergravel.share.MqttProperties;
import org.eclipse.paho.client.mqttv3.MqttClient;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.Resource;
import java.util.Arrays;
import java.util.function.IntFunction;
/**
* Description:
*
* @author DawnStar
* Date: 2023/7/7
*/
@Configuration
public class MqttConnectConfig {
@Resource
private MqttProperties mqttProperties;
@Bean
public MqttCallbackConfig mqttCallbackConfig() {
return new MqttCallbackConfig();
}
@Bean
public MqttClient mqttClient() throws MqttException {
MemoryPersistence persistence = new MemoryPersistence();
MqttClient mqttClient = new MqttClient(mqttProperties.getBroker(),
mqttProperties.getClientId(), persistence);
// topics.length == qos.length 两者长度一致
String[] topics = mqttProperties.getTopics();
mqttClient.setCallback(mqttCallbackConfig());
mqttClient.connect(createMqttConnectOptions());
Integer[] qosList = mqttProperties.getQosList();
int[] qosArray = Arrays.stream(qosList).mapToInt(Integer::intValue).toArray();
mqttClient.subscribe(topics, qosArray);
return mqttClient;
}
@Bean
public MqttClient mqttClient1() throws MqttException {
MemoryPersistence persistence = new MemoryPersistence();
MqttClient mqttClient = new MqttClient(mqttProperties.getBroker(),
mqttProperties.getClientId() + 1, persistence);
mqttClient.setCallback(mqttCallbackConfig());
mqttClient.connect(createMqttConnectOptions());
String topic = mqttProperties.getTopics()[0];
int qos = mqttProperties.getQosList()[0];
mqttClient.subscribe(topic, 1);
return mqttClient;
}
private MqttConnectOptions createMqttConnectOptions() {
MqttConnectOptions connOpts = new MqttConnectOptions();
connOpts.setUserName(mqttProperties.getUsername());
connOpts.setPassword(mqttProperties.getPassword().toCharArray());
connOpts.setCleanSession(false);
return connOpts;
}
}
controller
package com.silvergravel.base.controller;
import com.silvergravel.share.MqttProperties;
import org.eclipse.paho.client.mqttv3.MqttClient;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* Description:
*
* @author DawnStar
* Date: 2023/7/8
*/
@Configuration
@RestController
public class BaseController {
@Resource
private MqttProperties mqttProperties;
@Resource
private MqttClient mqttClient;
@GetMapping("/base/qos0")
public String qos0() throws MqttException {
mqttClient.publish(mqttProperties.getTopics()[0], "这是一条QoS 0的消息".getBytes(), 0, false);
return "这是一条QoS 0的消息";
}
@GetMapping("/base/qos1")
public String qos1() throws MqttException {
mqttClient.publish(mqttProperties.getTopics()[0], "这是一条QoS 1的消息".getBytes(), 1, false);
return "这是一条QoS 1的消息";
}
@GetMapping("/base/qos2")
public String qos2() throws MqttException {
mqttClient.publish(mqttProperties.getTopics()[0], "这是一条QoS 2的消息".getBytes(), 2, false);
return "这是一条QoS 2的消息";
}
@GetMapping("/base/qos2/retained")
public String retained() throws MqttException {
byte[] message = "这是一条QoS 2的消息 并且开启保留策略,接收之后重启系统查看retained的输出".getBytes();
mqttClient.publish(mqttProperties.getTopics()[0],message , 2, true);
return "这是一条QoS 2的消息 并且开启保留策略,接收之后重启系统查看retained的输出";
}
@GetMapping("/base/empty")
public String empty() throws MqttException {
mqttClient.publish(mqttProperties.getTopics()[0], "".getBytes(), 1,true);
return "发送一条空消息删除保留信息,空信息是带有保留策略的空信息,无关QoS等级";
}
}
启动类
@SpringBootApplication
public class MqttApplication {
public static void main(String[] args) {
SpringApplication.run(MqttApplication.class, args);
}
}
github链接
拂晓银砾的 mqtt-spring-boot-study(github.com)