hello, 大家好
我是一只不是在戏精,就是在戏精路上的极品二哈
新年上班第一天,给大家贡献一篇 MQTT 协议使用示例文章
也是本汪自己的一篇实用笔记
本汪先总的说下: MQTT协议进行数据交互,一共有两种方式:第一种,请求时不带任何参数的,走上报订阅
,设备直接往服务器上传,我们只需要在程序里订阅固定的主题,就可以接收到实时数据,然后进行解析入库就行(比如后面的接收道闸小门的人员上报打卡实时记录);第二种, 有查询条件的,走下发订阅
,请求时把查询条件下发给主题 report1,接收对应数据时,监听对应的主题request1,拿到符合查询条件的数据后进行处理(比如人脸数据下发和根据时间段查询人员打卡记录) report/aaa -> request/aaa,都是一一对应的哈。
一、MQTT 服务安装
windows安装包:
链接:https://pan.baidu.com/s/1wb7yNCeCRbjKxauigoaHHQ
提取码:pswp
解压后直接运行即可:
在服务中,可以看到对应的 MQTT 服务
二、MQTT 调用程序
1、 基本原理
RRPC 请求消息
: 服务器通过 MQTT 下发给设备端的消息;RRPC 响应消息
: 设备端通过 MQTT 回复给服务器的消息;RRPC 消息 ID
: 服务器为每次 RRPC 调用生成的唯一消息 ID;RRPC 订阅 Topic
: 设备端以及服务器订阅 RRPC 消息时传递的 Topic,含有通配符。2、推荐使用的MQTT 测试工具 MQTT.fx ,MQTT.fx 是一个老牌的 MQTT 客户端工具,Azure IoT Hub、AWS IoT、阿里云 IoT 等云服务提供商相关产品文档教程均以 MQTT.fx 为例。个人感觉简洁好用。
官网下载地址:https://mqttfx.jensd.de/index.php/download
本汪使用的是1.7.1,云盘地址:
链接:https://pan.baidu.com/s/1anp7PCgfq3EiCPvuaBD77Q
提取码:66w7
3、添加MQTT订阅
在Web管理后台的 设备管理 → HTTP/MQTT订阅 菜单,点击创建按钮,如下图所示。
然后填写上报MQTT服务器的地址和主题。如下图所示:
项目 填写参考示例
上报方式 MQTT
上报地址 tcp://:@:
MQTT topic <topic_name>/<设备ID>/report
是否启用 勾选
是否上报历史记录 不勾选
是否上传图片 勾选
该教程中设备的ID为5182618e-caaf-45ca-81d5-ae5dce6f8e9f,则MQTT topic为<topic_name>/5182618e-caaf-45ca-81d5-ae5dce6f8e9f/report。
其他字段如、、、和<topic_name>,需要根据MQTT服务器实际情况填写。
4、示例代码:
(1)MQTT 网关, common中的常量
package com.cccc.common.constant;
public class DeviceConstants {
public static final String GET_DEVICE_INFORMATION = "getDeviceInformation/ba3ec3e4-2e1f-4e8d-9ff8-e0235f6e1f41/request/abc";
public static final String OPEN_THE_DOOR = "openTheDoor/ba3ec3e4-2e1f-4e8d-9ff8-e0235f6e1f41/report";
public static final String LIST_PEOPLE_INFORMATION_BY_ID = "listPeopleInformationById/ba3ec3e4-2e1f-4e8d-9ff8-e0235f6e1f41/report";
public static final String SELECT_PEOPLE_INFORMATION_BY_FIELD = "selectPeopleInformationByField/ba3ec3e4-2e1f-4e8d-9ff8-e0235f6e1f41/report";
public static final String INSERT_PEOPLE = "insertPeople/ba3ec3e4-2e1f-4e8d-9ff8-e0235f6e1f41/request/abc";
public static final String ADD_BATCH = "addbatch/ba3ec3e4-2e1f-4e8d-9ff8-e0235f6e1f41/report";
public static final String DELETE = "delete/ba3ec3e4-2e1f-4e8d-9ff8-e0235f6e1f41/report";
public static final String DELETE_BATCH = "deleteBatch/ba3ec3e4-2e1f-4e8d-9ff8-e0235f6e1f41/report";
public static final String GET_CARD_RECORD = "getCardRecord/ba3ec3e4-2e1f-4e8d-9ff8-e0235f6e1f41/request/abc";
}
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.springframework.integration.annotation.MessagingGateway;
import org.springframework.integration.mqtt.support.MqttHeaders;
import org.springframework.messaging.handler.annotation.Header;
/**
* @ Author: yuezejian
* @ Description:
* @ Date: 2021/12/14 11:25
*/
@MessagingGateway(defaultRequestChannel = "mqttOutboundChannel")
public interface MqttGateway {
// 定义重载方法,用于消息发送
void sendToMqtt(String payload);
// 指定topic进行消息发送
void sendToMqtt(@Header(MqttHeaders.TOPIC) String topic, String payload);
void sendToMqtt(@Header(MqttHeaders.TOPIC) String topic, @Header(MqttHeaders.QOS) int qos, String payload);
void sendToMqtt(@Header(MqttHeaders.TOPIC) String topic, @Header(MqttHeaders.QOS) int qos, byte[] payload);
}
(2)MQTT config
package com.cccc.framework.config;
import com.cccc.common.constant.DeviceConstants;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.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.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;
/**
* @Description: 订阅并监听MQTT消息,进行相应的处理
* @Author: yuezejian
* @Date: 2021/12/14 11:22
* @Version: 1.0
**/
@Configuration
public class MQTTConfig {
private static final Logger LOG = LoggerFactory.getLogger(MQTTConfig.class);
/**
* 创建MqttPahoClientFactory,设置MQTT Broker连接属性,如果使用SSL验证,也在这里设置。
* @return factory
*/
@Bean
public MqttPahoClientFactory mqttClientFactory() {
DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory();
MqttConnectOptions options = new MqttConnectOptions();
// 设置代理端的URL地址,可以是多个
options.setServerURIs(new String[]{"tcp://192.168.20.48:1883"});
factory.setConnectionOptions(options);
return factory;
}
/**
* 入站通道
*/
@Bean
public MessageChannel mqttInputChannel() {
return new DirectChannel();
}
/**
* 入站
*/
@Bean
public MessageProducer inbound() {
// Paho客户端消息驱动通道适配器,主要用来订阅主题
MqttPahoMessageDrivenChannelAdapter adapter = new MqttPahoMessageDrivenChannelAdapter("consumerClient-paho",
mqttClientFactory(), DeviceConstants.GET_DEVICE_INFORMATION,
DeviceConstants.OPEN_THE_DOOR,
DeviceConstants.LIST_PEOPLE_INFORMATION_BY_ID,
DeviceConstants.SELECT_PEOPLE_INFORMATION_BY_FIELD,
DeviceConstants.INSERT_PEOPLE,
DeviceConstants.ADD_BATCH,
DeviceConstants.DELETE,
DeviceConstants.DELETE_BATCH,
DeviceConstants.GET_DEVICE_INFORMATION
);
adapter.setCompletionTimeout(5000);
// Paho消息转换器
DefaultPahoMessageConverter defaultPahoMessageConverter = new DefaultPahoMessageConverter();
// 按字节接收消息
// defaultPahoMessageConverter.setPayloadAsBytes(true);
adapter.setConverter(defaultPahoMessageConverter);
adapter.setQos(1); // 设置QoS
adapter.setOutputChannel(mqttInputChannel());
return adapter;
}
@Bean
// ServiceActivator注解表明:当前方法用于处理MQTT消息,inputChannel参数指定了用于消费消息的channel。
@ServiceActivator(inputChannel = "mqttInputChannel")
public MessageHandler handler() {
return message -> {
String payload = message.getPayload().toString();
// byte[] bytes = (byte[]) message.getPayload(); // 收到的消息是字节格式
String topic = message.getHeaders().get("mqtt_receivedTopic").toString();
// 根据主题分别进行消息处理。
if (topic.equals(DeviceConstants.GET_DEVICE_INFORMATION)) { // 匹配:1/sensor
String sensorSn = "getDeviceInformation";
LOG.debug("获取设备信息: " + payload);
} else if (topic.equals(DeviceConstants.OPEN_THE_DOOR)) {
LOG.debug("远程开门:" + payload);
} else if (topic.equals(DeviceConstants.LIST_PEOPLE_INFORMATION_BY_ID)) {
LOG.debug("根据ID查询录入人员" + payload);
} else if (topic.equals(DeviceConstants.SELECT_PEOPLE_INFORMATION_BY_FIELD)) {
LOG.debug("根据录入信息字段,查询录入人员" + payload);
} else if (topic.equals(DeviceConstants.INSERT_PEOPLE)) {
LOG.debug("新增人员录入" + payload);
} else if (topic.equals(DeviceConstants.ADD_BATCH)) {
LOG.debug("批量添加人员" + payload);
} else if (topic.equals(DeviceConstants.DELETE)) {
LOG.debug("删除单个人员" + payload);
} else if (topic.equals(DeviceConstants.DELETE_BATCH)) {
LOG.debug("批量删除多个人员录入" + payload);
} else if (topic.equals(DeviceConstants.GET_CARD_RECORD)) {
LOG.debug("获取打卡记录" + payload);
}
};
}
// 发送消息
/**
* 出站通道
*/
@Bean
public MessageChannel mqttOutboundChannel() {
return new DirectChannel();
}
/**
* 出站
*/
@Bean
@ServiceActivator(inputChannel = "mqttOutboundChannel")
public MessageHandler outbound() {
// 发送消息和消费消息Channel可以使用相同MqttPahoClientFactory
MqttPahoMessageHandler messageHandler = new MqttPahoMessageHandler("publishClient", mqttClientFactory());
messageHandler.setAsync(true); // 如果设置成true,即异步,发送消息时将不会阻塞。
messageHandler.setDefaultTopic("command");
messageHandler.setDefaultQos(1); // 设置默认QoS
// Paho消息转换器
DefaultPahoMessageConverter defaultPahoMessageConverter = new DefaultPahoMessageConverter();
// defaultPahoMessageConverter.setPayloadAsBytes(true); // 发送默认按字节类型发送消息
messageHandler.setConverter(defaultPahoMessageConverter);
return messageHandler;
}
}
(3)控制层,包括道闸小门门禁一体机人脸下发和获取打卡记录 2 个调用示例。
/**
* @Description: 道闸小门门禁一体机数据交互控制层
* @Author: yuezejian
* @Date: 2021/12/14 11:28
* @Version: 1.0
**/
@RestController
@RequestMapping("/mqtt")
public class MqttController extends BaseController {
@Autowired
private MqttGateway mqttGateway;
@Autowired
private ICcccHealthyEquipmentinfoService ccccHealthyEquipmentinfoService;
@Autowired
private Environment env;
/**
* 下发人脸
* @return
* @throws IOException
*/
@PostMapping("/send")
public String send() throws IOException {
// 发送消息到指定主题
ccccHealthyEquipmentinfoService.insertPersionInformation(new DeviceDto(1,"face.set"));
return "人脸批量新增" ;
}
/**
* 获取道小门的打卡记录
* @return
* @throws IOException
*/
@PostMapping("/record")
public String record() throws IOException {
// 发送消息到指定主题
ccccHealthyEquipmentinfoService.recordGet(new DeviceDto(2,"faceRecord.bulkGet"));
return "获取打卡记录" ;
}
}
(4)服务层
/**
* 设备信息Service接口
*
* @author jiezhaokai
* @date 2021-12-20
*/
public interface ICcccHealthyEquipmentinfoService
{
/**
* 闸道小门数据全员下发
* @param deviceDto
* @throws IOException
*/
public void insertPersionInformation(DeviceDto deviceDto) throws IOException;
/**
* get record from outdoor
* @param deviceDto
* @throws IOException
*/
public void recordGet(DeviceDto deviceDto) throws IOException;
/**
* 闸道小门数据下发通过用户id
* @param deviceDto
* @throws IOException
*/
public void insertPersionInformationByUserId(DeviceDto deviceDto, Long userId) throws IOException;
}
(5)服务层具体实现
/**
* 设备信息Service业务层处理
*
* @author jiezhaokai
* @date 2021-12-20
*/
@Service
public class CcccHealthyEquipmentinfoServiceImpl implements ICcccHealthyEquipmentinfoService
{
/**
* 闸道小门数据全员下发
* @param deviceDto
* @throws IOException
*/
@Override
public void insertPersionInformation(DeviceDto deviceDto) throws IOException {
//查询员工
//
List<EmployeeUserNameAndJobNumberAndFaceImage> faceImageList = ccccCheckonworkDeviceMapper.getEmployeeUserNameAndJobNumberAndFaceImage();
faceImageList.forEach( i -> {
if (StringUtils.isNotEmpty(i.getImageUrl()) && StringUtils.isNotEmpty(i.getJobNumber())
&& StringUtils.isNotEmpty(i.getUserName()) && i.getCardNo() != null) {
Image image = new Image();
image.setType("URL");
// 图片地址拼接
image.setUrl("http://192.168.20.48:8088/prod-api/faceimage/faceRegisterImage/"+i.getImageUrl());
PersonInformationDto personInformationDto = new PersonInformationDto();
personInformationDto.setId(0);
personInformationDto.setImage(image);
personInformationDto.setTrdID(Integer.valueOf(i.getJobNumber().replaceAll("[a-zA-Z]","" )));
personInformationDto.setName(i.getUserName());
//赋值卡号
if ( i.getCardNo()< 1000000000) {
String cardNo = "0" + i.getCardNo();
personInformationDto.setAccessCardNo(cardNo);
} else {
String cardNo = i.getCardNo()+"";
personInformationDto.setAccessCardNo(cardNo);
}
deviceDto.setParams(JSONObject.parseObject(JSONObject.toJSON(personInformationDto).toString()));
MyMqttMessage myMqttMessage = new MyMqttMessage();
myMqttMessage.setTopic(DeviceConstants.INSERT_PEOPLE);
myMqttMessage.setContent(JSON.toJSONString(deviceDto));
mqttGateway.sendToMqtt(myMqttMessage.getTopic(), 1, myMqttMessage.getContent());
LOG.info("闸道小门人脸下发成功,ip为192.168.20.34, 员工名字为:"+ i.getUserName()+ ",员工工号为:"+ i.getJobNumber());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
/**
* 获取道闸小门门禁一体机打卡记录
* @param deviceDto
* @throws IOException
*/
@Override
public void recordGet(DeviceDto deviceDto) throws IOException {
RecordHistoryDto recordHistoryDto = new RecordHistoryDto();
//每次获取当前时间前的两个小时内的数据,因此定时任务的执行时间也应当是没两小时执行一次
String startDate = DateUtils.getDate() +"T"+ DateUtils.dateRoll(6) + ".32+08:00";
String endDate = DateUtils.getDate() +"T"+ DateUtils.dateRoll(0)+ ".32+08:00";
recordHistoryDto.setStartDate(startDate);
recordHistoryDto.setEndDate(endDate);
recordHistoryDto.setLimit(30);
recordHistoryDto.setPage(1);
recordHistoryDto.setSortBy("id");
recordHistoryDto.setSortOrder("asc");
recordHistoryDto.setWithImage(false);
deviceDto.setParams((JSONObject)JSONObject.toJSON(recordHistoryDto));
MyMqttMessage myMqttMessage = new MyMqttMessage();
myMqttMessage.setTopic(DeviceConstants.GET_CARD_RECORD);
myMqttMessage.setContent(JSON.toJSONString(deviceDto));
System.out.println(myMqttMessage.toString());
mqttGateway.sendToMqtt(myMqttMessage.getTopic(), 1, myMqttMessage.getContent());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 每次新增人脸记录时,对道闸小门的门禁一体机进行人脸下发。
* @param deviceDto
* @param userId
* @throws IOException
*/
@Override
@Async("threadPoolTaskExecutor1")
public void insertPersionInformationByUserId(DeviceDto deviceDto, Long userId) throws IOException {
//查询员工
//
List<EmployeeUserNameAndJobNumberAndFaceImage> faceImageList = ccccCheckonworkDeviceMapper.getEmployeeUserNameAndJobNumberAndFaceImageByUserId(userId);
//如果当前员工还没有卡号,则工号去掉字母,开头拼接0为其卡号,for example: SC9879 -> 09879
if (faceImageList.get(0).getCardNo() == null) {
faceImageList.get(0).setCardNo(Long.valueOf(faceImageList.get(0).getJobNumber().replaceAll("[a-zA-Z]","" )));
}
faceImageList.forEach( i -> {
if (StringUtils.isNotEmpty(i.getImageUrl()) && StringUtils.isNotEmpty(i.getJobNumber())
&& StringUtils.isNotEmpty(i.getUserName()) && i.getCardNo() != null) {
Image image = new Image();
image.setType("URL");
// 图片地址拼接
image.setUrl("http://192.168.20.48:8088/prod-api/faceimage/faceRegisterImage/"+i.getImageUrl());
PersonInformationDto personInformationDto = new PersonInformationDto();
personInformationDto.setId(0);
personInformationDto.setImage(image);
personInformationDto.setTrdID(Integer.valueOf(i.getJobNumber().replaceAll("[a-zA-Z]","" )));
personInformationDto.setName(i.getUserName());
//赋值卡号
if ( i.getCardNo()< 1000000000) {
String cardNo = "0" + i.getCardNo();
personInformationDto.setAccessCardNo(cardNo);
} else {
String cardNo = i.getCardNo()+"";
personInformationDto.setAccessCardNo(cardNo);
}
deviceDto.setParams(JSONObject.parseObject(JSONObject.toJSON(personInformationDto).toString()));
MyMqttMessage myMqttMessage = new MyMqttMessage();
myMqttMessage.setTopic(DeviceConstants.INSERT_PEOPLE);
myMqttMessage.setContent(JSON.toJSONString(deviceDto));
mqttGateway.sendToMqtt(myMqttMessage.getTopic(), 1, myMqttMessage.getContent());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
5、配置MQTT协议监听对应的主题
import com.alibaba.fastjson.JSONObject;
import com.cccc.business.duty.ondutyhistoryrecord.service.ICcccCheckonworkHistoryRecordService;
import com.cccc.common.core.domain.entity.SysUser;
import com.cccc.common.utils.StringUtils;
import com.cccc.framework.config.BeanWiredAdvice;
import com.cccc.system.mapper.SysUserMapper;
import com.google.gson.JsonObject;
import org.eclipse.paho.client.mqttv3.*;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.Ordered;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* 道闸小门打卡记录监听,MQTT协议
* @auth yuezejian
*/
//@Component
public class MqttListener implements ApplicationRunner, Ordered {
@Autowired
private Environment env;
public static final Logger LOG = LoggerFactory.getLogger(MqttListener.class);
/* 打卡历史记录的服务层 */
ICcccCheckonworkHistoryRecordService ccccCheckonworkHistoryRecordService = BeanWiredAdvice
.getBean(ICcccCheckonworkHistoryRecordService.class);
private SysUserMapper sysUserMapper = BeanWiredAdvice.getBean(SysUserMapper.class);
@Override
public void run(ApplicationArguments args) throws Exception {
//MQTT协议前缀 urlFrontSuffix = tcp://:@192.168.20.48:1883
String urlFrontSuffix = env.getProperty("barriergate.urlFrontSuffix", String.class);
String clienId = String.valueOf(System.currentTimeMillis());
System.out.println(clienId);
//Topic前缀 allTopic = record/ba3ec3e4-2e1f-4e8d-9ff8-e0235f6e1f41/report
String allTopic = env.getProperty("barriergate.allTopic", String.class);
try {
MemoryPersistence memoryPersistence = new MemoryPersistence();
MqttClient client = new MqttClient(urlFrontSuffix, clienId, memoryPersistence);
MqttConnectOptions options = new MqttConnectOptions();
// 设置客户端和服务器是否应在重新启动和重新连接期间记住状态 默认false
options.setCleanSession(true);
// 设置会话心跳时间
options.setKeepAliveInterval(20);
// 设置超时时间
options.setConnectionTimeout(10);
// 设置回调函数,当订阅到信息时调用此方法
client.setCallback(new MqttCallback() {
@Override
public void connectionLost(Throwable throwable) {
// 连接失败时调用 重新连接订阅
try {
System.out.println("开始重连");
Thread.sleep(3000);
client.connect(options);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (MqttSecurityException e) {
e.printStackTrace();
} catch (MqttException e) {
e.printStackTrace();
}
}
@Override
public void messageArrived(String s, MqttMessage mqttMessage) throws Exception {
// 订阅成功,并接受信息时调用
String payload = new String(mqttMessage.getPayload()); // 获取消息内容
JSONObject pa=JSONObject.parseObject(payload);
String person = pa.getString("person");
if (StringUtils.isNotEmpty(person)) {
JSONObject personObject = JSONObject.parseObject(person);
String jobNum = personObject.getString("trdID");
if (StringUtils.isNotEmpty(jobNum)) {
SysUser sysUser = sysUserMapper.selectUserByJobNum(jobNum);
ccccCheckonworkHistoryRecordService.insertCcccCheckonworkHistoryRecordEntranceGuard(sysUser.getUserId(),env.getProperty("barriergate.ip", String.class),new Date(),0);
}
}
LOG.debug("MqttAccept_________"+ payload);
}
@Override
public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) {
LOG.debug("deliveryComplete");
}
});
client.connect(options);
client.subscribe(allTopic,0);
System.out.println("连接成功");
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public int getOrder() {
return 3;
}
}
最后附件1,这是协议的部分内容:
一个设备只需要一个上报主题和一个下发主题即可。调用不同的内容,对应的RPC 方法是不同的。