Springboot和EMQ搭建物联网平台

简介

这个项目是我学校的一个学长带着我去做的,是老师从企业接的,一个,算是小型的商业项目。具体内容就不在这里讲的很详细了。同时,由于这个项目甲方还没验收,所以,这个项目也只是一个小 Demo,即未使用到EMQ的高级特性(像钩子,认证链,规则,数据桥接等等),但是等甲方验收通过后,这些高级特性一定会用到的。所以这个项目也会连载(但是间隔时间会长点,毕竟要和甲方和硬件经常"交流"😭)。所以本文就是这个系列的第一集,非常适合用以学习。在后续的开发中也是会本文也是主要分析关于物联网项目中MQTT的使用,以及用部分设计模式改造出一个适合多数情况下使用的小项目。


# 代码和相关资料

链接:https://pan.baidu.com/s/16TQyTketPBvtHSdjebvvYA?pwd=2321** **
**提取码:2321 **
–来自百度网盘超级会员V4的分享

需要的技术储备

  • **Spring Boot ** 这个项目他本质上还是个 Web项目,只是里面用到了一些 MQTT的知识。
  • 设计模式 这个技术储备是一定要有的,这个小Demo 的核心亮点就是运用了设计模式
  • **MQTT **MQTT的基础知识是需要提交了解一下的
  • **简单的了解 EMQ ** 在这个项目调试的过程中,我们会用到 EMQ 来模拟硬件的发/接 消息,后面也会用到EMQ的高级特性,所以还是了解一下为好

以上需要的技术储备,如果有哪个还是不熟悉的话,建议大家可以先去了解一下。

  • **设计模式 **我推荐大家去看《大话设计模式》这本书,很适合萌新学
  • **MQTT 和 EMQ **我自己以前有写过关于这方面的笔记,大家感兴趣也可以看一下,链接中都有

需要引入的包

<!--     用于实现 mqtt客户端-->
<dependency>
  <groupId>org.eclipse.paho</groupId>
  <artifactId>org.eclipse.paho.client.mqttv3</artifactId>
  <version>1.2.5</version>
</dependency>
<!--     工具包,简化开发-->
<dependency>
  <groupId>cn.hutool</groupId>
  <artifactId>hutool-all</artifactId>
  <version>5.7.17</version>
</dependency>

整体架构设计


整个系统可分为:

  1. MQTTReq生成中心:用于生成向硬件发送的MQTTReq对象,但是最后要转换为Json格式的发送给硬件
  2. **MQTTProcess:**这个是实现MQTT的整体流程,也是最重要的。
  3. **代理工厂:**将硬件的消息解析后,生成对应的代理对象,去执行数据库的操作

下面我简单的介绍一下我这项目硬件那边简单的结构,但目的是为了便于理解代码的讲解,其实每个系统的硬件结构都是不同的,如果你硬件的那边结构已经确定,可以跳过介绍,直接看代码讲解就好

硬件结构简单介绍

硬件结构图

在这个硬件结构中,最主要的一条线就是 网关-节点-module,module 就是一个具体负责处理数据的结构
image.png

命令

由于这个硬件系统是要根据具体的命令来执行对应的操作,所以我们给硬件传消息的适合,要把消息带上。
image.png

MQTTReq生成中心

MQTTReq的组成

这个其实每个系统都不一样的,只要你和硬件那边沟通好就可以,那下面就用我的举个例子。
如果你跟硬件已经沟通好了,可以跳过这个环节
image.png

  • adr:是地址,是要告诉硬件命令的传递过程,例如下面这个地址,id1,id2,id3就是命令要经过的节点id,最后一个节点就是要执行命令的节点,对于地址的构成有很多种形式,有id,ids,domain等

image.png
image.png

  • msgid:每个发送消息的id,保证每个发送的消息的id都是唯一的,然后硬件回应这个发送的消息的msgid 与发送的消息的msgid 是一样的,这样就可以互相匹配上
  • cmd:就是具体命令的对应id
  • type:type是用来告诉硬件这次操作是只读取硬件的消息,还是要更新硬件的消息
  • time:time就是发送消息的时间,这里用数组代替是因为格式化的时间格式传输会消耗更多的字节,这个硬件那边传输的适合消耗的字节越少越好
  • data:data就是要具体传输给硬件的数据了,有数据的话,就用键值对的格式传输,如果没有要传输的数据,用 value:-1代替就好

image.png

package com.xiancai.lora.MQTT.bean;

import lombok.Builder;
import lombok.Data;

import java.util.List;
import java.util.Map;

@Data
    @Builder
    public class MQTTReq {
        /**
地址
*/
        private String adr;

        private String msgid;

        /**
* 具体的命令
*/
        private Integer command;

        /**
* 命令的类型,get,set
*/
        private String type;

        /**
* 发送时的时间 格式[23,1,9,15,56,23]
*/
        private List<Integer> time;

        /**
数据,因为要用键值对的形式,选择map就好
*/
        private Map<String,Object> data;

    }

RedisIdWorker

这个是生成唯一的消息id,具体怎么生成的就在这里不详细说了,这个黑马程序员的那个Redis课中有讲到,想要了解的同学可以看一下,当然,我以前也是记过对应的笔记的,链接放下面 了


