消息传递中间件提供了业务集成解决方案上下文中可靠灵活的连接服务。MQ Telemetry Transport (MQTT) 是 IBM 众多的消息传递中间件技术之一,是受 Lotus Expeditor micro broker 支持的一种协议。MQTT 是基于 TCP/IP 的发布/订阅消息传递协议,针对低负荷网络的通信而设计。micro broker 是一种小型的消息代理(只有不足 2 MB 的 Java 代码),主要在小型的应用程序型设备上部署,通常设备都在离企业数据中心很远的位置。在本文中,将创建一个能够连接到 Lotus Expeditor micro broker 的示例发布程序,并用它向一个主题发布消息和验证代理对该消息的接收。学习完本文后,您应该具备了按自己的业务需要创建简单的 MQTT 发布程序所需的知识。
有关向兼容 MQTT 的代理进行订阅的历史、设计动机、协议特点和操作指导,可以在另一篇文章 “配合使用 WebSphere Business Integration Message Broker 和 MQ Telemetry Transport,第 1 部分: 订阅” 中找到。本文扩展了上述文章中所触及的概念并继续介绍了如何使用 MQTT 发布消息。上述文章涵盖了订阅方面的内容,使用的是 IBM Websphere Message Broker;本文将探究消息发布,使用的是更新的 Lotus Expeditor micro broker。本文所涵盖的方方面面都可以直接应用到 WebSphere Message Broker,因为二者都支持 MQTT 协议。
本文是用 Java 编写 MQTT 发布程序的指南。本文所详细介绍的这个示例应用程序模仿了机场上有航班抵达时的通知消息发布。为了简化此示例,我们将此场景限制为只包括两个航空公司(Air Freedom 和 Northern Air)和两个机场(Raleigh-Durham/RDU 和 London Heathrow/LHR)。要完成本示例的编译,Lotus Expeditor micro broker 附带的 MQTT 库必须可用。此外,为了用示例 MQTT 客户机连接和发布消息,还要安装和运行 Lotus Expeditor micro broker。
在消息的发布/订阅中,消息的目的地称为主题。MQTT 协议具有分层的主题空间,这意味着主题可以按这样一种方式构成,即订阅者和发布者可以使用不同精度指定目的主题。MQTT 对于主题空间有一些强制规定,但设计适合您自己应用程序的逻辑信息空间则由您负责。
经验表明,详细描述的主题空间要比简单扼要的空间更可取。例如,不建议使用 a、b 或 c 作为主题。使用简短的主题虽然可以节约带宽,但一个设计良好的主题空间则应该更具描述性且允许在订阅应用程序中使用通配符,因此更有实际意义和益处。
主题是无格式的字符串,可以包含任何单一字节的字符。但字符 /、+ 和 # 具有特定意义,在本文稍后会详细讨论。主题长度最长可达 32,767 个字符。
图 1 的示例展示了为飞机抵达和起飞消息所设计的逻辑主题空间。此主题空间将贯穿全文。
该主题空间的层次为:Flight Times - Airport - Airline - Arrivals or Departures - Flight Number。要将主题层次转变成字符串,可以使用前斜线 (/) 字符分隔主题层次中的每个部分,这又增加了额外的逻辑层精度。例如,要发布有关 Air Freedom 1326 次航班抵达 RDU 的消息, 消息主题应该是 Flight Times/RDU/Air Freedom/Arrivals/Flight 1326。这种逻辑主题空间允许不同的订阅者组可以只订阅他们所感兴趣的消息。而设计糟糕的主题空间则会导致客户的过度订阅,即客户订阅了超出自身所需的主题空间,继而会收到很多无关的消息。这些无关消息需要客户应用程序提供额外的消息过滤以决定消息是否是所感兴趣的。过度订阅会导致带宽浪费和客户机效率的降低。
接下来,我们将描述三组截然不同的订阅者,他们均对全球范围内航班的抵达和起飞时间感兴趣。
在第一个场景中,假设此人的电话上安装了 MQTT 客户应用程序,他在机场等待接机。在本例中,此人只对特定机场(LHR)的某一个航班(Air Freedom 1024 次航班)感兴趣。为了接收航班抵达的通知,客户应用程序订阅了 Flight Times/LHR/Air Freedom/Arrivals/Flight 1024。
在第二个订阅场景中,希思罗机场 (LHR) 显示其航班抵达和起飞的信息。在本例中,客户只对 LHR 机场的航班抵达和起飞信息感兴趣。此逻辑主题空间应该允许对 LHR 机场的航班抵达和起飞信息进行单个订阅。所需的订阅主题字符串是 Flight Times/LHR/#。# 字符是个特殊的通配符,可匹配主题空间内合适位置中的所有主题,可以将其视为是订阅主题空间的一个子树。
最后一个场景是航空公司跟踪其准点航班的数据。我们的示例航空公司是 Northern Air,该公司很关注其全球范围的准点航班的百分比。因此 Northern Air 需要单一订阅全球范围内的航班抵达时间。在本例中,Northern Air 只关心航班抵达,不关心航班起飞。Northern Air 这一特定需求的主题字符串是 Flight Times/+/Northern Air/Arrivals/#。此主题字符串使用了特殊的通配符 + 字符,它让 Northern Air 得以订阅所有机场的航班抵达信息。与 # 通配符不同,它只匹配主题空间层次中的某一层,并不试图匹配层次中的所有稍低的层。
在创建 MQTT 应用程序时,必须认真将主题空间设计得符合逻辑,并且支持灵活订阅。这种方式确保了发布的大多数客户都无需多次订阅和过度订阅。
用 MQ Telemetry Transport 进行消息发布需要连接到 Lotus Expeditor micro broker 或任何支持 MQTT 协议的消息服务器,比如,WebSphere Message Broker。连接到 broker 需要几个步骤。首先,必须构成并向客户机创建工厂提供 MQTT 属性对象。此属性对象提供实例化客户机的配置。属性之一是 Boolean 标志,指定了客户应用程序是否是一个 clean session 客户机,如果是的话,每次连接客户机时都不依赖于先前连接到代理的既有知识(比如任何之前的订阅或任何等待发布的消息)。如果此标志为 false,客户机状态就在连接代理过程中保持不变;例如,客户应用程序无需每次在后续的重连接时都重新订阅。此外,clean session 设置为 false,客户机和代理都试图恢复在连接中断时所打断的进展中的消息交换(根据为消息指定的服务质量)。要使用非 clean session 客户机,必须提供 MqttPersistence 接口实现。包含此接口的实现对客户机创建工厂而言意味着客户应用程序需要使用持久(可靠)消息发布。本例使用的是 clean session 客户机,并假设网络足够可靠。属性配置之后,MQTT 客户实例会从 MQTT 客户机工厂获得。创建 MQTT 客户实例需要几个参数,包括一个惟一客户机 ID、代理 IP 地址和端口以及可选的 MqttProperties 对象。
客户机 ID 向代理表明客户机的身份。它主要用来启用持久消息的传递和在客户机数个连接和断开过程中保持订阅状态。每个连接到代理的客户机都要使用不同的客户机 ID。如果两个客户机试图在连接到代理时使用相同的客户机 ID,系统只承认后一个连接,前一个连接会强制断开。这种设计实际上实现了当之前的连接未完全消除时重新连接客户机。客户机 ID 最长为 23 字符。参见清单 1。
/** * Create a MqttClient object after configuring the MqttProperties object as * required. */ private MqttClient createClient() throws MqttException { MqttProperties mqttProps = new MqttProperties(); // Stateless "clean session" client mqttProps.setCleanStart(true); /** * Create the client from the factory. The client ID for this client is * "testClient" and the URL in the second parameter describes the * location of the broker, in this case, on the local machine. */ MqttClient mqttClient = MqttClientFactory.INSTANCE.createMqttClient( "testClient", "tcp://mybroker:1883", mqttProps); return mqttClient; } /** * Connect the MqttClient to a broker. * * @throws MqttException * If an error occurs during connection operations. */ private void connect() throws MqttException { /** * Register this application for callbacks from the client */ client.registerCallback(this); /** * Connect the client to a broker. */ client.connect(); } |
MQTT 成功连接之后,就可以发布消息了。应用程序通过 MQTT 客户对象发布消息。发布消息的方法签名是 int publish(String, MqttPayload, byte, Boolean)。这四个参数详细解释如下:
这种发布方法会返回一个整型消息 ID。它可以与已注册的 MqttAdvancedCallback 方法结合使用来检查消息是否已由代理接收。
清单 2 中的代码发布的消息表明 Air Freedom 的 1024 次航班已经抵达伦敦希思罗机场 (LHR)。
/** * Invoke from the command line with a single parameter, the broker URI, * e.g. tcp://mybroker:1883. */ public static void main(String args[]) { MqttPublisher publisher = null; try { publisher = new MqttPublisher(args[0]); /** * Connect the newly created publisher to the supplied broker. */ publisher.connect(); /** * Publish an "Arrival" message. */ publisher.publishMessage( "Flight Times/LHR/Air Freedom/Arrivals/Flight 1024", (byte) 2, "Arrived"); /** * Sleep for 1 second waiting to receive notification of * publication. Real applications should use appropriate * inter-thread signaling mechanisms such as wait/notify, * cyclic barriers or latches. */ Thread.sleep(1000); } catch (MqttException exception) { System.err.println("Exception occurred during either instantiation, connection, or publication: " + exception.getMessage()); } catch (InterruptedException exception) { System.err.println("Interrupted while waiting for publication: " + exception.getMessage()); } finally { try { /** * Close the publisher if instantiated. */ if (publisher != null) { publisher.disconnectClient(); } } catch (MqttException exception) { System.err.println("Exception occurred closing publisher: " + exception.getMessage()); } } } /** * Construct a new MqttPublisher containing an unconnected MqttClient. * * @param brokerURL * Broker URL to (eventually) connect to. * @throws MqttException * If an underlying MQTT error occurs instantiating the client * object. */ private MqttPublisher(String brokerURL) throws MqttException { this.brokerURL = brokerURL; this.client = createClient(); } /** * Publish a string as a message in byte form with the given quality of * service to the given topic. */ public void publishMessage(String topic, byte qos, String message) throws MqttException { client.publish(topic, new MqttPayload(message.getBytes(), 0), qos, false); } |
此示例客户机连接到代理,并具有发布消息的能力。回调可为发布程序提供增强的功能性,这与我们的另一篇文章中所描述的客户机订阅类似。要收到发布的确认,必须创建回调处理程序,此处理程序还必须用 MQTT 客户机对象注册。有两种回调处理程序,简单回调处理程序和高级回调处理程序。它们分别由 MqttCallback 接口和 MqttAdvancedCallback 接口实现。MqttAdvancedCallback 接口用来扩展 MqttCallback,所以在使用高级回调接口时,也必须实现在简单回调接口中定义的方法。要使用高级接口,除了从简单回调函数中继承的方法之外,还必须实现另外三个方法:subscribed(int, byte[])、unsubscribed(int) 和 published(int)。
前两个回调方法:subscribed(int, byte[]) 和 unsubscribed(int) 主要针对想要监视订阅确认的客户机。当代理确认了订阅请求后,即调用订阅方法。同样地,第二个方法 unsubscribed 在从主题取消订阅的请求得到确认后调用。由于本示例只侧重于发布,所以示例客户机没有使用这两个方法;然而,骨架实现还是成功的代码编译所必需的。
发布客户机最感兴趣的方法是 published(int)。此方法提供了消息已成功传递给代理的通知。此方法只有一个整型参数,即 messageID。应用程序可能希望将这个 messageID 与发布方法所返回的 messageID 进行匹配。回调只能用于以 QoS 级别 1 或 2 发布的那些消息。MqttPersistence 实现所提供的客户端持久性与 QoS 1 或 2 的结合使用实现了为确保消息传递而使用的回调冗余。MQTT 客户机可跟踪连接故障时的所有发布,并试图一旦重建连接就即刻完成消息传递。但某些应用程序却愿意进行分步消息传递或采用其自身的传递保障机制。
要接收通知,高级回调接口的实现必须要用 MQTT 客户端注册。registerCallback 方法提供 MQTT 客户机的注册特性。清单 3 中的示例代码由清单 2 扩展而来,是一个功能全面的 MQTT 发布程序。MqttAdvancedCallback 接口由此类实现,并由之前创建的 MQTT 对象注册。该类可使用单一一个参数从命令行开启,而此参数中包含代理 URI,比如,tcp://mybroker:1883。
MQTT 是面向发布/订阅消息范型的一个功能强大的传送机制。在需要小型客户机和低网络负荷的情况下,它能提供在其他发布/订阅协议上增强了的实用工具。本文介绍了如何创建功能全面的 MQTT 发布程序。示例客户机连接到代理并向主题发布消息。此示例还展示了 MqttAdvancedCallback 接口,以用来通知消息已发送给代理。
学习