最近项目中使用了EMQ,将使用情况记录总结下。
1、pom配置,引入相关jar
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-mqtt</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-stream</artifactId>
</dependency>
2、MQTT服务器信息配置
配置MQTT服务器基本信息,在springBoot配置文件application.properties中配置,添加如下:
config:
mqtt:
clientId: client002
host: tcp://1.10.2.4:1883
password: xxx@xxx
timeout: 2000
username: system
#默认订阅的topic $share/{group}/固定写法 相同group同一消息只会消费一次
topic-list:
- $share/aaa/data/+/upload
- $share/aaa/data/+/topic2
- $share/aa/data/+/topic3
data-save:
enable: true
#线程池相关配置
pool:
single:
poolSize: 200
coreSize: 1
keepAliveSeconds: 0
maxSize: 1
namePrefix: single-pool-
queueCapacity: 2000
3、创建MQTT相关配置类
import com.xx.MqttConfigProperties;
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.annotation.Bean;
import org.springframework.stereotype.Component;
import java.util.UUID;
@Component
@Slf4j
public class MqttPushClient {
private static MqttClient client;
@Autowired
private PushCallback pushCallback;
@Autowired
private MqttConfigProperties mqttConfigProperties;
public static MqttClient getClient() {
return client;
}
/**
* 发布,默认qos为0,非持久化
*/
public static void publish(String topic, String pushMessage) {
publish(0, false, topic, pushMessage);
}
/**
* 发布
*/
public static void publish(int qos, boolean retained, String topic, String pushMessage) {
MqttMessage message = new MqttMessage();
message.setQos(qos);
message.setRetained(retained);
message.setPayload(pushMessage.getBytes());
MqttTopic mTopic = MqttPushClient.getClient().getTopic(topic);
if (null == mTopic) {
log.error("topic not exist");
throw new RuntimeException("topic not exist");
}
MqttDeliveryToken token;
try {
token = mTopic.publish(message);
token.waitForCompletion();
} catch (MqttException e) {
log.error(e.getMessage(), e);
}
}
/**
* 订阅某个主题,qos默认为0
*/
public static void subscribe(String topic) {
subscribe(topic, 0);
}
/**
* 订阅某个主题
*/
public static void subscribe(String topic, int qos) {
try {
log.info("开始订阅topic:{},qos:{}", topic, qos);
MqttPushClient.getClient().subscribe(topic, qos);
log.info("{},订阅成功,qos:{}", topic, qos);
} catch (MqttException e) {
log.error(e.getMessage(), e);
}
}
/**
* 设置mqtt连接参数
*/
public MqttConnectOptions setMqttConnectOptions() {
MqttConnectOptions options = new MqttConnectOptions();
options.setUserName(mqttConfigProperties.getUsername());
options.setPassword(mqttConfigProperties.getPassword().toCharArray());
options.setConnectionTimeout(mqttConfigProperties.getTimeout());
options.setKeepAliveInterval(mqttConfigProperties.getKeepalive());
options.setCleanSession(false);
return options;
}
/**
* 连接emq服务器
*/
public void connect() {
try {
if (client == null) {
//clientId重复会导致服务启动失败
client = new MqttClient(mqttConfigProperties.getHost(), mqttConfigProperties.getClientId() + UUID.randomUUID(), new MemoryPersistence());
client.setCallback(pushCallback);
}
MqttConnectOptions mqttConnectOptions = setMqttConnectOptions();
if (client.isConnected()) {
client.disconnect();
}
client.connect(mqttConnectOptions);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
@Bean
public MqttPushClient getMqttPushClient() {
this.connect();
return this;
}
}
4、读取配置信息
Spring boot 读取自定义properties配置文件,同时初始化配置类。
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.Enumeration;
import java.util.List;
@Configuration
@ConfigurationProperties("config.mqtt")
@Data
@Slf4j
public class MqttConfigProperties {
private String host;
private String clientId;
private String username;
private String password;
private List<String> topicList;
private int timeout;
private int keepalive;
public void setTopicList(List<String> topicList) {
this.topicList = topicList;
}
/**
* @return 根据配置文件选举emq服务器
*/
public String getHost() {
String machineIp = "";
try {
machineIp = getIpAdd();
log.info("【emq注册】 获取到本机ip:{}", machineIp);
} catch (UnknownHostException | SocketException e) {
log.error("【emq注册】本机ip获取失败:{},{}", e.getMessage(), e);
}
if (host.contains("|")) {
log.info("【emq注册】开始匹配 获取到配置为:{}", host);
String[] pieceDataArray = host.split("\\|");
for (String pieceData : pieceDataArray) {
String ip = pieceData.split(",")[0];
String emqIp = pieceData.split(",")[1];
if (ip.equals(machineIp)) {
log.info("【emq注册】匹配成功,开始注册,目标emqIp:{}", emqIp);
return emqIp;
}
}
}
log.warn("【emq注册】配置文件未更新或格式错误,直接返回配置");
return host;
}
public String getIpAdd() throws SocketException, UnknownHostException {
String ip = "";
for (Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements(); ) {
NetworkInterface intf = en.nextElement();
String name = intf.getName();
if (!name.contains("docker") && !name.contains("lo")) {
for (Enumeration<InetAddress> enumIpAddr = intf.getInetAddresses(); enumIpAddr.hasMoreElements(); ) {
//获得IP
InetAddress inetAddress = enumIpAddr.nextElement();
if (!inetAddress.isLoopbackAddress()) {
String ipaddress = inetAddress.getHostAddress();
if (!ipaddress.contains("::") && !ipaddress.contains("0:0:") && !ipaddress.contains("fe80")) {
log.info("【获取ip】:{}", ipaddress);
if (!"127.0.0.1".equals(ip)) {
ip = ipaddress;
}
}
}
}
}
}
return ip;
}
}
5、消费监听类
在messageArrived方法中会收到mqtt中的数据,需要处理的话就在当前方法中操作
import com.alibaba.fastjson.JSONObject;
import com.enoesis.dgs.config.mqtt.MqttMessageHandler;
import com.enoesis.dgs.config.threadPool.SinglePoolProperties;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.MqttCallback;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.concurrent.ThreadPoolExecutor;
@Component
@Slf4j
public class PushCallback implements MqttCallback {
@Autowired
private MqttPushClient mqttPushClient;
@Autowired
private MqttMessageHandler mqttMessageHandler;
@Autowired
private Map<String, ThreadPoolExecutor> singlePoolMap;
@Autowired
private SinglePoolProperties singlePoolProperties;
@Override
public void connectionLost(Throwable cause) {
// 连接丢失后,一般在这里面进行重连
log.error("连接断开,尝试重连..");
long reconnectTimes = 1;
while (true) {
try {
if (MqttPushClient.getClient().isConnected()) {
log.info("mqtt重连成功");
return;
}
log.info("mqtt重连次数 = {} 再试一次...", reconnectTimes++);
mqttPushClient.connect();
} catch (Exception e) {
log.error("mqtt重连失败:{}{}", e.getMessage(), e);
}
try {
Thread.sleep(1000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
}
@Override
public void deliveryComplete(IMqttDeliveryToken token) {
log.info("deliveryComplete: " + token.isComplete());
}
@Override
public void messageArrived(String topic, MqttMessage message) {
//增加异常捕获,防止rocketMq异常导致mqtt重新连接
try {
String[] topicSplit = topic.split("/");
String gatewayId = topicSplit[1];
String messageType = topicSplit[2];
JSONObject jsonObject = buildPayload(message.getPayload(), gatewayId);
String deviceId = jsonObject.getString("device_id");
ThreadPoolExecutor threadPoolExecutor = switchPoolExecutor(deviceId);
if (StringUtils.isBlank(deviceId)) {
log.error("分区键 device_id 不存在,message:{}", message);
throw new RuntimeException("分区键 device_id 不存在");
}
threadPoolExecutor.execute(() ->mqttMessageHandler.pushToRocket(messageType, jsonObject));
} catch (Exception e) {
log.error(e.getMessage(), e);
}
// subscribe后得到的消息会执行到这里面
log.info("接收消息主题{},qos:{},payload:{} ", topic, message.getQos(), new String(message.getPayload()));
}
public ThreadPoolExecutor switchPoolExecutor(String deviceId) {
int h;
int i = (Math.abs(h = deviceId.hashCode()) ^ (h >>> 16)) % singlePoolProperties.getPoolSize();
return singlePoolMap.get(singlePoolProperties.getSinglePoolName(i));
}
/**
* 构造rocketMQ的payload
*
* @param mqttPayload mqttPayload
* @param gatewayId 网关id
* @return 用于推送rocketMQ的payload
*/
private JSONObject buildPayload(byte[] mqttPayload, String gatewayId) {
String message = new String(mqttPayload);
JSONObject jsonObject = JSONObject.parseObject(message);
if (jsonObject.getString("client_id") == null) {
log.warn("消息中没有client_id,自动补充。消息:{}", message);
jsonObject.put("client_id", gatewayId);
}
return jsonObject;
}
}
6、订阅
import com.x.MqttConfigProperties;
import lombok.AllArgsConstructor;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
@AllArgsConstructor
public class Subscribe implements ApplicationRunner {
private final MqttConfigProperties mqttConfigProperties;
/**
* 设置默认订阅的topic
*/
@Override
public void run(ApplicationArguments args) {
List<String> topicList = mqttConfigProperties.getTopicList();
topicList.forEach(MqttPushClient::subscribe);
}
}
7.controller样例
@Controller
public class MqttController {
@Autowired
private MqttPushClient mqttPushClient;
@RequestMapping("/hello")
@ResponseBody
public String sendHello(){
String kdTopic = "topic1";
mqttPushClient.publish(0, false, kdTopic, "15345715326");
mqttPushClient.subscribe(kdTopic);
return "123";
}