Guía de inicio para el desarrollo de cliente de protocolo MQTT

Introducción

Hola a todos, soy Anyin.

Hace algún tiempo, debido a que mi trabajo implicaba tratar con dispositivos de hardware, hice algunos trabajos relacionados con MQTT. Hoy, también haré un intercambio simple aquí.

conceptos básicos

Antes de realizar trabajos de desarrollo relacionados, debemos comprender qué es MQTT.

MQTT es un protocolo de mensajería de modo publicación-suscripción ligero, especialmente diseñado para aplicaciones IoT en entornos de red inestables y de bajo ancho de banda.

El protocolo MQTT tiene las siguientes características:

  • Protocolo de mensajes abierto, simple y fácil de implementar
  • Modelo de publicación-suscripción, publicación de mensajes de uno a muchos
  • Conexión de red basada en TCP/IP
  • Encabezado fijo de 1 byte, mensaje de latido de 2 bytes, estructura de mensaje compacta
  • Soporte QoS de mensajes, garantía de transmisión confiable

Los principales escenarios de aplicación de MQTT:

  • Comunicación IoT M2M, recopilación de big data IoT
  • Push de mensaje de Android, push de mensaje WEB
  • Mensajería instantánea móvil, como Facebook Messenger
  • Hardware inteligente, muebles inteligentes, electrodomésticos inteligentes
  • Comunicación de redes de vehículos, recolección de pilas de estaciones eléctricas
  • Ciudad inteligente, telemedicina, educación a distancia
  • Mercados industriales como la energía, el petróleo y la energía

Para obtener más detalles, consulte el sitio web oficial. No se repetirá aquí.

Para la instalación del servidor MQTT, lo usamos aquí EMQX, la dirección de su sitio web oficial: www.emqx.io/zh

Implementar un cliente MQTT

Cuando nuestro EMQservidor está instalado, podemos codificar nuestro cliente MQTT para recibir mensajes del dispositivo o enviar mensajes al dispositivo.Todo el proceso es 异步el mismo.

  1. pom.xmlagregar dependencias
        <dependency>
            <groupId>org.eclipse.paho</groupId>
            <artifactId>org.eclipse.paho.client.mqttv3</artifactId>
            <version>${mqtt.version}</version>
        </dependency>
复制代码
  1. MqttClientInstancia de paquete

En el paso 1, confiamos en una biblioteca de terceros de mqtt Para evitar que otras bibliotecas de terceros sean reemplazadas en el futuro, debemos MqttClientcrear un paquete simple para ella.

Agrega una MQTTClientclase.

    /**
     * 实例化MQTT 实例
     * @param properties 配置信息
     * @param factory 扩展点工厂
     */
    public MQTTClient(MQTTProperties properties,
                      IExtensionHandlerFactory factory,
                      List<SubscribeTopic> subscribeTopics) throws MqttException {
        if(CollectionUtils.isEmpty(subscribeTopics)){
            throw new CommonBusinessException("-1","订阅列表不能为空");
        }
        this.subscribeTopics = subscribeTopics;
        this.properties = properties;
        this.factory = factory;
        this.clientId = "SERVER_" + RandomUtil.randomString(8);
        this.init();
    }
复制代码
  • MQTTPropertiesEstar familiarizado con la configuración relacionada de MQTT
  • IExtensionHandlerFactoryEl componente de fábrica de puntos de extensión debe realizar el procesamiento comercial de acuerdo con diferentes instrucciones al recibir mensajes, por lo que este componente es obligatorio.
  • List<SubscribeTopic>El cliente debe suscribirse a la lista de temas en el lado del dispositivo