## Address > **由于Address处理起来比较复杂,所以这里的类也有很多,那对于Address的处理用到的设计模式是模板方法模式+策略模式**

我们再来回顾一下 地址的构成,可以由id组成,由ids组成,由domain组成,还可以混合组成。所以针对不同的地址,我们要进行不同的处理,但是他们的抽象处理步骤大体相同
传地址:

  1. 根据前端传来的节点id,往回找需要找的节点的对应属性(可能是节点id或ids或domain),然后把他们用对于的符号组合成字符串
  2. 然后用base64编码,不够16位的往前用 A 补够。

收地址:

  1. 先用base64解码
  2. 找到地址中最后一个节点的属性

大体上没什么区别,最主要的区别就在找节点的哪个属性,是id,ids还是domain。那既然除了选取属性外,其他的操作都相同,我们就可以用一个模板方法模式。

这个 AbstractAddressHandler就是一个模板,他里面封装了所有有关地址的方法,其中有的方法每个地址都一样,有的方法每个地址都不同,那么可以用abstarct,让他延迟到子类中实现

package com.xiancai.lora.MQTT.util.address;



import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.LinkedList;
import java.util.Map;

/**
 * 构造地址的模板方法
 */
public  abstract class AbstractAddressHandler {
    /**
     * 给硬件的地址
     * @param nodeId
     * @return
     */
    public String produceAddress(Integer nodeId){
        //先是把要所有的节点id找到
        LinkedList<String> address = findAddress(nodeId);
        //然后拼成串
        String normalAddress = combineAddress(address);
        //然后转Base64
        return toBase64(normalAddress);
    }

    /**
     * 解析硬件传来的地址,先把Base64的编码转换为正常格式,再进行拆分
     * @param address
     * @return
     */
    public abstract Map<String,String> parseHardWareAddress(String address);


    /**
     * 要给硬件的地址,要先找到前几个设备的id/ids/admin,
     * 因为最终执行命令的都是节点,所以我们只要找节点就可以了
     * 参数给的是最终的那个节点id,我们要往上找
     */
    public abstract LinkedList<String> findAddress(Integer nodeId);

    public abstract String combineAddress(LinkedList<String> addresses);

    /**
     * 转换为 Base64编码的格式
     * @param address
     * @return
     */
    public String toBase64(String address){
        String base64Address = Base64.getEncoder().encodeToString(address.getBytes(StandardCharsets.UTF_8));
        while (base64Address.length()<16){
            base64Address='A'+base64Address;
        }
        return base64Address;
    }

    /**
     * 将硬件传来的base64的地址转换为正常的
     * @param base64
     * @return
     */
    public String parseBase64(String base64){
        String removeAString = removeA(base64);
        return new String(Base64.getDecoder().decode(removeAString));

    }
//    AAAAABSADSDSDAAA
    //AAADSDSDASBAAAAAA
    //以不确定的长度的A作为前缀
    private String removeA(String normalAddress){
        if(normalAddress.charAt(0)!='A'){
            return normalAddress;
        }
        char[] chars = normalAddress.toCharArray();
        int i=0;
        for (  ; i < chars.length; i++) {
            if(chars[i]!='A') break;
        }
        String substring = normalAddress.substring(i);
        return substring;

    }

}

这是一个用 id 作为地址的一个地址处理器,还可以有用ids作为地址的处理器,用domain的,用混合的等等,只要继承了上面的那个模板后就可以生成很多的地址处理器。

package com.xiancai.lora.MQTT.util.address;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.xiancai.lora.model.entity.Node;
import com.xiancai.lora.service.NodeService;
import com.xiancai.lora.service.impl.NodeServiceImpl;

import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.*;



@Component
public class NormalAddressHandler extends AbstractAddressHandler {
    //因为这里的NormalAddress是反射new出来的,是用newInstance默认构造器造出来的,所以这个类为被

    @Resource
    private NodeService nodeService;




    @Override
    public LinkedList<String> findAddress(Integer nodeId) {
        LinkedList<String> addressQueue = new LinkedList<>();
        addressQueue.add(nodeId+"");
        Integer loraId = nodeService.getById(nodeId).getLoraId();
        List<String> list=new ArrayList<>();
        process(loraId,list);
        addressQueue.addAll(list);
        return addressQueue;
    }

    /**
     * 递归得到地址
     * @param loraId
     * @param list
     */
    private void process(Integer loraId,List<String> list){
        if(list.size()==2||loraId==-1){
            return;
        }
        //拿出来后面的id,找到这个id的节点的lora_id,看他的上一级节点是谁
        Node node = nodeService.getOne(new QueryWrapper<Node>().eq("is_lora", loraId));
        Integer beforeAddress = node.getId();
        list.add(beforeAddress + "");
        process(node.getLoraId(),list);
    }

    @Override
    public String combineAddress(LinkedList<String> addresses) {
        StringBuilder s= new StringBuilder();
        while (!addresses.isEmpty()){
            s.append("#").append(addresses.pollLast());
        }
        return s.toString();
    }

