1.MQTT协议概述
MQTT是一种基于发布/订阅模式的轻量级消息传输协议,常用于低带宽、不可靠网络环境下传输消息,适用于物联网设备之间的通信。
1.1 MQTT协议的组件
- 客户端(Client):连接到MQTT代理服务器的设备(发布者、订阅者)
- 代理服务器(Broker):负责接收来自客户端的消息并将其转发给订阅该主题的客户端
- 主题(Topic):消息的分类标识,客户端通过订阅或发布到特定主题来接受或发送消息
- 消息(Message):客户端通过主题传输的数据负载
客户端通过TCP/IP连接到代理,通过身份验证保持长连接
2.MQTT服务器搭建
用EMQX来搭建服务器
快速开始 | EMQX 4.3 文档https://docs.emqx.com/zh/emqx/v4.3/getting-started/getting-started.html 下载EMQX
https://www.emqx.com/zh/try?product=brokerhttps://www.emqx.com/zh/try?product=broker 安装虚拟机(环境:centOS7)
配置 EMQX Yum 源
curl -s https://assets.emqx.com/scripts/install-emqx-rpm.sh | sudo bash
安装依赖
yum install epel-release -y
yum install -y openssl11 openssl11-devel
安装 EMQX
sudo yum install emqx -y
启动 EMQX
sudo systemctl start emqx
访问管理后台:http://localhost:18083/#/login?to=/dashboard/overview
(默认用户名:admin 密码:public)
访问接口文档: http://localhost:18083/api-docs/index.html#
2.1 创建主题
3.使用EMQX服务器
3.1 导入依赖
<dependency>
<groupId>org.eclipse.paho</groupId>
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
<version>1.2.2</version>
</dependency>
3.2 创建MQTT连接
String broker = "tcp://localhost:1883";
// TLS/SSL
// String broker = "ssl://broker.emqx.io:8883";
String username = "emqx";
String password = "public";
String clientid = "publish_client";
MqttClient client = new MqttClient(broker, clientid, new MemoryPersistence());
MqttConnectOptions options = new MqttConnectOptions();
options.setUserName(username);
options.setPassword(password.toCharArray());
client.connect(options);
- MqttClient: 同步调用客户端,使用阻塞方法通信。
- MqttClientPersistence: 代表一个持久的数据存储,用于在传输过程中存储出站和入站的信息,使其能够传递到指定的 QoS。
- MqttConnectOptions: 连接选项,用于指定连接的参数,下面列举一些常见的方法。
- setUserName: 设置用户名
- setPassword: 设置密码
- setCleanSession: 设置是否清除会话
- setKeepAliveInterval: 设置心跳间隔
- setConnectionTimeout: 设置连接超时时间
- setAutomaticReconnect: 设置是否自动重连
3.3 发布MQTT消息
package com.mqttdemo.util;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.paho.client.mqttv3.*;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
/**
* 消息推送客户端
*/
@Slf4j
@Component
public class PublishSample implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
PublishSample.applicationContext = applicationContext;
}
private final static int QOS_1 = 1;
private final static String USER_NAME = "xxx";
private final static String PASSWORLD = "xxx";
private final static int KEEP_ALIVE = 60;
//连接地址
public static final String HOST = "tcp://xxx";
// 订阅主题
public static final String TOPIC = "/xxx/xxx/xxx";
//客户端唯一ID
private static final String clientid = "xxx";
public static MqttClient createMqtt() {
MqttClient client = null;
MqttConnectOptions connectOptions = new MqttConnectOptions();
// 设置会话心跳时间
connectOptions.setKeepAliveInterval(KEEP_ALIVE);
// 不建立持久会话
connectOptions.setCleanSession(true);
// 用户名
connectOptions.setUserName(USER_NAME);
// 密码
connectOptions.setPassword(PASSWORLD.toCharArray());
try {
client = new MqttClient(HOST, clientid, new MemoryPersistence());
// MQTT连接
client.connect(connectOptions);
// 获取 Spring 上下文中的 MqttCallBackHandle bean
MqttCallBackHandle callbackHandle = applicationContext.getBean(MqttCallBackHandle.class);
// 设置回调
client.setCallback(callbackHandle);
} catch (MqttException e) {
log.warn("MQTT消息异常{}", e);
}
return client;
}
/**
* 消息推送
*
* @param message 消息内容
* @param topic 发送的主题
* @author yanglingcong
* @date 2022/4/18 21:25
*/
public static void publishMessage(String message, String topic, MqttClient mqttClient) {
MqttMessage mqttMessage = new MqttMessage();
mqttMessage.setQos(QOS_1);
mqttMessage.setPayload(message.getBytes());
try {
mqttClient.publish(topic, mqttMessage);
log.info("MQTT消息发送成功:{}", message);
} catch (MqttException e) {
log.warn("MQTT消息推送失败", e);
}
}
}
3.4 订阅MQTT主题
package com.mqttdemo.util;
import com.mqttdemo.util.MqttCallBackHandle;
import lombok.extern.slf4j.Slf4j;
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.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
/**
* 消息订阅客户端
*/
@Component
@Slf4j
public class SubscribeSample implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
SubscribeSample.applicationContext = applicationContext;
try {
subscribe();
} catch (MqttException e) {
log.error("Failed to subscribe", e);
}
}
private final static int QOS_1 = 1;
private final static String USER_NAME = "xxx";
private final static String PASSWORLD = "xxx";
private final static int KEEP_ALIVE = 60;
public static final String HOST = "tcp://xxx";
public static final String[] TOPICS = {
"/xxx/xxx/xxx/a",
"/xxx/xxx/xxx/b",
"/xxx/xxx/xxx/c"
};
public static final int[] QOS = {QOS_1, QOS_1, QOS_1};
private static final String clientid = "subClient";
public SubscribeSample() {
// 构造函数中不再调用 subscribe
}
public static void subscribe() throws MqttException {
MqttClient client = null;
MqttConnectOptions connectOptions = new MqttConnectOptions();
connectOptions.setAutomaticReconnect(true);
connectOptions.setKeepAliveInterval(KEEP_ALIVE);
connectOptions.setCleanSession(true);
connectOptions.setUserName(USER_NAME);
connectOptions.setPassword(PASSWORLD.toCharArray());
try {
client = new MqttClient(HOST, clientid, new MemoryPersistence());
client.connect(connectOptions);
MqttCallBackHandle callbackHandle = applicationContext.getBean(MqttCallBackHandle.class);
client.setCallback(callbackHandle);
client.subscribe(TOPICS, QOS);
} catch (MqttException e) {
log.warn("MQTT消息订阅异常{}", e);
e.printStackTrace();
}
}
}
3.5 MQTT工具类发送消息
package com.mqttdemo.util;
import lombok.Data;
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.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class MQTTUtil {
private static String username = "xxx";
private static String password = "xxx";
//连接地址
public static String broker = "tcp://xxxx:xxxx";
//客户端唯一ID
private static String clientId = "xxx";
public MQTTUtil(String broker, String clientId, String username, String password) {
this.broker = broker;
this.clientId = clientId;
this.username = username;
this.password = password;
}
public static String sendMessage(String topic, String message, int qos) {
MqttClient client = null;
final String[] response = {null};
final CountDownLatch latch = new CountDownLatch(1);
try {
client = new MqttClient(broker, clientId);
MqttConnectOptions connOpts = new MqttConnectOptions();
connOpts.setCleanSession(true);
if (username != null && password != null) {
connOpts.setUserName(username);
connOpts.setPassword(password.toCharArray());
}
client.setCallback(new MqttCallback() {
@Override
public void connectionLost(Throwable cause) {
System.out.println("Connection lost: " + cause.getMessage());
}
@Override
public void messageArrived(String topic, MqttMessage message) {
String result = new String(message.getPayload());
System.out.println("Message arrived. Topic: " + topic + " Message: " + result);
response[0] = result;
latch.countDown(); // Signal that a message has arrived
}
@Override
public void deliveryComplete(IMqttDeliveryToken token) {
System.out.println("Delivery complete: " + token.getMessageId());
}
});
System.out.println("Connecting to broker: " + broker);
client.connect(connOpts);
System.out.println("Connected");
// Subscribe to the topic to receive replies
String subTopic = topic.replaceAll("command", "getcommand");
System.out.println("Subscribing to topic: " + subTopic);
client.subscribe(subTopic);
// Publish the message
MqttMessage mqttMessage = new MqttMessage(message.getBytes());
mqttMessage.setQos(qos);
System.out.println("Publishing message: " + message + " to topic: " + topic);
client.publish(topic, mqttMessage);
System.out.println("Message published");
// Wait for the response message
latch.await(2, TimeUnit.MINUTES); // Wait for up to 2 minutes for a response
client.disconnect();
System.out.println("Disconnected");
} catch (MqttException | InterruptedException me) {
me.printStackTrace();
} finally {
if (client != null) {
try {
client.close();
} catch (MqttException e) {
e.printStackTrace();
}
}
}
return response[0]; // Return the received message
}
}
3.6 MQTT配置类
package com.mqttdemo.config;
import com.mqttdemo.service.EventDataDocService;
import com.mqttdemo.service.SensorDataHourService;
import com.mqttdemo.service.SensorDataMinService;
import com.mqttdemo.util.MqttCallBackHandle;
import org.eclipse.paho.client.mqttv3.MqttClient;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MqttConfig {
private static final String BROKER_URL = "tcp://xxx";
private static final String CLIENT_ID = "xxx";
@Bean
public MqttClient mqttClient(SensorDataMinService sensorDataMinService, SensorDataHourService sensorDataHourService, EventDataDocService eventDataDocService) throws MqttException {
MqttClient client = new MqttClient(BROKER_URL, CLIENT_ID, new MemoryPersistence());
MqttConnectOptions options = new MqttConnectOptions();
options.setCleanSession(true);
client.connect(options);
// 创建 MqttCallBackHandle 实例并设置回调
MqttCallBackHandle mqttCallBackHandle = new MqttCallBackHandle(sensorDataMinService, sensorDataHourService,eventDataDocService, client);
client.setCallback(mqttCallBackHandle);
return client;
}
}
3.7 回调处理类
package com.mqttdemo.util;
import cn.hutool.core.lang.UUID;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mqttdemo.entity.EventDataDocPo;
import com.mqttdemo.entity.Password;
import com.mqttdemo.entity.SensorDataHour;
import com.mqttdemo.entity.SensorDataMin;
import com.mqttdemo.service.EventDataDocService;
import com.mqttdemo.service.SensorDataHourService;
import com.mqttdemo.service.SensorDataMinService;
import com.mysql.cj.util.StringUtils;
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.MqttMessage;
import org.eclipse.paho.client.mqttv3.MqttClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
@Slf4j
@Component
public class MqttCallBackHandle implements MqttCallback {
private final ABService abService;
private final MqttClient client;
@Autowired
public MqttCallBackHandle(ABService abService, MqttClient client) {
this.abService= abService;
this.client = client;
}
@Override
public void connectionLost(Throwable cause) {
log.warn("MQTT连接丢失", cause);
}
@Override
public void messageArrived(String topic, MqttMessage message) throws Exception {
String payload = new String(message.getPayload());
switch (topic) {
case "/xxx/xxx/xxx/xxx":
// 收到消息后调用发布者发布消息
String responseTopic = "/xxx/xxx/xxx/xxx";
ObjectMapper objectMapper = new ObjectMapper();
MqttClient publishClient = PublishSample.createMqtt();
if (publishClient != null) {
PublishSample.publishMessage("{\"a\": \"123\"}", responseTopic, publishClient);
publishClient.disconnect();
publishClient.close();
} else {
log.warn("无法创建发布者客户端");
}
break;
case "/xxx/xxx/xxx/aaa":
// 当收到 aaa 主题的消息时,打印消息
JSONObject jsonObject = JSONUtil.parseObj(payload);
Double temperatureMin = jsonObject.get("Temperature_min", Double.class);
AB abData= parsePayloadToAB(payload);
abService.save(abData);
break;
default:
log.warn("收到未处理的主题: {}", topic);
break;
}
}
public static AB parsePayloadToAB(String payload) {
JSONObject jsonObject = JSONUtil.parseObj(payload);
AB abData= new AB();
abData.setbbb(jsonObject.getDouble("bbb"));
abData.setaaa(jsonObject.getDouble("aaa"));
return abData;
}
@Override
public void deliveryComplete(IMqttDeliveryToken token) {
log.info("消息发送完成: {}", token.isComplete());
}
}
- connectionLost(Throwable cause): 连接丢失时被调用
- messageArrived(String topic, MqttMessage message): 接收到消息时被调用
- deliveryComplete(IMqttDeliveryToken token): 消息发送完成时被调用
4.需要nginx代理访问MQTT的情况
nginx需要配置网页端以及服务器端,一个http,一个tcp
# MQTT 代理
stream {
upstream mqtt_backend {
server 192.168.0.11:1883;
}
server {
listen 1883;
proxy_pass mqtt_backend;
}
}
# HTTP 代理
http {
server {
listen 18083;
server_name localhost;
location / {
proxy_pass http://192.168.0.11:18083;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}