MQTT、SpringBoot
SpringBoot 整合 Mqtt (发布、订阅)
文中部分内容借鉴了其他作者,在此感谢提供方法的各位作者。
1、MQTT 协议介绍(简单介绍)
1.1、Mqtt协议中的三个角色:
实现MQTT协议需要客户端和服务器端通讯完成,在通讯过程中,MQTT协议中有三种身份:发布者(Publish)、代理(Broker)(服务器)、订阅者(Subscribe)。其中,消息的发布者和订阅者都是客户端,消息代理是服务器,消息发布者可以同时是订阅者。
1.2、mqtt传输的消息内容
Topic(主题),可以理解为消息的类型,订阅者订阅(Subscribe)后,就会收到该主题的消息内容(payload)
payload,可以理解为消息的内容,是指订阅者具体要使用的内容。
1.3、mqtt客户端
一个使用MQTT协议的应用程序或者设备,它总是建立到服务器的网络连接。客户端可以:
A、发布其他客户端可能会订阅的信息;
B、订阅其它客户端发布的消息;
C、退订或删除应用程序的消息;
D、断开与服务器连接
1.4、mqtt服务端器
MQTT服务器以称为"消息代理"(Broker),可以是一个应用程序或一台设备。它是位于消息发布者和订阅者之间,它可以:
A、接受来自客户的网络连接;
B、接受客户发布的应用信息;
C、处理来自客户端的订阅和退订请求;
D、向订阅的客户转发应用程序消息。
1.5、mqtt的订阅、主题、会话
1.5.1、订阅(Subscription)
订阅包含主题筛选器(Topic Filter)和最大服务质量(QoS)。订阅会与一个会话(Session)关联。一个会话可以包含多个订阅。每一个会话中的每个订阅都有一个不同的主题筛选器。
1.5.2、会话(Session)
每个客户端与服务器建立连接后就是一个会话,客户端和服务器之间有状态交互。会话存在于一个网络之间,也可能在客户端和服务器之间跨越多个连续的网络连接。
1.5.3、主题名(Topic Name)
连接到一个应用程序消息的标签,该标签与服务器的订阅相匹配。服务器会将消息发送给订阅所匹配标签的每个客户端。
1.5.4、主题筛选器(Topic Filter)
一个对主题名通配符筛选器,在订阅表达式中使用,表示订阅所匹配到的多个主题。
1.5.5、负载(Payload)
消息订阅者所具体接收的内容。
2、工具介绍(介绍本文中使用的)
2.1、代理服务器:Emqx
EMQ X 是基于 Erlang/OTP 语言平台开发,支持大规模连接和分布式集群,发布订阅模式的开源 MQTT 消息服务器。
Linux docker 安装启动命令:
docker run -d --name emqx -p 1883:1883 -p 8083:8083 -p 8883:8883 -p 8084:8084 -p 18083:18083 emqx/emqx
安转后访问http://ip:18083/
默认用户名/密码: admin/public
访问如下表示安装成功:
2.2、MQTT.fx
MQTT.fx 是目前主流的mqtt客户端,可以快速验证是否可以与IoT Hub 服务交流发布或订阅消息。设备将当前所处的状态作为MQTT主题发送给IoT Hub,每个MQTT主题topic具有不同等级的名称,” MQTT代理服务器将接收到的主题topic发送给给所有订阅的客户端。
下载连接:http://www.jensd.de/apps/mqttfx/1.7.1/
参考:https://blog.csdn.net/tiantang_1986/article/details/85101366
3、环境简介:
3.1、SpringBoot-version:2.1.2
3.2、Maven-version:3.6.1
3.3、开发工具: IDEA 2019.3
3.4、JDK-version:1.8
4、MQTT 发布
4.1、新建工程:MQTT-SpringBoot
4.2、pom
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>
<!--MQTT-->
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-stream</artifactId>
<version>4.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-mqtt</artifactId>
<version>5.0.6.RELEASE</version>
</dependency>
</dependencies>
4.3、配置文件:
server:
port: 8090
spring:
application:
name: MQTT-SpringBoot
mqtt:
username: admin
password: public
# 推送信息的连接地址,如果有多个,用逗号隔开,如:tcp://ip:1883,tcp://ip:1883
url: tcp://ip:1883,tcp://ip:1883
sender:
# 默认发送的主题
defaultTopic: goods
# clientid
clientId: mqtttest
4.4、代码
4.4.1、modal
package com.riest.modal;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* ClassName:Send
* Describe:
* Author:DGJ
* Data:2020/10/29 10:06
*/
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class Send {
private String topic;
private String key;
private String value;
}
4.4.2、config
package com.riest.config;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.springframework.beans.factory.annotation.Value;
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.core.DefaultMqttPahoClientFactory;
import org.springframework.integration.mqtt.core.MqttPahoClientFactory;
import org.springframework.integration.mqtt.outbound.MqttPahoMessageHandler;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;
import org.springframework.util.StringUtils;
/**
* ClassName:MqttConfig
* Describe:
* Author:DGJ
* Data:2020/10/29 10:08
*/
@Configuration
public class MqttConfig {
/**
* 发布的bean名称
*/
public static final String CHANNEL_NAME_OUT = "mqttOutboundChannel";
/**
* 客户端与服务器之间的连接意外中断,服务器将发布客户端的"遗嘱"消息
*/
private static final byte[] WILL_DATA;
static {
WILL_DATA = "offline".getBytes();
}
@Value("${mqtt.username}")
private String username;
@Value("${mqtt.password}")
private String password;
@Value("${mqtt.url}")
private String url;
@Value("${mqtt.sender.clientId}")
private String clientId;
@Value("${mqtt.sender.defaultTopic}")
private String defaultTopic;
/**
* MQTT连接器选项
*/
@Bean
public MqttConnectOptions getSenderMqttConnectOptions(){
MqttConnectOptions options=new MqttConnectOptions();
// 设置连接的用户名
if(!username.trim().equals("")){
options.setUserName(username);
}
// 设置连接的密码
options.setPassword(password.toCharArray());
// 设置连接的地址
options.setServerURIs(StringUtils.split(url, ","));
// 设置超时时间 单位为秒
options.setConnectionTimeout(10);
// 设置会话心跳时间 单位为秒 服务器会每隔1.5*20秒的时间向客户端发送心跳判断客户端是否在线
// 但这个方法并没有重连的机制
options.setKeepAliveInterval(20);
// 设置 "遗嘱" 消息的话题,若客户端与服务器之间的连接意外中断,服务器将发布客户端的"遗嘱"消息。
options.setWill("willTopic", WILL_DATA, 2, false);
return options;
}
/**
* MQTT客户端
*/
@Bean
public MqttPahoClientFactory senderMqttClientFactory() {
DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory();
factory.setConnectionOptions(getSenderMqttConnectOptions());
return factory;
}
/**
* MQTT信息通道(生产者)
*/
@Bean(name = CHANNEL_NAME_OUT)
public MessageChannel mqttOutboundChannel() {
return new DirectChannel();
}
/**
* MQTT消息处理器(生产者)
*/
@Bean
@ServiceActivator(inputChannel = CHANNEL_NAME_OUT)
public MessageHandler mqttOutbound() {
MqttPahoMessageHandler messageHandler = new MqttPahoMessageHandler(clientId, senderMqttClientFactory());
messageHandler.setAsync(true);
messageHandler.setDefaultTopic(defaultTopic);
return messageHandler;
}
}
4.4.3、service
package com.riest.service;
import com.riest.config.MqttConfig;
import org.springframework.integration.annotation.MessagingGateway;
import org.springframework.integration.mqtt.support.MqttHeaders;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.stereotype.Component;
/**
* ClassName:SendInterface
* Describe:
* Author:DGJ
* Data:2020/10/29 10:08
*/
@Component
@MessagingGateway(defaultRequestChannel = MqttConfig.CHANNEL_NAME_OUT)
public interface ISend {
/**
* 发送信息到MQTT服务器
*
* @param data 发送的文本
*/
void sendToMqtt(String data);
/**
* 发送信息到MQTT服务器
*
* @param topic 主题
* @param payload 消息主体
*/
void sendToMqtt(@Header(MqttHeaders.TOPIC) String topic,
String payload);
/**
* 发送信息到MQTT服务器
*
* @param topic 主题
* @param qos 对消息处理的几种机制。
* 0 表示的是订阅者没收到消息不会再次发送,消息会丢失。
* 1 表示的是会尝试重试,一直到接收到消息,但这种情况可能导致订阅者收到多次重复消息。
* 2 多了一次去重的动作,确保订阅者收到的消息有一次。
* @param payload 消息主体
*/
void sendToMqtt(@Header(MqttHeaders.TOPIC) String topic,
@Header(MqttHeaders.QOS) int qos,
String payload);
}
4.4.4、controller
package com.riest.controller;
import com.alibaba.fastjson.JSONObject;
import com.riest.modal.Send;
import com.riest.service.ISend;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* ClassName:SendController
* Describe:
* Author:DGJ
* Data:2020/10/29 10:11
*/
@RestController
@Slf4j
public class SendController {
@Autowired
private ISend iMqttSender;
/**
* 发送自定义消息内容(使用默认主题)
* @param data
*/
@GetMapping(value = "/send")
public void test1(Send data) {
JSONObject json = (JSONObject) JSONObject.toJSON(data);
log.error("----->{}","mqtt 消息发布,使用默认主题发布:"+json.toJSONString());
iMqttSender.sendToMqtt(json.toJSONString());
}
/**
* 发送自定义消息内容,且指定主题
* @param data
*/
@RequestMapping("/send/topic")
public void test2(Send data) {
JSONObject json = (JSONObject) JSONObject.toJSON(data);
log.error("----->{}","mqtt 消息发布,指定主题:"+json.toJSONString());
iMqttSender.sendToMqtt(data.getTopic(), json.toJSONString());
}
}
4.4.5、主启动
package com.riest;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* ClassName:MqttApplication
* Describe:
* Author:DGJ
* Data:2020/10/29 10:05
*/
@SpringBootApplication
public class MqttApplication {
public static void main(String[] args) {
SpringApplication.run(MqttApplication.class,args);
}
}
4.5、工具设置
4.5.1、mqtt.fx
4.5.2、EMQX
4.6、测试
4.6.1 、调用(该请求时使用默认主题) http://localhost:8090/send?key=send&value=1234567
4.6.2、mqtt.fx收到订阅的消息
4.6.3 、emqx查看
4.6.4、调用(指定主题发布)http://localhost:8090/send/topic?key=send&value=1234567&topic=dgj-test
4.6.5、mqtt.fx收到订阅的消息
4.6.6 、emqx查看
5、MQTT 订阅
5.1 、config
package com.riest.config;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.springframework.beans.factory.annotation.Value;
/**
* ClassName:MQTTConnect
* Describe:
* Author:DGJ
* Data:2020/10/29 10:46
*/
public class MQTTConnect {
@Value("${mqtt.username}")
private String username;
@Value("${mqtt.password}")
private String password;
/**
* 把配置里的 cleanSession 设为false,客户端掉线后 服务器端不会清除session,
* 当重连后可以接收之前订阅主题的消息。当客户端上线后会接受到它离线的这段时间的消息,
* 如果短线需要删除之前的消息则可以设置为true
*
* @return
*/
public MqttConnectOptions getOptions() {
MqttConnectOptions options = new MqttConnectOptions();
options.setCleanSession(false);
options.setUserName("admin");
options.setPassword("public".toCharArray());
options.setConnectionTimeout(10);
//设置心跳
options.setKeepAliveInterval(20);
return options;
}
public MqttConnectOptions getOptions(MqttConnectOptions options) {
options.setCleanSession(false);
options.setUserName(username);
options.setPassword(password.toCharArray());
options.setConnectionTimeout(10);
options.setKeepAliveInterval(20);
return options;
}
}
5.2、sub端
package com.riest.service.sub;
import com.riest.config.MQTTConnect;
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.stereotype.Component;
/**
* ClassName:MqttSub
* Describe:
* Author:DGJ
* Data:2020/10/29 10:45
*/
@Component
public class MqttSub {
public static final String HOST = "tcp://122.51.240.115:1883";
private static final String clientid = "testrece";
private String topic = "goods";
public MqttClient client;
private MQTTConnect mqttConnect = new MQTTConnect();
/**true为非持久订阅
*
* 方法实现说明 断线重连方法,如果是持久订阅,重连是不需要再次订阅,如果是非持久订阅,重连是需要重新订阅主题 取决于options.setCleanSession(true);
*
* 就是这里的clientId,服务器用来区分用户的,不能重复,clientId不能和发布的clientId一样,否则会出现频繁断开连接和重连的问题
* 不仅不能和发布的clientId一样,而且也不能和其他订阅的clientId一样,如果想要接收之前的离线数据,这就需要将client的 setCleanSession
* 设置为false,这样服务器才能保留它的session,再次建立连接的时候,它就会继续使用这个session了。 这时此连接clientId 是不能更改的。
* 但是其实还有一个问题,就是使用热部署的时候还是会出现频繁断开连接和重连的问题,可能是因为刚启动时的连接没断开,然后热部署的时候又进行了重连,重启一 * 下就可以了
* System.currentTimeMillis()
* @throws MqttException
*/
public void connect() throws MqttException {
//防止重复创建MQTTClient实例
if (client==null) {
// MemoryPersistence设置clientid的保存形式,默认为以内存保存
client = new MqttClient(HOST, clientid, new MemoryPersistence());
//如果是订阅者则添加回调类,发布不需要
client.setCallback(new PubCallBack(MqttSub.this));
}
MqttConnectOptions options = mqttConnect.getOptions();
//判断拦截状态,这里注意一下,如果没有这个判断,是非常坑的
if (!client.isConnected()) {
client.connect(options);
System.out.println("连接成功");
}else {
client.disconnect();
client.connect(mqttConnect.getOptions(options));
System.out.println("连接成功");
}
}
public void init() {
try {
connect();
subscribe(topic);
} catch (MqttException e) {
e.printStackTrace();
}
}
/**
* 订阅某个主题,qos默认为0
*
* @param topic .
*/
public void subscribe(String topic) {
subscribe(topic,0);
}
/**
* 订阅某个主题
*
* @param topic .
* @param qos .
*/
public void subscribe(String topic, int qos) {
try {
client.subscribe(topic,0);
} catch (MqttException e) {
e.printStackTrace();
}
}
}
package com.riest.service.sub;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.MqttCallback;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.springframework.beans.factory.annotation.Value;
/**
* ClassName:PubCallBack
* Describe:
* Author:DGJ
* Data:2020/10/29 10:48
*/
@Slf4j
public class PubCallBack implements MqttCallback {
@Value("${mqtt.username}")
private String username;
@Value("${mqtt.password}")
private String password;
@Value("${mqtt.url}")
private String url;
@Value("${mqtt.receiver.clientId}")
private String clientId;
@Value("${mqtt.receiver.defaultTopic}")
private String defaultTopic;
private MqttSub mqttSub;
public PubCallBack(MqttSub subsribe) throws MqttException {
this.mqttSub = subsribe;
}
@Override
public void connectionLost(Throwable cause) {
// 连接丢失后,一般在这里面进行重连
log.error("---------------------连接断开,可以做重连");
while (true){
try {
//如果没有发生异常说明连接成功,如果发生异常,则死循环
Thread.sleep(1000);
mqttSub.init();
break;
}catch (Exception e){
// e.printStackTrace();
continue;
}
}
}
@Override
public void deliveryComplete(IMqttDeliveryToken token) {
System.out.println("deliveryComplete---------" + token.isComplete());
}
@Override
public void messageArrived(String topic, MqttMessage message) throws Exception {
// subscribe后得到的消息会执行到这里面
String result = new String(message.getPayload(),"UTF-8");
System.out.println("接收消息主题 : " + topic);
System.out.println("接收消息Qos : " + message.getQos());
System.out.println("接收消息内容 : " + result);
//这里可以针对收到的消息做处理
}
}
5.3、主启动
package com.riest;
import com.riest.service.sub.MqttSub;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import javax.annotation.PostConstruct;
/**
* ClassName:MqttApplication
* Describe:
* Author:DGJ
* Data:2020/10/29 10:05
*/
@SpringBootApplication
public class MqttApplication {
public static void main(String[] args) {
SpringApplication.run(MqttApplication.class,args);
}
@Autowired
private MqttSub mqttSub;
@PostConstruct
public void consumeMqttClient() throws MqttException {
mqttSub.init(); // 订阅 消息
}
}
6、测试订阅
6.1、mqtt.fx
6.2、emqx
6.3、程序日志