    @Override
    public Map<String, String> parseHardWareAddress(String Base634Address) {
        //先是把Base64转换为普通字符串
        String address = parseBase64(Base634Address);
        String[] split = address.split("#");
        Map<String,String> map=new HashMap<>();
        map.put("id",split[split.length-1]);
        return map;

    }
}

那现在各个地址处理器有了,怎么可以根据不同的情况选取不同的地址处理器呢?那这个就要用到策略模式了。
策略模式简单来说就是定义很多的策略,然后用一个类来针对不同的情况来实现不同的策略
那刚才我们创建的很多的地址处理器,都是策略,现在我们只用再实现一个类来实现分配策略就好了
分配策略有简单来说有两种方式,一种是静态的,用个switch或者map来分配,一种是动态的,用反射
对于静态来说,会有点耦合,我们要不断的修改用来分配策略的类,当策略很多的策略的时候,swtich和map也会显得有点臃肿,所以,下面我们就用反射来实现。


> **代码也很简单,这里我们简单的说一下反射在Spring boot中的一个常见的bug** > **因为在SpringBoot中我们将 bean都交给spring管理,但是反射出来的对象是用 newInstance 创建的,也就是用构造器创建的,那这样这个反射出来的对象就没有交给 spring 管理。那问题就出现了。比如说在下面这个类中 addressHandler 这个对要被反射创建出来,但是他没交给spring管理,如果 addressHandler中有其他的交给 spring 管理的对象的话,那他是获取不到的。因为 addressHandler 没有在容器中,那么 spring 找对象的话,如果这个对象本身没在spring的上下文,那么他里面的对象就不会去容器中找。 ** > **解决方案也很简单,我们不用反射的 newInstance创建,我们用反射得到类名后,让spring去容器帮我们创建就好了。那这也就是这个 BeanFactory 这个类的作用。**
package com.xiancai.lora.MQTT.util.address.context;

import com.xiancai.lora.MQTT.util.BeanFactory;
import com.xiancai.lora.MQTT.util.address.AbstractAddressHandler;
import com.xiancai.lora.enums.MQTT.MQTTAddress;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.Map;

import static com.xiancai.lora.constant.UsuStatus.ADDRESS_REFLECT_PREFIX;
import static com.xiancai.lora.constant.UsuStatus.ADDRESS_REFLECT_SUFFIX;


@Component
public class AddressHandlerContext {
    private AbstractAddressHandler addressHandler;

    @Autowired
    private MQTTAddress mqttAddress;

    @Resource
    private BeanFactory beanFactory;
    
    public void produceAddressHandler(String symbol){

        try {
            String classPath=ADDRESS_REFLECT_PREFIX + mqttAddress.getClassPath(symbol) + ADDRESS_REFLECT_SUFFIX;
            Class<?> aClass = Class.forName(classPath);
            addressHandler =(AbstractAddressHandler) beanFactory.getApplicationContext().getBean(aClass);
        } catch (Exception e) {
            throw new RuntimeException("地址处理器创建异常"+e.getMessage());
        }

    }

    /**
     * 给硬件的地址
     * @param nodeId
     * @return
     */
    public String produceAddress(Integer nodeId){
       return addressHandler.produceAddress(nodeId);
    }

    /**
     * 解析硬件传来的地址,先把Base64的编码转换为正常格式,再进行拆分
     * @param address
     * @return
     */
    public Map<String, String> parseHardWareAddress(String address){
        return addressHandler.parseHardWareAddress(address);
    }


}

package com.xiancai.lora.MQTT.util;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

@Component
public class BeanFactory implements ApplicationContextAware {
    private ApplicationContext applicationContext;
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext=applicationContext;
    }

    public ApplicationContext getApplicationContext() {
        return applicationContext;
    }
}


## **MqTTReqProperty** > **这个是用来生成除id,address外的其他MQTTReq的属性,其他属性的话,就没什么特别的我这里就直接处理了**
package com.xiancai.lora.MQTT.util.res;

import cn.hutool.core.bean.BeanUtil;
import com.xiancai.lora.exception.BusinessException;
import org.springframework.stereotype.Component;

import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Map;



@Component
public class MQTTReqProperty {
    /**
     * 解析时间
     * @return
     */
    public List<Integer> parseTime(){
        LocalDateTime now = LocalDateTime.now();
        String date = now.format(DateTimeFormatter.ofPattern("yy,MM,dd,HH,mm,ss"));
        ArrayList<Integer> list = new ArrayList<>();
        for (String s : date.split(",")) {
            list.add(Integer.parseInt(s));
        }
        return list;
    }
    /**
     * 解析地址
     */


    /**
     * 解析 data,无论是什么 DTO 对象,我们都只要把他转成字符串就行了
     */
    public  Map<String,Object>  parseData(Object data){
        Map<String, Object> stringObjectMap = BeanUtil.beanToMap(data);
        if(stringObjectMap.isEmpty()){
            throw new BusinessException("ss",123,"ss");
        }
        return stringObjectMap;
    }
}