A continuación, veamos el initmétodo.

    /**
     * 初始化MQTT Client 实例
     */
    public void init() throws MqttException {
        String broker = "tcp://" + properties.getHost() + ":" + properties.getPort();
        MemoryPersistence persistence = new MemoryPersistence();
        try {
            if(client == null){
                client = new MqttClient(broker, clientId, persistence);
            }
            MqttConnectOptions connOpts = this.getOptions();
            if (client.isConnected()) {
                client.disconnect();
            }
            client.connect(connOpts);
            client.setCallback(new MQTTSubscribe(this, factory, subscribeTopics));
            // 订阅路径
            client.subscribe(this.getSubscribeTopicList());
        }catch (MqttException ex) {
            log.error("mqtt服务初始化失败: {}", ex.getMessage(), ex);
            throw ex;
        }
        log.info("mqtt服务连接成功");
    }
复制代码

这里主要处理了客户端实例连接服务器的一些操作,主要有设置参数connOpts、设置接收消息的回调setCallback,设置订阅设备端的消息主题subscribe

这里有地方需要特别注意下,在进行连接的时候之前,做了一个client.isConnected()的判断,如果连接的状态,则需要手动的断开连接client.disconnect()。这里主要是为了做重连的时候,能够确保客户端是断开连接的状态,然后再进行重连。

客户端连接的逻辑处理了,我们还需要处理下发送消息的逻辑,简单的封装下即可。

    /**
     * 发送消息
     * @param path path
     * @param deviceId 设备ID
     * @param content 发送内容
     */
    public void publish(String path, String deviceId, byte[] content){
        try {
            MqttMessage message = new MqttMessage(content);
            message.setQos(properties.getQos());
            String topic = path + deviceId;
            client.publish( topic, message);
        }catch (Exception ex){
            log.error("mqtt服务发送消息失败: deviceId: {}  {}",deviceId, ex.getMessage(), ex);
        }
    }
复制代码
  1. 处理订阅的消息

基本的客户端实例化我们已经处理完了,接着需要处理上行的消息(就是订阅的消息)。

对于不同厂商上来的业务消息可能不一样,有可能是MQTT协议包含着JSON的字符串的业务数据,也有可能是MQTT协议包含的是二进制的私有协议

为了抽象上行的消息,我们定义了2个接口,分别抽象上行消息的整包对象和上行消息的某个指令。上行消息的整包对象就是从订阅接口返回的完整的byte[]数据包;而上行消息的某个指令是指在这个完整的数据包内肯定会有某个字段指明本次消息是属于什么业务的,可能是心跳、可能是状态等等。

分别新增一个MQTTProtocolCmd类。

@Data
public abstract class MQTTProtocol {
    /**
     * 设备ID
     */
    private String deviceId;

    /**
     * 消息唯一序号
     */
    private String msgId;
    /** 
     *  具体某个业务的指令
     */
    private Cmd cmd;
}

public interface Cmd {
    /**
     * 指令类型,上行的指令或者是下行的指令
     */
    CmdTypeEnum getCmdType();
    /**
     * 指令发送的目标topic
     */
    Topic getTopic();
}
复制代码

接着,我们再新增一个协议的处理器接口:MQTTProtocolHandler

public interface MQTTProtocolHandler<T extends MQTTProtocol> extends IExtensionHandler<BusinessType> {

    String getDeviceId(String topic, byte[] payload);
    /**
     * 解码
     * @param payload 原始数据
     * @return 协议
     */
    T decode(byte[] payload);

    /**
     * 校验
     * @param protocol 解析出来的基础协议
     * @param payload 原始数据
     * @return true 通过  false 不通过
     */
    boolean check(T protocol, byte[] payload);

    /**
     * 编码
     * @param protocol 协议
     * @param data 业务数据
     * @return 编码数据
     */
    byte[] encode(T protocol, byte[] data);

    /**
     * 业务处理
     * @param protocol 协议
     */
    byte[] handle(T protocol);

    /**
     * 错误响应
     * @param protocol 协议
     */
    byte[] error(T protocol);
}
复制代码

这个接口把整个消息的处理过程分为5个步骤:解码、校验、编码、业务处理、错误响应。该接口是一个扩展点,扩展点的枚举类是:BusinessType,它表示业务类型,即使不同的业务,可能会不同的编解码和处理规则。例如:JSON的数据和二进制的私有协议,它们的编解码就不一样。

