emqx系列:一、Java如何使用 mqtt——轻量化消息队列传输协议

2,732 阅读5分钟

mqtt java使用

本文是学习emqx系列文章的第一篇,主要讲下mqtt是什么,以及java中如何快速上手。

MQTT

MQTT(Message Queue Telemerty Transport)是一种二进制协议,主要用于服务器和那些低功耗的物联网设备(IoT)之间的通信。

它位于 TCP 协议的上层,除了提供发布-订阅这一基本功能外,也提供一些其它特性:不同的消息投递保障(delivery guarantee),“至少一次”和“最多一次”。通过存储最后一个被确认接受的消息来实现重连后的消息恢复。

它非常轻量级,并且从设计和实现层面都适合用于不稳定的网络环境中。

对比下我之前用过的RabbitMq实现的是AMQP高级消息队列协议(AMQP,Advanced Message Queuing Protocol). 功能强大可靠性强,但是不够轻量.

pom依赖

<dependency>
    <groupId>org.springframework.integration</groupId>
    <artifactId>spring-integration-mqtt</artifactId>
</dependency>
<!-- 实际依赖的是
 <dependency>
      <groupId>org.eclipse.paho</groupId>
      <artifactId>org.eclipse.paho.client.mqttv3</artifactId>
      <version>1.2.5</version>
 </dependency>
-->

创建client 建立链接

建立链接 , 使用的方式是新建client, 然后链接emq的broker地址

  1. 创建client, 配置emq的地址, clientId当前服务名(唯一),MqttClientPersistence 消息传输过程中,缓存内容
  2. 配置链接选项, 用户名, 密码,等...
  3. 建立链接
  4. 设置回调, 接收链接成功与否, 消息投递成功与否
  5. 订阅消息, 配置 topic以及对应消息监听器
@PostConstruct
public void createClient() throws MqttException {

    try {
        client = new MqttClient(mqtt.getBroker(), mqtt.getClientId(), new MemoryPersistence());

        // MQTT 连接选项
        MqttConnectOptions connOpts = new MqttConnectOptions();
        connOpts.setUserName(mqtt.getUsername());
        connOpts.setPassword(mqtt.getPassword().toCharArray());
        // 清除会话
        connOpts.setCleanSession(true);
        // 心跳间隔
        connOpts.setKeepAliveInterval(180);

        // 建立链接
        client.connect(connOpts);
        // 设置回调
        client.setCallback(new OnMessageCallback());

        // 订阅消息
        for (TopicListener topicListener : topicListeners) {
            client.subscribe(topicListener.getTopic(), topicListener);
        }

    } catch (MqttException me) {
        log.error("reason:{} ", me.getReasonCode());
        log.error("msg {}", me.getMessage());
        log.error("loc {}", me.getLocalizedMessage());
        log.error("cause :{}", JSONUtil.toJsonStr(me.getCause()));
        throw me;
    }

}

@Component
static class OnMessageCallback implements MqttCallback {

    @Override
    public void connectionLost(Throwable cause) {
        // 连接失败
        log.error("连接断开,cause: {} ", cause.getMessage(), cause);
    }

    @Override
    public void messageArrived(String topic, MqttMessage message) {
        // subscribe后得到的消息会执行到这里面
    }

    @Override
    public void deliveryComplete(IMqttDeliveryToken token) {
        log.info("消息投递成功 deliveryComplete---------" + token.isComplete());
    }

}

订阅消息

方法一

在订阅对应topic后 client.subscribe("test.topic"), 实现回调器client.setCallback(new OnMessageCallback())

即可在 MqttCallback.messageArrived(topic,message)中订阅到消息

方法二

使用client.subscribe("test.topic", IMqttMessageListener)

这种方式为每个 topic 配置监听器, 实现IMqttMessageListener.messageArrived(topic,message)得到订阅的消息

建议使用第二种, 业务逻辑复杂后,针对单个topic 实现监听器,更清晰

ps: callback和listener,分别对应 CommsCallback中的两处被调用