MQTTReq生成中心

  • MQTTReq生成中心,主要用到的设计模式是门面模式也叫外观模式(即 将生产MQTTReq各个熟悉的类都聚合到这个 MQTTReqFacade 外观类中,使得MQTTReq的生成可以更加方便的生成)
  • 这里可能有的朋友会想建造者模式。我这里不用是因为 我的MQTTTReq不会因为硬件的结构而发生太大的变化,只是 地址会变化,如果很多属性都会根据硬件的结构发生变化,那用建造者模式还蛮不错的。这里如果变化不是很大的话,我觉得没有太大的必要去用建造者模式,用了我反而还觉得可能有点浪费空间~~~。
  • 在这个外观类中,聚合了 redisIdWorker ,mqttReqProperty,addressHandlerContext。来分别装配MQTTReq。其实这里的优势我感觉已经体现了一点点,当MQTTReq的属性更多的时候,那门面模式的优势就很明显了。
package com.xiancai.lora.MQTT.util.res;

import com.xiancai.lora.MQTT.bean.MQTTReq;
import com.xiancai.lora.MQTT.util.address.context.AddressHandlerContext;
import com.xiancai.lora.MQTT.util.res.MQTTReqProperty;
import com.xiancai.lora.utils.RedisIdWorker;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;

import java.util.List;
import java.util.Map;



/**
 * 门面模式的外观类,用来装配一个MQTTReq
 */
@Component
public class MQTTReqFacade {
    /**
     * id生成器
     */
    @Resource
    private RedisIdWorker redisIdWorker;

    @Resource
    private MQTTReqProperty mqttReqProperty;

    /**
     * 生成对应的地址
     */
    @Resource
    private AddressHandlerContext addressHandlerContext;




    /**
     * 在这里构建的时候
     * @param data
     * @param symbol
     * @param commandId
     * @param type
     * @return
     */
    public MQTTReq combineMQTTReq(Object data,String symbol,Integer commandId,String type){
        //准备对应的addressHandler
        addressHandlerContext.produceAddressHandler(symbol);
        //解析对应的data
        Map<String, Object> dataMap = mqttReqProperty.parseData(data);
        if (dataMap.size()==0){
            dataMap.put("value",-1);
        }
        //生成对应的地址
        String address = addressHandlerContext.produceAddress((Integer) dataMap.remove("nodeId"));
        //生成消息id
        String messageId = redisIdWorker.nextId("command")+"";
        List<Integer> time = mqttReqProperty.parseTime();
        //生成对应的MQTTReq
        MQTTReq req = MQTTReq.builder().msgid(messageId)
                .command(commandId)
                .type(type).adr(address).data(dataMap).time(time).build();
        return req;
    }

    public Map<String,String> parseHardWareAddress(String address){
       return addressHandlerContext.parseHardWareAddress(address);
    }




}

MQTTProcess

这个类是最重要的类,MQTT所有的操作都封装到这个类当中

客户端收到消息后,存到Redis中

在架构图中,我们也看到了客户端收到消息后,要存到Redis中,这个操作是在写MQTT客户端的回调函数中的
messageArrived函数中实现的。

    /**
     * 应用收到消息后触发的回调
     * @param topic
     * @param mqttMessage
     * @throws Exception
     */
    @Override
    public void messageArrived(String topic, MqttMessage mqttMessage) throws Exception {
        String message = new String(mqttMessage.getPayload());
        String messageId = (String) JSONUtil.parseObj(message).get("msgid");
        log.info("订阅者订阅到了消息,topic={},messageid={},qos={},payload={}",
                topic,
                mqttMessage.getId(),
                mqttMessage.getQos(),
                message
                );
        stringRedisTemplate.opsForValue().set(MQTT_MESSAGE+messageId,message);
    }

具体的流程

这个也用到了门面模式(外观模式),每一个流程就不说了,就简单的梳理一下整体的流程

  1. 发消息:就是调用 MQTTReqFacade生成消息。然后发布,返回消息id
  2. 收消息:根据拿到的消息id,去redis中找对应的消息。

这里解释一下为什么要多加一层 Redis。一开始我也没加Redis,又开了一个独立线程单独去处理,但是最后还要返回给前端信息,如果发布完消息后就去返回给前端信息,如果硬件那边出现问题,对于用户来讲效果不是很好。所以这里就不是很适合单独开一个线程去处理。

  1. 解析拿到的硬件消息,这里硬件返回的rcmd 与发生的cmd一样,adr也是一样,这里为什么是根据硬件的消息去操作,是因为,硬件那里有一些命令是自动的(比如说自动上报数据之类的)。这个是不需要我们发送消息的。所以就统一用硬件的消息来处理
  2. 解析完消息后,操作数据库

最后我们把这几个流程封装一个方法中,对外界开放就好了。

package com.xiancai.lora.MQTT.util.process;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.xiancai.lora.MQTT.bean.MQTTReq;
import com.xiancai.lora.MQTT.bean.MQTTRes;
import com.xiancai.lora.MQTT.client.EmqClient;
import com.xiancai.lora.MQTT.publish.properties.MqttProperties;
import com.xiancai.lora.MQTT.service.context.MQTTServiceContext;
import com.xiancai.lora.MQTT.util.address.context.AddressHandlerContext;
import com.xiancai.lora.MQTT.util.res.MQTTReqFacade;
import com.xiancai.lora.enums.QosEnum;
import com.xiancai.lora.exception.BusinessException;
import com.xiancai.lora.service.CommandService;
import com.xiancai.lora.utils.Result;
import com.xiancai.lora.utils.wrong.check.CheckHardWareWrong;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;

