一:前提
假设你已经安装了Emqx开源版、MQTTX客户端。若没有安装请移步官网下载安装。
Emqx:下载 EMQX 开源版。MQTTX:MQTTX:全功能 MQTT 客户端工具。安装非常简单,这里不多做说明。
二:订阅发布实现步骤
1.引入依赖
<!--MQTT客户端-->
<dependency>
<groupId>org.eclipse.paho</groupId>
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
<version>1.2.2</version>
</dependency>
<!--hutool工具类-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.17</version>
</dependency>
首先引入MQTT客户端用来连接Emqx,此外引入糊涂工具类方便我们处理JSON格式数据。
2.编辑配置文件
server:
port: 8080
mqtt:
broker:
uri: tcp://localhost:1883
client:
id: mqtt-demo-client
inTopic: test/topic
outTopic: out/topic
注意MQTT协议一般使用tcp而不是http。client.id是此Java程序的客户端id,因为当你想使用MQTT协议往某个主题发消息时,Emq服务器就会把你也当做一个客户端处理。inTopic为接收消息主题,outTopic为发布消息主题,这两个可以不用写在配置文件里。
3.读取配置文件
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties(prefix = "mqtt")
@Data
public class MqttProperties {
private Broker broker;
private Client client;
private String inTopic;
private String outTopic;
@Data
public static class Broker {
private String uri;
}
@Data
public static class Client {
private String id;
}
}
此类用来读取配置文件信息。
4.创建Mqtt客户端
import com.mqtt.mqttdemo.demos.properties.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.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MqttConfig {
@Autowired
private MqttProperties mqttProperties;
@Bean
public MqttClient mqttClient() throws MqttException {
MqttClient client = new MqttClient(mqttProperties.getBroker().getUri(), mqttProperties.getClient().getId(), new MemoryPersistence());
MqttConnectOptions options = new MqttConnectOptions();
// 此客户端的用户名和密码
options.setUserName("admin");
options.setPassword("gcl12345679".toCharArray());
options.setCleanSession(true);
// 设置遗嘱消息
options.setWill(mqttProperties.getOutTopic(), "我是mqtt-demo-client,我已下线,这是我的遗嘱".getBytes(), 2, true);
// 连接超时重试
options.setConnectionTimeout(30);
client.connect(options);
return client;
}
}
此类用来设置Mqtt客户端的连接配置。遗嘱消息是MQTT协议支持的消息类型,简单的可以理解为当此客户端非正常下线时此消息会被发布。
5.controller层
import com.mqtt.mqttdemo.demos.service.MqttService;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
@RestController
@Slf4j
@RequestMapping("/mqtt")
public class MqttController {
@Autowired
private MqttService mqttService;
@PostMapping("/publish")
public void publish(@RequestParam String message) {
try {
mqttService.publish(message);
} catch (MqttException e) {
log.error("发布消息失败{}", e.getMessage());
}
log.info("发布消息成功");
}
@PostMapping("/publishRetain")
public void publishWithRetain(@RequestParam String message) {
try {
mqttService.publishWithRetain(message);
} catch (MqttException e) {
log.error("发布保留消息失败{}", e.getMessage());
}
log.info("发布保留消息成功");
}
}
保留消息也是MQTT协议支持的消息类型,设想一个场景当你往一个主题发布消息后,在发布之前已经订阅此主题的客户端可以收到此条消息,但在发布之后才订阅此主题的客户端就收不到此条消息了,但保留消息可以让发布前订阅和发布后订阅的客户端都可以收到这条消息,保留消息只允许有一条,新的保留消息会覆盖旧的。
6.service层
import cn.hutool.json.JSONUtil;
import com.influxdb.client.*;
import com.influxdb.client.domain.WritePrecision;
import com.influxdb.client.write.Point;
import com.influxdb.query.FluxTable;
import com.mqtt.mqttdemo.demos.entity.Payload;
import com.mqtt.mqttdemo.demos.properties.InfluxdbProperties;
import com.mqtt.mqttdemo.demos.properties.MqttProperties;
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.MqttClient;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.servlet.http.HttpServletRequest;
import java.time.Instant;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
@Service
@Slf4j
public class MqttService implements MqttCallback {
@Autowired
private MqttClient mqttClient;
@Autowired
private MqttProperties mqttProperties;
@PostConstruct
public void init() throws MqttException {
mqttClient.setCallback(this);
mqttClient.subscribe(mqttProperties.getInTopic());
log.info("订阅主题{}", mqttProperties.getInTopic());
}
@PreDestroy
public void destroy() throws MqttException {
mqttClient.disconnect();
log.info("与服务器断开连接");
}
/**
* @description: 发送消息
* @param: [message]
* @return: void
**/
public void publish(String message) throws MqttException {
MqttMessage mqttMessage = new MqttMessage(message.getBytes());
// 设置消息质量为1
mqttMessage.setQos(1);
mqttClient.publish(mqttProperties.getOutTopic(), mqttMessage);
log.info("向主题【{}】发布消息:【{}】", mqttProperties.getOutTopic(), message);
}
/**
* @description: 发送保留消息
* @param: [message]
* @return: void
**/
public void publishWithRetain(String message) throws MqttException {
MqttMessage mqttMessage = new MqttMessage(message.getBytes());
// 设置消息质量为2
mqttMessage.setQos(2);
// 设置保留消息标志
mqttMessage.setRetained(true);
mqttClient.publish(mqttProperties.getOutTopic(), mqttMessage);
log.info("向主题【{}】发布保留消息:【{}】", mqttProperties.getOutTopic(), message);
}
/**
* @description: 接收消息
* @param: [topic, message]
* @return: void
**/
@Override
public void messageArrived(String topic, MqttMessage message) throws MqttException {
Payload payload = JSONUtil.toBean(new String(message.getPayload()), Payload.class);
log.info("接收到来自【{}】的消息【{}】", topic, payload.getTemperature());
if (payload.getTemperature() > 37) {
publish("发烧");
}
}
@Override
public void connectionLost(Throwable cause) {
log.error("连接丢失:{}", cause.getMessage());
}
@Override
public void deliveryComplete(IMqttDeliveryToken token) {
log.info("消息已送达");
}
}
此类实现了MqttCallback回调接口,重写了里面的三个方法,connectionLost为连接丢失时回调,deliveryComplete为消息送达确认回调,messageArrived为接收到消息时回调。
Qos为MQTT协议的消息质量等级。具体细节请看一篇文章带你彻底搞懂MQTT协议-CSDN博客。
init方法为Bean创建时调用订阅主题。
destory方法为Bean销毁时调用,断开连接。
7.dao层
import lombok.Data;
@Data
public class Payload {
private Integer temperature;
}
只有一个字段,假设发送消息格式为JSON。
{
"temperature": 39
}
三:测试
1.启动Emqx、MQTTX、Java程序
启动Emqx后访问http://localhost:18083/#/clients,可以看到还没有客户端上线。
启动MQTX客户端创建一个连接并让它订阅out/topic主题。
启动Java程序查看日志可以看到他已经订阅了test/topic主题。
此时Emqx管理平台可以看到两个客户端均已经上线。
2.普通消息发布订阅
MQTTX客户端向Java客户端发送一个温度数据,大于37度时,Java客户端会回复发烧。
可以看到普通消息发布订阅成功,并且也收到了送达确认的回调。
至此普通消息发布订阅完成。
3.保留消息的发布订阅
调用接口请求http://localhost:8080/mqtt/publishRetain?message=我是mqtt-demo-client的保留消息,Java客户端会发送一条保留消息。
此时之前订阅了这个主题的客户端会受到这个保留消息。
我们再使用MQTTX客户端创建一个连接并订阅out/topic主题。
可以看到这个新上线的客户端也收到了这条保留消息。
有兴趣的话可以把保留消息修改一个下重新发布,然后再新上线一个客户端,看他收到的保留消息是最新的还是之前的,即可验证保留消息只能有一条的结论。
至此保留消息发布订阅成功。
4.遗嘱消息的发布订阅
打开Emqx管理平台,将Java客户端踢除,模拟程序意外中断的场景。
此时订阅了out/topic的客户端收到Java客户端的遗嘱消息。
至此遗嘱消息发布订阅完成。