本文介绍MQTT消息,使用Eclipse Paho 库作为MQTT java客户端发送、接收消息。

MQTT 介绍

MQTT (MQ Telemetry Transport) 是一种消息协议,用于解决需要简单、轻量方法在低能耗设备间传输数据,如在工业领域。随着物联网(IoT)设备的日益普及,MQTT的使用也越来越多,以致于OASIS宣布将MQTT(消息队列遥测传输)作为新兴的物联网消息传递协议的首选标准。

该协议支持单一消息传递模式:发布-订阅模式。客户端发送的每个消息都包含一个关联的“主题”,消息服务器使用该主题将消息路由到订阅的客户端。主题名称可以是简单的字符串,如“oiltemp”或类似路径的字符串“motor/1/rpm”。
消费者为了接收消息,需用其明确的主题名称或包含支持通配符的字符串订阅一个或多个主题(“#”表示多级主题,“+”表示单级主题)。

依赖库

需要Paho 库的 Maven 依赖:

<dependency>
  <groupId>org.eclipse.paho</groupId>
  <artifactId>org.eclipse.paho.client.mqttv3</artifactId>
  <version>1.2.0</version>
</dependency>

准备客户端

要使用Paho 库,手续需要实现IMattClient 接口,用于从MQTT服务端接收或发送消息。该接口包括所有方法,如建立连接、发送或接收消息等。

Paho 默认提供了两个IMattClient 接口的实现,一个异步客户端MqttAsyncClient、一个同步MqttClient。本文聚焦同步版本,它的语义相对简单。准备客户端需要两个步骤,第一实例化MqttClient类,第二连接至服务器。下面详细说明。

创建MqttClient实例

下面代码片段显示如何创建IMqttClient同步实例:

String publisherId = UUID.randomUUID().toString();
IMqttClient publisher = new MqttClient("tcp://iot.eclipse.org:1883",publisherId);

上面使用最简单的构造函数,一个服务端地址参数,另一个客户端表示(需唯一)。这里使用UUID确保每个客户端不重复,实际应用中ID命名应该有一定意义。
Paho还提供了其他的构造函数,我们可以使用它们来定制用于存储未确认消息的持久性机制和/或用于运行协议引擎实现所需的后台任务的ScheduledExecutorService。代码中的服务器地址是Paho项目托管的公共MQTT代理,它允许任何有互联网连接的人测试客户机,而不需要任何身份验证。

连接MQTT服务器

前面定义的MqttClient 实例并没有连接至服务器,我们需要调用connect方法,可以传入MqttConnectOptions实例作为参数,指定协议的选项,如指定用户明和密码、session恢复模式,重连接等。连接代码如下:

public class MqttUtils {
    String publisherId = "t001";
    String url = "tcp://localhost:1883";
    IMqttClient mqttClient;

    public MqttUtils() {
        MqttConnectOptions options = new MqttConnectOptions();
        options.setAutomaticReconnect(true);
        options.setCleanSession(true);
        options.setConnectionTimeout(10);
        options.setUserName("admin");
        options.setPassword("a123".toCharArray());

        try{
            mqttClient = new MqttClient(url, this.publisherId);
            mqttClient.connect(options);
        }catch (Exception e){
            e.printStackTrace();
        }

    }
}

上面连接选项解释如下:

  • 客户端在遇到网络问题时会自动重新连接
  • 丢弃上一次运行中未发送的消息
  • 连接超时设置为10秒

发送消息

使用已经连接的MqttClient发送消息非常简单。我们使用publish()方法的一个变体将有效负载(总是一个字节数组)发送到给定的主题,使用以下服务质量选项之一:

  • 0 -“最多一次”语义,也称为“发了就忘了”。当可以接受消息丢失时使用此选项,因为它不需要任何形式的确认或持久性
  • 1 -“至少一次”语义。当消息丢失不可接受且您的订阅者可以处理副本时,请使用此选项
  • 2 -“恰好一次”语义。当消息丢失不可接受且订阅者无法处理副本时,请使用此选项

在我们的示例中,EngineTemperatureSensor 类扮演模拟传感器的角色,每当我们调用它的call()方法时,它都会产生一个新的温度读数。
这个类实现了Callable接口,所以我们可以很容易地将它与java.util.concurrent包中可用的ExecutorService实现类一起使用:

import org.eclipse.paho.client.mqttv3.IMqttClient;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Random;
import java.util.concurrent.Callable;

public class EngineTempSensor implements Callable<Void> {

    private static final Logger log = LoggerFactory.getLogger(EngineTempSensor.class);
    public static final String TOPIC = "engine/t001";

    private final IMqttClient client;
    private final Random rnd = new Random();

    public EngineTempSensor(IMqttClient client) {
        this.client = client;
    }

    @Override
    public Void call() throws Exception {

        if ( !client.isConnected()) {
            log.info("[I31] Client not connected.");
            return null;
        }

        MqttMessage msg = readEngineTemp();
        msg.setQos(0);
        msg.setRetained(true);
        client.publish(TOPIC, msg);

        return null;
    }

    /**
     * This method simulates reading the engine temperature
     * @return MqttMessage
     */
    private MqttMessage readEngineTemp() {
        double temp =  80 + rnd.nextDouble() * 20.0;
        byte[] payload = String.format("T:%04.2f",temp).getBytes();
        return new MqttMessage(payload);
    }
}

MqttMessage封装有效负载(消息体)、请求的服务质量以及为消息保留的标志。此标志指示代理应该保留此消息,直到订阅者使用该消息为止。利用该特性可以实现当新的订阅者连接至服务器时(可能是同一客户端断开了连接),会立刻接收到保留消息。

接收消息

为了接收服务器消息,需要使用subscribe()方法,可以指定下列参数:

  • 一个或多个主题过滤器
  • 服务质量QoS
  • 回调方法用于处理接收消息

在下面的示例中,我们将展示如何向现有的IMqttClient实例添加消息侦听器,以接收来自给定主题的消息。我们使用CountDownLatch作为回调和主执行线程之间的同步机制,每当有新消息到达时,就递减它。

在示例代码中使用了不同的IMqttClient实例来接收消息,这样做只是为了更清楚哪个客户端做什么。这不是Paho的限制-如果你愿意,你可以使用同一客户端来发布和接收消息:

CountDownLatch receivedSignal = new CountDownLatch(10);
subscriber.subscribe(EngineTemperatureSensor.TOPIC, (topic, msg) -> {
    byte[] payload = msg.getPayload();
    // ... payload handling omitted
    receivedSignal.countDown();
});    
receivedSignal.await(1, TimeUnit.MINUTES);

上面调用subscribe()方法的subscriber变量将IMqttMessageListener实例作为它的第二个参数。我们使用简单的lambda函数来处理有效负载并减少计数器。如果在指定的时间窗口(1分钟)内没有足够的消息到达,await()方法将抛出异常。

在使用Paho时,我们不需要显式地确认收到消息。如果回调正常返回,Paho假定它是成功的消费,并向服务器发送一个确认。

如果回调抛出异常,则客户端将被关闭。请注意,这将导致在QoS级别为0时发送的任何消息丢失。当客户端重新连接并再次订阅主题时,以QoS级别1或2发送的消息将被服务器重发。

完整的测试接收代码如下:

@Test
public void whenSendMultipleMessages_thenSuccess() throws Exception {

    String publisherId = UUID.randomUUID().toString();
    MqttClient publisher = new MqttClient("tcp://iot.eclipse.org:1883",publisherId);
    
    String subscriberId = UUID.randomUUID().toString();
    MqttClient subscriber = new MqttClient("tcp://iot.eclipse.org:1883",subscriberId);
    
    
    MqttConnectOptions options = new MqttConnectOptions();
    options.setAutomaticReconnect(true);
    options.setCleanSession(true);
    options.setConnectionTimeout(10);
    

    publisher.connect(options);        
    subscriber.connect(options);
    
    CountDownLatch receivedSignal = new CountDownLatch(10);
    
    subscriber.subscribe(EngineTemperatureSensor.TOPIC, (topic, msg) -> {
        byte[] payload = msg.getPayload();
        log.info("[I82] Message received: topic={}, payload={}", topic, new String(payload));
        receivedSignal.countDown();
    });
    
    
    Callable<Void> target = new EngineTemperatureSensor(publisher);
    
    ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
    executor.scheduleAtFixedRate(() -> {
            try {
                target.call();
            }
            catch(Exception ex) {
                throw new RuntimeException(ex);
            }
        }, 1, 1, TimeUnit.SECONDS);
    

    receivedSignal.await(1, TimeUnit.MINUTES);
    executor.shutdown();
    
    assertTrue(receivedSignal.getCount() == 0 , "Countdown should be zero");

    log.info("[I105] Success !");
}

总结

在本文中,我们演示了如何使用Eclipse Paho提供的库在Java应用程序中添加对MQTT协议的支持。该库处理所有低级协议细节,让我们专注于解决方案的业务方面,同时留出良好的空间来定制其内部特性,例如消息持久性。