import java.util.Map;

import static com.xiancai.lora.constant.RedisConstants.MQTT_MESSAGE;
import static com.xiancai.lora.enums.MQTT.MQTTReqType.MQTT_REQ_GET;

/**
 * MQTT整体流程
 */
@Component
public class MQTTProcess {

    @Resource
    private MQTTReqFacade mqttReqFacade;
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private CommandService commandService;

    @Resource
    private CheckHardWareWrong checkHardWareWrong;

    @Resource
    private EmqClient emqClient;
    @Resource
    private MqttProperties mqttProperties;

    @Resource
    private MQTTServiceContext mqttServiceContext;

    /**
     * 准备MQTTReq
     *
     * @return
     */
    protected MQTTReq prepareMQTTReq(Object data, String symbol, Integer commandId, String type) {
        return mqttReqFacade.combineMQTTReq(data, symbol, commandId, type);
    }

    /**
     * 发布消息
     */
    public String publishMessage(Object data, String symbol, Integer commandId, String type) {
        //封装请求对象
        MQTTReq mqttReq = prepareMQTTReq(data, symbol, commandId, type);
        //发布消息
        emqClient.publish(mqttProperties.getWebTopic(), JSONUtil.toJsonPrettyStr(mqttReq), QosEnum.QOS0, false);
        return mqttReq.getMsgid();
    }

    /**
     * 接收消息
     */
    public String receiveMessage(String messageId) {
        int count = 1;
        while (count <= 5) {
            String jsonMessage = stringRedisTemplate.opsForValue().get(MQTT_MESSAGE + messageId);
            if (StrUtil.isNotBlank(jsonMessage)) return jsonMessage;
            try {
                //睡0.2秒再去查,不要查太频繁
                Thread.sleep(200);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            count++;
        }
        //5次还没得到消息,就直接返回null
        return null;
    }

    /**
     * 解析消息,判断错误,还是根据硬件的地址去找,因为后面会有数据自动上报,我们是不发消息的
     */
    public Map<String, Object> parseMessage(String jsonMessage) {
        //先检查传过来的信息是不是null
        checkHardWareWrong.checkJsonMessage(jsonMessage);
        //然后用json包将硬件发送的json信息转化为MQTTRes对象
        MQTTRes mqttRes = JSONUtil.toBean(jsonMessage, MQTTRes.class);
        //先判断res的状态
        String res = mqttRes.getRes();
        checkHardWareWrong.checkRes(mqttRes.getRes());
        //解析硬件传过来的地址,截取到的可能是id,ids,domain,所以我们用一个map标识一下
        Map<String, String> address = mqttReqFacade.parseHardWareAddress(mqttRes.getAdr());
        //获取硬件传来的信息
        Map<String, Object> data = mqttRes.getData();
        //获取硬件的命令,这里也是看硬件的,原因还是那个自动上报的那个东西
        Integer rcmd = mqttRes.getRcmd();
        String commandContent = commandService.getById(rcmd).getContent();
        checkHardWareWrong.checkString(commandContent, "未找到硬件传来的id为" + rcmd + "的命令");
        //将命令和地址都放进data中
        data.put("commandContent", commandContent);
        data.putAll(address);
        return data;
    }


    /**
     * 操作数据库
     */
    public Result executeDataBase(Map<String, Object> data) {
        //这里还是用一个remove方法,因为我们操作数据库是用不到这个键值对的
        return mqttServiceContext.executeService((String) data.remove("commandContent"), data);
    }

    /**
     * 总的MQTT的操作流程
     *
     * @param data
     * @param symbol
     * @param commandId
     * @return
     * 模块开启(模块 - 传感器13)
      {
      "adr":"AAAAIzE0IzE3IzE4",
      "msgid":"143954423154999297",
      "rcmd":13,
      "res":"0",
      "dytype":1,
      "data":{"port":0},
      "time":[23,1,19,22,40,10]
      }
     * 模块关闭(模块-传感器13)
      {
      "adr":"AAAAIzE0IzE3IzE4",
      "msgid":"143955166184341506",
      "rcmd":12,
      "res":"0",
      "dytype":1,
      "data":{"port":0},
      "time":[23,1,20,22,40,10]
      }
     * 节点重启(节点12)
      {
      "adr":"AAAAIzE0IzE3IzE4",
      "msgid":"142713181901422595",
      "rcmd":3,
      "res":"0",
      "dytype":1,
      "data":{"value":-1},
      "time":[23,1,20,22,40,10]
      }
     * 节点初始化(节点12)
      {
      "adr":"AAAAIzE0IzE3IzE4",
      "msgid":"142477199218311170",
      "rcmd":2,
      "res":"0",
      "dytype":1,
      "data":{"value":-1},
      "time":[23,1,20,22,40,10]
      }
     * 修改上报周期(节点12)
      {
      "adr":"AAAAIzE0IzE3IzE4",
      "msgid":"142712511886524418",
      "rcmd":17,
      "res":"0",
      "dytype":1,
      "data":{"report_interval":10},
      "time":[23,1,20,22,40,10]
      }

      获取定位(节点12)
       {
        "adr":"AAAAIzE0IzE3IzE4",
        "msgid":"142725864939847692",
        "rcmd":5,
        "res":"0",
        "dytype":1,
        "data":{"gps":[122.322,45.556]},
        "time":[23,1,20,22,40,10]
        }
     */
    public Result MQTTProcess(Object data, String symbol, Integer commandId, String type) {
        String messageId = publishMessage(data, symbol, commandId, type);
        String jsonMessage = receiveMessage(messageId);
        Map<String, Object> hardWareData = parseMessage(jsonMessage);
        return executeDataBase(hardWareData);
    }

    /**
     * 解析 data,无论是什么 DTO 对象,我们都只要把他转成Map就行了
     */
    public Map<String, Object> parseData(Object data) {
        Map<String, Object> stringObjectMap = BeanUtil.beanToMap(data);
        if (stringObjectMap.isEmpty()) {
            throw new BusinessException("ss", 123, "ss");
        }
        return stringObjectMap;
    }
}

代理工厂

到这里就差去数据库中操作数据了。但是我们这里并没有直接用 service 层来处理,是因为我们后面的阶段(现在没有实现)获得数据后不可能会直接去处理数据的,还有对数据进行检查处理,或者还有进行一些统计之类的事情。而且未来的命令会会很多很多,如果直接把 sevice对象来处理可能会对命令的处理的效率有一定的影响或者说对空间的一些浪费等。现在因为没有在代理中做更多的事情,现在优势不是很明显,等后期甲方通过后,再加一些功能,应该就可以看出来一些优势了。

Cglib代理

这里的代理方式我们选择Cglib代理的方式。一般可能用那个 jdk的方式来实现,但是那个需要被代理对象再去实现一个接口,如果被代理对象太多,那实现的接口也要很多。所以这里我们选择用Cglib代理来实现
Cglib代理是生成被代理对象的子类来实现代理的。被代理的类也不用再额外实现接口,比较方便。

package com.xiancai.lora.MQTT.service.proxy;

import cn.hutool.core.bean.BeanUtil;
import com.xiancai.lora.service.NodeService;
import com.xiancai.lora.utils.Result;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.cglib.proxy.*;
import org.springframework.stereotype.Component;


import java.lang.reflect.Method;


/**
 * Cglib代理,通过生成一个被代理对象的子类实现代理效果
 */
@Component
public class ProxyFactory implements MethodInterceptor {
    //维护一个目标对象
    private Object target;