CommsCallback.java
protected boolean deliverMessage(String topicName, int messageId, MqttMessage aMessage) throws Exception{		
    boolean delivered = false;

    Enumeration<String> keys = callbacks.keys(); // 获取topic订阅列表
    while (keys.hasMoreElements()) {
        String topicFilter = (String)keys.nextElement();
        // callback may already have been removed in the meantime, so a null check is necessary
        // topic 对应的 messageListener
        IMqttMessageListener callback = callbacks.get(topicFilter); 
        if(callback == null) {
           continue;
        }
        //  匹配 topic, topic匹配规则(通配符等...)
        if (MqttTopic.isMatched(topicFilter, topicName)) { 
            aMessage.setId(messageId);
            // 匹配成功就调用 IMqttMessageListener.messageArrived()
            ((IMqttMessageListener)callback).messageArrived(topicName, aMessage);
            delivered = true;
        }
    }

    /* if the message hasn't been delivered to a per subscription handler, give it to the default handler */
    if (mqttCallback != null && !delivered) {
        aMessage.setId(messageId);
        // topic未匹配到监听器,就调用callback.messageArrived()
        mqttCallback.messageArrived(topicName, aMessage);
        delivered = true;
    }

    return delivered;
}

topic规则

主题名中可以包含通配符,单层通配符“+”和多层通配符“#”。使用包含通配符的主题名可以订阅满足匹配条件的所有主题。为了和 PUBLISH 中的主题区分,我们叫 SUBSCRIBE 中的主题名为主题过滤器(Topic Filter)。

  • 单层通配符“+”:“+”可以用来指代任意一个层级。 如“sensor/+/tem”,

    可以匹配:

    • sensor/data/tem
    • sensor/cmd/tem

    不可以匹配

    • sensor/data/01/tem
  • 多层通配符“#”: “#”和“+”的区别在于,“#”可以用来指代任意多个层。**但是"#"必须是Topic Filter的最后一个字符,同时必须跟在“/“后面,除非Topic Filter只包含一个”#“这一个字符。**如“#”是一个合法的Topic Filter,而“sensor#”不是一个合法的Topic Filter。

    如“sensor/data/#”,

    可匹配:

    • sensor/data
    • sensor/data/tem
    • sensor/data/tem/01
    • ensor/data/tem/01/02

    不可以匹配:

    • sensor/cmd/tem

对应的源码就是上面提到的 MqttTopic.isMatched(topicFilter,topicName). 太长了,不贴了。

发送消息

@Resource
private MqttClient client;

public void send(@RequestParam String message) {
    // 消息内容
    MqttMessage mqttMessage = new MqttMessage(message.getBytes());
    // 消息qos模式
    mqttMessage.setQos(2);
    try {
        // 推送消息到指定 topic
        client.publish("/test", mqttMessage);
    } catch (MqttException e) {
        e.printStackTrace();
        log.info(e.getMessage());
    }
}

qos–quality of service 要使用的“服务质量”。设置为0、1、2。

qos:0—表示消息最多只能传递一次(零次或一次)。消息将不会持久化到磁盘,也不会通过网络进行确认。此QoS是最快的,但仅适用于没有价值的消息。如果服务器无法处理该消息(例如,存在授权问题),会直接显示发送完成,去回调这个接口MqttCallback.deliveryComplete(IMqttDeliveryToken)。这种模式也被称为“fire and forget”,即发送后就不管了。

qos:1—表示消息应至少传递一次(一次或多次)。只有能够持久化消息,才能安全地传递消息,因此应用程序必须使用MqttConnectOptions提供持久化方法。如果未指定持久性机制,则在客户端发生故障时不会传递消息。消息将通过网络得到确认。这是默认的QoS。

qos:2-表示消息应传递一次。消息将被保存到磁盘上,并将通过网络进行两阶段确认。只有能够持久化消息,才能安全地传递消息,因此应用程序必须使用MqttConnectOptions提供持久化方法。如果未指定持久性机制,则在客户端发生故障时不会传递消息。 如果没有配置持久性,当网络或服务器出现问题时,QoS 1和QoS 2消息仍将被传递,因为客户端将在内存中保持状态。如果MQTT客户机关闭或失败,并且未配置持久性,则无法维护QoS 1和2消息的传递,因为客户端状态将丢失。

参考文档

emq官方文档