然后,我们再看看当接收到消息的时候,我们如何使用这个扩展点进行业务逻辑处理。

@Override
    public void messageArrived(String subscribeTopic, MqttMessage message) throws Exception {
        try {
            // 根据topic解析不同的业务类型
            BusinessType businessType = this.matchBusinessTypeBySubscribeTopic(subscribeTopic);
            // 根据业务类型拿到具体的协议处理器
            MQTTProtocolHandler protocolHandler = extensionHandlerFactory.getExtensionHandler(businessType, MQTTProtocolHandler.class);
           // 获取设备ID
            String deviceId = protocolHandler.getDeviceId(subscribeTopic, message.getPayload());
            // 整包协议解码
            MQTTProtocol protocol = protocolHandler.decode(message.getPayload());
            if (protocol == null) {
                log.error("协议解析异常,无法进行应答");
                return;
            }
            // 指令
            Cmd cmd = protocol.getCmd();
            if(cmd == null){
                log.error("解析后指令为空,无法进行应答");
                return;
            }
            // 设置基础信息
            protocol.setMsgId(String.valueOf(message.getId()));
            protocol.setDeviceId(deviceId);

            // 校验
            boolean success = protocolHandler.check(protocol, message.getPayload());
            if(!success){
                this.errorHandle(protocolHandler, protocol, cmd.getTopic());
                return;
            }

            try {
                // 业务处理
                byte[] result = protocolHandler.handle(protocol);

                // 应答
                if(CmdTypeEnum.DOWN == cmd.getCmdType()){
                    log.info("下行消息,无需应答");
                    return;
                }
                Topic topic = cmd.getTopic();
                if(topic == null){
                    log.error("上行消息的发布Topic为空,无需进行应答");
                    return;
                }
                // 编码后进行应答
                byte[] content = protocolHandler.encode(protocol, result);
                client.publish(topic.getTopic(), deviceId, content);
            } catch (Exception ex) {
                log.error("业务逻辑处理异常: {}, 原始数据:{}", ex.getMessage(),  ByteUtil.byte2Str(message.getPayload()), ex);
                this.errorHandle(protocolHandler, protocol, cmd.getTopic());
            }
        }catch (Exception ex){
            log.error("unknown error: {}, 原始数据:{}", ex.getMessage(),  ByteUtil.byte2Str(message.getPayload()), ex);
        }
    }
复制代码
  1. 处理需要发送的消息

在步骤3,我们处理的是上行的消息,会涉及到解码、业务处理、编码、应答等步骤。接着我们需要处理发送的消息,即下行的消息。

El procesamiento de mensajes de enlace descendente será relativamente simple, siempre que obtenga la MQTTClientinstancia correspondiente y el procesador de protocolo, puede codificarlo y luego enviar el mensaje

@Slf4j
public class MQTTPublish {
    private MQTTClient client;
    private MQTTProtocolHandler protocolHandler;
    public MQTTPublish(MQTTClient client, MQTTProtocolHandler protocolHandler) {
        this.client = client;
        this.protocolHandler = protocolHandler;
    }
    public void publish(MQTTProtocol protocol, byte[] data){
        byte[] content = protocolHandler.encode(protocol, data);
        String deviceId = protocol.getDeviceId();
        String topic = protocol.getCmd().getTopic().getTopic();
        client.publish(topic, deviceId, content);
    }
}
复制代码

El código anterior solo puede manejar el protocolo de enlace descendente asíncrono. En algunos escenarios, el protocolo de enlace descendente también debe esperar la respuesta del dispositivo. En este momento, este código no puede satisfacer las necesidades.

Por lo tanto, también necesitamos volver a empaquetar este código. Diseñamos un punto de extensión, diferentes tipos de negocios tienen diferente lógica de envío

public interface MQTTPublishHandler extends IExtensionHandler<BusinessType> {
    <T extends BaseCmd, C extends BaseCmd> T handle(C cmd, Class<T> clazz);
}
复制代码

Luego trate con su clase de implementación.