    //传入一个被代理的对象
    public void produceProxy(Object target){
        this.target=target;
    }
    //返回一个代理对象,是target对象的代理对象
    public Object getProxyInstance(){
        //1.创建一个工具类
        Enhancer enhancer = new Enhancer();
        //2.设置父类
        enhancer.setSuperclass(target.getClass());
        //3.设置回调函数
        enhancer.setCallback(this);
        //4.创建子类对象,即代理对象
        return enhancer.create();
    }

    public void produceProxyByName(String classPath){

    }

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        return method.invoke(target,objects);
    }
}

实现流程:我们解析完硬件发过来的消息后,会解析到一个 Map 对象,而map对象中有一个 commandContent的键。对应的值就是具体的命令。我们再来回顾一下命令的组成

image.png

前面的node,是操作的对象,而后面的是具体的操作。所以这里我又用了一个策略模式,因为现在命令具体的操作对象要么是node,要么是module。所以我这里用了两个类来各自封装有关node的数据库操作,已经有关module的数据库操作。
这里还有一个原因不直接把操作nodeService或moduleService,是因为多重代理的问题,mybatis-plus本身也是用代理来实现的,具体的原理,最后的参考文章有。

package com.xiancai.lora.MQTT.service.proxy;

import com.xiancai.lora.service.ModuleService;
import com.xiancai.lora.utils.Result;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.Map;
@Component
public class ProxyModuleService {
    /**
     * 拿到的命令中
     */
    @Resource
    private ModuleService moduleService;

    public Result poweron(Map<String,Object> data){
        return moduleService.poweron(data);
    }

    public Result poweroff(Map<String,Object> data){
        return moduleService.poweroff(data);
    }
}

package com.xiancai.lora.MQTT.service.proxy;


import com.xiancai.lora.model.entity.Node;
import com.xiancai.lora.service.NodeService;
import com.xiancai.lora.utils.Result;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.io.Serializable;
import java.util.Map;


@Service
public class ProxyNodeService  {


    @Resource
    private NodeService nodeService;


    public Result init(Map<String,Object> data){
        return nodeService.init(data);
    }

    public Node getById(Serializable id){
        return nodeService.getById(id);
    }

    public Result setDataUploadInterval(Map<String,Object> data){
        return nodeService.setDataUploadInterval(data);
    }

    public Result restart(Map<String,Object> data){
        return nodeService.restart(data);
    }

    public Result getLocation(Map<String,Object> data){
        return nodeService.getLocation(data);
    }

}

这两个类也算是策略了。那我们现在还需要一个类来根据不同的情况分配策略,但是这个类有点特殊,刚刚我们是只建了两个策略,没有为每一个命令单独建一个策略(如果为每一个命令单独建一个策略,那就方便多了,但是命令太多了,只是Demo阶段就又将近30个命令,如果后续还要添加命令,那就要创建非常多的类。)。那么如果用反射的话,我们这里不仅要得到对应的对象,我们还要得到对应的方法,恰好我们可以用命令的后半段来作为方法名。代码如下

package com.xiancai.lora.MQTT.service.context;


import com.xiancai.lora.MQTT.service.proxy.ProxyFactory;
import com.xiancai.lora.MQTT.service.proxy.ProxyModuleService;
import com.xiancai.lora.MQTT.service.proxy.ProxyNodeService;

import com.xiancai.lora.utils.Result;

import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.Map;


@Component
public class MQTTServiceContext {

    @Resource
    private ProxyFactory proxyFactory;
    @Resource
    private ProxyNodeService proxyNodeService;
    @Resource
    private ProxyModuleService proxyModuleService;

    //利用反射去操作数据库
    //先是通过反射去调用proxyFactory的对应方法
    public Result executeService(String classPath, Map<String,Object> data){
        //先进来判断是不是不用去处理库,如果不用就直接返回。
        boolean isNoRes = isNoRes(data);
        if (isNoRes) return Result.success(true);
        Class[] paramTypes={Map.class};
        Object[] params={data};
        String[] split = classPath.split("_");
        if(split[0].equals("node")){
            proxyFactory.produceProxy(proxyNodeService);
            ProxyNodeService proxyInstance = (ProxyNodeService) proxyFactory.getProxyInstance();
            return  (Result) CallMethod.call(classPath, paramTypes, params, proxyInstance);
        }else {
            proxyFactory.produceProxy(proxyModuleService);
            ProxyModuleService proxyInstance = (ProxyModuleService) proxyFactory.getProxyInstance();
            return  (Result) CallMethod.call(classPath, paramTypes, params, proxyInstance);
        }
    }
    private boolean isNoRes(Map<String, Object> data){
        if(data.size()==2&&data.containsKey("value")&&data.get("value").equals(-1)){
            return true;
        }
        return false;
    }
}

**CallMethod是根据命令,去执行对象的方法 **
这里我们是让传进来的对象调用方法,而不是用反射创建出的对象调用,还是因为反射创建出的对象没让
spring 管理。

package com.xiancai.lora.MQTT.service.context;

import cn.hutool.core.util.StrUtil;
import com.xiancai.lora.utils.StringUtils;

import java.lang.reflect.Method;

import static com.xiancai.lora.constant.UsuStatus.REFLECT_PREFIX;
import static com.xiancai.lora.constant.UsuStatus.REFLECT_SUFFIX;

/**
 * 利用反射去调用方法
 */
public class CallMethod {
    /**
     * 通过字符串串调用方法
     * @param classAndMethod 类名-方法名,通过此字符串调用类中的方法
     * @param paramTypes 方法类型列表(因为方法可能重载)
     * @param params 需要调用的方法的参数列表
     * @return
     */
    public static Object call(String classAndMethod,Class[] paramTypes,Object[] params,Object o){
        String[] args=classAndMethod.split("_",2);
        //要调用的类名
        String className=args[0];
        className=REFLECT_PREFIX+StringUtils.getMethodName(className)+REFLECT_SUFFIX;
        //要调用的方法名
        String method="";
        method=StrUtil.toCamelCase(args[1]);
        try {
            //加载类,参数是完整类名 //第一个参数是方法名,后面的参数指示方法的参数类型和个数
            Method newMethod=Class.forName(className).getMethod(method,paramTypes);
            //accessiable设为true表示忽略java语言访问检查(可访问private方法)
            //method.setAccessible(true);
            //第一个参数类实例(必须有对象才能调用非静态方法,如果是静态方法此参数可为null)
            //第二个是要传给方法的参数
            Object result=newMethod.invoke(o,params);
            return result;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }



}

那到这里整个流程就已经结束了,接下来的就是要对于具体的命令,数据库采取不同的措施了。

流程梳理

为了更好的看到效果,我们把刚才写好的代码具体的用一下

  1. 这是一个Controller层的一个方法,我们在这个方法中只需要调用MQTTProcess对象的MQTTProcess方法就好了,很简洁吧,而且也很好理解,把需要的东西给mqttProcess这个对象,他就可以帮你完成整个流程,而且还会直接返回前端要的通用返回对象

image.png

  1. 在MQTTProcess这个方法中,流程也很简单,就是发消息,收消息,解析消息,操作数据库,逻辑非常的清晰

image.png