@Override
    public <T extends BaseCmd, C extends BaseCmd> T handle(C cmd, Class<T> clazz) {
        CmdEnum cmdEnum = CmdEnum.get(cmd.getCmd());
        // 编码
        EncodeCmdHandler<C, T> handler = factory.getExtensionHandler(cmdEnum, EncodeCmdHandler.class);
        byte[] data = handler.encode(cmd);

        // 根据业务类型,拿到具体的协议处理器
        MQTTProtocolHandler protocolHandler = factory.getExtensionHandler(BusinessType.CHARGING, MQTTProtocolHandler.class);
        MQTTPublish publish = new MQTTPublish(client, protocolHandler);
        Long serial = this.getSerial(cmd.getDeviceId());

        // TODO 这里是具体的实现类,需要具体业务实现
        ChargingMQTTProtocol protocol = new ChargingMQTTProtocol();
        protocol.setSerial(serial.shortValue());
        protocol.setDeviceId(cmd.getDeviceId());
        protocol.setVersion("10");
        protocol.setMac(cmd.getDeviceId());
        protocol.setCode(cmd.getCmd());
        protocol.setCmd(cmd);
        publish.publish(protocol, data);

        // 阻塞应答
        RedisMessageTask task = new RedisMessageTask();
        RedisMessageListener listener = new RedisMessageListener(task);
        try {
            // 配置RedisKey
            String key = MQTTRedisKeyUtil.callbackKey(cmd.getTopic().getTopic(), cmd.getDeviceId(), serial);
            ChannelTopic topic = new ChannelTopic(key);
            redisMessageListenerContainer.addMessageListener(listener, topic);
            // 同步阻塞
            Message message = (Message)task.getFuture().get(60000, TimeUnit.MILLISECONDS);
            return JsonUtil.fromJson(message.toString(), clazz);
        }catch (Exception ex){
            log.error("消息获取失败: {}", ex.getMessage(), ex);
            throw new CommonBusinessException("-1", "Redis应答失败: " + ex.getMessage());
        } finally {
            redisMessageListenerContainer.removeMessageListener(listener);
        }
    }
复制代码

Aquí está el uso de clases de Java CompletableFuturepara el bloqueo asíncrono. Además, implementamos un monitor en Redis mediante el uso del mecanismo MQ de Redis. Cuando hay un mensaje ascendente como respuesta descendente, StringRedisTemplate#convertAndSendenviaremos un mensaje, monitorearemos el mensaje recibido y lo configuraremos CompletableFuturepara que responda.

RedisMessageTaskContendrá una instancia de CompletableFuture y una referencia a RedisMessageListener. Su código es el siguiente:

@Data
public class RedisMessageTask<T>{
    private CompletableFuture<T> future = new CompletableFuture<>();
    // Redis的监听器
    private RedisMessageListener listener;

}
复制代码

RedisMessageListenerMantenga RedisMessageTaskla referencia, cuando se reciba el mensaje, establezca el mensaje en CompletableFuturey CompletableFutureel bloqueo de la instancia recibirá una respuesta.

public class RedisMessageListener implements MessageListener {
    private RedisMessageTask task;
    public RedisMessageListener(RedisMessageTask task) {
        this.task = task;
    }
    @Override
    public void onMessage(Message message, byte[] bytes) { ;
        task.getFuture().complete(message);
    }
}
复制代码

Finalmente, el mensaje se puede enviar cuando se recibe la respuesta del enlace ascendente.

stringRedisTemplate.convertAndSend(key, JsonUtil.toJson(data));
复制代码

finalmente

Bueno, lo anterior son algunas notas sobre el tiempo de inicio de contacto con contenido relacionado con MQTT. El código relacionado todavía está parcialmente acoplado con el negocio porque no se han realizado un diseño y desacoplamiento más detallados durante el proceso de implementación. Sin embargo, también se organizará en un paquete Lib y se colocará en el proyecto Anyin Cloud en el futuro, así que permanezca atento.

Supongo que te gusta

Origin juejin.im/post/7079348060286877732
Recomendado
Clasificación