  1. 发布消息就是调用 MQTTReqFacade 这个门面类来封装MQTTReq,然后发布消息image.png
  2. 接收消息和解析消息由于都是同样的操作,就直接在MQTTProcess中写了image.pngimage.png
  3. 然后将处理好的数据传给 **MQTTServiceContext **这个类让他自己根据数据去找对应的数据库操作处理。这样在这个MQTTProcess这个类中逻辑非常清晰,那个类干那个类的事,那个方法处理那些操作。都互不影响
  4. **MQTTServiceContext ** 他就负责根据传过来的对象,选择代理对象,已经方法,最后让代理对象去处理image.png

高并发问题的解决

是否要用分布式的架构呢?

分布式它是将我们一个大型的系统的多个服务拆解到多个服务器中,从而减少单个服务器的压力,
而我们要知道我们现阶段的痛点是什么

  1. 设备的数量可能很多,设备都要存到我们这个系统,这个数据库中
  2. 设备产生的数据非常多,如何高效的管理这些数据
  3. 同一时间多个请求同时打到服务器上

所以现在我们有很多个解决方案

  1. 多线程
  2. 中间件
  3. 分布式

我们要具体搞清他们都有什么用,

  1. 多线程:当一个任务很大需要很多操作才可以完成的话,可以让这些操作都交给多个线程去处理,把_程序中占据时间长的任务放到后台去处理。当我们发现_一个业务逻辑执行效率特别低,耗时特别长,就可以考虑使用多线程

image.png
我们的MQTT流程是前端传过来数据后,我们先是转换成req对象,然后发给mqtt服务器,服务器等收到的硬件发过来的消息后再传给消息接受中心。现在的架构还是想把当前端发过来消息的时候,它可以接收到它对应消息的请求结果。
如果,我们这样,服务器发消息后,我们就是把他放到消息队列中,消息带有id,能不能根据id找到用户呢?是不是相当于一个定时任务呢?
目的是,既能把mqtt的延迟响应降到最低,还能让用户接收到他自己发的消息的结果。
一个前端可以允许好多个用户操作,我们针对的对象应该是 用户,而不是某一个前端,对了,web容器里面每一个请求不就是一个线程嘛

或者说,我们后端就
那我们现在明白了,其实是先登录,获得token,然后每次访问接口的时候都会被refrash拦截,在拦截中,根据token找到用户,然后存到threadlocal里面,然后等这次请求结束的时候再将local里面的用户清除掉。那我们是不是可以用这个特点做一点事。
就是因为mqtt延迟较高嘛,比如说用户发了一个命令,发完后,我们就给他返回一个信息,这个信息中只是说命令已经成功发送,请稍后检查设备之类的。然后mqtt服务器接收到硬件后的命令会将硬件发来的信息发给消息队列中,然后我们用一个线程池去循环不停的去消息队列中去处理数据。但是有一个隐患,如果这个线程池
这个想法是不行的,因为返回后,用户信息就没有了。对呀,我什么要返回呢?我都给用户说好了,你稍后检查设备,就说明已经弄好了呀。对呀,所以我就开线程池去处理就好了。

那其他的还有什么要改进的吗。
分库分表,读写分离?
读写分离是用于那种读的场景很多,写的场景很少的情况,那data表就不适合。
分度分表还行
当设备量大后就应该考虑负载均衡了。
自动上报数据,就是传感器可能每5秒就去上报一次数据。看着间隔是比较长,但是设备一多起来后就不行了,这里也可以用多线程给他把这个类似高并发的问题处理了,但是不断循环的话也比较费性能。

因为还有数据展示功能,可以用redis缓存一下,并且实时性不高。-----为了放置同一时刻大量请求来访问数据查询接口,导致数据库瘫痪

1. 解决MQTT延迟较高的问题:

首要解决的问题:保证消息安全,准确,按顺序地送达

2. 传感器数据自动上报问题

因为传感器的设备可以设置每间隔几秒钟上传,那如果设备开几个小时,都不说开一天,那产生的数据也是很多的,所以这些大量的数据如果一条一条的去插是不是太慢了,那我们还可以让服务器直接把数据发到服务器里面,然后我们多线程地去插入到库中。

3. 数据展示问题

但是这些数据也是要入库的,对于这些数据来说,一般是不会修改的,主要是给用户展示并且作为分析用的
所以对于data表可以进行读写分离,并且将一些“热点数据”加载到缓存里,不用过度的访问数据库

总结

这个小 Demo 呢,理论上来讲应该是可以应对大多数的比较简单的物联网项目了,只要把这个 Demo 中的硬件结构换成自己的,再根据硬件结构调整一下类的关系就好了。对于我个人而言,这也算是第一次用设计模式改良代码。当时就是感觉当成自己写的MQTT的逻辑很复杂,而且非常耦合,基本上只要有一小小的改动,就要调好多代码。毕竟,这也是企业的项目,自己也想做的好一些,让自己的代码更有价值,于是便开始有了这个 Demo

后续因为会继续完善这个项目,用微服务将其扩充为物联网API和数据分析平台

参考的文章

下面是我在遇到问题时参考的文章
通过反射调用方法: https://blog.csdn.net/csdn_ljh/article/details/51502567
解决反射生成对象,spring注入无效:https://blog.csdn.net/qq_30023773/article/details/81035617
多重代理的问题:https://blog.csdn.net/weixin_45839894/article/details/110921243
代理模式的对比:https://blog.csdn.net/weixin_43829047/article/details/113885861

  • 7
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值