Python-MQTT-编程实用指南(全)

Python MQTT 编程实用指南(全)

原文:zh.annas-archive.org/md5/948E1F407C9BFCC597B979028EF5EE22

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

MQTT 是首选的物联网发布-订阅轻量级消息传递协议。Python 绝对是最流行的编程语言之一。它是开源的,多平台的,您可以使用它开发任何类型的应用程序。如果您开发物联网、Web 应用程序、移动应用程序或这些解决方案的组合,您必须学习 MQTT 及其轻量级消息传递系统的工作原理。Python 和 MQTT 的结合使得开发能够与传感器、不同设备和其他应用程序进行通信的强大应用程序成为可能。当然,在使用该协议时,考虑安全性是非常重要的。

大多数情况下,当您使用现代 Python 3.6 编写的复杂物联网解决方案时,您将使用可能使用不同操作系统的不同物联网板。MQTT 有自己的特定词汇和不同的工作模式。学习 MQTT 是具有挑战性的,因为它包含太多需要真实示例才能易于理解的抽象概念。

本书将使您深入了解最新版本的 MQTT 协议:3.1.1。您将学习如何使用最新的 Mosquitto MQTT 服务器、命令行工具和 GUI 工具,以便了解 MQTT 的一切工作原理以及该协议为您的项目提供的可能性。您将学习安全最佳实践,并将其用于 Mosquitto MQTT 服务器。

然后,您将使用 Python 3.6 进行许多真实示例。您将通过与 Eclipse Paho MQTT 客户端库交换 MQTT 消息来控制车辆、处理命令、与执行器交互和监视冲浪比赛。您还将使用基于云的实时 MQTT 提供程序进行工作。

您将能够在各种现代物联网板上运行示例,例如 Raspberry Pi 3 Model B+、Qualcomm DragonBoard 410c、BeagleBone Black、MinnowBoard Turbot Quad-Core、LattePanda 2G 和 UP Core 4GB。但是,任何支持 Python 3.6 的其他板都可以运行这些示例。

本书适合对象

本书面向希望开发能够与其他应用程序和设备交互的 Python 开发人员,例如物联网板、传感器和执行器。

本书涵盖内容

第一章,安装 MQTT 3.1.1 Mosquitto 服务器,开始我们的旅程,使用首选的物联网发布-订阅轻量级消息传递协议在不同的物联网解决方案中,结合移动应用程序和 Web 应用程序。我们将学习 MQTT 及其轻量级消息传递系统的工作原理。我们将了解 MQTT 的谜题:客户端、服务器(以前称为代理)和连接。我们将学习在 Linux、macOS 和 Windows 上安装 MQTT 3.1.1 Mosquitto 服务器的程序。我们将学习在云上(Azure、AWS 和其他云提供商)运行 Mosquitto 服务器的特殊注意事项。

第二章,使用命令行和 GUI 工具学习 MQTT 的工作原理,教我们如何使用命令行和 GUI 工具详细了解 MQTT 的工作原理。我们将学习 MQTT 的基础知识,MQTT 的特定词汇和其工作模式。我们将使用不同的实用工具和图表来理解与 MQTT 相关的最重要的概念。我们将在编写 Python 代码与 MQTT 协议一起工作之前,了解一切必须知道的内容。我们将使用不同的服务质量级别,并分析和比较它们的开销。

第三章,保护 MQTT 3.1.1 Mosquitto 服务器,着重介绍如何保护 MQTT 3.1.1 Mosquitto 服务器。我们将进行所有必要的配置,以使用数字证书加密 MQTT 客户端和服务器之间发送的所有数据。我们将使用 TLS,并学习如何为每个 MQTT 客户端使用客户端证书。我们还将学习如何强制所需的 TLS 协议版本。

第四章,使用 Python 和 MQTT 消息编写控制车辆的代码,侧重于使用加密连接(TLS 1.2)通过 MQTT 消息控制车辆的 Python 3.x 代码。我们将编写能够在不同流行的 IoT 平台上运行的代码,例如树莓派 3 板。我们将了解如何利用我们对 MQTT 协议的了解来构建基于需求的解决方案。我们将学习如何使用最新版本的 Eclipse Paho MQTT Python 客户端库。

第五章,测试和改进我们的 Python 车辆控制解决方案,概述了如何使用 MQTT 消息和 Python 代码来处理我们的车辆控制解决方案。我们将学习如何使用 Python 代码处理接收到的 MQTT 消息中的命令。我们将编写 Python 代码来组成和发送带有命令的 MQTT 消息。我们将使用阻塞和线程化的网络循环,并理解它们之间的区别。最后,我们将利用遗嘱功能。

第六章,使用基于云的实时 MQTT 提供程序和 Python 监控冲浪比赛,介绍了如何编写 Python 代码,使用 PubNub 基于云的实时 MQTT 提供程序与 Mosquitto MQTT 服务器结合,监控冲浪比赛。我们将通过分析需求从头开始构建一个解决方案,并编写 Python 代码,该代码将在连接到冲浪板上的多个传感器的防水 IoT 板上运行。我们将定义主题和命令,并与基于云的 MQTT 服务器一起使用,结合了前几章中使用的 Mosquitto MQTT 服务器。

附录,解决方案,每章的测试你的知识部分的正确答案都包含在附录中。

为了充分利用本书

您需要对 Python 3.6.x 和 IoT 板有基本的了解。

下载示例代码文件

您可以从www.packtpub.com的帐户中下载本书的示例代码文件。如果您在其他地方购买了本书,可以访问www.packtpub.com/support并注册,以便直接通过电子邮件接收文件。

您可以按照以下步骤下载代码文件:

  1. 登录或注册www.packtpub.com

  2. 选择支持选项卡。

  3. 单击“代码下载和勘误”。

  4. 在搜索框中输入书名,然后按照屏幕上的说明操作。

下载文件后,请确保使用最新版本的解压缩或提取文件夹:

  • WinRAR/7-Zip for Windows

  • Zipeg/iZip/UnRarX for Mac

  • 7-Zip/PeaZip for Linux

该书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Hands-On-MQTT-Programming-with-Python。如果代码有更新,将在现有的 GitHub 存储库中更新。

我们还有其他代码包,来自我们丰富的书籍和视频目录,可以在github.com/PacktPublishing/上找到。去看看吧!

下载彩色图片

我们还提供了一个 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图片。您可以在这里下载:www.packtpub.com/sites/default/files/downloads/HandsOnMQTTProgrammingwithPython_ColorImages.pdf

使用的约定

本书中使用了许多文本约定。

CodeInText:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄。例如:"将下载的WebStorm-10*.dmg磁盘映像文件挂载为系统中的另一个磁盘。"

代码块设置如下:

@staticmethod
    def on_subscribe(client, userdata, mid, granted_qos):
        print("I've subscribed with QoS: {}".format(
            granted_qos[0]))

当我们希望引起您对代码块的特定部分的注意时,相关行或项目会以粗体显示:

 time.sleep(0.5) 
       client.disconnect() 
       client.loop_stop() 

任何命令行输入或输出都以以下方式书写:

 sudo apt-add-repository ppa:mosquitto-dev/mosquitto-ppa

粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词会在文本中以这种方式出现。例如:"从管理面板中选择系统信息。"

警告或重要提示会以这种方式出现。技巧和窍门会以这种方式出现。

第一章:安装 MQTT 3.1.1 Mosquitto 服务器

在本章中,我们将开始使用首选的物联网发布-订阅轻量级消息传递协议,在不同的物联网解决方案中与移动应用和 Web 应用程序相结合。我们将学习 MQTT 及其轻量级消息系统的工作原理。

我们将理解 MQTT 谜题:客户端、服务器(以前称为经纪人)和连接。我们将学习在 Linux、macOS 和 Windows 上安装 MQTT 3.1.1 Mosquitto 服务器的程序。我们将学习在云中运行 Mosquitto 服务器(Azure、AWS 和其他云提供商)的特殊注意事项。我们将了解以下内容:

  • 理解 MQTT 协议的便利场景

  • 使用发布-订阅模式

  • 使用消息过滤

  • 理解 MQTT 谜题:客户端、服务器和连接

  • 在 Linux 上安装 Mosquitto 服务器

  • 在 macOS 上安装 Mosquitto 服务器

  • 在 Windows 上安装 Mosquitto 服务器

  • 在云中运行 Mosquitto 服务器的注意事项

理解 MQTT 协议的便利场景

想象一下,我们有数十个不同的设备必须在它们之间交换数据。这些设备必须从其他设备请求数据,接收请求的设备必须用所需的数据做出响应。请求数据的设备必须处理来自响应所需数据的设备的数据。

这些设备是物联网(IoT)板,上面连接了数十个传感器。我们有以下不同处理能力的物联网板:

  • Raspberry Pi 3 Model B+

  • Qualcomm DragonBoard 410c

  • Udoo Neo

  • BeagleBone Black

  • Phytec phyBoard-i.MX7-Zeta

  • e-con Systems eSOMiMX6-micro

  • MinnowBoard Turbot Quad-Core

每个这些板都必须能够发送和接收数据。此外,我们希望 Web 应用程序能够发送和接收数据。我们希望能够在互联网上实时发送和接收数据,并且可能会遇到一些网络问题:我们的无线网络有些不可靠,而且有些高延迟的环境。一些设备功耗低,许多设备由电池供电,它们的资源有限。此外,我们必须小心网络带宽的使用,因为一些设备使用按流量计费的连接。

按流量计费的连接是指每月有限的数据使用量的网络连接。如果超出此数据量,将额外收费。

我们可以使用 HTTP 请求并构建发布-订阅模型来在不同设备之间交换数据。然而,有一个专门设计的协议比 HTTP 1.1 和 HTTP/2 协议更轻。MQ Telemetry Transport(MQTT)更适合于许多设备在互联网上实时交换数据并且需要消耗尽可能少的网络带宽的场景。当涉及不可靠的网络和连接不稳定时,该协议比 HTTP 1.1 和 HTTP/2 更有效。

MQTT 协议是一种机器对机器(M2M)和物联网连接协议。MQTT 是一种轻量级的消息传递协议,使用基于服务器的发布-订阅机制,并在 TCP/IP(传输控制协议/互联网协议)之上运行。以下图表显示了 MQTT 协议在 TCP/IP 堆栈之上的情况:

MQTT 最流行的版本是 3.1.1 和 3.1。在本书中,我们将使用 MQTT 3.1.1。每当我们提到 MQTT 时,我们指的是 MQTT 3.1.1,这是协议的最新版本。MQTT 3.1.1 规范已经由 OASIS 联盟标准化。此外,MQTT 3.1.1 在 2016 年成为 ISO 标准(ISO/IEC 20922)。

MQTT 比 HTTP 1.1 和 HTTP/2 协议更轻,因此在需要以发布-订阅模式实时发送和接收数据时,同时需要最小的占用空间时,它是一个非常有趣的选择。MQTT 在物联网、M2M 和嵌入式项目中非常受欢迎,但也在需要可靠消息传递和高效消息分发的 Web 应用和移动应用中占据一席之地。总之,MQTT 适用于以下需要数据交换的应用领域:

  • 资产跟踪和管理

  • 汽车远程监控

  • 化学检测

  • 环境和交通监测

  • 现场力量自动化

  • 火灾和气体测试

  • 家庭自动化

  • 车载信息娱乐IVI

  • 医疗

  • 消息传递

  • 销售点POS)自助服务亭

  • 铁路

  • 射频识别RFID

  • 监控和数据采集SCADA

  • 老丨虎丨机

总之,MQTT 旨在支持物联网、M2M、嵌入式和移动应用中的以下典型挑战:

  • 轻量化,使得能够在没有巨大开销的情况下传输大量数据

  • 在大量数据中分发最小的数据包

  • 支持异步、双向、低延迟推送消息的事件驱动范式

  • 轻松地从一个客户端向多个客户端发出数据

  • 使得能够在事件发生时监听事件(面向事件的架构)

  • 支持始终连接和有时连接的模式

  • 在不可靠的网络上发布信息,并在脆弱的连接上提供可靠的传递

  • 非常适合使用电池供电的设备或需要低功耗

  • 提供响应性,使得能够实现信息的准实时传递

  • 为所有数据提供安全性和隐私

  • 能够提供必要的可扩展性,将数据分发给数十万客户端

使用发布-订阅模式工作

在深入研究 MQTT 之前,我们必须了解发布-订阅模式,也称为发布-订阅模式。在发布-订阅模式中,发布消息的客户端与接收消息的其他客户端或客户端解耦。客户端不知道其他客户端的存在。客户端可以发布特定类型的消息,只有对该特定类型的消息感兴趣的客户端才会接收到发布的消息。

发布-订阅模式需要一个服务器,也称为代理。所有客户端都与服务器建立连接。通过服务器发送消息的客户端称为发布者。服务器过滤传入的消息,并将其分发给对该类型接收消息感兴趣的客户端。向服务器注册对特定类型消息感兴趣的客户端称为订阅者。因此,发布者和订阅者都与服务器建立连接。

通过简单的图表很容易理解事物是如何工作的。以下图表显示了一个发布者和两个订阅者连接到服务器:

连接有高度传感器的树莓派 3 型 B+板是一个发布者,它与服务器建立连接。BeagleBone Black板和Udoo Neo板是两个订阅者,它们与服务器建立连接。

BeagleBone Black板向服务器指示要订阅属于传感器/无人机 01/高度主题的所有消息。Udoo Neo板也向服务器指示相同的内容。因此,两个板都订阅了传感器/无人机 01/高度主题。

主题是一个命名的逻辑通道,也称为通道或主题。服务器只会向订阅了特定主题的订阅者发送消息。

Raspberry Pi 3 Model B+板发布了一个有效负载为100 英尺,主题为sensors/drone01/altitude的消息。这个板,也就是发布者,向服务器发送了发布请求。

消息的数据称为有效负载。消息包括它所属的主题和有效负载。

服务器将消息分发给订阅了sensors/drone01/altitude主题的两个客户端:BeagleBone BlackUdoo Neo板。

发布者和订阅者在空间上是解耦的,因为它们彼此不知道。发布者和订阅者不必同时运行。发布者可以发布一条消息,订阅者可以稍后接收。此外,发布操作与接收操作不是同步的。

发布者请求服务器发布一条消息,已订阅适当主题的不同客户端可以在不同时间接收消息。发布者可以将消息作为异步操作发送,以避免在服务器接收消息之前被阻塞。但是,也可以将消息作为同步操作发送到服务器,并且仅在操作成功后继续执行。在大多数情况下,我们将希望利用异步操作。

一个需要向数百个客户端发送消息的出版商可以通过向服务器进行单次发布操作来完成。服务器负责将发布的消息发送给所有已订阅适当主题的客户端。由于发布者和订阅者是解耦的,因此发布者不知道是否有任何订阅者会收听它即将发送的消息。因此,有时需要使订阅者也成为发布者,并发布一条消息,表明它已收到并处理了一条消息。具体要求取决于我们正在构建的解决方案的类型。MQTT 提供了许多功能,使我们在分析的许多场景中更轻松。我们将在整本书中使用这些不同的功能。

使用消息过滤

服务器必须确保订阅者只接收他们感兴趣的消息。在发布-订阅模式中,可以根据不同的标准过滤消息。我们将专注于分析基于主题的过滤,也称为基于主题的过滤。

考虑到每条消息都属于一个主题。当发布者请求服务器发布一条消息时,它必须同时指定主题和消息。服务器接收消息并将其传递给所有已订阅消息所属主题的订阅者。

服务器不需要检查消息的有效负载以将其传递给相应的订阅者;它只需要检查已到达的每条消息的主题,并在发布给相应订阅者之前进行过滤。

订阅者可以订阅多个主题。在这种情况下,服务器必须确保订阅者接收属于其订阅的所有主题的消息。通过另一个简单的图表,很容易理解事情是如何工作的。

以下图表显示了两个尚未发布任何消息的未来发布者,一个服务器和两个连接到服务器的订阅者:

一个Raspberry Pi 3 Model B+板上连接了一个高度传感器,另一个Raspberry Pi 3板上连接了一个温度传感器,它们将成为两个发布者。一个BeagleBone Black板和一个Udoo Neo板是两个订阅者,它们与服务器建立连接。

BeagleBone Black板告诉服务器它想订阅属于sensors/drone01/altitude主题的所有消息。Udoo Neo板告诉服务器它想订阅属于以下两个主题之一的所有消息:sensors/drone01/altitudesensors/drone40/temperature。因此,Udoo Neo板订阅了两个主题,而BeagleBone Black板只订阅了一个主题。

下图显示了两个发布者连接并通过服务器发布不同主题的消息后会发生什么:

Raspberry Pi 3 Model B+板发布了一个以120 英尺为有效载荷和sensors/drone01/altitude为主题的消息。即发布者的板发送发布请求到服务器。服务器将消息分发给订阅了sensors/drone01/altitude主题的两个客户端:BeagleBone BlackUdoo Neo板。

Raspberry Pi 3板发布了一个以75 F为有效载荷和sensors/drone40/temperature为主题的消息。即发布者的板发送发布请求到服务器。服务器将消息分发给唯一订阅了sensors/drone40/temperature主题的客户端:Udoo Neo板。因此,Udoo Neo板从服务器接收了两条消息,一条属于sensors/drone01/altitude主题,另一条属于sensors/drone40/temperature主题。

下图显示了当一个发布者通过服务器发布消息到一个主题,而这个主题只有一个订阅者时会发生什么:

Raspberry Pi 3板发布了一个以76 F为有效载荷和sensors/drone40/temperature为主题的消息。即发布者的板发送发布请求到服务器。服务器将消息分发给唯一订阅了sensors/drone40/temperature主题的客户端:Udoo Neo板。

理解 MQTT 谜题-客户端、服务器和连接

在低于 3.1.1 版本的 MQTT 协议中,MQTT 服务器被称为 MQTT 代理。从 MQTT 3.1.1 开始,MQTT 代理被重命名为 MQTT 服务器,因此我们将称其为服务器。然而,我们必须考虑到 MQTT 服务器、工具和客户端库的文档可能会使用旧的 MQTT 代理名称来指代服务器。MQTT 服务器也被称为消息代理。

MQTT 服务器使用先前解释的基于主题的过滤器来过滤和分发消息给适当的订阅者。有许多 MQTT 服务器实现提供了通过提供自定义插件来提供额外的消息过滤功能。但是,我们将专注于作为 MQTT 协议要求一部分的功能。

如前所述,在 MQTT 中,发布者和订阅者是完全解耦的。发布者和订阅者都是仅与 MQTT 服务器建立连接的 MQTT 客户端。一个 MQTT 客户端可以同时是发布者和订阅者,也就是说,客户端可以向特定主题发布消息,同时接收订阅了的主题的消息。

各种流行的编程语言和平台都有 MQTT 客户端库可用。在选择 MQTT 客户端库时,我们必须考虑的最重要的事情之一是它们支持的 MQTT 功能列表以及我们解决方案所需的功能。有时,我们可以在特定编程语言和平台之间选择多个库,其中一些可能不实现所有功能。在本书中,我们将使用支持各种平台的现代 Python 版本的最完整的库。

任何具有 TCP/IP 协议栈并能够使用 MQTT 库的设备都可以成为 MQTT 客户端,即发布者、订阅者,或者既是发布者又是订阅者。MQTT 库使设备能够在 TCP/IP 协议栈上与 MQTT 通信,并与特定类型的 MQTT 服务器进行交互。例如,以下设备都可以成为 MQTT 客户端,除其他设备外:

  • 一个 Arduino 板

  • 一个树莓派 3 Model B+板

  • 一个 BeagleBone Black 板

  • 一个 Udoo Neo 板

  • 一个 iPhone

  • 一个 iPad

  • 一个安卓平板电脑

  • 一个安卓智能手机

  • 运行 Windows 的笔记本电脑

  • 运行 Linux 的服务器

  • 运行 macOS 的 MacBook

许多 MQTT 服务器适用于最流行的平台,包括 Linux、Windows 和 macOS。其中许多是可以作为 MQTT 服务器工作并提供额外功能的服务器。MQTT 服务器可能只实现 MQTT 功能的子集,并可能具有特定的限制。因此,在选择 MQTT 服务器之前,检查我们解决方案中所需的所有功能非常重要。与其他中间件一样,我们有开源版本、免费版本和付费版本。因此,我们还必须确保根据我们的预算和特定需求选择适当的 MQTT 服务器。

在本书中,我们将使用 Eclipse Mosquitto MQTT 服务器(www.mosquitto.org)。Mosquitto 是一个开源的 MQTT 服务器,具有 EPL/EDL 许可证,与 MQTT 版本 3.1.1 和 3.1 兼容。我们可以利用我们学到的一切与其他 MQTT 服务器一起工作,比如Erlang MQTT BrokerEMQ),也称为 Emqttd(www.emqtt.io),以及 HiveMQ(hivemq.com),等等。此外,我们可能会利用我们的知识与基于云的 MQTT 服务器一起工作,比如 CloudMQTT(www.cloudmqtt.com)或 PubNub MQTT 桥接器(pubnub.com)。我们还将专门与基于云的 MQTT 提供商一起工作。

MQTT 服务器是我们之前分析的发布-订阅模型的中心枢纽。MQTT 服务器负责对将能够成为发布者和/或订阅者的 MQTT 客户端进行身份验证和授权。因此,MQTT 客户端必须做的第一件事就是与 MQTT 服务器建立连接。

为了建立连接,MQTT 客户端必须向 MQTT 服务器发送一个带有有效载荷的CONNECT控制数据包,该有效载荷必须包括启动连接和进行身份验证和授权所需的所有必要信息。MQTT 服务器将检查CONNECT数据包,执行身份验证和授权,并向客户端发送一个CONNACK控制数据包的响应,我们将在理解CONNECT控制数据包后详细分析。如果 MQTT 客户端发送了无效的CONNECT控制数据包,服务器将自动关闭连接。

以下图显示了 MQTT 客户端与 MQTT 服务器之间建立连接的交互:

在 MQTT 客户端和 MQTT 服务器之间建立成功连接后,服务器将保持连接开放,直到客户端失去连接或向服务器发送DISCONNECT控制数据包以关闭连接。

CONNECT控制数据包的有效载荷必须包括以下字段的值,以及包含在控制数据包中的特殊标志字节的位。我们希望理解这些字段和标志的含义,因为当我们使用 Python 中的 MQTT 工具和 MQTT 客户端库时,我们将能够指定它们的值:

  • ClientId:客户端标识符,也称为客户端 ID,是一个字符串,用于标识连接到 MQTT 服务器的每个 MQTT 客户端。连接到 MQTT 服务器的每个客户端必须具有唯一的ClientId,服务器使用它来标识与客户端和服务器之间的 MQTT 会话相关的状态。如果客户端将空值指定为ClientId,MQTT 服务器必须生成一个唯一的ClientId来标识客户端。但是,此行为取决于为CleanSession字段指定的值。

  • CleanSession:清理会话标志是一个布尔值,指定 MQTT 客户端从 MQTT 服务器断开连接然后重新连接后会发生什么。如果CleanSession设置为1True,客户端向 MQTT 服务器指示会话只会持续到网络连接保持活跃。MQTT 客户端从 MQTT 服务器断开连接后,与会话相关的任何信息都会被丢弃。同一 MQTT 客户端重新连接到 MQTT 服务器时,不会使用上一个会话的数据,而会创建一个新的清理会话。如果CleanSession设置为0False,我们将使用持久会话。在这种情况下,MQTT 服务器会存储 MQTT 客户端的所有订阅,当 MQTT 客户端断开连接时,MQTT 服务器会存储与订阅匹配的特定服务质量级别的所有消息。这样,当同一 MQTT 客户端与 MQTT 服务器建立新连接时,MQTT 客户端将拥有相同的订阅,并接收在失去连接时无法接收的所有消息。我们将在后面的第二章中深入探讨消息的服务质量级别及其与清理会话标志或持久会话选项的关系。

当清理会话标志设置为0False时,客户端向服务器指示它需要一个持久会话。我们只需要记住,清理会话是持久会话的相反。

  • UserName:如果客户端想要指定一个用户名来请求 MQTT 服务器的认证和授权,它必须将UserName标志设置为1True,并为UserName字段指定一个值。

  • Password:如果客户端想要指定一个密码来请求 MQTT 服务器的认证和授权,它必须将Password标志设置为1True,并为Password字段指定一个值。

我们将专门为 MQTT 安全性撰写一整章,因此我们只提及CONNECT控制数据包中包含的字段和标志。

  • ProtocolLevel:协议级别值指示 MQTT 客户端请求 MQTT 服务器使用的 MQTT 协议版本。请记住,我们将始终使用 MQTT 版本 3.1.1。

  • KeepAliveKeepAlive是以秒为单位表示的时间间隔。如果KeepAlive的值不等于0,MQTT 客户端承诺在指定的KeepAlive时间内向服务器发送控制数据包。如果 MQTT 客户端不必发送任何控制数据包,它必须向 MQTT 服务器发送一个PINGREQ控制数据包,以告知 MQTT 服务器客户端连接仍然活跃。MQTT 服务器会用PINGRESP响应控制数据包回应 MQTT 客户端,以告知 MQTT 客户端与 MQTT 服务器的连接仍然活跃。当缺少这些控制数据包时,连接将被关闭。如果KeepAlive的值为0,则保持活动机制将被关闭。

  • Will,WillQoS,WillRetain,WillTopic 和 WillMessage:这些标志和字段允许 MQTT 客户端利用 MQTT 的遗嘱功能。如果 MQTT 客户端将 Will 标志设置为 1 或 True,则指定它希望 MQTT 服务器存储与会话关联的遗嘱消息。WillQoS 标志指定了遗嘱消息的期望服务质量,而 WillRetain 标志指示发布此消息时是否必须保留。如果 MQTT 客户端将 Will 标志设置为 1 或 True,则必须在 WillTopic 和 WillMessage 字段中指定 Will 消息的主题和消息。如果 MQTT 客户端断开连接或与 MQTT 服务器失去连接,MQTT 服务器将使用 WillTopic 字段中指定的主题以所选的服务质量发布 WillMessage 字段中指定的消息。我们将稍后详细分析此功能。

MQTT 服务器将处理有效的 CONNECT 控制数据包,并将以 CONNACK 控制数据包作出响应。此控制数据包将包括标头中包含的以下标志的值。我们希望了解这些标志的含义,因为在使用 MQTT 工具和 MQTT 客户端库时,我们将能够检索它们的值:

  • SessionPresent: 如果 MQTT 服务器收到了一个将 CleanSession 标志设置为 1 或 True 的连接请求,SessionPresent 标志的值将为 0 或 False,因为不会重用任何存储的会话。如果连接请求中的 CleanSession 标志设置为 0 或 False,MQTT 服务器将使用持久会话,并且如果服务器从先前的连接中为客户端检索到持久会话,则 SessionPresent 标志的值将为 1 或 True。否则,SessionPresent 将为 0 或 False。想要使用持久会话的 MQTT 客户端可以使用此标志的值来确定是否必须请求订阅所需主题,或者订阅是否已从持久会话中恢复。

  • ReturnCode: 如果授权和认证通过,并且连接成功建立,ReturnCode 的值将为 0。否则,返回代码将不同于 0,客户端和服务器之间的网络连接将被关闭。以下表格显示了 ReturnCode 的可能值及其含义:

ReturnCode 值 描述
0 连接被接受
1 由于 MQTT 服务器不支持 MQTT 客户端在 CONNECT 控制数据包中请求的 MQTT 协议版本,连接被拒绝
2 由于指定的 ClientId(客户端标识符)已被拒绝,连接被拒绝
3 由于网络连接已建立但 MQTT 服务不可用,连接被拒绝
4 由于用户名或密码数值格式不正确,连接被拒绝
5 由于授权失败,连接被拒绝

在 Linux 上安装 Mosquitto 服务器

现在,我们将学习在最流行的操作系统上安装 Mosquitto 服务器所需的步骤:Linux,macOS 和 Windows。

使用最新版本的 Mosquitto 非常重要,以确保解决了先前版本中发现的许多安全漏洞。例如,Mosquitto 1.4.15 解决了影响版本 1.0 至 1.4.14(含)的两个重要安全漏洞。

首先,我们将从 Linux 开始;具体来说,我们将使用 Ubuntu Linux。如果您想使用其他 Linux 发行版,您可以在 Mosquitto 下载部分找到有关安装过程的详细信息:mosquitto.org/download

按照以下步骤在 Ubuntu Linux 上安装 Mosquitto 服务器;请注意,您需要 root 权限:

  1. 打开终端窗口或使用安全 shell 访问 Ubuntu,并运行以下命令以添加 Mosquitto 存储库:
 sudo apt-add-repository ppa:mosquitto-dev/mosquitto-ppa 

您将看到类似于下面的输出(临时文件名将不同):

 gpg: keyring `/tmp/tmpi5yrsz7i/secring.gpg' created
 gpg: keyring `/tmp/tmpi5yrsz7i/pubring.gpg' created
 gpg: requesting key 262C4500 from hkp server keyserver.ubuntu.com
 gpg: /tmp/tmpi5yrsz7i/trustdb.gpg: trustdb created
 gpg: key 262C4500: public key "Launchpad mosquitto" imported
 gpg: Total number processed: 1
 gpg: imported: 1 (RSA: 1)
 OK
  1. 运行以下命令以更新最近添加的 Mosquitto 存储库中的软件包:
 sudo apt-get update

您将看到类似于下面的输出。请注意,下面的行显示了作为 Windows Azure 虚拟机运行的 Ubuntu 服务器的输出,因此输出将类似:

 Hit:1 http://azure.archive.ubuntu.com/ubuntu xenial InRelease
      Get:2 http://azure.archive.ubuntu.com/ubuntu xenial-updates       
      InRelease [102 kB]
      Get:3 http://azure.archive.ubuntu.com/ubuntu xenial-backports 
      InRelease [102 kB]

      ...

      Get:32 http://security.ubuntu.com/ubuntu xenial-security/universe        
      Translation-en [121 kB]
      Get:33 http://security.ubuntu.com/ubuntu xenial-
      security/multiverse amd64 Packages [3,208 B]
      Fetched 12.8 MB in 2s (4,809 kB/s)
      Reading package lists... Done
  1. 现在,运行以下命令以安装 Mosquitto 服务器的软件包:
 sudo apt-get install mosquitto

您将看到类似于下面的输出。

  1. 输入Y并按Enter回答问题,完成安装过程:
 Building dependency tree
      Reading state information... Done
      The following additional packages will be installed:
        libev4 libuv1 libwebsockets7
      The following NEW packages will be installed:
        libev4 libuv1 libwebsockets7 mosquitto
      0 upgraded, 4 newly installed, 0 to remove and 29 not upgraded.
      Need to get 280 kB of archives.
      After this operation, 724 kB of additional disk space will be 
      used.
      Do you want to continue? [Y/n] Y
  1. 最后几行应包括一行,其中说Setting up mosquitto,后面跟着版本号,如下所示:
 Setting up libuv1:amd64 (1.8.0-1) ...
 Setting up libev4 (1:4.22-1) ...
 Setting up libwebsockets7:amd64 (1.7.1-1) ...
 Setting up mosquitto (1.4.15-0mosquitto1~xenial1) ...
 Processing triggers for libc-bin (2.23-0ubuntu10) ...
 Processing triggers for systemd (229-4ubuntu21.1) ...
 Processing triggers for ureadahead (0.100.0-19) ...
  1. 现在,运行以下命令以安装 Mosquitto 客户端软件包,这将允许我们运行命令以发布消息到主题和订阅主题过滤器:
 sudo apt-get install mosquitto-clients

您将看到类似于下面的输出。

  1. 输入Y并按Enter回答问题,完成安装过程:
 Reading package lists... Done
 Building dependency tree
 Reading state information... Done
 The following additional packages will be installed:
 libc-ares2 libmosquitto1
 The following NEW packages will be installed:
 libc-ares2 libmosquitto1 mosquitto-clients
 0 upgraded, 3 newly installed, 0 to remove and 29 not upgraded.
 Need to get 144 kB of archives.
 After this operation, 336 kB of additional disk space will be   
      used.
 Do you want to continue? [Y/n] Y

最后几行应包括一行,其中说Setting up mosquitto-clients,后面跟着版本号,如下所示:

 Setting up libmosquitto1:amd64 (1.4.15-0mosquitto1~xenial1) ...
      Setting up mosquitto-clients (1.4.15-0mosquitto1~xenial1) ...
      Processing triggers for libc-bin (2.23-0ubuntu10) ... 
  1. 最后,运行以下命令来检查最近安装的mosquitto服务的状态:
 sudo service mosquitto status

输出的前几行应类似于以下行,显示active (running)状态。CGroup后面的详细信息指示启动服务的命令行。-c选项后跟/etc/mosquitto/mosquitto.conf指定 Mosquitto 正在使用此配置文件:

mosquitto.service - LSB: mosquitto MQTT v3.1 message broker
 Loaded: loaded (/etc/init.d/mosquitto; bad; vendor preset: enabled)
 Active: active (running) since Sun 2018-03-18 19:58:15 UTC; 3min 8s ago
 Docs: man:systemd-sysv-generator(8)
 CGroup: /system.slice/mosquitto.service
 └─15126 /usr/sbin/mosquitto -c /etc/mosquitto/mosquitto.conf

您还可以运行以下命令来检查 Mosquitto MQTT 服务器是否在默认端口1883上监听:

netstat -an | grep 1883

以下行显示了上一个命令的结果,指示 Mosquitto MQTT 服务器已在端口1883上打开了 IPv4 和 IPv6 监听套接字:

tcp 0 0 0.0.0.0:1883 0.0.0.0:* LISTEN

tcp6 0 0 :::1883 :::* LISTEN 

在 macOS 上安装 Mosquitto 服务器

按照以下步骤在 macOS 上安装 Mosquitto 服务器,即 macOS Sierra 之前的 OS X:

  1. 如果您尚未安装 Homebrew,请打开终端窗口并运行 Homebrew 主页上指定的命令brew.sh,以安装 macOS 的这个流行软件包管理器。以下命令将完成工作。但是,最好检查 Homebrew 主页并查看所有始终更新为最新 macOS 版本的详细说明。如果您已经安装了 Homebrew,请转到下一步:
 /usr/bin/ruby -e "$(curl -fsSL      
    https://raw.githubusercontent.com/Homebrew/install/master/install)"
  1. 打开终端窗口并运行以下命令以请求 Homebrew 安装 Mosquitto:
 brew install mosquitto

请注意,在某些情况下,Homebrew 可能需要在您安装 Mosquitto 之前在计算机上安装其他软件。如果需要安装其他软件,例如 Xcode 命令行工具,Homebrew 将为您提供必要的说明。

  1. 以下行显示了在终端中显示的最后消息,指示 Homebrew 已安装 Mosquitto 并启动 MQTT 服务器的说明:
 ==> Installing dependencies for mosquitto: c-ares, openssl, 
 libev, libuv, libevent, libwebsockets
 ==> Installing mosquitto dependency: c-ares
 ==> Caveats
 A CA file has been bootstrapped using certificates from the 
 SystemRoots
 keychain. To add additional certificates (e.g. the certificates 
 added in the System keychain), place .pem files in
 /usr/local/etc/openssl/certs and run
 /usr/local/opt/openssl/bin/c_rehash

 This formula is keg-only, which means it was not symlinked into 
 /usr/local, because Apple has deprecated use of OpenSSL in favor 
 of its own TLS and crypto libraries. If you need to have this 
 software first in your PATH run:
 echo 'export PATH="/usr/local/opt/openssl/bin:$PATH"' >> 
 ~/.bash_profile

 For compilers to find this software you may need to set:
 LDFLAGS: -L/usr/local/opt/openssl/lib
 CPPFLAGS: -I/usr/local/opt/openssl/include

 ==> Installing mosquitto
 ==> Downloading https://homebrew.bintray.com/bottles/mosquitto- 
 1.4.14_2.el_capit
 ##################################################
 #####################100.0%
 ==> Pouring mosquitto-1.4.14_2.el_capitan.bottle.tar.gz
 ==> Caveats
 mosquitto has been installed with a default configuration file.
 You can make changes to the configuration by editing:
 /usr/local/etc/mosquitto/mosquitto.conf

 To have launchd start mosquitto now and restart at login:
 brew services start mosquitto

 Or, if you don't want/need a background service you can just run:
 mosquitto -c /usr/local/etc/mosquitto/mosquitto.conf
  1. Mosquitto 安装完成后,在新的终端窗口中运行以下命令以使用默认配置文件启动 Mosquitto。 -c选项后跟/usr/local/etc/mosquitto/mosquitto.conf指定我们要使用此配置文件:
 /usr/local/sbin/mosquitto -c       
     /usr/local/etc/mosquitto/mosquitto.conf

在运行上一个命令后,以下是输出结果:

 1521488973: mosquitto version 1.4.14 (build date 2017-10-22 
 16:34:20+0100) starting
 1521488973: Config loaded from 
 /usr/local/etc/mosquitto/mosquitto.conf.
 1521488973: Opening ipv4 listen socket on port 1883.
 1521488973: Opening ipv6 listen socket on port 1883.

最后几行指示 Mosquitto MQTT 服务器已在默认 TCP 端口1883上打开了 IPv4 和 IPv6 监听套接字。保持终端窗口打开,因为我们需要在本地计算机上运行 Mosquitto 以使用下面的示例。

在 Windows 上安装 Mosquitto 服务器

按照以下步骤在 Windows 上安装 Mosquitto 服务器。请注意,您需要 Windows Vista 或更高版本(Windows 7、8、8.1、10 或更高版本)。这些说明也适用于 Windows Server 2008、2012、2016 或更高版本:

  1. 在 Mosquitto 下载网页上下载提供本机构建的可执行文件,该网页列出了二进制安装和 Windows 下的文件:mosquitto.org/download。对于 Mosquitto 1.4.15,文件名为mosquitto-1.4.15-install-win32.exe。您必须单击或点击文件名,然后将被重定向到 Eclipse 存储库,其中包括默认推荐的许多镜像选项,您可以从中下载可执行文件。

  2. 运行先前下载的可执行文件,mosquitto 设置向导将显示其欢迎对话框。单击“下一步>”继续。设置向导将显示您必须安装的依赖项:OpenSSL 和 pthreads。对话框将显示您可以使用的链接来下载和运行这两个要求的安装程序,如下面的屏幕截图所示:

  1. 如果您在 Windows 上没有安装 Win32 OpenSSL v1.0.2j Light,请转到 Win32 OpenSSL 网页,slproweb.com/products/Win32OpenSSL.html,并下载Win32 OpenSSL v1.1.0g Light文件。不要下载 Win64 版本,因为您需要 Win32 版本才能使 Mosquitto 具有其依赖项。如果您已经安装了 Win32 OpenSSL v1.1.0g Light,请转到第 7 步。对于 Win32 OpenSSL v1.1.0g Light,文件名为Win32OpenSSL_Light-1_1_0g.exe。运行下载的可执行文件,OpenSSL Light(32 位)将显示其欢迎对话框。单击“下一步>”继续。

  2. 设置向导将显示许可协议。阅读并选择“我接受协议”,然后单击“下一步>”。如果您不想使用默认文件夹,请选择要安装 OpenSSL Light(32 位)的文件夹。请记住您指定的文件夹,因为您稍后需要从此文件夹复制一些 DLL 文件。默认文件夹为C:\OpenSSL-Win32

  3. 单击“下一步>”继续,如有必要,指定不同的开始菜单文件夹,然后单击“下一步>”。选择 OpenSSL 二进制文件(/bin)目录作为“复制 OpenSSL DLLs”的所需选项。这样,安装将把 DLL 复制到先前指定文件夹内的bin子文件夹中,默认为C:\OpenSSL-Win32\bin

  4. 单击“下一步>”继续。查看所选的安装选项,然后单击“安装”以完成 OpenSSL Light(32 位)的安装。最后,考虑向 Win32 OpenSSL 项目捐赠,然后单击“完成”退出设置。

  5. 在 Web 浏览器中转到以下地址:ftp://sources.redhat.com/pub/pthreads-win32/dll-latest/dll/x86。浏览器将显示此 FTP 目录的许多文件。右键单击pthreadVC2.dll,然后将文件保存在您的Downloads文件夹中。稍后您需要将此 DLL 复制到 Mosquitto 安装文件夹中。

  6. 现在,返回到 Mosquitto 设置窗口,单击“下一步>”继续。默认情况下,Mosquitto 将安装文件和 Mosquitto 服务。保留默认组件以安装所选内容,然后单击“下一步>”继续。

  7. 如果您不想使用默认文件夹,请选择要安装 Mosquitto 的文件夹。请记住您指定的文件夹,因为您稍后需要将一些 DLL 文件复制到此文件夹。默认文件夹为C:\Program Files (x86)\mosquitto。单击“安装”以完成安装。请注意,mosquitto 设置向导可能会显示与缺少 DLL 相关的错误。我们将在接下来的步骤中解决此问题。安装完成后,单击“完成”关闭 mosquitto 设置向导。

  8. 打开文件资源管理器窗口,转到您安装 OpenSSL Light(32 位)的文件夹中的bin子文件夹,默认情况下为C:\OpenSSL-Win32\bin

  9. 复制以下四个 DLL 文件:libcrypto-1_1.dlllibeay32.dllssleay32.dlllibssl-1_1.dll。现在,转到您安装 Mosquitto 的文件夹,并将这四个 DLL 粘贴进去。默认情况下,Mosquitto 安装文件夹是C:\Program Files (x86)\mosquitto。您需要提供管理员权限才能将 DLL 粘贴到默认文件夹中。

  10. 打开文件资源管理器窗口,转到您的下载文件夹。复制您在先前步骤中下载的 pthreads DLL,pthreadVC2.dll。现在,转到您安装 Mosquitto 的文件夹,并将此 DLL 粘贴进去。您需要提供管理员权限才能将 DLL 粘贴到默认的 Mosquitto 安装文件夹中。

  11. 现在,所有依赖项都包含在 Mosquitto 安装文件夹中,需要再次运行安装程序以使 Mosquitto 设置配置 Windows 服务。再次运行先前下载的 Mosquitto 安装可执行文件。对于 Mosquitto 1.4.15,文件名是mosquito-1.4.15-install-win32.exe。确保指定与您复制 DLL 的文件夹相同的安装文件夹,并激活Service组件。点击下一步多次,然后点击安装以完成 Windows 服务的配置。安装完成后,点击完成以关闭 Mosquitto 设置向导。

  12. 在 Windows 中打开服务应用程序,并搜索服务名称为Mosquitto Broker的服务。右键单击服务名称,然后选择启动。状态将变为运行。默认情况下,服务配置为其启动类型设置为自动。如果您不想自动启动 Mosquitto Broker 服务,请将启动类型更改为手动。在 Windows 计算机上使用 Mosquitto 之前,您必须重复手动启动服务的步骤。请注意,服务的描述为 MQTT v3.1 代理,如下图所示。该描述已过时,因为该服务提供了一个与 MQTT 3.1 兼容的 MQTT 3.1.1 服务器。

打开 Windows PowerShell 或命令提示符窗口,并运行以下命令以检查 Mosquitto MQTT 服务器是否在默认 TCP 端口1883上监听:

 netstat -an | findstr 1883

以下行显示了先前命令的结果,表明 Mosquitto MQTT 服务器已在端口1883上打开了 IPv4 和 IPv6 监听套接字:

 TCP 0.0.0.0:1883 0.0.0.0:0 LISTENING
 TCP [::]:1883 [::]:0 LISTENING

在云中运行 Mosquitto 服务器时需要考虑的事项

我们已经在 Linux、macOS 和 Windows 上使用了 Mosquitto 服务器的默认配置。Mosquitto 服务器将使用 TCP 端口1883。如果您想从其他设备或计算机与 Mosquitto 服务器交互,您必须确保运行在您计算机上的防火墙对该端口号有适当的配置。

当您在云中的 Linux 或 Windows 虚拟机上运行 Mosquitto 服务器时,您还必须确保虚拟机网络过滤器对入站和出站流量都有适当的配置,以允许端口1883上的入站和出站流量。您必须授权端口1883上的入站和出站流量。

测试您的知识

让我们看看您是否能正确回答以下问题:

  1. MQTT 运行在以下之上:

  2. MQIP 协议

  3. TCP/IP 协议

  4. 物联网协议

  5. MQTT 消息的数据称为:

  6. 有效载荷

  7. 数据包

  8. 上传

  9. 在 MQTT 3.1.1 版本中,代理被命名为:

  10. MQTT 代理

  11. MQTT 客户端

  12. MQTT 服务器

  13. Mosquitto 是:

  14. 仅在 Windows Azure 上可用的基于云的 MQTT 服务器

  15. 仅在亚马逊网络服务上可用的基于云的 MQTT 服务器

  16. 与 MQTT 版本 3.1.1 和 3.1 兼容的开源 MQTT 服务器

  17. Mosquitto 服务器使用的默认 TCP 端口是:

  18. 22

  19. 1883

  20. 9000

正确答案包含在附录的Solutions部分中。

总结

在本章中,我们开始了解 MQTT 协议。我们了解了该协议的便利场景,发布-订阅模式的细节以及消息过滤。我们学习了与 MQTT 相关的基本概念,并了解了不同的组件:客户端、服务器或代理和连接。

我们学会了在 Linux、macOS 和 Windows 上安装 Mosquitto 服务器。我们使用了默认配置,因为这样可以让我们在使用 Mosquitto 的同时了解其内部工作原理。然后,我们将保护服务器。这样,我们就可以更容易地开始使用 Python 客户端库来发布 MQTT 消息和订阅 MQTT 主题过滤器。

现在我们的环境已经准备好开始使用尚未安全保护的 Mosquitto 服务器进行工作,我们将使用命令行和图形界面工具来详细了解 MQTT 的工作原理。我们将学习 MQTT 的基础知识,MQTT 的特定词汇以及其工作模式,这些都是我们将在第二章中讨论的主题,使用命令行和图形界面工具来学习 MQTT 的工作原理

第二章:使用命令行和 GUI 工具学习 MQTT 的工作原理

在本章中,我们将使用命令行和 GUI 工具详细了解 MQTT 3.1.1 的工作原理。我们将学习 MQTT 的基础知识,MQTT 的特定词汇以及其工作模式。我们将使用不同的实用程序和图表来了解与 MQTT 相关的最重要的概念。在编写 Python 代码与 MQTT 协议一起工作之前,我们将了解我们需要知道的一切。我们将使用不同的服务质量(QoS)级别,并分析和比较它们的开销。我们将了解以下内容:

  • 使用命令行工具订阅主题

  • 使用 GUI 工具订阅主题

  • 使用命令行工具发布消息

  • 使用 GUI 工具发布消息

  • 使用 GUI 工具取消订阅主题

  • 学习主题的最佳实践

  • 理解 MQTT 通配符

  • 了解不同的服务质量级别

  • 使用至少一次传递(QoS 级别 1)工作

  • 使用恰好一次传递(QoS 级别 2)工作

  • 理解不同服务质量级别的开销

使用命令行工具订阅主题

无人机是一种与许多传感器和执行器进行交互的物联网设备,包括与发动机、螺旋桨和伺服电机连接的数字电子调速器。无人机也被称为无人驾驶飞行器UAV),但我们肯定会称其为无人机。假设我们必须监视许多无人机。具体来说,我们必须显示它们的高度和每个伺服电机的速度。并非所有无人机都具有相同数量的发动机、螺旋桨和伺服电机。我们必须监视以下类型的无人机:

名称 螺旋桨数量
四旋翼 4
六旋翼 6
八旋翼 8

每架飞行器将每 2 秒发布一次其高度到以下主题:sensors/dronename/altitude,其中dronename必须替换为分配给每架飞行器的名称。例如,名为octocopter01的飞行器将其高度值发布到sensors/octocopter01/altitude主题,名为quadcopter20的飞行器将使用sensors/quadcopter20/altitude主题。

此外,每架飞行器将每 2 秒发布一次其每个转子的速度到以下主题:sensors/dronename/speed/rotor/rotornumber,其中dronename必须替换为分配给每架飞行器的名称,rotornumber必须替换为将要发布速度的转子编号。例如,名为octocopter01的飞行器将其转子编号1的速度值发布到sensors/octocopter01/speed/rotor/1主题。

我们将使用 Mosquitto 中包含的mosquitto_sub命令行实用程序生成一个简单的 MQTT 客户端,该客户端订阅主题并打印接收到的所有消息。在 macOS 或 Linux 中打开终端,或在 Windows 中打开命令提示符,转到 Mosquitto 安装的目录,并运行以下命令:

mosquitto_sub -V mqttv311 -t sensors/octocopter01/altitude -d

如果您想使用 Windows PowerShell 而不是命令提示符,您将不得不在mosquitto_sub之前添加.\作为前缀。

上述命令将创建一个 MQTT 客户端,该客户端将与本地 MQTT 服务器建立连接,然后将使客户端订阅在-t选项之后指定的主题:sensors/octocopter01/altitude。当客户端建立连接时,我们指定要使用的 MQTT 协议的版本为-V mqttv311。这样,我们告诉 MQTT 服务器我们要使用 MQTT 版本 3.11。我们指定-d选项以启用调试消息,这将使我们能够了解底层发生了什么。稍后我们将分析连接和订阅的其他选项。

终端或命令提示符窗口将显示类似以下行的调试消息。请注意,生成的ClientId将与Client mosqsub|17040-LAPTOP-5D之后显示的不同:

Client mosqsub|17040-LAPTOP-5D sending CONNECT
Client mosqsub|17040-LAPTOP-5D received CONNACK
Client mosqsub|17040-LAPTOP-5D sending SUBSCRIBE (Mid: 1, Topic: sensors/octocopter01/altitude, QoS: 0)
Client mosqsub|17040-LAPTOP-5D received SUBACK
Subscribed (mid: 1): 0

终端或命令提示符窗口将显示从 MQTT 服务器到 MQTT 客户端的到达的消息。保持窗口打开。您将看到客户端向 MQTT 服务器发送PINGREQ数据包,并从 MQTT 服务器接收PINQRESP数据包。以下行显示了这些数据包的消息示例:

Client mosqsub|17040-LAPTOP-5D sending PINGREQ
Client mosqsub|17040-LAPTOP-5D received PINGRESP

使用 GUI 工具订阅主题

MQTT.fx 是使用 JavaFX 实现的 GUI 实用程序,适用于 Windows、Linux 和 macOS。该工具允许我们连接到 MQTT 服务器,订阅主题过滤器,查看接收到的消息,并向主题发布消息。您可以从此实用程序的主网页的下载部分下载适合您操作系统的版本:www.mqttfx.org

现在,我们将使用 MQTT.fx GUI 实用程序生成另一个订阅相同主题sensors/octocopter01/altitude并显示所有接收到的消息的 MQTT 客户端。我们将使用 MQTT.fx 版本 1.6.0。按照以下步骤:

  1. 启动 MQTT.fx,在位于左上角的下拉菜单中选择本地 mosquitto,并单击该下拉菜单右侧和连接按钮左侧的配置图标。MQTT.fx 将显示带有名为本地 mosquitto 的连接配置文件的不同选项的编辑连接配置文件对话框。当我们学习 MQTT 客户端发送到 MQTT 服务器以建立连接的数据时,我们分析了许多这些选项。

  2. 确保按下“General”按钮,并确保取消激活“MQTT 版本使用默认”复选框。确保在“MQTT 版本”下拉菜单中选择 3.1.1。这样,我们告诉 MQTT 服务器我们要使用 MQTT 版本 3.11。注意,客户端 ID 文本框指定了 MQTT_FX_Client。这是 MQTT.fx 将发送到 MQTT 服务器(Mosquitto)的CONNECT控制数据包中的ClientId值。以下屏幕截图显示了所选选项的对话框:

  1. 单击“确定”,然后单击“连接”按钮。MQTT.fx 将与本地 Mosquitto 服务器建立连接。请注意,连接按钮已禁用,断开连接按钮已启用,因为客户端已连接到 MQTT 服务器。

  2. 单击“订阅”,并在“订阅”按钮左侧的下拉菜单中输入sensors/octocopter01/altitude。然后,单击“订阅”按钮。MQTT.fx 将在左侧显示一个新面板,显示我们已订阅的主题,如下图所示:

如果您不想使用 MQTT.fx 实用程序,可以运行另一个mosquitto_sub命令,生成另一个订阅主题并打印接收到的所有消息的 MQTT 客户端。您只需要在 macOS 或 Linux 中打开另一个终端,或者在 Windows 中打开另一个命令提示符,转到安装 Mosquitto 的目录,并再次运行以下命令。在这种情况下,不需要指定此处给出的-d选项:

mosquitto_sub -V mqttv311 -t sensors/octocopter01/altitude

现在,我们有两个订阅相同主题sensors/octocopter01/altitude的 MQTT 客户端。现在,我们将了解客户端订阅主题时发生的情况。

MQTT 客户端向 MQTT 服务器发送一个带有标识符(PacketId)的SUBSCRIBE数据包,并在有效载荷中包含一个或多个主题过滤器及其所需的服务质量级别。

服务质量被称为QoS

因此,单个SUBSCRIBE数据包可以要求 MQTT 服务器订阅客户端到多个主题。SUBSCRIBE数据包必须至少包括一个主题过滤器和一个 QoS 对,以符合协议。

在我们请求订阅的两种情况下,我们使用特定的主题名称作为主题过滤器的值,因此我们要求 MQTT 服务器订阅单个主题。稍后我们将学习主题过滤器中通配符的使用。

我们使用了默认选项,因此请求的服务质量是默认级别 0。我们稍后将深入研究 QoS 级别。现在,我们将专注于最简单的订阅情况。如果 QoS 级别等于 0,则PacketId字段的值将为 0。如果 QoS 级别等于 1 或 2,则数据包标识符将具有一个数字值,以标识数据包并使其能够识别与此数据包相关的响应。

MQTT 服务器将处理有效的SUBSCRIBE数据包,并将用SUBACK数据包做出响应,该数据包指示订阅确认并确认了SUBSCRIBE数据包的接收和处理。 SUBACK数据包将在标头中包括与在SUBSCRIBE数据包中收到的PacketId相同的数据包标识符(PacketId)。 SUBACK数据包将包括每对主题过滤器和在SUBSCRIBE数据包中收到的所需 QoS 级别的返回代码。返回代码的数量将与SUBSCRIBE数据包中包含的主题过滤器的数量相匹配。以下表显示了这些返回代码的可能值。前三个返回代码表示成功订阅,每个值都指定了根据请求的 QoS 和 MQTT 服务器授予请求的 QoS 的可能性来交付的最大 QoS:

ReturnCode value Description
0 成功订阅,最大 QoS 为 0
1 成功订阅,最大 QoS 为 1
2 成功订阅,最大 QoS 为 2
128 订阅失败

如果订阅成功,MQTT 服务器将开始将与订阅中指定的主题过滤器匹配的每条发布的消息以指定的 QoS 发送到 MQTT 客户端。

以下图表显示了 MQTT 客户端与 MQTT 服务器之间订阅一个或多个主题过滤器的交互:

使用命令行工具发布消息

我们将使用 Mosquitto 中包含的mosquitto_pub命令行实用程序生成一个简单的 MQTT 客户端,该客户端将向主题发布一条消息。在 macOS 或 Linux 中打开终端,或在 Windows 中打开命令提示符,转到安装 Mosquitto 的目录,并运行以下命令:

mosquitto_pub -V mqttv311 -t sensors/octocopter01/altitude -m  "25 f" -d

上述命令将创建一个 MQTT 客户端,该客户端将与本地 MQTT 服务器建立连接,然后使客户端发布一条消息到-t选项后指定的主题:sensors/octocopter01/altitude。我们在-m选项后指定消息的有效载荷:"25 f"。当客户端建立连接时,我们指定要使用的 MQTT 协议的版本为-V mqttv311。这样,我们告诉 MQTT 服务器我们要使用 MQTT 版本 3.11。我们指定-d选项以启用调试消息,这将使我们能够了解底层发生了什么。稍后我们将分析连接和发布的其他选项。

终端或命令提示符窗口将显示类似以下行的调试消息。请注意,生成的ClientId将与Client mosqpub|17912-LAPTOP-5D后显示的不同。发布消息后,客户端将断开连接:

Client mosqpub|17912-LAPTOP-5D sending CONNECT
Client mosqpub|17912-LAPTOP-5D received CONNACK
Client mosqpub|17912-LAPTOP-5D sending PUBLISH (d0, q0, r0, m1, 'sensors/octocopter01/altitude', ... (4 bytes))
Client mosqpub|17912-LAPTOP-5D sending DISCONNECT

使用 GUI 工具发布消息

现在,我们将使用 MQTT.fx GUI 实用程序生成另一个 MQTT 客户端,该客户端将发布另一条消息到相同的主题“sensors/octocopter01/altitude”。按照以下步骤进行:

  1. 转到您建立连接并订阅主题的 MQTT.fx 窗口。

  2. 单击“Publish”,并在发布按钮左侧的下拉菜单中输入sensors/octocopter01/altitude

  3. 在发布按钮下的文本框中输入以下文本:32 f,如下图所示:

  1. 然后,单击发布按钮。 MQTT.fx 将发布输入的文本到指定的主题。

如果您不想使用 MQTT.fx 实用程序,可以运行另一个mosquitto_pub命令,以生成另一个发布消息到主题的 MQTT 客户端。您只需在 macOS 或 Linux 中打开另一个终端,或在 Windows 中打开另一个命令提示符,转到 Mosquitto 安装的目录,并运行以下命令:

mosquitto_pub -V mqttv311 -t sensors/octocopter01/altitude -m "32 f"

现在,返回到您执行mosquitto_sub命令并订阅sensors/octocopter01/atitude主题的终端或命令提示符窗口。您将看到类似以下内容的行:

Client mosqsub|3476-LAPTOP-5DO received PUBLISH (d0, q0, r0, m0, 'sensors/octocopter01/altitude', ... (4 bytes))
25 f
Client mosqsub|3476-LAPTOP-5DO received PUBLISH (d0, q0, r0, m0, 'sensors/octocopter01/altitude', ... (4 bytes))
32 f

如果我们清除以 Client 前缀开头的调试消息,我们将只看到接下来的两行。这些行显示了我们订阅sensors/octocopter01/altitude主题后收到的两条消息的有效负载:

25 f
32 f

转到 MQTT.fx 窗口,单击订阅。您将在窗口左侧的面板中看到用于订阅的主题过滤器标题右侧的 2。MQTT.fx 告诉您,您已在sensors/octocopter01/altitude主题中收到两条消息。单击此面板,MQTT.fx 将在面板右侧显示所有收到的消息。 MQTT.fx 将在每条消息的右侧显示一个数字,以指定自订阅主题过滤器以来的消息编号。单击每条消息,MQTT.fx 将显示消息的 QoS 级别(0),接收日期和时间,以及消息的默认纯文本格式的有效负载。以下屏幕截图显示了订阅者由 MQTT.fx 生成的已收到的第二条消息的有效负载:

我们创建了两个发布者,每个发布者都向相同的主题sensors/octocopter01/altitude发布了一条消息。此主题的两个订阅者都收到了这两条消息。现在,我们将了解当客户端向主题发布消息时发生了什么。

已经建立连接的 MQTT 客户端向 MQTT 服务器发送一个包含以下字段和标志的PUBLISH数据包的标头。我们需要理解这些字段和标志的含义,因为当我们使用 MQTT 工具和 Python 中的 MQTT 客户端库时,我们将能够指定其中一些值:

  • PacketId:如果 QoS 级别等于 0,则此字段的值将为 0 或不存在。如果 QoS 级别等于 1 或 2,则数据包标识符将具有一个数字值,用于标识数据包并使其能够识别与此数据包相关的响应。

  • Dup:如果 QoS 级别等于 0,则此字段的值将为 0。如果 QoS 级别等于 1 或 2,则 MQTT 客户端库或 MQTT 服务器可以在订阅者未确认第一条消息时重新发送先前由客户端发布的消息。每当尝试重新发送已经发布的消息时,Dup 标志的值必须为 1 或True

  • QoS:指定消息的 QoS 级别。我们将深入研究消息的服务质量级别,以及它们与许多其他标志的关系。到目前为止,我们一直在使用 QoS 级别 0。

  • Retain:如果此标志的值设置为1True,MQTT 服务器将使用指定的 QoS 级别存储消息。每当新的 MQTT 客户端订阅与存储或保留消息的主题匹配的主题过滤器时,将向新订阅者发送此主题的最后存储的消息。如果此标志的值设置为0False,MQTT 服务器将不会存储消息,并且不会替换具有相同主题的保留消息。

  • TopicName:要发布消息的主题名称的字符串。主题名称具有层次结构,斜杠(/)用作分隔符。在我们的示例中,TopicName的值是"sensors/octocopter01/altitude"。我们稍后将分析主题名称的最佳实践。

有效载荷包含 MQTT 客户端希望 MQTT 服务器发布的实际消息。MQTT 是数据无关的,因此我们可以发送任何二进制数据,我们不受 JSON 或 XML 等所施加的限制。当然,如果愿意,我们可以使用这些或其他方式来组织有效载荷。在我们的示例中,我们发送了一个包含表示海拔的数字,后跟一个空格和一个表示单位为feet"f"的字符串。

MQTT 服务器将读取有效的PUBLISH数据包,并且只会对大于 0 的 QoS 级别做出响应。如果 QoS 级别为 0,则 MQTT 服务器不会做出响应。MQTT 服务器将识别所有订阅主题与消息指定的主题名称匹配的订阅者,并将消息发布给这些客户端。

以下图表显示了 MQTT 客户端与 MQTT 服务器之间以 QoS 级别 0 发布消息的交互:

其他 QoS 级别具有不同的流程,发布者和 MQTT 服务器之间有额外的交互,并增加了我们稍后将分析的开销。

使用 GUI 工具取消订阅主题

每当我们不希望订阅者接收更多与一个或多个主题过滤器匹配的目标主题名称的消息时,订阅者可以向 MQTT 服务器发送取消订阅到主题过滤器列表的请求。显然,取消订阅主题过滤器与订阅主题过滤器相反。我们将使用 MQTT.fx GUI 实用程序从sensors/octocopter01/altitude主题中取消订阅 MQTT 客户端。按照以下步骤:

  1. 转到您建立连接并订阅主题的 MQTT.fx 窗口。

  2. 单击“订阅”。

  3. 单击窗口左侧显示sensors/octocopter01/altitude主题名称的面板。然后,单击此面板中的“取消订阅”按钮。以下屏幕截图显示了此按钮:

  1. MQTT.fx 将取消订阅客户端的sensors/octocopter01/altitude主题,因此客户端将不会接收发布到sensors/octocopter01/altitude主题的任何新消息。

现在,我们将使用 MQTT.fx GUI 实用程序使 MQTT 客户端向sensors/octocopter01/altitude发布另一条消息。按照以下步骤:

  1. 转到您建立连接并订阅主题的 MQTT.fx 窗口。

  2. 单击“发布”并在“发布”按钮左侧的下拉菜单中输入sensors/octocopter01/altitude

  3. 然后,单击“发布”按钮。MQTT.fx 将向指定的主题发布输入的文本。

  4. 在发布按钮下方的文本框中输入以下文本:37 f,如下面的屏幕截图所示:

如果您不想使用 MQTT.fx 实用程序,您可以运行mosquitto_pub命令生成另一个 MQTT 客户端,以向主题发布消息。您只需要在 macOS 或 Linux 中打开另一个终端,或者在 Windows 中打开另一个命令提示符,转到 Mosquitto 安装的目录,并运行以下命令:

mosquitto_pub -V mqttv311 -t sensors/octocopter01/altitude -m "37 f"

现在,返回到 MQTT.fx 窗口,点击订阅以检查已接收的消息。在我们发布新消息到sensors/octocopter01/altitude主题之前,客户端已经取消订阅了该主题,因此最近发布的带有负载"37 f"的消息没有显示出来。

返回到您执行mosquitto_sub命令并订阅sensors/octocopter01/atitude主题的终端或命令提示符窗口。您将看到类似以下的行:

Client mosqsub|3476-LAPTOP-5DO received PUBLISH (d0, q0, r0, m0, 'sensors/octocopter01/altitude', ... (4 bytes))
37 f

该客户端仍然订阅sensors/octocopter01/altitude主题,因此它接收到了负载为"37 f"的消息。

MQTT 客户端向 MQTT 服务器发送一个带有头部中的数据包标识符(PacketId)和负载中的一个或多个主题过滤器的UNSUBSCRIBE数据包。与SUBSCRIBE数据包的主要区别在于,对于每个主题过滤器并不需要包括 QoS 等级,因为 MQTT 客户端只是想要取消订阅。

当 MQTT 客户端取消订阅一个或多个主题过滤器后,MQTT 服务器仍然保持连接打开;与UNSUBSCRIBE数据包中指定的主题过滤器不匹配的主题过滤器的订阅将继续工作。

因此,一个UNSUBSCRIBE数据包可以要求 MQTT 服务器取消订阅客户端的多个主题。UNSUBSCRIBE数据包必须至少包括一个主题过滤器的负载,以符合协议。

在前面的例子中,我们要求 MQTT 服务器取消订阅时,我们使用了特定的主题名称作为主题过滤器的值,因此我们请求 MQTT 服务器取消订阅单个主题。如前所述,我们将在后面学习主题过滤器中通配符的使用。

数据包标识符将具有一个数字值,用于标识数据包并使其能够识别与此UNSUBSCRIBE数据包相关的响应。MQTT 服务器将处理有效的UNSUBSCRIBE数据包,并将以UNSUBACK数据包作出响应,该数据包表示取消订阅的确认,并确认了UNSUBSCRIBE数据包的接收和处理。UNSUBACK数据包将在头部中包含与UNSUBSCRIBE数据包中接收到的相同的数据包标识符(PacketId)。

MQTT 服务器将删除UNSUBSCRIBE数据包的负载中指定的特定客户端的订阅列表中完全匹配的任何主题过滤器。主题过滤器匹配必须是精确的才能被删除。在 MQTT 服务器从客户端的订阅列表中删除主题过滤器后,服务器将停止向客户端添加要发布的新消息。只有已经以 QoS 等级为 1 或 2 开始传递到客户端的消息将被发布到客户端。此外,服务器可能会发布已经缓冲以分发给订阅者的现有消息。

以下图表显示了 MQTT 客户端与 MQTT 服务器在取消订阅一个或多个主题过滤器时的交互:

学习主题的最佳实践

我们已经知道 MQTT 允许我们在主题上发布消息。发布者必须始终指定要发布消息的主题名称。理解 MQTT 中主题名称的最简单方法是将它们视为文件系统中的路径。

如果我们需要保存数十个文件,这些文件包含有关不同类型传感器的信息,用于各种无人机,我们可以创建一个目录层次结构来组织我们将保存的所有文件。我们可以创建一个名为sensors的目录,然后为每个无人机创建一个子目录,比如octocopter01,最后再创建一个传感器名称的子目录,比如altitude。在 macOS 或 Linux 中的路径将是sensors/octocopter01/altitude,因为这些操作系统使用正斜杠(/)作为分隔符。在 Windows 中,路径将是sensors\drone\altitude,因为这个操作系统使用反斜杠(\)作为分隔符。

然后,我们将保存有关名为octocopter01的无人机的高度传感器信息的文件在创建的路径中。我们可以考虑发布消息到一个路径,使用与我们用于组织文件路径的相同机制来安排主题中的消息。

与目录或文件夹不同,主题具有主题级别,具体是主题级别的层次结构,并且斜杠(/)被用作分隔符,即主题级别分隔符。如果我们将sensors/octocopter01/altitude用作主题名称,sensors是第一个主题级别,octocopter01是第二个主题级别,altitude是第三个主题级别。

主题名称区分大小写,因此sensors/octocopter01/altitudesensors/Octocopter01/altitudeSensors/octocopter01/altitudeSensors/Octocopter01/Altitude是不同的。实际上,这四个字符串将被视为四个单独的主题名称。我们必须确保为主题名称选择一个大小写方案,并将其用于所有主题名称和主题过滤器。

我们可以在主题名称中使用任何 UTF-8 字符,除了我们稍后将分析的两个通配符字符:加号(+)和井号(#)。因此,我们必须避免在主题名称中使用+#。然而,限制字符集以避免客户端库出现意外问题是一个好的做法。例如,我们可以避免使用重音符号和在英语中不常见的字符,就像我们在构建 URL 时所做的那样。虽然可以使用这些字符,但在使用它们时可能会遇到问题。

我们应该避免创建以美元符号($)开头的主题,因为许多 MQTT 服务器会在以$开头的主题中发布与服务器相关的统计数据。具体来说,第一个主题级别是$SYS

在发送消息到不同主题名称时,我们必须保持一致性,就像我们在不同路径中保存文件时一样。例如,如果我们想要发布名为hexacopter20的无人机的高度,我们将使用sensors/hexacopter20/altitude。我们必须使用与为octocopter01相同目标使用的相同主题级别,只需将无人机名称从octocopter01更改为hexacopter20。使用不同结构或不一致大小写的主题将是一个非常糟糕的做法,比如altitude/sensors/hexacopter20Sensors/Hexacopter20/Altitude。我们必须考虑到我们可以通过使用主题过滤器订阅多个主题,因此创建主题名称非常重要。

了解 MQTT 通配符

当我们分析订阅操作时,我们了解到 MQTT 客户端可以订阅一个或多个主题过滤器。如果我们将主题名称指定为主题过滤器,我们将只订阅一个单一主题。我们可以利用以下两个通配符来创建订阅与过滤器匹配的所有主题的主题过滤器:

  • 加号(+):这是一个单级通配符,匹配特定主题级别的任何名称。我们可以在主题过滤器中使用这个通配符,代替指定主题级别的名称。

  • 井号 (#):这是一个多级通配符,我们只能在主题过滤器的末尾使用它,作为最后一级,并且它匹配任何主题,其第一级与#符号左侧指定的主题级别相同。

例如,如果我们想接收所有无人机海拔相关的消息,我们可以使用+单级通配符,而不是特定的无人机名称。我们可以使用以下主题过滤器:sensors/+/altitude

如果我们发布消息到以下主题,使用sensors/+/altitude主题过滤器的订阅者将会收到所有这些消息:

  • sensors/octocopter01/altitude

  • sensors/hexacopter20/altitude

  • sensors/superdrone01/altitude

  • sensors/thegreatestdrone/altitude

使用sensors/+/altitude主题过滤器的订阅者将不会收到发送到以下任何主题的消息,因为它们不匹配主题过滤器:

  • sensors/octocopter01/speed/rotor/1

  • sensors/superdrone01/speed/rotor/2

  • sensors/superdrone01/remainingbattery

如果我们想接收所有名为octocopter01的无人机所有传感器相关的消息,我们可以在无人机名称后使用#多级通配符和斜杠(/)。我们可以使用以下主题过滤器:sensors/octocopter01/#

如果我们发布消息到以下主题,使用sensors/octocopter01/#主题过滤器的订阅者将会收到所有这些消息:

  • sensors/octocopter01/altitude

  • sensors/octocopter01/speed/rotor/1

  • sensors/octocopter01/speed/rotor/2

  • sensors/octocopter01/speed/rotor/3

  • sensors/octocopter01/speed/rotor/4

  • sensors/octocopter01/remainingbattery

我们使用了多级通配符,因此,无论sensors/octocopter01/后面有多少额外的主题级别,我们都会收到所有这些消息。

使用sensors/octocopter01/#主题过滤器的订阅者将不会收到发送到以下任何主题的消息,因为它们不匹配主题过滤器。以下任何主题都没有sensors/octocopter01/作为前缀,因此它们不匹配主题过滤器:

  • sensors/hexacopter02/altitude

  • sensors/superdrone01/altitude

  • sensors/thegreatestdrone/altitude

  • sensors/drone02/speed/rotor/1

  • sensors/superdrone02/speed/rotor/2

  • sensors/superdrone02/remainingbattery

显然,当我们使用通配符时,必须小心,因为我们可能会使用单个主题过滤器订阅大量主题。我们必须避免订阅对客户端不感兴趣的主题,以避免浪费不必要的带宽和服务器资源。

稍后我们将在订阅中使用这些通配符,以分析不同的 QoS 级别如何与 MQTT 一起工作。

学习不同的 QoS 级别

现在我们了解了连接、订阅和发布如何与主题名称和带通配符的主题过滤器结合使用,我们可以深入了解 QoS 级别。到目前为止,我们已经分析了订阅和发布如何使用 QoS 级别等于 0。现在,我们将了解这个数字的含义,以及当我们使用其他可用的发布和订阅 QoS 级别时,事情是如何工作的。

记住,发布涉及从 MQTT 客户端到 MQTT 服务器的发布,然后从服务器到订阅的客户端。非常重要的是要理解,我们可以使用一个 QoS 级别进行发布,使用另一个 QoS 级别进行订阅。因此,发布过程中有一个 QoS 级别,用于发布者和 MQTT 服务器之间的过程,另一个 QoS 级别用于 MQTT 服务器和订阅者之间的发布过程。我们将使用发送者和接收者来识别参与不同 QoS 级别消息传递的各方。在发布者和 MQTT 服务器之间的发布过程中,发布者将是发送者,MQTT 服务器将是接收者。在 MQTT 服务器和订阅者之间的发布过程中,发送者将是 MQTT 服务器,接收者将是订阅者。

根据 QoS 级别,在 MQTT 协议中,发送方和接收方之间关于实际传递消息的保证的含义有所不同。QoS 级别是关于发送方和接收方之间消息保证的协议。这些保证可能包括消息到达的次数以及重复的可能性(或不可能性)。MQTT 支持以下三种可能的 QoS 级别:

  • 0,至多一次交付:此 QoS 级别提供与基础 TCP 协议相同的保证。消息不会被接收方或目的地确认。发送方只是将消息发送到目的地,然后什么都不会发生。发送方既不存储也不安排任何可能未能到达目的地的消息的新交付。这个 QoS 级别的主要优势是与其他 QoS 级别相比,它具有最低的开销。

  • 1,至少一次交付:此 QoS 级别向目的地添加了一个必须接收消息的确认要求。因此,QoS 级别 1 提供了消息至少一次传递给订阅者的保证。这个 QoS 级别的一个关键缺点是它可能会产生重复,也就是说,同一条消息可能会被发送多次到同一个目的地。发送方将消息存储,直到它收到订阅者的确认。如果发送方在特定时间内没有收到确认,它将再次向接收方发布消息。最终的接收方必须具有必要的逻辑来检测重复,如果它们不应该被处理两次的话。

  • 2,仅一次交付:此 QoS 级别提供了消息仅一次传递到目的地的保证。与其他 QoS 级别相比,QoS 级别 2 具有最高的开销。此 QoS 级别需要发送方和接收方之间的两个流。使用 QoS 级别 2 发布的消息在发送方确信它已被目的地成功接收一次后被视为成功传递。

有时,我们只希望以最少的带宽使用交付消息,我们有一个非常可靠的网络,如果由于某种原因丢失了一些消息,也无所谓。在这种情况下,QoS 级别 0 是合适的选择。

在其他情况下,消息非常重要,因为它们代表了控制物联网设备的命令,网络不可靠,我们必须确保消息到达目的地。此外,重复的命令可能会产生大问题,因为我们不希望物联网设备处理特定命令两次。在这种情况下,QoS 级别 2 将是合适的选择。

如果发布者使用比订阅者指定的 QoS 级别更高的 QoS 级别,MQTT 服务器将不得不将 QoS 级别降级到特定订阅者使用的最低级别,当它从 MQTT 服务器向该订阅者发布消息时。例如,如果我们使用 QoS 级别 2 从发布者向 MQTT 服务器发布消息,但一个订阅者在订阅时请求了 QoS 级别 1,那么从 MQTT 服务器到该订阅者的发布将使用 QoS 级别 1。

使用至少一次交付(QoS 级别 1)

首先,我们将使用通配符订阅具有 QoS 级别 1 的主题过滤器,然后我们将向与 QoS 级别 1 匹配的主题名称发布一条消息。这样,我们将分析发布和订阅如何使用 QoS 级别 1。

我们将使用 Mosquitto 中包含的mosquitto_sub命令行实用程序生成一个简单的 MQTT 客户端,该客户端订阅具有 QoS 级别 1 的主题过滤器,并打印它接收到的所有消息。在 macOS 或 Linux 中打开终端,或在 Windows 中打开命令提示符,转到安装 Mosquitto 的目录,并运行以下命令:

mosquitto_sub -V mqttv311 -t sensors/+/altitude -q 1 -d

上述命令将创建一个 MQTT 客户端,该客户端将与本地 MQTT 服务器建立连接,然后将使客户端订阅在-t选项之后指定的主题过滤器:sensors/+/altitude。我们指定要使用 QoS 级别 1 来订阅-q 1选项指定的主题过滤器。我们指定-d选项以启用调试消息,这将使我们能够了解底层发生的事情以及与使用 QoS 级别 0 发布消息时的差异。

终端或命令提示窗口将显示类似以下行的调试消息。请注意,生成的ClientId将与Client mosqsub|16736-LAPTOP-5D之后显示的不同。请注意,QoS: 1表示使用 QoS 级别 1 进行订阅:

Client mosqsub|16736-LAPTOP-5D sending CONNECT
Client mosqsub|16736-LAPTOP-5D received CONNACK
Client mosqsub|16736-LAPTOP-5D sending SUBSCRIBE (Mid: 1, Topic: sensors/+/altitude, QoS: 1)
Client mosqsub|16736-LAPTOP-5D received SUBACK
Subscribed (mid: 1): 1

我们将使用 Mosquitto 中包含的mosquitto_pub命令行实用程序生成一个简单的 MQTT 客户端,该客户端将以 QoS 级别 1 发布消息到主题,而不是我们之前发布消息时使用的 QoS 级别 0。在 macOS 或 Linux 中打开终端,或在 Windows 中打开命令提示符,转到安装 Mosquitto 的目录,并运行以下命令:

mosquitto_pub -V mqttv311 -t sensors/hexacopter02/altitude -m  "75 f" -q 1 -d

上述命令将创建一个 MQTT 客户端,该客户端将与本地 MQTT 服务器建立连接,然后将使客户端发布一条消息到-t选项之后指定的主题:sensors/hexacopter02/altitude。我们在-m选项之后指定消息的有效载荷:"75 f"。我们指定要使用 QoS 级别 1 来发布消息,使用-q 1选项。我们指定 X 选项以启用调试消息,这将使我们能够了解底层发生的事情以及与使用 QoS 级别 0 发布消息时的差异。

终端或命令提示窗口将显示类似以下行的调试消息。请注意,生成的ClientId将与Client mosqpub|19544-LAPTOP-5D之后显示的不同。发布消息后,客户端将断开连接:

Client mosqpub|19544-LAPTOP-5D sending CONNECT
Client mosqpub|19544-LAPTOP-5D received CONNACK
Client mosqpub|19544-LAPTOP-5D sending PUBLISH (d0, q1, r0, m1, 'sensors/drone02/altitude', ... (4 bytes))
Client mosqpub|19544-LAPTOP-5D received PUBACK (Mid: 1)
Client mosqpub|19544-LAPTOP-5D sending DISCONNECT

上述行显示,生成的 MQTT 客户端向 MQTT 服务器发送PUBLISH数据包,然后从服务器接收PUBACK数据包。

现在,回到您执行mosquitto_sub命令并订阅sensors/+/atitude主题过滤器的终端或命令提示窗口。您将看到类似以下行的内容:

Client mosqsub|16736-LAPTOP-5D received PUBLISH (d0, q1, r0, m1, 'sensors/drone02/altitude', ... (4 bytes))
Client mosqsub|16736-LAPTOP-5D sending PUBACK (Mid: 1)
75 f

上述行显示,生成的 MQTT 客户端,即订阅者,从 MQTT 服务器接收了PUBLISH数据包,然后向服务器发送了PUBACK数据包以确认消息。如果我们清除以Client前缀开头的调试消息,我们将只看到最后一行,显示我们订阅sensors/+/altitude主题过滤器后收到的消息的有效载荷:75 f

已经建立连接的 MQTT 客户端,即发布者,向 MQTT 服务器发送了一个PUBLISH数据包,其中包含我们已经描述的标头,QoS 设置为 1,并包括一个PacketId数值,该数值对于此客户端是唯一的。此时,发布者将PacketId标识的PUBLISH数据包视为未确认的PUBLISH数据包。

MQTT 服务器读取有效的PUBLISH数据包,并使用与PUBLISH数据包相同的PacketId值向发布者发送PUBACK数据包。一旦发布者收到PUBACK数据包,它将丢弃消息,MQTT 服务器负责将其发布给适当的订阅者。

以下图表显示了发布者与 MQTT 服务器之间以 QoS 级别 1 发布消息的交互:

MQTT 服务器可以在向发布者发送PUBACK数据包之前开始向适当的订阅者发布消息。因此,当发布者从 MQTT 服务器接收到PUBACK数据包时,并不意味着所有订阅者都已收到消息。理解这个PUBACK数据包的含义非常重要。

对于每个需要发布消息的订阅者,MQTT 服务器将发送一个PUBLISH数据包,订阅者必须通过向 MQTT 服务器发送一个PUBACK数据包来确认收到消息。下图显示了当使用 QoS 级别为 1 发布消息时,MQTT 服务器与订阅者之间的交互:

如果应用程序能够容忍重复,并且我们必须确保消息至少到达订阅者一次,QoS 级别 1 是一个很好的选择。如果没有办法处理重复,我们必须使用 QoS 级别 2。

使用仅一次传递(QoS 级别 2)

首先,我们将使用通配符订阅一个带有 QoS 级别 2 的主题过滤器,然后我们将向与 QoS 级别 2 匹配的主题发布一条消息。这样,我们将分析发布和订阅在 QoS 级别 2 下的工作方式。

我们将使用 Mosquitto 中包含的mosquitto_sub命令行实用程序生成一个简单的 MQTT 客户端,该客户端订阅带有 QoS 级别 1 的主题过滤器,并打印接收到的所有消息。在 macOS 或 Linux 中打开终端,或在 Windows 中打开命令提示符,转到安装 Mosquitto 的目录,并运行以下命令:

mosquitto_sub -V mqttv311 -t sensors/quadcopter30/# -q 2 -d

上述命令将创建一个 MQTT 客户端,该客户端将与本地 MQTT 服务器建立连接,然后将客户端订阅到-t选项后指定的主题过滤器:sensors/quadcopter30/#。我们指定要使用-q 2选项订阅带有 QoS 级别 2 的主题过滤器。我们指定-d选项以启用调试消息,以便我们了解底层发生了什么以及与使用 QoS 级别 0 和 1 发布消息时的区别。

终端或命令提示符窗口将显示类似以下行的调试消息。请注意,生成的ClientId将与Client mosqsub|8876-LAPTOP-5DO后显示的不同。请注意,QoS: 2表示使用 QoS 级别 2 进行订阅:

Client mosqsub|8876-LAPTOP-5DO sending CONNECT
Client mosqsub|8876-LAPTOP-5DO received CONNACK
Client mosqsub|8876-LAPTOP-5DO sending SUBSCRIBE (Mid: 1, Topic: sensors/quadcopter30/#, QoS: 2)
Client mosqsub|8876-LAPTOP-5DO received SUBACK
Subscribed (mid: 1): 2

我们将使用 Mosquitto 中包含的mosquitto_pub命令行实用程序生成一个简单的 MQTT 客户端,该客户端将向具有 QoS 级别 2 的主题发布消息,而不是我们之前发布消息时使用的 QoS 级别 0 和 1。在 macOS 或 Linux 中打开终端,或在 Windows 中打开命令提示符,转到安装 Mosquitto 的目录,并运行以下命令:

mosquitto_pub -V mqttv311 -t sensors/quadcopter30/speed/rotor/1 -m  "123 f" -q 2 -d

上述命令将创建一个 MQTT 客户端,该客户端将与本地 MQTT 服务器建立连接,然后将客户端发布一条消息到-t选项后指定的主题:sensors/quadcopter30/speed/rotor/1。我们在-m选项后指定消息的有效载荷:"123 f"。我们指定要使用-q 2选项发布消息的 QoS 级别 2。我们指定-d选项以启用调试消息,以便我们了解底层发生了什么以及与使用 QoS 级别 0 和 1 发布消息时的区别。

终端或命令提示符窗口将显示类似以下行的调试消息。请注意,生成的ClientId将与Client mosqpub|14652-LAPTOP-5D后显示的不同。发布消息后,客户端将断开连接:

Client mosqpub|14652-LAPTOP-5D sending CONNECT
Client mosqpub|14652-LAPTOP-5D received CONNACK
Client mosqpub|14652-LAPTOP-5D sending PUBLISH (d0, q2, r0, m1, 'sensors/quadcopter30/speed/rotor/1', ... (5 bytes))
Client mosqpub|14652-LAPTOP-5D received PUBREC (Mid: 1)
Client mosqpub|14652-LAPTOP-5D sending PUBREL (Mid: 1)
Client mosqpub|14652-LAPTOP-5D received PUBCOMP (Mid: 1)
Client mosqpub|14652-LAPTOP-5D sending DISCONNECT

上述行显示生成的 MQTT 客户端(即发布者)与 MQTT 服务器的数据包交换如下:

  1. 发布者向 MQTT 服务器发送一个PUBLISH数据包

  2. 发布者从 MQTT 服务器接收到一个PUBREC数据包

  3. 发布者向 MQTT 服务器发送了PUBREL数据包

  4. 发布者从 MQTT 服务器接收了PUBCOMP数据包

现在,回到您执行mosquitto_sub命令并订阅sensors/quadcopter30/#主题过滤器的终端或命令提示符窗口。您将看到类似以下行的行:

Client mosqsub|8876-LAPTOP-5DO received PUBLISH (d0, q2, r0, m1, 'sensors/quadcopter30/speed/rotor/1', ... (5 bytes))
Client mosqsub|8876-LAPTOP-5DO sending PUBREC (Mid: 1)
Client mosqsub|8876-LAPTOP-5DO received PUBREL (Mid: 1)
123 f
Client mosqsub|8876-LAPTOP-5DO sending PUBCOMP (Mid: 1)

前面的行显示了生成的 MQTT 客户端,即订阅者,与 MQTT 服务器进行的数据包交换:

  1. 订阅者从 MQTT 服务器接收了PUBLISH数据包

  2. 订阅者向 MQTT 服务器发送了PUBREC数据包

  3. 订阅者从 MQTT 服务器接收了PUBREL数据包

  4. 订阅者在成功接收有效载荷为消息的消息后向 MQTT 服务器发送了PUBCOMP数据包

如果我们清除以Client前缀开头的调试消息,我们将只看到最后一行,它显示了我们订阅sensors/quadcopter30/#主题过滤器收到的消息的有效载荷:123 f

已经建立连接的 MQTT 客户端,即发布者,发送了带有我们已经描述的标头的PUBLISH数据包到 MQTT 服务器,QoS 设置为 2,并包括一个对于此客户端将是唯一的PacketId数值。此时,发布者将把带有PacketIdPUBLISH数据包视为未被确认的PUBLISH数据包。

MQTT 服务器读取有效的PUBLISH数据包,并将用相同的PacketId值向发布者发送PUBREC数据包作为响应PUBLISH数据包。PUBREC数据包表示 MQTT 服务器接受了消息的所有权。一旦发布者收到PUBREC数据包,它会丢弃消息,并存储与消息相关的PacketIdPUBREC数据包。

出版商将PUBREL数据包发送到 MQTT 服务器,作为对收到的PUBREC数据包的响应。直到它收到与 MQTT 服务器相关的PacketIdPUBCOMP数据包,这个PUBREL数据包将被视为未被确认。最后,MQTT 服务器向发布者发送带有PacketIdPUBCOMP数据包,此时,发布者和 MQTT 服务器都确信消息已成功传递。

以下图表显示了发布者和 MQTT 服务器之间以 QoS 级别 2 发布消息的交互:

对于每个具有 QoS 级别 2 的订阅者,消息必须被发布到 MQTT 服务器,MQTT 服务器将发送一个PUBLISH数据包,并且我们已经分析过的与发布者和 MQTT 服务器之间的相同数据包交换将在 MQTT 服务器和订阅者之间发生。但是,在这种情况下,MQTT 服务器将作为发布者并启动流程。以下图表显示了在使用 QoS 级别 2 发布消息时 MQTT 服务器和订阅者之间的交互:

如果应用程序无法容忍重复,并且我们必须确保消息只到达订阅者一次,那么 QoS 级别 2 是合适的选择。然而,魔法是有代价的:我们必须考虑到 QoS 级别 2 与其他 QoS 级别相比具有最高的开销。

了解不同服务质量级别的开销

以下图表总结了 MQTT 客户端和 MQTT 服务器之间交换的不同数据包,以发布具有 QoS 级别 0、1 和 2 的消息。通过这种方式,我们可以轻松识别随着 QoS 级别的增加而增加的开销:

非常重要的是要考虑 QoS 级别 2 所需的额外开销,并且只在真正必要时使用它。

测试你的知识

让我们看看你是否能正确回答以下问题:

  1. MQTT 的 QoS 级别 0 表示:

  2. 确切一次传递

  3. 至多一次传递

  4. 至少一次交付

  5. MQTT 的 QoS 级别 1 意味着:

  6. 恰好一次交付

  7. 至多一次交付

  8. 至少一次交付

  9. MQTT 的 QoS 级别 2 意味着:

  10. 恰好一次交付

  11. 至多一次交付

  12. 至少一次交付

  13. 如果应用程序无法容忍重复,并且我们必须确保消息仅一次到达订阅者,那么适当的选择是:

  14. QoS 级别 0

  15. QoS 级别 1

  16. QoS 级别 2

  17. 哪个 QoS 级别的开销最高:

  18. QoS 级别 0

  19. QoS 级别 1

  20. QoS 级别 2

正确答案包含在[附录](d9cf708f-f027-4bfa-a2d2-9fd3653165d9.xhtml)中,解决方案

摘要

在本章中,我们使用不同的工具与我们在[第一章](d20ae00b-2bb7-4d81-b3eb-5c47215bce1f.xhtml)中安装的 Mosquitto MQTT 3.1.1 服务器进行交互,安装 MQTT 3.1.1 Mosquitto 服务器。我们使用了一个未经保护的 MQTT 服务器,以便轻松理解 MQTT 客户端与 MQTT 服务器之间的交互。

我们通过命令行和 GUI 工具订阅了主题。然后,我们以 QoS 级别 0 发布消息,并从主题中取消订阅。我们学习了与主题相关的最佳实践;以及单级和多级通配符。我们详细研究了 MQTT 支持的不同服务质量级别,以及在何时使用每个级别是适当的。我们分析了它们的优点和缺点。

现在我们了解了 MQTT 3.1.1 基础知识的工作原理,我们将学习如何保护 MQTT 服务器并遵循与安全相关的最佳实践,这些是我们将在[第三章](89bdce8f-72bc-4fda-82a0-5cab33fa4bd8.xhtml)中讨论的主题,保护 MQTT 3.1.1 Mosquitto 服务器

第三章:保护 MQTT 3.1.1 Mosquitto 服务器

在本章中,我们将保护 MQTT 3.1.1 Mosquitto 服务器。我们将进行所有必要的配置,以使用数字证书加密 MQTT 客户端和服务器之间发送的所有数据。我们将使用 TLS,并学习如何为每个 MQTT 客户端使用客户端证书。我们还将学习如何强制所需的 TLS 协议版本。我们将了解以下内容:

  • 保护 Mosquitto 服务器的重要性

  • 生成用于与 Mosquitto 使用 TLS 的私有证书颁发机构

  • 为 Mosquitto 服务器创建证书

  • 在 Mosquitto 中配置 TLS 传输安全性

  • 使用命令行工具测试 MQTT TLS 配置

  • 使用 GUI 工具测试 MQTT TLS 配置

  • 为每个 MQTT 客户端创建证书

  • 在 Mosquitto 中配置 TLS 客户端证书认证

  • 使用命令行工具测试 MQTT TLS 客户端认证

  • 使用 GUI 工具测试 MQTT TLS 配置

  • 强制 TLS 协议版本为特定数字

理解保护 Mosquitto 服务器的重要性

物联网应用程序的安全性是一个非常重要的话题,值得有很多专门的书籍来讨论。每个解决方案都有自己的安全要求,当开发解决方案的每个组件时,考虑所有这些要求非常重要。

如果我们使用 MQTT 发布既不机密也不关键的值,我们唯一关心的可能是控制每个主题的最大订阅者数量,以确保消息始终可用。这样,我们可以防止 MQTT 服务器无法向大量订阅者传递消息。

然而,大多数情况下,我们不会在一个可以无限制地与整个世界共享数据并且不需要关心数据机密性和完整性以及数据可用性的解决方案上工作。想象一下,我们正在开发一个允许用户控制一个巨大的八旋翼无人机的解决方案。如果无人机飞错了方向,我们可能会对真实的人造成伤害。我们不能允许任何未知的发布者能够向允许我们控制八旋翼的主题发送消息。我们必须确保正确的人在控制八旋翼,并且作为消息的一部分发送的命令不能被中间的入侵者更改;也就是说,我们需要数据完整性。

不同级别的安全性都是有代价的;也就是说,总是会有额外的开销。因此,我们应该始终保持平衡,以避免可能使整个解决方案不可行和无法使用的开销。每当我们增加更多的安全性时,我们将需要额外的带宽,并且我们将在客户端和服务器中增加处理开销。我们必须考虑到,一些在现代智能手机上可以无问题运行的加密算法并不适合处理能力受限的物联网板。有时,安全要求可能会迫使我们使用特定的硬件,比如更强大的物联网板。在购买解决方案的所有硬件之前,我们绝对必须考虑安全性。

我们必须考虑的另一件重要事情是,许多安全级别需要维护任务,在某些情况下可能是不可行的,或者在其他情况下可能非常难以实现。例如,如果我们决定为每个将成为 MQTT 服务器客户端的设备使用证书,我们将不得不为每个设备生成和分发证书。我们必须访问设备的文件系统,将新文件复制到其中。如果我们必须使证书无效,就需要为受影响的设备提供新的证书。考虑一种情况,所有设备分布在难以访问的不同位置;我们必须有一种机制来远程访问设备,并能够为其提供新的证书。这项任务还需要安全性,因为我们不希望任何人都能访问设备的文件系统。因此,一旦我们开始分析所有安全要求和可能必要的维护任务,事情可能变得非常复杂。

每个 MQTT 服务器或代理实现都可以提供特定的安全功能。我们将使用 Mosquitto 开箱即用提供的一些功能。特定的安全要求可能会使我们决定使用特定的 MQTT 服务器或代理实现。

当我们使用 Mosquitto 时,我们可以在以下级别实施安全性:

  • 网络:我们可以使用 VPN(虚拟专用网络)在互联网上扩展私有网络。

  • 传输:MQTT 使用 TCP 作为传输协议,因此默认情况下通信不加密。TLS(传输层安全)通常被称为 TLS/SSL,因为SSL(安全套接字层)是其前身。我们可以使用 TLS 来保护和加密 MQTT 客户端和 MQTT 服务器之间的通信。使用 TLS 与 MQTT 有时被称为 MQTTS。TLS 允许我们同时提供隐私和数据完整性。我们可以使用 TLS 客户端证书来提供身份验证。

  • 应用:在这个级别,我们可以利用 MQTT 中包含的功能来提供应用级别的身份验证和授权。我们可以使用ClientId(客户端标识符)来标识每个客户端,并将其与用户名和密码身份验证结合使用。我们可以在这个级别添加额外的安全机制。例如,我们可以加密消息有效载荷和/或添加完整性检查以确保数据完整性。但是,主题仍将是未加密的,因此 TLS 是确保一切都加密的唯一方法。我们可以使用插件来提供更复杂的身份验证和授权机制。我们可以授予或拒绝每个用户的权限,以控制他们可以订阅哪些主题以及他们可以向哪些主题发布消息。

大多数流行的 MQTT 实现都支持 TLS。但是,在选择适合您解决方案的 MQTT 服务器之前,请确保您检查其功能。

我们不会涵盖所有安全主题,因为这将需要一个或多个专门致力于这些主题的整本书。相反,我们将首先专注于传输级别安全中最常用的功能,然后再转向应用级别安全。VPN 的使用超出了本书的全局范围。但是,您必须根据您的特定需求考虑其使用。我们将在示例中使用 Mosquitto,但您可以为任何其他决定使用的 MQTT 服务器遵循许多类似的程序。我们将学到的一切对于任何其他提供与我们将与 Mosquitto 一起使用的相同安全功能支持的 MQTT 服务器都将是有用的。

使用 Mosquitto 生成私有证书颁发机构以使用 TLS

到目前为止,我们一直在使用其默认配置的 Mosquitto 服务器,它在端口1883上监听,并使用纯 TCP 作为传输协议。每个 MQTT 客户端和 MQTT 服务器之间发送的数据都没有加密。订阅者或发布者没有任何限制。如果我们打开防火墙端口并在路由器中重定向端口,或者为运行 MQTT 服务器的基于云的虚拟机配置端口安全性,任何具有 MQTT 服务器的 IP 地址或主机名的 MQTT 客户端都可以发布到任何主题并订阅任何主题。

在我们的示例中第二章中,使用命令行和 GUI 工具学习 MQTT 的工作原理,我们没有对允许连接到端口 1883 的传入连接进行任何更改,因此我们没有将我们的 Mosquitto 服务器开放到互联网。

我们希望在我们的开发环境中使用 TLS 与 MQTT 和 Mosquitto。这样,我们将确保我们可以信任 MQTT 服务器,因为我们相信它是它所说的那样,我们的数据将是私密的,因为它将被加密,它将具有完整性,因为它不会被篡改。如果您有HTTP协议的经验,您会意识到我们所做的转变与我们从使用HTTP转移到HTTPS时所做的转变是一样的。

网站从主要的证书颁发机构购买证书。如果我们想为服务器使用购买的证书,我们就不需要生成自己的证书。事实上,当我们有一个公开的 MQTT 服务器并且转移到生产环境时,这是最方便的选择。

在这种情况下,我们将使用免费的 OpenSSL 实用程序为服务器生成必要的证书,以便在我们的开发环境中启用 TLS 与 Mosquitto。非常重要的是要注意,我们不会生成一个生产就绪的配置,我们专注于一个安全的开发环境,它将模拟一个安全的生产环境。

OpenSSL 已经安装在 macOS 和大多数现代 Linux 发行版中。在 Windows 中,我们已经将 OpenSSL 安装为 Mosquitto 的先决条件之一。使用 OpenSSL 实用程序需要一本完整的书,因此我们将专注于使用最常见的选项生成我们需要的证书。如果您有特定的安全需求,请确保您探索使用 OpenSSL 实现您的目标所需的选项。

具体来说,我们将生成一个使用 X.509 PKI(公钥基础设施的缩写)标准的 X.509 数字证书。这个数字证书允许我们确认特定的公钥属于证书中包含的主体。有一个发行证书的身份,它的详细信息也包含在证书中。

数字证书仅在特定期限内有效,因此我们必须考虑数字证书某一天会过期,我们将不得不提供新的证书来替换过期的证书。根据我们使用的特定 X.509 版本,证书有特定的数据要求。根据版本和我们用来生成证书的选项,我们可能需要提供特定的数据。

我们将运行命令来生成不同的 X.509 数字证书,并提供将包含在证书中的所有必要细节。我们将在创建证书时了解证书将具有的所有数据。

我们将创建我们自己的私有证书颁发机构,也称为 CA。我们将创建一个根证书,然后我们将生成服务器密钥。

检查您安装 OpenSSL 的目录或文件夹。

在 macOS 上,OpenSSL 安装在/usr/bin/openssl中。但是,这是一个旧版本,需要在运行命令之前安装一个新版本。可以使用homebrew软件包管理器安装新版本,并且您将能够在另一个目录中运行新版本。例如,使用homebrew安装的版本 1.0.2n 的路径将在/usr/local/Cellar/openssl/1.0.2n/bin/openssl中。确保您不使用默认的旧版本。

在 Windows 中,我们安装为 Mosquitto 先决条件的 OpenSSL 版本,在第二章中,使用命令行和 GUI 工具学习 MQTT 工作,默认的C:\OpenSSL-Win32\bin文件夹中有openssl.exe可执行文件。如果您使用 Windows,可以使用命令提示符或 Windows PowerShell。

在任何操作系统中,使用下一个以openssl开头的命令中适当的 OpenSSL 版本的完整路径。

创建一个名为mosquitto_certificates的新目录,并更改此目录的必要权限,以确保您只能访问其内容。

在 macOS 或 Linux 中打开终端,或者在 Windows 中打开命令提示符,并转到之前创建的mosquitto_certificates目录。运行以下命令来创建一个 2,048 位的根密钥,并将其保存在ca.key文件中:

openssl genrsa -out ca.key 2048

以下行显示了上一个命令生成的示例输出:

Generating RSA private key, 2048 bit long modulus
......+++
.............+++
e is 65537 (0x010001)

上一个命令将在ca.key文件中生成私有根密钥。确保您保持此文件私密,因为任何拥有此文件的人都将能够生成证书。也可以使用openssl的其他选项来保护此文件的密码。但是,如前所述,我们将遵循使用 TLS 的必要步骤,您可以探索与 OpenSSL 和证书相关的其他选项。

转到 macOS 或 Linux 中的终端,或者在 Windows 中的命令提示符中。运行以下命令以自签名根证书。下一个命令使用先前创建的 2,048 位私钥保存在ca.key文件中,并生成一个带有自签名 X.509 数字证书的ca.crt文件。该命令使自签名证书在3650天内有效。该值在-days选项之后指定:

openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 -out ca.crt

在这种情况下,我们指定了-sha256选项来使用 SHA-256 哈希函数。如果我们想要增加安全性,我们可以在所有使用-sha256的情况下使用-sha512选项。这样,我们将使用 SHA-512 哈希函数。然而,我们必须考虑到 SHA-512 可能不适合某些功耗受限的物联网设备。

在输入上述命令后,OpenSSL 会要求输入将被合并到证书中的信息。您必须输入信息并按Enter。如果您不想输入特定信息,只需输入一个点(.)并按Enter。可以将所有值作为openssl命令的参数传递,但这样做会使我们难以理解我们正在做什么。事实上,也可以使用更少的调用openssl命令来执行前面的任务。但是,我们运行了更多的步骤来理解我们正在做什么。

以下行显示了示例输出和带有示例答案的问题。请记住,我们正在生成我们的私有证书颁发机构:

You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:US
State or Province Name (full name) [Some-State]:NEW YORK CITY
Locality Name (eg, city) []:NEW YORK
Organization Name (eg, company) [Internet Widgits Pty Ltd]:MOSQUITTO CERTIFICATE AUTHORITY
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:MOSQUITTO CERTIFICATE AUTHORITY
Email Address []:mosquittoca@example.com

运行以下命令以显示最近生成的证书颁发机构证书文件的数据和详细信息:

Certificate:
 Data:
 Version: 3 (0x2)
 Serial Number:
 96:f6:f6:36:ad:63:b2:1f
 Signature Algorithm: sha256WithRSAEncryption
 Issuer: C = US, ST = NEW YORK, L = NEW YORK, O = MOSQUITTO 
        CERTIFICATE AUTHORITY, CN = MOSQUITTO CERTIFICATE AUTHORITY, 
        emailAddress = mosquittoca@example.com
 Validity
 Not Before: Mar 22 15:43:23 2018 GMT
 Not After : Mar 19 15:43:23 2028 GMT
 Subject: C = US, ST = NEW YORK, L = NEW YORK, O = MOSQUITTO 
            CERTIFICATE AUTHORITY, CN = MOSQUITTO CERTIFICATE 
            AUTHORITY, emailAddress = mosquittoca@example.com
 Subject Public Key Info:
 Public Key Algorithm: rsaEncryption
 Public-Key: (2048 bit)
 Modulus:
 00:c0:45:aa:43:d4:76:e7:dc:58:9b:19:85:5d:35:
 54:2f:58:61:72:6a:42:81:f9:64:1b:51:18:e1:95:
 ba:50:99:56:c5:9a:c2:fe:07:8e:26:12:47:a6:be:
 8b:ce:23:bf:4e:5a:ea:ab:2e:51:99:0f:23:ea:38:
 68:f3:80:16:5d:5f:51:cf:ce:ee:c9:e9:3a:34:ac:
 ee:24:a6:50:31:59:c5:db:75:b3:33:0e:96:31:23:
 1b:9c:6f:2f:96:1f:6d:cc:5c:4e:20:10:9e:f2:4e:
 a9:f6:31:83:54:11:b6:af:86:0e:e0:af:69:a5:b3:
 f2:5a:b5:da:b6:64:73:87:86:bb:e0:be:b3:10:9f:
 ef:91:8f:e5:68:8c:ab:38:75:8d:e1:33:bc:fb:00:
 d8:d6:d2:d3:6e:e3:a0:3f:08:b6:9e:d6:da:94:ad:
 61:74:90:6c:71:98:88:e8:e1:2b:2d:b1:18:bb:6d:
 b8:65:43:cf:ac:79:ab:a7:a4:3b:65:a8:8a:6f:be:
 c1:66:71:d6:9c:2d:d5:0e:81:13:69:23:65:fa:d3:
 cb:79:e5:75:ea:a2:22:72:c7:e4:f7:5c:be:e7:64:
 9b:54:17:dd:ca:43:7f:93:be:b6:39:20:e7:f1:21:
 0f:a7:e6:24:99:57:9b:02:1b:6d:e4:e5:ee:ad:76:
 2f:69
 Exponent: 65537 (0x10001)
 X509v3 extensions:
 X509v3 Subject Key Identifier:
 F7:C7:9E:9D:D9:F2:9D:38:2F:7C:A6:8F:C5:07:56:57:48:7D:07:35
 X509v3 Authority Key Identifier: keyid:F7:C7:9E:9D:D9:F2:9D:38:2F:7C:A6:8F:C5:07:56:57:48:7D:07:35
 X509v3 Basic Constraints: critical
 CA:TRUE
 Signature Algorithm: sha256WithRSAEncryption
 a2:64:5d:7b:f4:85:81:f7:d0:30:8b:8d:7c:83:83:63:2c:4e:
 a8:56:fb:fc:f0:4f:d4:d8:9c:cd:ac:c7:e9:bc:4b:b5:87:9e:
 02:0b:9f:e0:4b:a3:da:3f:84:b4:1c:e3:42:d4:9f:4e:c0:29:
 f7:ae:18:d3:2d:bf:93:e2:2b:5c:d9:9a:82:53:d8:6a:fb:c8:
 47:9f:02:d4:05:11:e9:8f:2a:54:09:c4:a4:f1:00:eb:35:1d:
 6b:e9:55:3b:4b:a6:27:d0:52:cf:86:c1:03:32:ce:22:41:55:
 32:1e:93:4f:6b:a5:b5:19:9e:8c:a7:de:91:2b:2c:c6:95:a9:
 b6:44:18:e7:40:23:38:87:5d:89:b6:25:d7:32:60:28:0b:41:
 5b:6e:46:20:bf:36:9d:ba:26:6d:63:71:0f:fd:c3:e3:0d:6b:
 b6:84:34:06:ea:67:7c:4e:2e:df:fe:b6:ec:48:f5:7b:b5:06:
 c5:ad:6f:3e:0c:25:2b:a3:9d:49:f7:d4:b7:69:9e:3e:ca:f8:
 65:f2:77:ae:50:63:2b:48:e0:72:93:a7:60:99:b7:40:52:ab:
 6f:00:78:89:ad:92:82:93:e3:30:ab:ac:24:e7:82:7f:51:c7:
 2d:e7:e1:2d:3f:4d:c1:5c:27:15:d9:bc:81:7b:00:a0:75:07:
 99:ee:78:70

在运行上述命令之后,我们将在mqtt_certificates目录中有以下两个文件:

  • ca.key:证书颁发机构密钥

  • ca.crt:证书颁发机构证书文件

证书颁发机构证书文件采用PEM(即隐私增强邮件)格式。我们必须记住这种格式,因为一些 MQTT 工具将要求我们指定证书是否采用 PEM 格式。在此选项中输入错误的值将不允许 MQTT 客户端与使用 PEM 格式证书的 MQTT 服务器建立连接。

为 Mosquitto 服务器创建证书

现在我们有了一个私有证书颁发机构,我们可以为 Mosquitto 服务器创建证书,也就是为将运行 MQTT 服务器的计算机创建证书。

首先,我们必须生成一个新的私钥,该私钥将与我们为自己的私有证书颁发机构生成的私钥不同。

转到 macOS 或 Linux 中的终端,或者 Windows 中的命令提示符。运行以下命令以创建一个 2048 位密钥并将其保存在server.key文件中:

openssl genrsa -out server.key 2048

以下行显示了由上一个命令生成的示例输出:

Generating RSA private key, 2048 bit long modulus
..................................................................................................+++
..............................................................................................................................+++
e is 65537 (0x010001)

上一个命令将在server.key文件中生成私钥。返回到 macOS 或 Linux 中的终端,或者 Windows 中的命令提示符。运行以下命令以生成证书签名请求。下一个命令使用先前创建的 2048 位私钥保存在server.key文件中,并生成server.csr文件:

openssl req -new -key server.key -out server.csr

输入上述命令后,OpenSSL 会要求输入将纳入证书中的信息。您必须输入信息并按Enter。如果您不想输入特定信息,只需输入一个点(.)并按Enter。在这种情况下,最重要的值是通用名称。在此字段中,输入运行 Mosquitto 服务器的计算机的 IPv4 或 IPv6 地址,而不是下一行中显示的192.168.1.1值。以下行显示了示例输出和示例答案的问题。不要忘记输入通用名称的适当值:

You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:US
State or Province Name (full name) [Some-State]:FLORIDA
Locality Name (eg, city) []:ORLANDO
Organization Name (eg, company) [Internet Widgits Pty Ltd]:MQTT 3.1.1 SERVER
Organizational Unit Name (eg, section) []:MQTT
Common Name (e.g. server FQDN or YOUR name) []:192.168.1.1
Email Address []:mosquittoserver@example.com

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:Mosquitto MQTT Server

转到 macOS 或 Linux 中的终端,或者 Windows 中的命令提示符。运行以下命令以签署先前创建的证书签名请求,即server.csr文件。下一个命令还使用了我们之前生成的自签名 X.509 数字证书的证书颁发机构和私钥:ca.crtca.key文件。

该命令生成了一个带有 Mosquitto 服务器签名的 X.509 数字证书的server.crt文件。该命令使签名证书有效期为 3650 天。该值在-days选项之后指定:

openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 3650 -sha256

与我们为证书颁发机构创建自签名 X.509 数字证书时一样,我们还指定了-sha256选项,以便为 Mosquitto 服务器证书使用 SHA-256 哈希函数。如果您想要使用 SHA-512 哈希函数以增加安全性,可以使用-sha512选项代替-sha256

以下行显示了由上一个命令生成的示例输出。在subject之后显示的值将在您的配置中有所不同,因为您在生成证书签名请求时输入了自己的值,这些值保存在server.csr文件中:

Signature ok
subject=C = US, ST = FLORIDA, L = ORLANDO, O = MQTT 3.1.1 SERVER, OU = MQTT, CN = 192.168.1.1, emailAddress = mosquittoserver@example.com
Getting CA Private Key

运行以下命令以显示生成的服务器证书文件的数据和详细信息:

openssl x509 -in server.crt -noout -text

以下行显示了显示有关签名算法、颁发者、有效性、主题和签名算法的详细信息的示例输出:

Certificate:
 Data:
 Version: 1 (0x0)
 Serial Number:
 a1:fa:a7:26:53:da:24:0b
 Signature Algorithm: sha256WithRSAEncryption
 Issuer: C = US, ST = NEW YORK, L = NEW YORK, O = MOSQUITTO     
        CERTIFICATE AUTHORITY, CN = MOSQUITTO CERTIFICATE AUTHORITY, 
        emailAddress = mosquittoca@example.com
 Validity
 Not Before: Mar 22 18:20:01 2018 GMT
 Not After : Mar 19 18:20:01 2028 GMT
 Subject: C = US, ST = FLORIDA, L = ORLANDO, O = MQTT 3.1.1 
        SERVER, OU = MQTT, CN = 192.168.1.1, emailAddress = 
        mosquittoserver@example.com
 Subject Public Key Info:
 Public Key Algorithm: rsaEncryption
 Public-Key: (2048 bit)
 Modulus:
 00:f5:8b:3e:76:0a:ab:65:d2:ee:3e:47:6e:dc:be:
 74:7e:96:5c:93:25:45:54:a4:97:bc:4d:34:3b:ed:
 33:89:39:f4:df:8b:cd:9f:63:fa:4d:d4:01:c8:a5:
 0b:4f:c7:0d:35:a0:9a:20:4f:66:be:0e:4e:f7:1a:
 bc:4a:86:a7:1f:69:30:36:01:2f:93:e6:ff:8f:ca:
 1f:d0:58:fa:37:e0:90:5f:f8:06:7c:2c:1c:c7:21:
 c8:b4:12:d4:b7:b1:4e:5e:6d:41:68:f3:dd:03:33:
 f5:d5:e3:de:37:08:c4:5f:8c:db:21:a2:d7:20:12:
 f2:a4:81:20:3d:e4:d7:af:81:32:82:31:a2:2b:fd:
 02:c2:ee:a0:fa:53:1b:ca:2d:43:b3:7e:b7:b8:12:
 9c:3e:26:66:cd:90:34:ba:aa:6b:ad:e4:eb:0d:15:
 cf:0b:ce:f6:b1:07:1f:7c:33:05:11:4b:57:6c:48:
 0d:f8:e5:f3:d3:f0:88:92:53:ec:3e:04:d7:fc:81:
 75:5e:ef:01:56:f1:66:fe:a4:34:9b:13:8a:b6:5d:
 cc:8f:72:11:0e:9c:c9:65:71:e3:dd:0e:5a:b7:9d:
 8f:18:3e:09:62:52:5f:fa:a5:96:4d:2b:35:23:26:
 ca:74:5d:f9:04:64:f1:f8:f6:f6:7a:d7:31:4c:b7:
 e8:53
 Exponent: 65537 (0x10001)
 Signature Algorithm: sha256WithRSAEncryption
 9c:2f:b5:f9:fa:06:9f:a3:1e:a3:38:94:a7:aa:4c:11:e9:30:
 2e:4b:cf:16:a3:c6:46:ad:e5:3b:d9:43:f0:41:37:62:93:94:
 72:56:1a:dd:27:50:f7:89:2f:4b:56:55:59:d6:da:2e:8f:0a:
 d8:1e:dd:41:0e:1c:36:1b:eb:8d:32:2c:24:ef:58:93:18:e1:
 fc:ce:71:f6:b2:ed:84:5e:06:52:b8:f1:87:f3:13:ca:b9:41:
 3f:a2:1d:a0:52:5d:52:37:6c:2b:8c:28:ab:7f:7d:ed:fc:07:
 9f:60:8b:ad:3d:48:17:95:fe:20:b8:96:87:44:9a:32:b8:9c:
 a8:d7:3c:cf:98:ba:a4:5c:c9:6e:0c:10:ee:45:3a:23:4a:e8:
 34:28:63:c4:8e:6e:1b:d9:a0:1b:e5:cc:33:69:ae:6f:e1:bb:
 99:df:04:fa:c9:bd:8c:c5:c7:e9:a9:fd:f2:dc:2c:b3:a9:7c:
 8a:ef:bf:66:f6:09:01:9a:0e:8f:27:a4:a1:45:f7:90:d2:bb:
 6d:4f:12:46:56:29:85:cd:c8:d6:d7:d3:60:e4:d1:27:a3:88:
 52:41:6a:7d:b2:06:8e:10:ec:ae:b5:7e:58:3e:ae:33:7c:f7:
 3a:21:a6:ae:61:5f:4d:c8:44:86:48:3d:c4:32:f2:db:05:e9:
 c9:f1:0c:be

运行上述命令后,我们将在mqtt_certificates目录中有以下三个文件:

  • server.key:服务器密钥

  • server.csr:服务器证书签名请求

  • server.crt:服务器证书文件

服务器证书文件采用 PEM 格式,证书颁发机构证书文件也是如此。

在 Mosquitto 中配置 TLS 传输安全

现在,我们将配置 Mosquitto 使用 TLS 传输安全,并与不同客户端进行加密通信。请注意,我们尚未为客户端生成证书,因此我们不会使用客户端证书进行身份验证。这样,任何拥有ca.crt文件的客户端都将能够与 Mosquitto 服务器建立通信。

转到 Mosquitto 安装目录,并创建一个名为certificates的新子目录。在 Windows 中,您需要管理员权限才能访问默认安装文件夹。

mqtt_certificates目录中复制以下文件(我们在其中保存了证书颁发机构证书和服务器证书)到我们最近在 Mosquitto 安装目录中创建的certificates子目录:

  • ca.crt

  • server.crt

  • server.key

如果您在 macOS 或 Linux 的终端窗口中运行 Mosquitto 服务器,请按下Ctrl + C来停止它。在 Windows 中,使用Services应用程序停止适当的服务。如果您在 Linux 中运行 Mosquitto 服务器,请运行以下命令停止服务:

sudo service mosquitto stop

转到 Mosquitto 安装目录,并使用您喜欢的文本编辑器打开mosquitto.conf配置文件。默认情况下,此文件的所有行都被注释掉,即以井号(#)开头。每个设置的默认值已指示,并包括适当的注释。这样,我们很容易知道所有默认值。设置按不同部分组织。

在对其进行更改之前,最好先备份现有的mosquitto.conf配置文件。每当我们对mosquitto.conf进行更改时,如果出现问题,能够轻松回滚到先前的配置是一个好主意。

在 macOS 或 Linux 中,在配置文件的末尾添加以下行,并确保将/usr/local/etc/mosquitto/certificates替换为我们在Mosquitto安装文件夹中创建的certificates目录的完整路径:

# MQTT over TLS
listener 8883
cafile /usr/local/etc/mosquitto/certificates/ca.crt
certfile /usr/local/etc/mosquitto/certificates/server.crt
keyfile /usr/local/etc/mosquitto/certificates/server.key

在 Windows 中,在配置文件的末尾添加以下行,并确保将C:\Program Files (x86)\mosquitto\certificates替换为我们在Mosquitto安装文件夹中创建的certificates目录的完整路径。请注意,当您运行文本编辑器打开文件时,您将需要管理员权限;也就是说,您将需要以管理员身份运行文本编辑器:

# MQTT over TLS
listener 8883
cafile C:\Program Files (x86)\mosquitto\certificates\ca.crt
certfile C:\Program Files (x86)\mosquitto\certificates\server.crt
keyfile C:\Program Files (x86)\mosquitto\certificates\server.key

我们为监听器选项指定了8883值,以使 Mosquitto 在 TCP 端口号8883上监听传入的网络连接。此端口是具有 TLS 的 MQTT 的默认端口号。

cafile选项指定提供 PEM 编码证书颁发机构证书文件ca.crt的完整路径。

certfile选项指定提供 PEM 编码服务器证书server.crt的完整路径。

最后,keyfile选项指定提供 PEM 编码服务器密钥文件server.key的完整路径。

保存更改到mosquitto.conf配置文件,并使用我们在上一章中学到的相同机制再次启动 Mosquitto,以在端口8883而不是1883上监听 Mosquitto 服务器。

使用命令行工具测试 MQTT TLS 配置

我们将使用 Mosquitto 中包含的mosquitto_sub命令行实用程序尝试生成一个简单的 MQTT 客户端,该客户端订阅一个主题并打印其接收到的所有消息。我们将使用默认配置,尝试使用默认的1883端口与 Mosquitto 服务器建立通信,而不指定证书颁发机构证书。在 macOS 或 Linux 中打开终端,或在 Windows 中打开命令提示符,转到 Mosquitto 安装的目录,并运行以下命令:

mosquitto_sub -V mqttv311 -t sensors/octocopter01/altitude -d

mosquitto_sub实用程序将显示以下错误。Mosquitto 服务器不再接受端口1883上的任何连接。请注意,错误消息可能因平台而异:

Error: No connection could be made because the target machine actively refused it.

使用-p选项运行以下命令,后跟我们要使用的端口号:8883。这样,我们将尝试连接到端口8883,而不是默认端口1883

mosquitto_sub -V mqttv311 -p 8883 -t sensors/octocopter01/altitude -d

mosquitto_sub实用程序将显示调试消息,指示它正在向 MQTT 服务器发送CONNECT数据包。但是,连接将永远不会建立,因为潜在的 MQTT 客户端未提供所需的证书颁发机构。按下Ctrl + C停止实用程序尝试连接。以下行显示了上一个命令生成的示例输出:

Client mosqsub|14064-LAPTOP-5D sending CONNECT
Client mosqsub|14064-LAPTOP-5D sending CONNECT
Client mosqsub|14064-LAPTOP-5D sending CONNECT
Client mosqsub|14064-LAPTOP-5D sending CONNECT
Client mosqsub|14064-LAPTOP-5D sending CONNECT
Client mosqsub|14064-LAPTOP-5D sending CONNECT

以下命令使用-h选项,后跟 MQTT 服务器主机。在这种情况下,我们指定运行 Mosquitto MQTT 服务器的计算机的 IPv4 地址:192.168.1.1。请注意,此值必须与我们在生成server.csr文件时指定为通用名称字段中的 IPv4 或 IPv6 地址相匹配,即服务器证书签名请求。如果您在通用名称字段中使用主机名作为值,而不是 IPv4 或 IPv6 地址,则必须使用相同的主机名。如果-h选项指定的值与通用名称字段中指示的值不匹配,则 Mosquitto 服务器将拒绝客户端。因此,请确保您在下一行中用适当的值替换192.168.1.1。此外,该命令在--cafile选项之后指定证书颁发机构证书文件,并指示我们要使用端口8883。您只需将ca.crt替换为您在mqtt_certificates目录中创建的ca.crt文件的完整路径。例如,在 Windows 中可能是C:\mqtt_certificates\ca.crt,在 macOS 或 Linux 中可能是/Users/gaston/mqtt_certificates/ca.crtmosquitto_sub实用程序将创建一个与 Mosquitto 建立加密连接的 MQTT 订阅者:

mosquitto_sub -h 192.168.1.1 -V mqttv311 -p 8883 --cafile ca.crt -t sensors/octocopter01/altitude -d

如果您为-h选项指定的值与您在生成server.csr文件时指定的通用名称字段中的值不匹配,则将看到以下错误消息作为上一个命令的结果:

Client mosqsub|14064-LAPTOP-5D sending CONNECT
Error: A TLS error occurred.

如果命令生成了上一个错误消息,请确保查看生成server.csr文件的先前步骤。确保不要将localhost用作-h选项的值。

使用类似的语法,我们将使用 Mosquitto 中包含的mosquitto_pub命令行实用程序生成一个简单的 MQTT 客户端,该客户端将发布消息到一个主题,并使用加密连接。在 macOS 或 Linux 中打开终端,或在 Windows 中打开命令提示符,转到安装 Mosquitto 的目录,并运行以下命令。

请记住在下一行中用适当的值替换192.168.1.1。此外,请用mqtt_certificates目录中创建的ca.crt文件的完整路径替换ca.crt

mosquitto_pub -h 192.168.1.1 -V mqttv311 -p 8883 --cafile ca.crt -t sensors/octocopter01/altitude -m "123 f" -d

在命令发布消息后,您将在使用mosquitto_sub命令订阅sensors/octocopter01/altitude主题的窗口中看到该消息。

使用 GUI 工具测试 MQTT TLS 配置

现在,我们将使用 MQTT.fx GUI 实用程序生成另一个 MQTT 客户端,该客户端使用加密连接将消息发布到相同的主题:sensors/octocopter01/altitude。我们必须更改连接选项以启用 TLS 并指定证书颁发机构证书文件。请按照以下步骤操作:

  1. 启动 MQTT.fx,在左上角的下拉菜单中选择本地 mosquitto,并单击此下拉菜单右侧和连接按钮左侧的配置图标。MQTT.fx 将显示带有不同连接配置文件选项的编辑连接配置文件对话框,名为本地 mosquitto。

  2. 转到经纪人地址文本框,并输入我们在生成server.csr文件时指定为通用名称字段值的 IPv4 或 IPv6 地址,即服务器证书签名请求。如果您在通用名称字段中使用的是主机名而不是 IPv4 或 IPv6 地址,您将需要使用相同的主机名。如果经纪人地址中指定的值与通用名称字段中指示的值不匹配,Mosquitto 服务器将拒绝客户端。

  3. 转到经纪人端口文本框,并输入 8883。

  4. 单击 SSL/TLS 按钮。

  5. 激活启用 SSL/TLS 复选框。

  6. 激活 CA 证书文件单选按钮。

  7. 在 CA 证书文件文本框中输入或选择您在 CA 证书文件夹中创建的ca.crt文件的完整路径,然后单击确定。以下屏幕截图显示了所选选项的对话框:

  1. 单击连接按钮。MQTT.fx 将与本地 Mosquitto 服务器建立加密连接。请注意,连接按钮已禁用,断开按钮已启用,因为客户端已连接到 Mosquitto 服务器。

  2. 点击订阅并在订阅按钮左侧的下拉菜单中输入sensors/octocopter01/altitude。然后,点击订阅按钮。MQTT.fx 将在左侧显示一个新面板,显示我们已订阅的主题。

  3. 单击发布,并在发布按钮左侧的下拉菜单中输入sensors/octocopter01/altitude

  4. 在发布按钮下方的文本框中输入以下文本:250 f

  5. 然后,单击发布按钮。MQTT.fx 将发布输入的文本到指定的主题。

  6. 点击订阅,您将看到已发布的消息。

通过我们对 Mosquitto 服务器所做的配置更改,任何具有证书颁发机构证书文件(即我们生成的ca.crt文件)的客户端都将能够与 Mosquitto 建立连接,订阅和发布主题。 MQTT 客户端和 MQTT 服务器之间发送的数据是加密的。在此配置中,我们不需要 MQTT 客户端提供证书进行身份验证。但是,请不要忘记我们正在为开发环境进行配置。我们不应该在生产 Mosquitto 服务器上使用自签名证书。

还有另一个非常受欢迎的 GUI 实用程序,我们可以使用它来生成可以订阅主题和发布主题的 MQTT 客户端:MQTT-spy。该实用程序是开源的,可以在安装了 Java 8 或更高版本的任何计算机上运行。您可以在此处找到有关 MQTT-spy 的更多信息:github.com/eclipse/paho.mqtt-spy。使用证书颁发机构证书文件与 MQTT 服务器建立连接的选项与我们为 MQTT.fx 分析的选项类似。但是,如果您还想使用此实用程序,最好详细分析这些选项。

现在,我们将使用 MQTT-spy GUI 实用程序生成另一个使用加密连接发布消息到相同主题sensors/octocopter01/altitude的 MQTT 客户端。按照以下步骤:

  1. 启动 MQTT-spy。

  2. 选择连接 | 新连接。连接列表对话框将出现。

  3. 单击连接选项卡,并在协议版本下拉菜单中选择 MQTT 3.1.1。我们希望使用 MQTT 版本 3.1.1。

  4. 转到服务器 URI(s)文本框,并输入我们在生成server.csr文件时指定为通用名称字段值的 IPv4 或 IPv6 地址,即服务器证书签名请求。如果您在通用名称字段中使用的是主机名而不是 IPv4 或 IPv6 地址,您将需要使用相同的主机名。如果经纪人地址中指定的值与通用名称字段中指示的值不匹配,Mosquitto 服务器将拒绝由 MQTT-spy 实用程序生成的客户端。

  5. 点击安全选项卡,在用户认证选项卡下方的 TLS 选项卡中。

  6. 在 TLS/SSL 模式下拉菜单中选择 CA 证书。

  7. 在协议下拉菜单中选择 TLSv1.2。

  8. 输入或选择在mqtt_certificates文件夹中创建的ca.crt文件的完整路径,然后点击打开连接。以下屏幕截图显示了具有所选选项的对话框:

  1. MQTT-spy 将关闭对话框,并显示一个具有绿色背景的新选项卡,连接名称已在连接列表对话框的左侧突出显示并被选中。确保点击新连接的选项卡。

  2. 在主题下拉菜单中输入sensors/octocopter01/altitude

  3. 在数据文本框中输入以下文本:178 f。以下屏幕截图显示了新连接的选项卡以及在不同控件中输入的数据:

  1. 点击发布按钮。MQTT-spy 将向指定主题发布输入的文本,您将能够在 MQTT.fx 订阅者和mosquitto-sub订阅者中看到消息。

为每个 MQTT 客户端创建证书

现在,我们希望要求每个 MQTT 客户端提供有效的证书以建立与 MQTT 服务器的连接。这样,只有拥有有效证书的客户端才能发布或订阅主题。我们将使用先前创建的私有证书颁发机构来为认证创建客户端证书。

我们将为我们的本地计算机生成一个样本证书,该证书将充当客户端。我们可以按照相同的步骤为我们想要连接到 Mosquitto 服务器的其他设备生成额外的证书。我们只需要为文件使用不同的名称,并在相应的选项中使用不同的设备名称。

我们必须使用与生成服务器证书相同的证书颁发机构证书来生成客户端证书。如前所述,对于生产环境,我们不应该使用自签名证书。这个过程对于开发环境是有用的。

首先,我们必须生成一个新的私钥,该私钥将与我们为自己的私有证书颁发机构和服务器证书生成的私钥不同。

转到 macOS 或 Linux 中的终端,或者 Windows 中的命令提示符。运行以下命令以创建一个 2,048 位的密钥,并将其保存在board001.key文件中。要为其他设备重复此过程,请将board001替换为标识将使用该证书的设备的任何其他名称。在所有使用board001的不同文件名和值的以下命令中都要这样做:

openssl genrsa -out board001.key 2048

以下行显示了上一个命令生成的示例输出:

Generating RSA private key, 2048 bit long modulus
..........................................................................................+++
.....................................+++
e is 65537 (0x10001)

上一个命令将在board001.key文件中生成私钥。

返回到 macOS 或 Linux 中的终端,或者 Windows 中的命令提示符。运行以下命令以生成证书签名请求,也称为 CSR。下一个命令使用先前创建的 2,048 位私钥,保存在board001.key文件中,并生成一个board001.csr文件:

openssl req -new -key board001.key -out board001.csr

在输入上一个命令后,OpenSSL 会要求输入将被合并到证书中的信息。您必须输入信息并按Enter。如果您不想输入特定信息,只需输入一个点(.)并按Enter。在这种情况下,最重要的值是通用名称。在此字段中输入设备名称:

You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:US 
State or Province Name (full name) [Some-State]:CALIFORNIA
Locality Name (eg, city) []:SANTA MONICA
Organization Name (eg, company) [Internet Widgits Pty Ltd]:MQTT BOARD 001
Organizational Unit Name (eg, section) []:MQTT BOARD 001
Common Name (e.g. server FQDN or YOUR name) []:MQTT BOARD 001
Email Address []:mttboard001@example.com

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:.
An optional company name []:.

转到 macOS 或 Linux 中的终端,或者转到 Windows 中的命令提示符。运行以下命令以签署先前创建的证书签名请求,即board001.csr文件。下一个命令还使用我们之前生成的自签名 X.509 数字证书用于证书颁发机构和其私钥:ca.crtca.key文件。该命令生成一个带有 MQTT 客户端签名的 X.509 数字证书的board001.crt文件。该命令使签名证书在 3,650 天内有效,这是在-days选项之后指定的值。-addTrust clientAuth选项表示我们要使用证书来验证客户端:

openssl x509 -req -in board001.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out board001.crt -days 3650 -sha256 -addtrust clientAuth

以下行显示了先前命令生成的示例输出。在您的配置中,主题后显示的值将不同,因为在生成保存在board001.csr文件中的证书签名请求时,您输入了自己的值:

Signature ok
subject=/C=US/ST=CALIFORNIA/L=SANTA MONICA/O=MQTT BOARD 001/OU=MQTT BOARD 001/CN=MQTT BOARD 001/emailAddress=mttboard001@example.com
Getting CA Private Key

运行以下命令以显示生成的服务器证书文件的数据和详细信息:

openssl x509 -in board001.crt -noout -text

以下行显示了显示有关签名算法、发行者、有效性和主题的详细信息的示例输出:

Certificate:
 Data:
 Version: 1 (0x0)
 Serial Number:
 dd:34:7a:3c:a6:cd:c1:94
 Signature Algorithm: sha256WithRSAEncryption
 Issuer: C=US, ST=CALIFORNIA, L=SAN FRANCISCO, O=CERTIFICATE 
 AUTHORITY, CN=CERTIFICATE 
 AUTHORITY/emailAddress=CERTIFICATE@EXAMPLE.COM
 Validity
 Not Before: Mar 23 22:10:05 2018 GMT
 Not After : Mar 20 22:10:05 2028 GMT
 Subject: C=US, ST=CALIFORNIA, L=SANTA MONICA, O=MQTT BOARD 001, 
 OU=MQTT BOARD 001, CN=MQTT BOARD 
 001/emailAddress=mttboard001@example.com
 Subject Public Key Info:
 Public Key Algorithm: rsaEncryption
 RSA Public Key: (2048 bit)
 Modulus (2048 bit):
 00:d0:9c:dd:9f:3e:db:3f:15:9c:23:40:12:5f:4e:
 56:2a:30:34:df:88:51:d7:ca:61:bb:99:b5:ab:b4:
 a6:61:e9:f1:ed:2e:c3:61:7a:f2:0b:70:5b:24:7a:
 12:3f:cb:5d:76:f7:10:b2:08:24:94:31:0d:80:35:
 78:2c:19:70:8b:c0:fe:c1:cb:b2:13:5e:9a:d3:68:
 5d:4d:78:47:5a:a3:d5:63:cd:3c:2f:8b:b1:48:4d:
 12:11:0b:02:17:f3:4c:56:91:67:9f:98:3d:90:1f:
 47:09:c0:1b:3a:04:09:2f:b9:fe:f1:e9:df:38:35:
 f8:12:ee:59:96:b1:ca:57:90:53:19:2b:4f:d3:45:
 9e:f2:6a:09:95:46:f9:68:6b:c6:4e:89:33:78:4f:
 0f:5b:2f:d3:00:d0:12:d7:ca:92:df:f4:86:6e:22:
 9d:63:a2:f7:de:09:f4:8c:02:ad:03:9c:13:7b:b4:
 9e:03:d6:99:f4:c0:3f:3f:c3:31:52:12:f1:66:cd:
 22:5d:48:fb:7f:ca:ac:84:cf:24:c5:c4:85:af:61:
 de:59:84:a8:e0:fd:ce:44:5d:f2:85:c0:5d:f2:c5:
 ec:71:04:2c:83:94:cd:71:a1:14:1b:f7:e4:1b:b4:
 2f:12:70:cb:b7:17:9e:db:c9:23:d1:56:bd:f5:02:
 c8:3b
 Exponent: 65537 (0x10001)

 Signature Algorithm: sha256WithRSAEncryption
 55:6a:69:0f:3a:e5:6f:d4:16:0a:4f:67:46:ec:36:ea:a4:54:
 db:04:86:e9:48:ed:0e:83:52:56:75:65:f0:85:34:32:75:0a:
 0a:15:13:73:21:a4:a9:9c:89:b4:73:15:06:2a:b3:e8:ab:7b:
 f4:16:37:17:a9:0e:eb:74:1d:78:c8:df:5e:5f:41:af:53:ca:
 a1:94:d8:d2:f5:87:a5:a9:8a:6a:d1:0e:e0:b7:30:92:d2:94:
 98:65:4c:bf:f9:a7:60:f8:c2:df:7c:4e:28:3c:02:f0:d4:a8:
 f7:16:d5:38:88:43:e4:c4:2e:02:72:ee:4b:6f:cd:2a:d7:3b:
 c4:e8:f4:7d:0e:3b:9b:5b:20:00:69:75:76:ce:79:a1:ed:25:
 f7:f1:3c:96:f8:7d:35:dd:5c:f8:4d:d2:04:32:bb:41:b2:3d:
 1a:5d:f6:63:ff:63:48:ec:85:c2:b3:9c:02:d3:ad:17:59:46:
 3e:10:6f:82:2f:d8:ef:6c:a5:42:3f:55:74:bb:f6:17:59:a0:
 39:e5:16:55:a3:f9:5a:b5:04:c0:61:2a:55:32:56:c2:12:0a:
 2c:c8:8a:23:b1:60:d5:a3:93:f3:a0:e4:e0:a8:98:3b:e1:83:
 ea:43:06:bc:d0:96:0b:c2:0b:95:6b:ce:39:02:7f:19:01:ea:
 47:83:25:c5
 Trusted Uses:
 TLS Web Client Authentication
 No Rejected Uses.

运行上述命令后,我们将在证书目录中有以下三个新文件:

  • board001.key: 客户端密钥

  • board001.csr:客户端证书签名请求

  • board001.crt:客户端证书文件

客户端证书文件以 PEM 格式存储,证书颁发机构证书文件和服务器证书文件也是如此。

我们将不得不向任何要连接到 Mosquitto 服务器的设备提供以下三个文件:

  • ca.crt

  • board001.crt

  • board001.key

永远不要向必须与 MQTT 服务器建立连接的设备提供额外的文件。您不希望设备能够生成额外的证书。您只希望它们使用有效的证书进行身份验证。

openssl实用程序允许我们使用附加的命令行选项为许多参数提供值。因此,可以自动化许多先前的步骤,以便更容易生成多个设备证书。

在 Mosquitto 中配置 TLS 客户端证书认证

现在,我们将配置 Mosquitto 以使用 TLS 客户端证书认证。这样,任何客户端都将需要ca.crt文件和客户端证书,例如最近生成的board001.crt文件,才能与 Mosquitto 服务器建立通信。

如果您在 macOS 或 Linux 的终端窗口中运行 Mosquitto 服务器,请按Ctrl + C停止它。在 Windows 中,请停止适当的服务。

转到 Mosquitto 安装目录并打开mosquitto.conf配置文件。

在 macOS、Linux 或 Windows 中,在配置文件的末尾添加以下行:

require_certificate true

我们为require_certificate选项指定了true值,以使 Mosquitto 要求任何请求与 Mosquitto 建立连接的客户端都需要有效的客户端证书。

保存更改到mosquitto.conf配置文件并重新启动 Mosquitto。我们将使用 Mosquitto 中包含的mosquitto_sub命令行实用程序生成一个简单的 MQTT 客户端,该客户端订阅主题过滤器并打印其接收到的所有消息。

使用命令行工具测试 MQTT TLS 客户端认证

现在,我们将使用 Mosquitto 命令行工具来测试客户端认证配置。

以下命令指定证书颁发机构证书文件、客户端证书和客户端密钥。您必须用证书目录中创建的这些文件的完整路径替换ca.crtboard001.crtboard001.key。但是,最好将这些文件复制到一个新目录,就好像我们正在处理的文件只能供希望与 Mosquitto 建立连接的设备使用。与以前的命令一样,此命令使用-h选项,后面跟着 MQTT 服务器主机。在这种情况下,我们指定运行 Mosquitto MQTT 服务器的计算机的 IPv4 地址:192.168.1.1。请注意,此值必须与我们在生成server.csr文件时指定为值的 IPv4 或 IPv6 地址相匹配,即服务器证书签名请求的Common Name字段。如果您在Common Name字段中使用主机名作为值,而不是 IPv4 或 IPv6 地址,您将不得不使用相同的主机名。mosquitto_sub实用程序将创建一个 MQTT 订阅者,将与 Mosquitto 建立加密连接,并提供客户端证书和客户端密钥以进行身份验证:

mosquitto_sub -h 192.168.1.1 -V mqttv311 -p 8883 --cafile ca.crt --cert board001.crt --key board001.key -t sensors/+/altitude -d

使用类似的语法,我们将使用 Mosquitto 中包含的mosquitto_pub命令行实用程序生成一个简单的 MQTT 客户端,该客户端将向与先前指定的主题过滤器匹配的主题发布消息,使用加密连接和客户端身份验证。在 macOS 或 Linux 中打开终端,或者在 Windows 中打开命令提示符,转到安装 Mosquitto 的目录,并运行以下命令。记得用ca.crtboard001.crtboard001.key替换mqtt_certificates目录中创建的这些文件的完整路径。此外,用我们在生成server.csr文件时指定为值的 IPv4 或 IPv6 地址替换 192.168.1.1,即服务器证书签名请求的Common Name字段。如果您在Common Name字段中使用主机名作为值,而不是 IPv4 或 IPv6 地址,您将不得不使用相同的主机名:

mosquitto_pub -h 192.168.1.1 -V mqttv311 -p 8883 --cafile ca.crt --cert board001.crt --key board001.key -t sensors/quadcopter12/altitude -m "361 f" -d

有时,需要使客户端证书失效。Mosquitto 允许我们指定一个 PEM 编码的证书吊销列表文件。我们必须在 Mosquitto 配置文件的crlfile选项的值中指定此文件的路径。

使用 GUI 工具测试 MQTT TLS 配置

现在,我们将使用 MQTT.fx GUI 实用程序生成另一个 MQTT 客户端,该客户端使用加密连接和 TLS 客户端身份验证来发布消息到与我们用于订阅的主题过滤器匹配的主题sensors/hexacopter25/altitude。我们必须对启用 TLS 时使用的连接选项进行更改。我们必须指定客户端证书和客户端密钥文件。按照以下步骤操作:

  1. 启动 MQTT.fx,并在连接到 Mosquitto MQTT 服务器时单击断开连接。

  2. 在左上角的下拉菜单中选择本地 mosquitto,并单击该下拉菜单右侧和连接按钮左侧的配置图标。MQTT.fx 将显示带有不同连接配置选项的编辑连接配置对话框,名称为本地 mosquitto。

  3. 转到 Broker Address 文本框,并输入我们在生成server.csr文件时指定为值的 IPv4 或 IPv6 地址,即服务器证书签名请求的Common Name字段。如果您在Common Name字段中使用主机名作为值,而不是 IPv4 或 IPv6 地址,您将不得不使用相同的主机名。如果 Broker Address 中指定的值与Common Name字段中指示的值不匹配,Mosquitto 服务器将拒绝客户端。

  4. 单击 SSL/TLS 按钮。

  5. 确保启用 SSL/TLS 复选框已激活。

  6. 激活自签名证书单选按钮。

  7. 在 CA 文件文本框中输入或选择您在mqtt_certificates文件夹中创建的ca.crt文件的完整路径。

  8. 在客户端证书文件文本框中输入或选择您在mqtt_ertificates文件夹中创建的board001.crt文件的完整路径。

  9. 在客户端密钥文件文本框中输入或选择您在mqtt_certificates文件夹中创建的board001.key文件的完整路径。

  10. 确保激活 PEM 格式复选框。以下屏幕截图显示了具有所选选项和不同文本框的示例值的对话框:

  1. 单击确定。然后,单击连接按钮。MQTT.fx 将使用我们指定的证书和密钥文件与本地 Mosquitto 服务器建立加密连接。请注意,连接按钮被禁用,断开按钮被启用,因为客户端已连接到 Mosquitto 服务器。

  2. 单击订阅并在订阅按钮左侧的下拉菜单中输入sensors/+/altitude。然后,单击订阅按钮。MQTT.fx 将在左侧显示一个新面板,其中包含我们已订阅的主题过滤器。

  3. 单击发布并在发布按钮左侧的下拉菜单中输入sensors/hexacopter25/altitude

  4. 在发布按钮下的文本框中输入以下文本:1153 f

  5. 然后,单击发布按钮。MQTT.fx 将向指定的主题发布输入的文本。

  6. 单击订阅,您将看到发布的消息,如下图所示:

现在,我们将使用 MQTT-spy GUI 实用程序生成另一个 MQTT 客户端,该客户端使用加密连接发布消息到另一个与sensors/+/altitude主题过滤器匹配的主题:sensors/quadcopter500/altitude。按照以下步骤:

  1. 启动 MQTT-spy。

  2. 如果您已经在运行 MQTT-spy 或保存了以前的设置,请选择连接|新连接或连接|管理连接。连接列表对话框将出现。

  3. 单击连接选项卡,确保在协议版本下拉菜单中选择了 MQTT 3.1.1。

  4. 转到服务器 URI(s)文本框,并输入我们在生成server.csr文件时指定为值的 IPv4 或 IPv6 地址,即通用名称字段。如果您在通用名称字段中使用主机名作为值,而不是 IPv4 或 IPv6 地址,则必须使用相同的主机名。如果在经纪人地址中指定的值与通用名称字段中指示的值不匹配,则 Mosquitto 服务器将拒绝 MQTT-spy 实用程序生成的客户端。

  5. 单击安全选项卡,然后单击用户 auth.选项卡下方的 TLS 选项卡。

  6. 在 TLS/SSL 模式下拉菜单中选择 CA 证书和客户端证书/密钥。

  7. 在协议下拉菜单中选择 TLSv1.2。

  8. 在 CA 证书文件文本框中输入或选择您在mqtt_certificates文件夹中创建的ca.crt文件的完整路径。

  9. 在客户端证书文件文本框中输入或选择您在mqtt_ertificates文件夹中创建的board001.crt文件的完整路径。

  10. 在客户端密钥文件文本框中输入或选择您在mqtt_certificates文件夹中创建的board001.key文件的完整路径。

  11. 激活 PEM 格式中的客户端密钥复选框。最后,单击打开连接或关闭并重新打开现有连接。以下屏幕截图显示了具有所选选项和文本框示例值的对话框:

  1. MQTT-spy 将关闭对话框,并显示一个具有绿色背景和在连接列表对话框中出现的连接名称的新选项卡。确保单击新连接的选项卡。

  2. 在主题下拉菜单中输入sensors/quadcopter500/altitude

  3. 在“数据”文本框中输入以下文本:1417 f

  4. 单击“发布”按钮。 MQTT-spy 将输入的文本发布到指定的主题,您将能够在 MQTT.fx 订阅者和mosquitto-sub订阅者中看到消息。

任何安全配置都会发生的情况,如果根据先前的说明未激活任何复选框,MQTT 客户端将无法与 Mosquitto 建立连接。请记住证书使用 PEM 格式。

将 TLS 协议版本强制为特定数字

使用最高可能的 TLS 协议版本是一个好习惯。默认情况下,Mosquitto 服务器接受 TLS 1.0、1.1 和 1.2。如果所有客户端都能够使用 Mosquitto 支持的最高 TLS 协议版本,我们应该强制 Mosquitto 仅使用最高版本。这样,我们可以确保不会受到先前 TLS 版本的攻击。

现在,我们将在配置文件中进行必要的更改以强制使用 TLS 1.2。如果您在 macOS 或 Linux 的终端窗口中运行 Mosquitto 服务器,请按*Ctrl *+ C停止它。在 Windows 中,停止适当的服务。

转到 Mosquitto 安装目录并打开mosquitto.conf配置文件。

在 macOS、Linux 或 Windows 中,在配置文件的末尾添加以下行:

tls_version tlsv1.2

我们为tls_version选项指定了tlsv1.2值,以便 Mosquitto 仅使用 TLS 1.2。任何使用先前 TLS 版本的客户端将无法与 Mosquitto 服务器建立连接。

保存更改到mosquitto.conf配置文件并重新启动 Mosquitto。我们在 MQTT.fx 和 MQTT-spy GUI 实用程序中配置连接时指定了 TLS 版本;具体来说,我们为客户端指定了 TLS 1.2 作为期望的 TLS 版本,因此不需要额外的更改。我们必须在mosquitto_submosquitto_pub命令行实用程序中使用--tls-version tlsv1.2选项。

在 macOS 或 Linux 中打开终端,或在 Windows 中打开命令提示符,转到 Mosquitto 安装的目录,并运行以下命令。记得使用ca.crtdevice.001device.key文件的完整路径。此外,将192.168.1.1替换为我们在生成server.csr文件时指定为“通用名称”字段值的 IPv4 或 IPv6 地址,即服务器证书签名请求。如果您在“通用名称”字段中使用主机名而不是 IPv4 或 IPv6 地址作为值,则必须使用相同的主机名:

mosquitto_pub -h 192.168.1.1 --tls-version tlsv1.2 -V mqttv311 -p 8883 --cafile ca.crt -t sensors/octocopter01/altitude -m "1025 f" -d

上一个命令指定了使用 TLS 1.2,因此 MQTT 客户端可以与 Mosquitto 服务器建立连接并发布消息。如果我们指定不同的 TLS 版本,mosquitto_pub命令将无法连接到 Mosquitto 服务器。

测试您的知识

让我们看看您是否能正确回答以下问题:

  1. MQTT 上 TLS 的默认端口号是多少:

  2. 1883

  3. 5883

  4. 8883

  5. 以下哪个实用程序允许我们生成 X.509 数字证书:

  6. OpenX509

  7. TLS4Devs

  8. OpenSSL

  9. 当我们使用 MQTT 上的 TLS 时:

  10. 与没有 TLS 的 MQTT 上 TCP 相比,存在带宽和处理开销

  11. 与没有 TLS 的 MQTT 上 TCP 相比,只有一点带宽开销,但没有处理开销

  12. 与没有 TLS 的 MQTT 上 TCP 相比,没有开销

  13. 以下哪项可以用来保护和加密 MQTT 客户端和 MQTT 服务器之间的通信:

  14. TCPS

  15. TLS

  16. HTTPS

  17. 如果我们在 Mosquitto 配置文件(mosquitto.conf)的require_certificate选项中指定true作为值:

  18. 想要连接到 MQTT 服务器的客户端将需要一个客户端证书

  19. 想要连接到 MQTT 服务器的客户端不需要客户端证书

  20. 想要连接到 MQTT 服务器的客户端可以提供一个可选的客户端证书

正确答案包括在附录中的Solutions中。

总结

在本章中,我们生成了一个私有证书颁发机构、一个服务器证书和客户端证书,以实现 Mosquitto 的 TLS 传输安全和 TLS 客户端认证。MQTT 客户端和 MQTT 服务器之间的通信是加密的。

我们与 OpenSSL 合作为我们的开发环境生成自签名数字证书。我们使用 MQTT.fx,MQTT-spy 和 Mosquitto 命令行工具测试了 MQTT TLS 配置。我们强制 Mosquitto 仅使用特定的 TLS 版本。

与 MQTT 服务器和 Mosquitto 相关的许多其他安全主题。我们将在接下来的章节中处理其中一些,届时我们将开发将使用 Python 与 MQTT 的应用程序。

现在我们了解了如何加密 MQTT 客户端和 Mosquitto 服务器之间的通信,我们将了解 MQTT 库,并编写 Python 代码来通过加密连接传递 MQTT 消息来控制车辆,这是我们将在第四章中讨论的主题,使用 Python 和 MQTT 消息编写控制车辆的代码

第四章:使用 Python 和 MQTT 消息编写控制车辆的代码

在本章中,我们将编写 Python 3.x 代码,以通过加密连接(TLS 1.2)传递 MQTT 消息来控制车辆。我们将编写能够在不同流行的物联网平台上运行的代码,例如 Raspberry Pi 3 板。我们将了解如何利用我们对 MQTT 协议的了解来构建基于要求的解决方案。我们将学习如何使用最新版本的 Eclipse Paho MQTT Python 客户端库。我们将深入研究以下内容:

  • 理解使用 MQTT 控制车辆的要求

  • 定义主题和命令

  • 学习使用 Python 的好处

  • 使用 Python 3.x 和 PEP 405 创建虚拟环境

  • 理解虚拟环境的目录结构

  • 激活虚拟环境

  • 停用虚拟环境

  • 为 Python 安装 paho-mqtt

  • 使用 paho-mqtt 将客户端连接到安全的 MQTT 服务器

  • 理解回调

  • 使用 Python 订阅主题

  • 为将作为客户端工作的物联网板配置证书

  • 创建代表车辆的类

  • 在 Python 中接收消息

  • 使用多次调用循环方法

理解使用 MQTT 控制车辆的要求

在前三章中,我们详细了解了 MQTT 的工作原理。我们了解了如何在 MQTT 客户端和 MQTT 服务器之间建立连接。我们了解了当我们订阅主题过滤器时以及当发布者向特定主题发送消息时会发生什么。我们安装了 Mosquitto 服务器,然后对其进行了安全设置。

现在,我们将使用 Python 作为我们的主要编程语言,生成将充当发布者和订阅者的 MQTT 客户端。我们将连接 Python MQTT 客户端到 MQTT 服务器,并处理命令以通过 MQTT 消息控制小型车辆。这辆小车复制了现实道路车辆中发现的许多功能。

我们将使用 TLS 加密和 TLS 认证,因为我们不希望任何 MQTT 客户端能够向我们的车辆发送命令。我们希望我们的 Python 3.x 代码能够在许多平台上运行,因为我们将使用相同的代码库来控制使用以下物联网板的车辆:

  • Raspberry Pi 3 Model B+

  • 高通龙板 410c

  • BeagleBone Black

  • MinnowBoard Turbot Quad-Core

  • LattePanda 2G

  • UP Core 4GB

  • UP Squared

根据平台的不同,每辆车将提供额外的功能,因为一些板比其他板更强大。但是,我们将专注于基本功能,以保持我们的示例简单,并集中在 MQTT 上。然后,我们将能够将此项目用作其他需要我们在运行 Python 3.x 代码的物联网板上运行代码,连接到 MQTT 服务器并处理命令的解决方案的基线。

驱动车辆的板上运行的代码必须能够处理在特定主题的消息中接收到的命令。我们将在有效载荷中使用 JSON 字符串。

另外,还必须使用 Python 编写的客户端应用程序能够控制一个或多个车辆。我们还将使用 Python 编写客户端应用程序,并且它将向每辆车的主题发布带有 JSON 字符串的 MQTT 消息。客户端应用程序必顶要显示执行每个命令的结果。每辆车在成功执行命令时必须向特定主题发布消息。

定义主题和命令

我们将使用以下主题名称发布车辆的命令:vehicles/vehiclename/commands,其中vehiclename必须替换为分配给车辆的唯一名称。例如,如果我们将vehiclepi01分配为由 Raspberry Pi 3 Model B+板驱动的车辆的名称,我们将不得不向vehicles/vehiclepi01/commands主题发布命令。在该板上运行的 Python 代码将订阅此主题,以接收带有命令的消息并对其做出反应。

我们将使用以下主题名称使车辆发布有关成功执行命令的详细信息:vehicles/vehiclename/executedcommands,其中vehiclename必须替换为分配给车辆的唯一名称。例如,如果我们将vehiclebeagle03分配为由 BeagleBone Black 板提供动力的车辆的名称,那么想要接收有关成功处理命令的信息的客户端必须订阅vehicles/vehiclebeagle03/executedcommands主题。

命令将以 JSON 字符串的形式发送,其中包含键值对。键必须等于 CMD,值必须指定以下任何有效命令之一。当命令需要额外参数时,参数名称必须包含在下一个键中,而此参数的值必须包含在此键的值中:

  • 启动车辆的发动机。

  • 关闭车辆的发动机。

  • 锁上车门。

  • 解锁并打开车门。

  • 停车:停车。

  • 在为车辆配置的安全位置停车。

  • 打开车辆的前灯。

  • 关闭车辆的前灯。

  • 打开车辆的停车灯,也称为侧灯。

  • 关闭车辆的停车灯,也称为侧灯。

  • 加速:加速车辆,即踩油门。

  • 刹车:刹车车辆,即踩刹车踏板。

  • 向右旋转:使车辆向右旋转。我们必须在 DEGREES 键的值中指定我们希望车辆向右旋转多少度。

  • 向左旋转:使车辆向左旋转。我们必须在 DEGREES 键的值中指定我们希望车辆向左旋转多少度。

  • 设置我们允许车辆的最高速度。我们必须在 MPH 键的值中指定所需的最高速度(以每小时英里为单位)。

  • 设置我们允许车辆的最低速度。我们必须在 MPH 键的值中指定所需的最低速度(以每小时英里为单位)。

以下一行显示了将车辆的发动机打开的命令的有效负载示例:

{"CMD": "TURN_ON_ENGINE"}

以下一行显示了将车辆的最高速度设置为每小时五英里的命令的有效负载示例:

{"CMD": "SET_MAX_SPEED", "MPH": 5}

我们已经准备好开始使用 Python 编码所需的所有细节。

使用 Python 3.6.x 和 PEP 405 创建虚拟环境

在接下来的章节中,我们将编写不同的 Python 代码片段,这些代码片段将订阅主题,并且还将向主题发布消息。每当我们想要隔离需要额外软件包的环境时,最好使用 Python 虚拟环境。Python 3.3 引入了轻量级虚拟环境,并在 Python 3.4 中进行了改进。我们将使用这些虚拟环境,因此,您需要 Python 3.4 或更高版本。您可以在此处阅读有关 PEP 405 Python 虚拟环境的更多信息,该文档介绍了 venv 模块:www.python.org/dev/peps/pep-0405

本书的所有示例都在 macOS 和 Linux 上的 Python 3.6.2 上进行了测试。这些示例还在本书中提到的物联网板上进行了测试,以及它们最流行的操作系统。例如,所有示例都在 Raspbian 上进行了测试。 Raspbian 基于 Debian Linux,因此,所有 Linux 的说明都适用于 Raspbian。

如果您决定使用流行的virtualenvpypi.python.org/pypi/virtualenv)第三方虚拟环境构建器或您的 Python IDE 提供的虚拟环境选项,您只需确保在必要时激活您的虚拟环境,而不是按照使用 Python 中集成的venv模块生成的虚拟环境的步骤来激活它。

我们使用venv创建的每个虚拟环境都是一个隔离的环境,并且它将在其站点目录(文件夹)中具有其自己独立安装的 Python 软件包集。在 Python 3.4 及更高版本中,使用venv创建虚拟环境时,pip已包含在新的虚拟环境中。在 Python 3.3 中,需要在创建虚拟环境后手动安装pip。请注意,所提供的说明与 Python 3.4 或更高版本兼容,包括 Python 3.6.x。以下命令假定您在 Linux、macOS 或 Windows 上已安装了 Python 3.5.x 或更高版本。

首先,我们必须选择我们轻量级虚拟环境的目标文件夹或目录。以下是我们在 Linux 和 macOS 示例中将使用的路径。虚拟环境的目标文件夹将是我们的主目录中的HillarMQTT/01文件夹。例如,如果我们在 macOS 或 Linux 中的主目录是/Users/gaston,则虚拟环境将在/Users/gaston/HillarMQTT/01中创建。您可以在每个命令中用您想要的路径替换指定的路径:

~/HillarMQTT/01

以下是我们在 Windows 示例中将使用的路径。虚拟环境的目标文件夹将是用户个人资料文件夹中的HillarMQTT\01文件夹。例如,如果我们的用户个人资料文件夹是C:\Users\gaston,则虚拟环境将在C:\Users\gaston\HillarMQTT\01中创建。您可以在每个命令中用您想要的路径替换指定的路径:

%USERPROFILE%\HillarMQTT\01

在 Windows PowerShell 中,上一个路径将是:

$env:userprofile\HillarMQTT\01

现在,我们必须使用-m选项,后跟venv模块名称和所需的路径,使 Python 运行此模块作为脚本,并在指定的路径中创建虚拟环境。根据我们创建虚拟环境的平台,指令是不同的。

在 Linux 或 macOS 中打开终端并执行以下命令创建虚拟环境:

python3 -m venv ~/HillarMQTT/01

在 Windows 的命令提示符中,执行以下命令创建虚拟环境:

python -m venv %USERPROFILE%\HillarMQTT\01

如果要在 Windows PowerShell 中工作,请执行以下命令创建虚拟环境:

python -m venv $env:userprofile\HillarMQTT\01

上述任何命令都不会产生任何输出。脚本通过调用ensurepip安装了pip,因为我们没有指定--without-pip选项。

了解虚拟环境的目录结构

指定的目标文件夹具有一个新的目录树,其中包含 Python 可执行文件和其他文件,表明它是一个 PEP405 虚拟环境。

在虚拟环境的根目录中,pyenv.cfg配置文件指定了虚拟环境的不同选项,其存在表明我们处于虚拟环境的根文件夹中。在 Linux 和 macOS 中,该文件夹将具有以下主要子文件夹:binincludeliblib/python3.6lib/python3.6/site-packages。请注意,文件夹名称可能根据具体的 Python 版本而有所不同。在 Windows 中,该文件夹将具有以下主要子文件夹:IncludeLibLib\site-packagesScripts。每个平台上的虚拟环境的目录树与这些平台上 Python 安装的布局相同。

以下屏幕截图显示了在 macOS 和 Linux 平台上为01虚拟环境生成的目录树中的文件夹和文件:

下面的屏幕截图显示了在 Windows 为虚拟环境生成的目录树中的主要文件夹:

激活虚拟环境后,我们将在虚拟环境中安装第三方软件包,模块将位于lib/python3.6/site-packagesLib\site-packages文件夹中,根据平台和特定的 Python 版本。可执行文件将被复制到binScripts文件夹中,根据平台而定。我们安装的软件包不会对其他虚拟环境或我们的基本 Python 环境进行更改。

激活虚拟环境

现在我们已经创建了一个虚拟环境,我们将运行一个特定于平台的脚本来激活它。激活虚拟环境后,我们将安装软件包,这些软件包只能在此虚拟环境中使用。这样,我们将使用一个隔离的环境,在这个环境中,我们安装的所有软件包都不会影响我们的主 Python 环境。

在 Linux 或 macOS 的终端中运行以下命令。请注意,如果您在终端会话中没有启动与默认 shell 不同的其他 shell,此命令的结果将是准确的。如果您有疑问,请检查您的终端配置和首选项:

echo $SHELL

该命令将显示您在终端中使用的 shell 的名称。在 macOS 中,默认值为/bin/bash,这意味着您正在使用 bash shell。根据 shell 的不同,您必须在 Linux 或 macOS 中运行不同的命令来激活虚拟环境。

如果您的终端配置为在 Linux 或 macOS 中使用 bash shell,请运行以下命令来激活虚拟环境。该命令也适用于zsh shell:

source ~/HillarMQTT/01/bin/activate

如果您的终端配置为使用cshtcsh shell,请运行以下命令来激活虚拟环境:

source ~/HillarMQTT/01/bin/activate.csh

如果您的终端配置为使用fish shell,请运行以下命令来激活虚拟环境:

source ~/HillarMQTT/01/bin/activate.fish

激活虚拟环境后,命令提示符将显示虚拟环境根文件夹名称括在括号中作为默认提示的前缀,以提醒我们正在虚拟环境中工作。在这种情况下,我们将看到(01)作为命令提示符的前缀,因为激活的虚拟环境的根文件夹是01

下面的屏幕截图显示在 macOS High Sierra 终端中使用bash shell 激活的虚拟环境,在执行先前显示的命令后:

从先前的屏幕截图中可以看出,在激活虚拟环境后,提示从Gastons-MacBook-Pro:~ gaston$变为(01) Gastons-MacBook-Pro:~ gaston$

在 Windows 中,您可以在命令提示符中运行批处理文件或 Windows PowerShell 脚本来激活虚拟环境。

如果您喜欢使用命令提示符,请在 Windows 命令行中运行以下命令来激活虚拟环境:

%USERPROFILE%\HillarMQTT\01\Scripts\activate.bat

下面的屏幕截图显示在 Windows 10 命令提示符中激活的虚拟环境,在执行先前显示的命令后:

从先前的屏幕截图中可以看出,在激活虚拟环境后,提示从C:\Users\gaston变为(01) C:\Users\gaston

如果您喜欢使用 Windows PowerShell,请启动它并运行以下命令来激活虚拟环境。请注意,您必须在 Windows PowerShell 中启用脚本执行才能运行该脚本:

cd $env:USERPROFILE
.\HillarMQTT\01\Scripts\Activate.ps1

如果您收到类似以下错误的错误,这意味着您没有启用脚本执行:

C:\Users\gaston\HillarMQTT\01\Scripts\Activate.ps1 : File C:\Users\gaston\HillarMQTT\01\Scripts\Activate.ps1 cannot be loaded because running scripts is disabled on this system. For more information, see about_Execution_Policies at
http://go.microsoft.com/fwlink/?LinkID=135170.
At line:1 char:1
+ C:\Users\gaston\HillarMQTT\01\Scripts\Activate.ps1
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 + CategoryInfo : SecurityError: (:) [], PSSecurityException
 + FullyQualifiedErrorId : UnauthorizedAccess

Windows PowerShell 的默认执行策略是Restricted。此策略允许执行单个命令,但不运行脚本。因此,如果要使用 Windows PowerShell,必须更改策略以允许执行脚本。非常重要的是确保您了解允许运行未签名脚本的 Windows PowerShell 执行策略的风险。有关不同策略的更多信息,请查看以下网页:docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_execution_policies?view=powershell-6

以下屏幕截图显示了在 Windows 10 PowerShell 中激活的虚拟环境,执行了先前显示的命令:

取消激活虚拟环境

使用先前解释的过程生成的虚拟环境非常容易取消激活。取消激活将删除环境变量中的所有更改,并将提示更改回其默认消息。取消激活虚拟环境后,您将返回到默认的 Python 环境。

在 macOS 或 Linux 中,只需键入deactivate并按Enter

在命令提示符中,您必须运行Scripts文件夹中包含的deactivate.bat批处理文件。在我们的示例中,此文件的完整路径为%USERPROFILE%\HillarMQTT\01\Scripts\deactivate.bat

在 Windows PowerShell 中,您必须在Scripts文件夹中运行Deactivate.ps1脚本。在我们的示例中,此文件的完整路径为$env:userprofile\HillarMQTT\01\Scripts\Deactivate.ps1。请记住,必须在 Windows PowerShell 中启用脚本执行,才能运行该脚本。

下一节的说明假定我们创建的虚拟环境已激活。

安装 Python 的 paho-mqtt

Eclipse Paho 项目提供了 MQTT 的开源客户端实现。该项目包括 Python 客户端,也称为 Paho Python 客户端或 Eclipse Paho MQTT Python 客户端库。此 Python 客户端是从 Mosquitto 项目贡献的,最初被称为 Mosquitto Python 客户端。以下是 Eclipse Paho 项目的网页:www.eclipse.org/paho。以下是 Eclipse Paho MQTT Python 客户端库版本 1.3.1 的网页,即paho-mqtt模块版本 1.3.1:pypi.python.org/pypi/paho-mqtt/1.3.1

我们可以在许多支持 Python 3.x 或更高版本的现代物联网板上使用paho-mqtt。我们只需要确保安装了pip,以便更容易安装paho-mqtt。您可以使用开发计算机来运行示例,也可以使用前面提到的任何一个物联网板。

确保在继续下一步之前,我们在上一步中创建的虚拟环境已激活。

如果要使用物联网板运行示例,请确保在 SSH 终端或运行在板子上的终端窗口中运行所有命令。如果使用开发计算机,请在 macOS 或 Linux 中的终端或 Windows 中的命令提示符中运行命令。

现在,我们将使用pip安装程序安装paho-mqtt 1.3.1。我们只需要在 SSH 终端或我们与板子一起使用的本地终端窗口中运行以下命令,或者在用于安装软件包的计算机上运行:

pip install paho-mqtt==1.3.1

一些物联网板具有需要您在运行上述命令之前安装pip的操作系统。在带有 Raspbian 的 Raspberry Pi 3 板上,pip已经安装。如果您使用计算机,则 Python 安装通常包括pip

如果您在 Windows 的默认文件夹中安装了 Python,并且没有使用 Python 虚拟环境,您将不得不在管理员命令提示符中运行上一个命令。如果您在 Raspbian 中没有使用 Python 虚拟环境,您将不得不在前面加上sudo前缀运行上一个命令:sudo pip install paho-mqtt。然而,如前所述,强烈建议使用虚拟环境。

输出的最后几行将指示paho-mqtt包版本 1.3.1 已成功安装。输出将类似于以下行,但不完全相同,因为它将根据您运行命令的平台而变化:

Collecting paho-mqtt==1.3.1
 Downloading paho-mqtt-1.3.1.tar.gz (80kB)
 100% |################################| 81kB 1.2MB/s 
Installing collected packages: paho-mqtt
 Running setup.py install for paho-mqtt ... done
Successfully installed paho-mqtt-1.3.1

使用 paho-mqtt 将客户端连接到安全的 MQTT 服务器

首先,我们将使用paho-mqtt创建一个连接到 Mosquitto MQTT 服务器的 MQTT 客户端。我们将编写几行 Python 代码来建立一个安全连接并订阅一个主题。

在第三章中,保护 MQTT 3.1.1 Mosquitto 服务器,我们保护了我们的 Mosquitto 服务器,因此,我们将使用我们创建的数字证书来对客户端进行身份验证。大多数情况下,我们将使用 TLS 的 MQTT 服务器,因此,学习如何建立 TLS 和 TLS 身份验证连接是一个好主意。建立与 MQTT 服务器的非安全连接更容易,但在开发与 MQTT 配合工作的应用程序时,这不会是我们面临的最常见情况。

首先,我们需要复制以下文件,这些文件是我们在第三章中创建的,保护 MQTT 3.1.1 Mosquitto 服务器,到计算机或设备上的目录,我们将用它来运行 Python 脚本。我们将文件保存在一个名为mqtt_certificates的目录中。在您将用作 MQTT 客户端的计算机或板上创建一个名为board_certificates的新目录。将以下三个文件复制到这个新目录中:

  • ca.crt:证书颁发机构证书文件

  • board001.crt:客户端证书文件

  • board001.key:客户端密钥

现在,我们将在主虚拟环境文件夹中创建一个名为config.py的新的 Python 文件。以下几行显示了该文件的代码,该代码定义了许多配置值,这些值将用于与 Mosquitto MQTT 服务器建立连接。这样,所有配置值都包含在一个特定的 Python 脚本中。您必须将certificates_path字符串中的/Users/gaston/board_certificates值替换为您创建的board_certificates目录的路径。此外,用 Mosquitto 服务器或任何其他您决定使用的 MQTT 服务器的 IP 地址或主机名替换mqtt_server_host的值。示例的代码文件包含在mqtt_python_gaston_hillar_04_01文件夹中的config.py文件中:

import os.path

# Replace /Users/gaston/python_certificates with the path
# in which you saved the certificate authority file,
# the client certificate file and the client key
certificates_path = "/Users/gaston/python_certificates"
ca_certificate = os.path.join(certificates_path, "ca.crt")
client_certificate = os.path.join(certificates_path, "board001.crt")
client_key = os.path.join(certificates_path, "board001.key")
# Replace 192.168.1.101 with the IP or hostname for the Mosquitto
# or other MQTT server
# Make sure the IP or hostname matches the value 
# you used for Common Name
mqtt_server_host = "192.168.1.101"
mqtt_server_port = 8883
mqtt_keepalive = 60

该代码声明了certificates_path变量,该变量初始化为一个字符串,指定了您保存证书颁发机构文件、客户端证书文件和客户端密钥(ca.crtboard001.crtboard001.key)的路径。然后,该代码声明了以下字符串变量,这些变量包含了我们需要配置 TLS 和 TLS 客户端身份验证的证书和密钥文件的完整路径:ca_certificateclient_certificateclient_key

调用os.path.join使得将certificates_path变量中指定的路径与文件名连接并生成完整路径变得容易。os.path.join函数适用于任何平台,因此我们不必担心是使用斜杠(/)还是反斜杠(\)来将路径与文件名连接。有时,我们可以在 Windows 中开发和测试,然后在可以使用不同 Unix 或 Linux 版本的 IoT 板上运行代码,例如 Raspbian 或 Ubuntu。在我们在不同平台之间切换的情况下,使用os.path.join使得我们的工作更加容易。

mqtt_server_hostmqtt_server_portmqtt_keepalive变量指定了 MQTT 服务器(Mosquitto 服务器)的 IP 地址(192.168.1.101),我们要使用的端口(8883),以及保持连接的秒数。非常重要的是要用 MQTT 服务器的 IP 地址替换192.168.1.101。我们将mqtt_server_port指定为8883,因为我们使用 TLS,这是 MQTT over TLS 的默认端口,正如我们在第三章中学到的,Securing an MQTT 3.1.1 Mosquitto Server

现在,我们将在主虚拟环境文件夹中创建一个名为subscribe_with_paho.py的新 Python 文件。以下行显示了该文件的代码,该代码与我们的 Mosquitto MQTT 服务器建立连接,订阅vehicles/vehiclepi01/tests主题过滤器,并打印出订阅主题过滤器中接收到的所有消息。示例的代码文件包含在mqtt_python_gaston_hillar_04_01文件夹中的subscribe_with_paho.py文件中。

from config import *
import paho.mqtt.client as mqtt

def on_connect(client, userdata, flags, rc):
    print("Result from connect: {}".format(
        mqtt.connack_string(rc)))
    # Subscribe to the vehicles/vehiclepi01/tests topic filter
    client.subscribe("vehicles/vehiclepi01/tests", qos=2)

def on_subscribe(client, userdata, mid, granted_qos):
    print("I've subscribed with QoS: {}".format(
        granted_qos[0]))

def on_message(client, userdata, msg):
    print("Message received. Topic: {}. Payload: {}".format(
        msg.topic, 
        str(msg.payload)))

if __name__ == "__main__":
    client = mqtt.Client(protocol=mqtt.MQTTv311)
    client.on_connect = on_connect
    client.on_subscribe = on_subscribe
    client.on_message = on_message
    client.tls_set(ca_certs = ca_certificate,
        certfile=client_certificate,
        keyfile=client_key)
    client.connect(host=mqtt_server_host,
        port=mqtt_server_port,
        keepalive=mqtt_keepalive)
    client.loop_forever()

请注意,该代码与paho-mqtt版本 1.3.1 兼容。早期版本的paho-mqtt与该代码不兼容。因此,请确保按照先前解释的步骤安装paho-mqtt版本 1.3.1。

理解回调

前面的代码使用了最近安装的paho-mqtt版本 1.3.1 模块与 MQTT 服务器建立加密连接,订阅vehicles/vehiclepi01/tests主题过滤器,并在接收到主题中的消息时运行代码。我们将使用这段代码来了解paho-mqtt的基础知识。该代码是一个非常简单的 MQTT 客户端版本,订阅了一个主题过滤器,我们将在接下来的部分中对其进行改进。

第一行导入了我们在先前编写的config.py文件中声明的变量。第二行将paho.mqtt.client导入为mqtt。这样,每当我们使用mqtt别名时,我们将引用paho.mqtt.client

当我们声明一个函数时,我们将此函数作为参数传递给另一个函数或方法,或者将此函数分配给一个属性,然后一些代码在某个时候调用此函数;这种机制被称为回调。之所以称之为回调,是因为代码在某个时候回调函数。paho-mqtt版本 1.3.1 包要求我们使用许多回调,因此了解它们的工作原理非常重要。

该代码声明了以下三个我们稍后指定为回调的函数:

  • on_connect:当 MQTT 客户端从 MQTT 服务器接收到CONNACK响应时,即成功与 MQTT 服务器建立连接时,将调用此函数。

  • on_subscribe:当 MQTT 客户端从 MQTT 服务器接收到SUBACK响应时,即成功完成订阅时,将调用此函数。

  • on_message:当 MQTT 客户端从 MQTT 服务器接收到PUBLISH消息时,将调用此函数。每当 MQTT 服务器基于客户端的订阅发布消息时,将调用此函数。

下表总结了基于从 MQTT 服务器接收到的响应调用的函数:

来自 MQTT 服务器的响应 将被调用的函数
CONNACK on_connnect
SUBACK on_subscribe
PUBLISH on_message

主要代码块创建了代表 MQTT 客户端的mqtt.Client类(paho.mqtt.client.Client)的实例。我们使用这个实例与我们的 MQTT 服务器 Mosquitto 进行通信。如果我们使用默认参数创建新实例,我们将使用 MQTT 版本 3.1。我们想要使用 MQTT 版本 3.11,因此我们将mqtt.MQTTv311指定为协议参数的值。

然后,代码将函数分配给属性。以下表总结了这些分配:

属性 分配的函数
client.on_connect on_connect
client.on_message on_message
client.on_subscribe on_subscribe

调用client.tls_set方法配置加密和认证选项非常重要,在运行client.connect方法之前调用此方法。我们在ca_certscertfilekeyfile参数中指定证书颁发机构证书文件、客户端证书和客户端密钥的完整字符串路径。ca_certs参数名称有点令人困惑,但我们只需要指定证书颁发机构证书文件的字符串路径,而不是多个证书。

最后,主要代码块调用client.connect方法,并指定hostportkeepalive参数的值。这样,代码要求 MQTT 客户端与指定的 MQTT 服务器建立连接。

connect方法以异步执行方式运行,因此它是一个非阻塞调用。

成功与 MQTT 服务器建立连接后,将执行client.on_connect属性中指定的回调,即on_connect函数。此函数在 client 参数中接收与 MQTT 服务器建立连接的mqtt.Client实例。

如果要与不使用 TLS 的 MQTT 服务器建立连接,则无需调用client.tls_set方法。此外,您需要使用适当的端口,而不是在使用 TLS 时指定的8883端口。请记住,当不使用 TLS 时,默认端口是1883

使用 Python 订阅主题

代码调用client.subscribe方法,参数为"vehicles/vehiclepi01/tests",以订阅这个特定的单个主题,并将qos参数设置为2,以请求 QoS 级别为 2。

在这种情况下,我们只订阅一个主题。但是,非常重要的是要知道,我们不限于订阅单个主题过滤器;我们可以通过一次调用subscribe方法订阅许多主题过滤器。

在 MQTT 服务器确认成功订阅指定主题过滤器并返回SUBACK响应后,将执行client.on_subscribe属性中指定的回调,即on_subscribe函数。此函数在granted_qos参数中接收一个整数列表,提供 MQTT 服务器为每个主题过滤器订阅请求授予的 QoS 级别。on_subscribe函数中的代码显示了 MQTT 服务器为我们指定的主题过滤器授予的 QoS 级别。在这种情况下,我们只订阅了一个单一的主题过滤器,因此代码从接收到的granted_qos数组中获取第一个值。

每当收到与我们订阅的主题过滤器匹配的新消息时,将执行client.on_messsage属性中指定的回调,即on_message函数。此函数在 client 参数中接收与 MQTT 服务器建立连接的mqtt.Client实例,并在msg参数中接收一个mqtt.MQTTMessage实例。mqtt.MQTTMessage类描述了一条传入消息。

在这种情况下,每当执行on_message函数时,msg.topic中的值将始终匹配"vehicles/vehiclepi01/tests",因为我们刚刚订阅了一个主题,没有其他主题名称与主题过滤器匹配。但是,如果我们订阅了一个或多个主题过滤器,其中可能有多个主题匹配,那么始终需要检查msg.topic属性的值来确定消息是发送到哪个主题。

on_message函数中的代码打印已接收消息的主题msg.topic和消息的有效负载的字符串表示形式,即msg.payload属性。

最后,主块调用client.loop_forever方法,该方法以无限阻塞循环为我们调用loop方法。在这一点上,我们只想在我们的程序中运行 MQTT 客户端循环。我们将接收与我们订阅的主题匹配的消息。

loop方法负责处理网络事件,即确保与 MQTT 服务器的通信进行。您可以将loop方法视为将电子邮件客户端同步以接收传入消息并发送发件箱中的消息的等效方法。

确保 Mosquitto 服务器或您可能要用于此示例的任何其他 MQTT 服务器正在运行。然后,在要用作 MQTT 客户端并使用 Linux 或 macOS 的任何计算机或设备上执行以下行以启动示例:

python3 subscribe_with_paho.py

在 Windows 中,您必须执行以下行:

python subscribe_with_paho.py

如果您看到类似以下行的SSLError的回溯,这意味着 MQTT 服务器的主机名或 IP 与生成名为server.crt的服务器证书文件时指定的Common Name属性的值不匹配。确保检查 MQTT 服务器(Mosquitto 服务器)的 IP 地址,并使用指定为Common Name的适当 IP 地址或主机名再次生成服务器证书文件和密钥,如第三章中所述,Securing an MQTT 3.1.1 Mosquitto Server,如果您正在使用我们生成的自签名证书。如果您正在使用自签名证书、IP 地址和 DHCP 服务器,请还要检查 DHCP 服务器是否更改了 Mosquitto 服务器的 IP 地址:

Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
 File "/Users/gaston/HillarMQTT/01/lib/python3.6/site-packages/paho/mqtt/client.py", line 612, in connect
 return self.reconnect()
 File "/Users/gaston/HillarMQTT/01/lib/python3.6/site-packages/paho/mqtt/client.py", line 751, in reconnect
 self._tls_match_hostname()
 File "/Users/gaston/HillarMQTT/01/lib/python3.6/site-packages/paho/mqtt/client.py", line 2331, in _tls_match_hostname
 raise ssl.SSLError('Certificate subject does not match remote hostname.')

现在,按照以下步骤使用 MQTT.fx GUI 实用程序向vehicles/vehiclepi01/tests主题发布两条消息:

  1. 启动 MQTT.fx,并按照我们在第三章中学到的步骤与 MQTT 服务器建立连接,Securing an MQTT 3.1.1 Mosquitto Server

  2. 单击 Publish 并在 Publish 按钮左侧的下拉菜单中输入vehicles/vehiclepi01/tests

  3. 单击 Publish 按钮右侧的 QoS 2。

  4. 在 Publish 按钮下的文本框中输入以下文本:{"CMD": " UNLOCK_DOORS"}。然后,单击 Publish 按钮。MQTT.fx 将输入的文本发布到指定的主题。

  5. 在 Publish 按钮下的文本框中输入以下文本:{"CMD": "TURN_ON_HEADLIGHTS"}。然后,单击 Publish 按钮。MQTT.fx 将输入的文本发布到指定的主题。

如果您不想使用 MQTT.fx 实用程序,可以运行两个mosquitto_pub命令来生成发布消息到主题的 MQTT 客户端。您只需要在 macOS 或 Linux 中打开另一个终端,或者在 Windows 中打开另一个命令提示符,转到 Mosquitto 安装的目录,并运行以下命令。在这种情况下,不需要指定-d选项。将192.168.1.101替换为 MQTT 服务器的 IP 或主机名。记得将ca.crtboard001.crtboard001.key替换为在board_certificates目录中创建的这些文件的完整路径。示例的代码文件包含在mqtt_python_gaston_hillar_04_01文件夹中的script_01.txt文件中:

mosquitto_pub -h 192.168.1.101 -V mqttv311 -p 8883 --cafile ca.crt --cert board001.crt --key board001.key -t vehicles/vehiclepi01/tests -m '{"CMD": "UNLOCK_DOORS"}' -q 2 --tls-version tlsv1.2

mosquitto_pub -h 192.168.1.101 -V mqttv311 -p 8883 --cafile ca.crt --cert board001.crt --key board001.key -t vehicles/vehiclepi01/tests -m '{"CMD": "TURN_ON_HEADLIGHTS"}' -q 2 --tls-version tlsv1.2

转到您执行 Python 脚本的设备和窗口。您将看到以下输出:

Result from connect: Connection Accepted.
I've subscribed with QoS: 2
Message received. Topic: vehicles/vehiclepi01/tests. Payload: b'{"CMD": "UNLOCK_DOORS"}'
Message received. Topic: vehicles/vehiclepi01/tests. Payload: b'{"CMD": "TURN_ON_HEADLIGHTS"}'

Python 程序成功地与 MQTT 服务器建立了安全加密的连接,并成为了vehicles/vehiclepi01/tests主题的订阅者,授予了 QoS 级别 2。该程序显示了它在vehicles/vehiclepi01/tests主题中接收到的两条消息。

按下Ctrl + C停止程序的执行。生成的 MQTT 客户端将关闭与 MQTT 服务器的连接。您将看到类似以下输出的错误消息,因为循环执行被中断:

Traceback (most recent call last):
 File "subscribe_with_paho.py", line 33, in <module>
 client.loop_forever()
 File "/Users/gaston/HillarMQTT/01/lib/python3.6/site-packages/paho/mqtt/client.py", line 1481, in loop_forever
 rc = self.loop(timeout, max_packets)
 File "/Users/gaston/HillarMQTT/01/lib/python3.6/site-packages/paho/mqtt/client.py", line 988, in loop
 socklist = select.select(rlist, wlist, [], timeout)
KeyboardInterrupt

为将作为客户端工作的物联网板配置证书

现在,我们将编写 Python 代码,该代码将准备在不同的物联网板上运行。当然,您可以在单个开发计算机或开发板上工作。无需在不同设备上运行代码。我们只是想确保我们可以编写能够在不同设备上运行的代码。

记得将我们在上一章中创建的文件复制到代表控制车辆的板的计算机或设备上,并且我们将用它来运行 Python 脚本。如果您将继续使用到目前为止一直在使用的同一台计算机或设备,您无需遵循下一步。

我们将文件保存在一个名为mqtt_certificates的目录中。在您将用作此示例的 MQTT 客户端的计算机或板上创建一个board_certificates目录。将以下三个文件复制到这个新目录中:

  • ca.crt:证书颁发机构证书文件

  • board001.crt:客户端证书文件

  • board001.key:客户端密钥

创建一个代表车辆的类

我们将创建以下两个类:

  • Vehicle:这个类将代表一个车辆,并提供在处理命令时将被调用的方法。为了保持示例简单,我们的方法将在每次调用后将车辆执行的操作打印到控制台输出。代表车辆的真实类将在每次调用每个方法时与发动机、灯、执行器、传感器和车辆的其他不同组件进行交互。

  • VehicleCommandProcessor:这个类将代表一个命令处理器,它将与 MQTT 服务器建立连接,订阅一个主题,其中 MQTT 客户端将接收带有命令的消息,分析传入的消息,并将命令的执行委托给Vehicle类的相关实例。VehicleCommandProcessor类将声明许多静态方法,我们将指定为 MQTT 客户端的回调。

在主虚拟环境文件夹中创建一个名为vehicle_commands.py的新 Python 文件。以下行声明了许多变量,这些变量具有标识车辆支持的每个命令的值。此外,代码还声明了许多变量,这些变量具有我们将用于指定命令的关键字符串以及我们将用于指定成功执行的命令的关键字符串。所有这些变量都以全大写字母定义,因为我们将把它们用作常量。示例的代码文件包含在mqtt_python_gaston_hillar_04_01文件夹中的vehicle_commands.py文件中。

# Key strings
COMMAND_KEY = "CMD"
SUCCESFULLY_PROCESSED_COMMAND_KEY = "SUCCESSFULLY_PROCESSED_COMMAND"
# Command strings
# Turn on the vehicle's engine.
CMD_TURN_ON_ENGINE = "TURN_ON_ENGINE"
# Turn off the vehicle's engine
CMD_TURN_OFF_ENGINE = "TURN_OFF_ENGINE"
# Close and lock the vehicle's doors
CMD_LOCK_DOORS = "LOCK_DOORS"
# Unlock and open the vehicle's doors
CMD_UNLOCK_DOORS = "UNLOCK_DOORS"
# Park the vehicle
CMD_PARK = "PARK"
# Park the vehicle in a safe place that is configured for the vehicle
CMD_PARK_IN_SAFE_PLACE = "PARK_IN_SAFE_PLACE"
# Turn on the vehicle's headlights
CMD_TURN_ON_HEADLIGHTS = "TURN_ON_HEADLIGHTS"
# Turn off the vehicle's headlights
CMD_TURN_OFF_HEADLIGHTS = "TURN_OFF_HEADLIGHTS"
# Turn on the vehicle's parking lights, also known as sidelights
CMD_TURN_ON_PARKING_LIGHTS = "TURN_ON_PARKING_LIGHTS"
# Turn off the vehicle's parking lights, also known as sidelights
CMD_TURN_OFF_PARKING_LIGHTS = "TURN_OFF_PARKING_LIGHTS"
# Accelerate the vehicle, that is, press the gas pedal
CMD_ACCELERATE = "ACCELERATE"
# Brake the vehicle, that is, press the brake pedal
CMD_BRAKE = "BRAKE"
# Make the vehicle rotate to the right. We must specify the degrees 
# we want the vehicle to rotate right in the value for the DEGREES key
CMD_ROTATE_RIGHT = "ROTATE_RIGHT"
# Make the vehicle rotate to the left. We must specify the degrees 
# we want the vehicle to rotate left in the value for the DEGREES key
CMD_ROTATE_LEFT = "ROTATE_LEFT"
# Set the maximum speed that we allow to the vehicle. We must specify 
# the desired maximum speed in miles per hour in the value for the MPH key
CMD_SET_MAX_SPEED = "SET_MAX_SPEED"
# Set the minimum speed that we allow to the vehicle. We must specify 
# the desired minimum speed in miles per hour in the value for the MPH key
CMD_SET_MIN_SPEED = "SET_MIN_SPEED"
# Degrees key
KEY_DEGREES = "DEGREES"
# Miles per hour key
KEY_MPH = "MPH"

COMMAND_KEY变量定义了一个关键字符串,该字符串定义了代码将理解为命令。每当我们接收包含指定关键字符串的消息时,我们知道字典中与此关键相关联的值将指示消息希望代码在板上运行的命令被处理。MQTT 客户端不会接收消息作为字典,因此,当它们不仅仅是一个字符串时,有必要将它们从字符串转换为字典。

SUCCESSFULLY_PROCESSED_COMMAND_KEY变量定义了一个关键字符串,该字符串定义了代码将在发布到适当主题的响应消息中用作成功处理的命令键。每当我们发布包含指定关键字符串的消息时,我们知道字典中与此关键相关联的值将指示板成功处理的命令。

在主虚拟环境文件夹中创建一个名为vehicle_mqtt_client.py的新 Python 文件。以下行声明了必要的导入和与前面示例中使用的相同变量,以建立与 MQTT 服务器的连接。然后,这些行声明了Vehicle类。示例的代码文件包含在mqtt_python_gaston_hillar_04_01文件夹中的vehicle_mqtt_client.py文件中。

class Vehicle:
    def __init__(self, name):
        self.name = name
        self.min_speed_mph = 0
        self.max_speed_mph = 10

    def print_action_with_name_prefix(self, action):
        print("{}: {}".format(self.name, action))

    def turn_on_engine(self):
        self.print_action_with_name_prefix("Turning on the engine")

    def turn_off_engine(self):
        self.print_action_with_name_prefix("Turning off the engine")

    def lock_doors(self):
        self.print_action_with_name_prefix("Locking doors")

    def unlock_doors(self):
        self.print_action_with_name_prefix("Unlocking doors")

    def park(self):
        self.print_action_with_name_prefix("Parking")

    def park_in_safe_place(self):
        self.print_action_with_name_prefix("Parking in safe place")

    def turn_on_headlights(self):
        self.print_action_with_name_prefix("Turning on headlights")

    def turn_off_headlights(self):
        self.print_action_with_name_prefix("Turning off headlights")

    def turn_on_parking_lights(self):
        self.print_action_with_name_prefix("Turning on parking lights")

    def turn_off_parking_lights(self):
        self.print_action_with_name_prefix("Turning off parking 
         lights")

    def accelerate(self):
        self.print_action_with_name_prefix("Accelerating")

    def brake(self):
        self.print_action_with_name_prefix("Braking")

    def rotate_right(self, degrees):
        self.print_action_with_name_prefix("Rotating right {} 
          degrees".format(degrees))

    def rotate_left(self, degrees):
        self.print_action_with_name_prefix("Rotating left {} 
           degrees".format(degrees))

    def set_max_speed(self, mph):
        self.max_speed_mph = mph
        self.print_action_with_name_prefix("Setting maximum speed to {} 
        MPH".format(mph))

    def set_min_speed(self, mph):
        self.min_speed_mph = mph
        self.print_action_with_name_prefix("Setting minimum speed to {} 
        MPH".format(mph))

与前面的示例一样,用于与 Mosquitto MQTT 服务器建立连接的所有配置值都在名为config.py的 Python 文件中定义在主虚拟环境文件夹中。如果要在不同的设备上运行此示例,您将不得不创建一个新的config.py文件,并更改导入config模块的行,以使用新的配置文件。不要忘记将certificates_path字符串中的值/Users/gaston/board_certificates替换为您创建的board_certificates目录的路径。此外,将mqtt_server_host的值替换为 Mosquitto 服务器或其他您决定使用的 MQTT 服务器的 IP 地址或主机名。

我们必须在所需的名称参数中指定车辆的名称。构造函数,即__init__方法,将接收的名称保存在具有相同名称的属性中。然后,构造函数为两个属性设置了初始值:min_speed_mphmax_speed_mph。这些属性确定了车辆的最小和最大速度值,以英里每小时表示。

Vehicle类声明了print_action_with_name_prefix方法,该方法接收一个包含正在执行的动作的字符串,并将其与保存在name属性中的值一起作为前缀打印出来。此类中定义的其他方法调用print_action_with_name_prefix方法,以打印指示车辆正在执行的动作的消息,并以车辆的名称作为前缀。

在 Python 中接收消息

我们将使用最近安装的paho-mqtt版本 1.3.1 模块订阅特定主题,并在接收到主题消息时运行代码。我们将在同一个 Python 文件中创建一个名为vehicle_mqtt_client.pyVehicleCommandProcessor类,该文件位于主虚拟环境文件夹中。这个类将代表一个与先前编码的Vehicle类实例相关联的命令处理器,配置 MQTT 客户端和订阅客户端,并声明当与 MQTT 相关的某些事件被触发时将要执行的回调代码。

我们将VehicleCommandProcessor类的代码拆分成许多代码片段,以便更容易理解每个代码部分。您必须将下面的代码添加到现有的vehicle_mqtt_client.py Python 文件中。以下代码声明了VehicleCommandProcessor类及其构造函数,即__init__方法。示例的代码文件包含在mqtt_python_gaston_hillar_04_01文件夹中的vehicle_mqtt_client.py文件中:

class VehicleCommandProcessor:
    commands_topic = ""
    processed_commands_topic = ""
    active_instance = None

    def __init__(self, name, vehicle):
        self.name = name
        self.vehicle = vehicle
        VehicleCommandProcessor.commands_topic = \
            "vehicles/{}/commands".format(self.name)
        VehicleCommandProcessor.processed_commands_topic = \
            "vehicles/{}/executedcommands".format(self.name)
        self.client = mqtt.Client(protocol=mqtt.MQTTv311)
        VehicleCommandProcessor.active_instance = self
        self.client.on_connect = VehicleCommandProcessor.on_connect
        self.client.on_subscribe = VehicleCommandProcessor.on_subscribe
        self.client.on_message = VehicleCommandProcessor.on_message
        self.client.tls_set(ca_certs = ca_certificate,
            certfile=client_certificate,
            keyfile=client_key)
        self.client.connect(host=mqtt_server_host,
                            port=mqtt_server_port,
                            keepalive=mqtt_keepalive)

我们必须为命令处理器和命令处理器将控制的Vehicle实例指定一个名称,分别在namevehicle参数中。构造函数,即__init__方法,将接收到的namevehicle保存在同名的属性中。然后,构造函数设置了commands_topicprocessed_commands_topic类属性的值。构造函数使用接收到的name来确定命令和成功处理的命令的主题名称,根据我们之前讨论的规范。MQTT 客户端将在command_topic类属性中保存的主题名称接收消息,并将消息发布到processed_commands_topic类属性中保存的主题名称。

然后,构造函数创建了一个mqtt.Client类的实例(paho.mqtt.client.Client),表示一个 MQTT 客户端,我们将使用它与 MQTT 服务器进行通信。代码将此实例分配给client属性(self.client)。与我们之前的示例一样,我们希望使用 MQTT 版本 3.11,因此我们将mqtt.MQTTv311指定为协议参数的值。

代码还将此实例的引用保存在active_instance类属性中,因为我们必须在构造函数指定为 MQTT 客户端触发的不同事件的回调中访问该实例。我们希望将与车辆命令处理器相关的所有方法都放在VehicleCommandProcessor类中。

然后,代码将静态方法分配给self.client实例的属性。以下表总结了这些分配:

属性 分配的静态方法
client.on_connect VehicleCommandProcessor.on_connect
client.on_message VehicleCommandProcessor.on_message
client.on_subscribe VehicleCommandProcessor.on_subscribe

静态方法不接收selfcls作为第一个参数,因此我们可以将它们用作具有所需数量参数的回调。请注意,我们将在下一段编码和分析这些静态方法。

self.client.tls_set方法的调用配置了加密和认证选项。最后,构造函数调用client.connect方法,并指定hostportkeepalive参数的值。这样,代码要求 MQTT 客户端与指定的 MQTT 服务器建立连接。请记住,connect方法以异步执行方式运行,因此它是一个非阻塞调用。

如果要与未使用 TLS 的 MQTT 服务器建立连接,则需要删除对self.client.tls_set方法的调用。此外,您需要使用适当的端口,而不是在使用 TLS 时指定的8883端口。请记住,当您不使用 TLS 时,默认端口是1883

以下行声明了on_connect静态方法,该方法是VehicleCommandProcessor类的一部分。您需要将这些行添加到现有的vehicle_mqtt_client.py Python 文件中。示例的代码文件包含在mqtt_python_gaston_hillar_04_01文件夹中的vehicle_mqtt_client.py文件中:

    @staticmethod
    def on_connect(client, userdata, flags, rc):
        print("Result from connect: {}".format(
            mqtt.connack_string(rc)))
        # Check whether the result form connect is the CONNACK_ACCEPTED  
          connack code
        if rc == mqtt.CONNACK_ACCEPTED:
            # Subscribe to the commands topic filter
            client.subscribe(
                VehicleCommandProcessor.commands_topic, 
                qos=2)

成功与 MQTT 服务器建立连接后,将执行self.client.on_connect属性中指定的回调,即on_connect静态方法(使用@staticmethod装饰器标记)。此静态方法接收了与 MQTT 服务器建立连接的mqtt.Client实例作为 client 参数。

该代码检查rc参数的值,该参数提供了 MQTT 服务器返回的CONNACK代码。如果此值与mqtt.CONNACK_ACCEPTED匹配,则意味着 MQTT 服务器接受了连接请求,因此,代码调用client.subscribe方法,并将VehicleCommandProcessor.commands_topic作为参数订阅到commands_topic类属性中指定的主题,并为订阅指定了 QoS 级别为 2。

以下行声明了on_subscribe静态方法,该方法是VehicleCommandProcessor类的一部分。您需要将这些行添加到现有的vehicle_mqtt_client.py Python 文件中。示例的代码文件包含在mqtt_python_gaston_hillar_04_01文件夹中的vehicle_mqtt_client.py文件中:

    @staticmethod
    def on_subscribe(client, userdata, mid, granted_qos):
        print("I've subscribed with QoS: {}".format(
            granted_qos[0]))

on_subscribe静态方法显示了 MQTT 服务器为我们指定的主题过滤器授予的 QoS 级别。在这种情况下,我们只订阅了一个单一主题过滤器,因此,代码从接收的granted_qos数组中获取第一个值。

以下行声明了on_message静态方法,该方法是VehicleCommandProcessor类的一部分。您需要将这些行添加到现有的vehicle_mqtt_client.py Python 文件中。示例的代码文件包含在mqtt_python_gaston_hillar_04_01文件夹中的vehicle_mqtt_client.py文件中:

    @staticmethod
    def on_message(client, userdata, msg):
        if msg.topic == VehicleCommandProcessor.commands_topic:
            print("Received message payload: 
            {0}".format(str(msg.payload)))
            try:
                message_dictionary = json.loads(msg.payload)
                if COMMAND_KEY in message_dictionary:
                    command = message_dictionary[COMMAND_KEY]
                    vehicle = 
                    VehicleCommandProcessor.active_instance.vehicle
                    is_command_executed = False
                    if KEY_MPH in message_dictionary:
                        mph = message_dictionary[KEY_MPH]
                    else:
                        mph = 0
                    if KEY_DEGREES in message_dictionary:
                        degrees = message_dictionary[KEY_DEGREES]
                    else:
                        degrees = 0
                    command_methods_dictionary = {
                        CMD_TURN_ON_ENGINE: lambda: 
                        vehicle.turn_on_engine(),
                        CMD_TURN_OFF_ENGINE: lambda: 
                        vehicle.turn_off_engine(),
                        CMD_LOCK_DOORS: lambda: vehicle.lock_doors(),
                        CMD_UNLOCK_DOORS: lambda: 
                        vehicle.unlock_doors(),
                        CMD_PARK: lambda: vehicle.park(),
                        CMD_PARK_IN_SAFE_PLACE: lambda: 
                        vehicle.park_in_safe_place(),
                        CMD_TURN_ON_HEADLIGHTS: lambda: 
                        vehicle.turn_on_headlights(),
                        CMD_TURN_OFF_HEADLIGHTS: lambda: 
                        vehicle.turn_off_headlights(),
                        CMD_TURN_ON_PARKING_LIGHTS: lambda: 
                        vehicle.turn_on_parking_lights(),
                        CMD_TURN_OFF_PARKING_LIGHTS: lambda: 
                        vehicle.turn_off_parking_lights(),
                        CMD_ACCELERATE: lambda: vehicle.accelerate(),
                        CMD_BRAKE: lambda: vehicle.brake(),
                        CMD_ROTATE_RIGHT: lambda: 
                        vehicle.rotate_right(degrees),
                        CMD_ROTATE_LEFT: lambda: 
                        vehicle.rotate_left(degrees),
                        CMD_SET_MIN_SPEED: lambda: 
                        vehicle.set_min_speed(mph),
                        CMD_SET_MAX_SPEED: lambda: 
                        vehicle.set_max_speed(mph),
                    }
                    if command in command_methods_dictionary:
                        method = command_methods_dictionary[command]
                        # Call the method
                        method()
                        is_command_executed = True
                    if is_command_executed:

           VehicleCommandProcessor.active_instance.
            publish_executed_command_message(message_dictionary)
                    else:
                        print("I've received a message with an   
                          unsupported command.")
            except ValueError:
                # msg is not a dictionary
                # No JSON object could be decoded
                print("I've received an invalid message.")

每当在我们订阅的commands_topic类属性中保存的主题中收到新消息时,将执行self.client.on_messsage属性中指定的回调,即先前编码的on_message静态方法(使用@staticmethod装饰器标记)。此静态方法接收了与 MQTT 服务器建立连接的mqtt.Client实例作为 client 参数,并在msg参数中接收了一个mqtt.MQTTMessage实例。

mqtt.MQTTMessage类描述了传入的消息。

msg.topic属性指示接收消息的主题。因此,静态方法检查msg.topic属性是否与commands_topic类属性中的值匹配。在这种情况下,每当执行on_message方法时,msg.topic中的值将始终与主题类属性中的值匹配,因为我们只订阅了一个主题。但是,如果我们订阅了多个主题,则始终需要检查消息发送的主题以及我们接收消息的主题。因此,我们包含了代码以清楚地了解如何检查接收消息的topic

代码打印了已接收消息的 payload,即msg.payload属性。然后,代码将json.loads函数的结果分配给msg.payload以将其反序列化为 Python 对象,并将结果分配给message_dictionary本地变量。如果msg.payload的内容不是 JSON,则会捕获ValueError异常,代码将打印一条消息,指示消息不包含有效命令,并且不会执行更多代码。如果msg.payload的内容是 JSON,则message_dictionary本地变量中将有一个字典。

然后,代码检查COMMAND_KEY字符串中保存的值是否包含在message_dictionary字典中。如果表达式求值为True,则意味着将 JSON 消息转换为字典后包含我们必须处理的命令。但是,在我们处理命令之前,我们必须检查是哪个命令,因此需要检索与与COMMAND_KEY字符串中保存的值相等的键关联的值。当值是我们分析为要求的命令之一时,代码能够运行特定的代码。

代码使用active_instance类属性,该属性引用了活动的VehicleCommandProcessor实例,以调用基于必须处理的命令的相关车辆的必要方法。我们必须将回调声明为静态方法,因此我们使用此类属性来访问活动实例。一旦命令成功处理,代码将is_command_executed标志设置为True。最后,代码检查此标志的值,如果等于True,则代码将为active_instance类属性中保存的VehicleCommandProcessor实例调用publish_executed_command_message

当然,在实际示例中,我们应该添加更多的验证。前面的代码被简化,以便我们可以将注意力集中在 MQTT 上。

以下行声明了publish_executed_command_message方法,该方法是VehicleCommandProcessor类的一部分。您需要将这些行添加到现有的vehicle_mqtt_client.py Python 文件中。示例的代码文件包含在mqtt_python_gaston_hillar_04_01文件夹中的vehicle_mqtt_client.py文件中:

    def publish_executed_command_message(self, message):
        response_message = json.dumps({
            SUCCESFULLY_PROCESSED_COMMAND_KEY:
                message[COMMAND_KEY]})
        result = self.client.publish(
            topic=self.__class__.processed_commands_topic,
            payload=response_message)
        return result

publish_executed_command_message方法接收了带有消息参数的命令的消息字典。该方法调用json.dumps函数将字典序列化为 JSON 格式的字符串,其中包含指示命令已成功处理的响应消息。最后,代码调用client.publish方法,将processed_commands_topic变量作为主题参数,并将 JSON 格式的字符串(response_message)作为payload参数。

在这种情况下,我们不评估从publish方法接收到的响应。此外,我们使用了qos参数的默认值,该参数指定所需的服务质量。因此,我们将以 QoS 级别等于 0 发布此消息。在第五章中,《在 Python 中测试和改进我们的车辆控制解决方案》,我们将处理更高级的场景,在这些场景中,我们将添加代码来检查方法的结果,并且我们将添加代码到on_publish回调中,该回调在成功发布消息时触发,就像我们在之前的示例中所做的那样。在这种情况下,我们仅对接收到的带有命令的消息使用 QoS 级别 2。

使用多次调用循环方法

以下行声明了process_incoming_commands方法,该方法是VehicleCommandProcessor类的一部分。 您必须将这些行添加到现有的vehicle_mqtt_client.py Python 文件中。 示例的代码文件包含在mqtt_python_gaston_hillar_04_01文件夹中的vehicle_mqtt_client.py文件中:

    def process_incoming_commands(self):
        self.client.loop()

process_incoming_commands方法调用 MQTT 客户端的loop方法,并确保与 MQTT 服务器的通信已完成。 将调用loop方法视为同步您的邮箱。 将发送要发布的任何未决消息,任何传入消息将到达收件箱,并且我们先前分析过的事件将被触发。 这样,车辆命令处理器将接收消息并处理命令。

最后,以下行声明了代码的主要块。 您必须将这些行添加到现有的vehicle_mqtt_client.py Python 文件中。 示例的代码文件包含在mqtt_python_gaston_hillar_04_01文件夹中的vehicle_mqtt_client.py文件中:

if __name__ == "__main__":
    vehicle = Vehicle("vehiclepi01")
    vehicle_command_processor = VehicleCommandProcessor("vehiclepi01", 
      vehicle)
    while True:
        # Process messages and the commands every 1 second
        vehicle_command_processor.process_incoming_commands()
        time.sleep(1)

__main__方法创建了Vehicle类的一个实例,命名为 vehicle,名称参数的值为"vehiclepi01"。 下一行创建了VehicleCommandProcessor类的一个实例,命名为vehicle_command_processor,名称参数的值为"vehiclepi01",先前创建的Vehicle实例X的值为vehicle参数。 这样,vehicle_command_processor将把命令的执行委托给vehicle中的实例方法。

VehicleCommandProcessor类的构造函数将订阅 MQTT 服务器上的vehicles/vehiclepi01/commands主题,因此,我们必须发布消息到此主题,以便发送代码将处理的命令。 每当成功处理命令时,将发布新消息到vehicles/vehiclepi01/executedcommands主题。 因此,我们必须订阅此主题以检查车辆执行的命令。

while 循环调用vehicle_command_processor.process_commands方法并休眠一秒钟。 process_commands方法调用 MQTT 客户端的循环方法,并确保与 MQTT 服务器的通信已完成。

还有一个线程化的接口,我们可以通过调用 MQTT 客户端的loop_start方法来运行。 这样,我们可以避免多次调用循环方法。 但是,我们调用循环方法使得调试代码和理解底层工作变得更容易。 我们将在第五章中使用线程化接口,在 Python 中测试和改进我们的车辆控制解决方案

测试你的知识

让我们看看你是否能正确回答以下问题:

  1. 以下哪个 Python 模块是 Paho Python 客户端?

  2. paho-mqtt

  3. paho-client-pip

  4. paho-python-client

  5. 要与使用 TLS 的 MQTT 服务器建立连接,必须在调用connect之前为paho.mqtt.client.Client实例调用哪个方法?

  6. connect_with_tls

  7. tls_set

  8. configure_tls

  9. paho.mqtt.client.Client实例与 MQTT 服务器建立连接后,将调用分配给以下哪个属性的回调函数?

  10. on_connection

  11. on_connect

  12. connect_callback

  13. paho.mqtt.client.Client实例从其订阅的主题过滤器之一接收到消息后,将调用分配给以下哪个属性的回调函数?

  14. on_message_arrived

  15. on_message

  16. message_arrived_callback

  17. paho.mqtt.client.Client实例的以下哪个方法会以无限阻塞循环为我们调用循环方法?

  18. infinite_loop

  19. loop_while_true

  20. loop_forever

正确答案包含在附录中,解决方案

摘要

在本章中,我们分析了使用 MQTT 消息控制车辆的要求。我们定义了要使用的主题以及消息有效载荷中将成为控制车辆一部分的命令。然后,我们使用 Paho Python 客户端编写了 Python 代码,将 MQTT 客户端连接到 MQTT 服务器。

我们了解了 Paho Python 客户端需要调用的方法及其参数。我们分析了回调函数的工作原理,并编写了代码来订阅主题过滤器,以及接收和处理消息。

我们编写了使用 Python 处理车辆命令的代码。该代码能够在不同的物联网平台上运行,包括树莓派 3 系列板,高通 DragonBoard,BeagleBone Black,MinnowBoard Turbot,LattePanda,UP squared,以及任何能够执行 Python 3.6.x 代码的计算机。我们还使用了 Python 中的 MQTT 客户端的网络循环。

现在我们已经了解了使用 Python 与 MQTT 一起工作的基础知识,我们将使用并改进我们的车辆控制解决方案,使用 MQTT 消息和 Python 代码,并利用其他 MQTT 功能,这些功能将在第五章中讨论,在 Python 中测试和改进我们的车辆控制解决方案

第五章:测试和改进我们的 Python 车辆控制解决方案

在本章中,我们将使用我们的车辆控制解决方案与 MQTT 消息和 Python 代码。我们将学习如何使用 Python 代码处理接收到的 MQTT 消息中的命令。我们将编写 Python 代码来组成和发送带有命令的 MQTT 消息。我们将使用阻塞和线程化的网络循环,并理解它们的区别。最后,我们将利用遗嘱功能。我们将深入研究以下内容:

  • 使用 Python 处理命令

  • 使用 Python 发送消息

  • 使用 Python 处理网络循环

  • 使用 Python 处理遗嘱和遗嘱消息

  • 使用保留的遗嘱消息

  • 理解阻塞和非阻塞代码

  • 使用线程化客户端接口

使用 Python 处理命令

在第四章中,使用 Python 和 MQTT 消息编写控制车辆的代码,我们编写了一个能够使用 Python 代码处理作为 MQTT 消息接收的车辆命令的解决方案。现在,我们想让车辆处理多条命令,以检查所有部件如何协同工作。我们想执行以下命令:

{"CMD": "LOCK_DOORS"} 
{"CMD": "TURN_OFF_PARKING_LIGHTS"} 
{"CMD": "SET_MAX_SPEED", "MPH": 10} 
{"CMD": "SET_MIN_SPEED", "MPH": 1} 
{"CMD": "TURN_ON_ENGINE"} 
{"CMD": "TURN_ON_HEADLIGHTS"} 
{"CMD": "ACCELERATE"} 
{"CMD": "ROTATE_RIGHT", "DEGREES": 45} 
{"CMD": "ACCELERATE"} 
{"CMD": "TURN_ON_PARKING_LIGHTS"} 
{"CMD": "BRAKE"} 
{"CMD": "TURN_OFF_ENGINE"} 

确保 Mosquitto 服务器,或者您可能想要用于此示例的任何其他 MQTT 服务器正在运行。

启动 MQTT.fx 并按照第四章中解释的所有步骤,使用 Python 和 MQTT 消息编写控制车辆的代码,配置 TLS 和 TLS 身份验证的连接,如果您之前没有使用 MQTT.fx 与 MQTT 服务器建立安全连接。然后,点击连接按钮。

点击订阅并在订阅按钮左侧的下拉菜单中输入vehicles/vehiclepi01/executedcommands。然后,点击订阅按钮。MQTT.fx 将在左侧显示一个新面板,其中包含我们已订阅的主题过滤器,QoS 级别为 0。

然后,在任何您想要用作使用 Linux 或 macOS 的 MQTT 客户端的计算机或设备上执行以下命令以启动车辆控制器示例:

    python3 subscribe_with_paho.py 

在 Windows 中,您必须执行以下命令:

    python subscribe_with_paho.py

保持代码在您选择用作此示例的车辆控制器的本地计算机或 IoT 板上运行。

在 MQTT.fx 中,点击发布并在发布按钮左侧的下拉菜单中输入vehicles/vehiclepi01/commands。点击 QoS 2,因为我们想使用 QoS 级别 2。

在发布按钮下方的文本框中输入以下文本:{"CMD": "LOCK_DOORS"}

然后,点击发布按钮。MQTT.fx 将以 QoS 级别 2 将输入的文本发布到指定主题。

转到您可以看到由接收消息并控制车辆的 Python 代码生成的输出的窗口。如果您在 IoT 板上运行代码,您可能正在使用 SSH 终端或连接到 IoT 板的屏幕。如果您在本地计算机上运行代码,请转到终端或命令提示符,根据您使用的操作系统。您将看到以下输出:

    Result from connect: Connection Accepted.
    Received message payload: b'{"CMD": "LOCK_DOORS"}'
    vehiclepi01: Locking doors

代码已收到带有命令的消息,Vehicle实例执行了lock_doors方法,并且输出显示了执行此代码的结果。

返回到 MQTT.fx,点击订阅,您将看到vehicles/vehiclepi01/executedcommands主题中已经有一条新消息到达,其有效载荷如下:{"SUCCESSFULLY_PROCESSED_COMMAND": "LOCK_DOORS"}。以下屏幕截图显示了在 MQTT.fx 中接收到的消息:

现在,对先前显示的列表中包含的每个命令重复以下过程。我们希望我们的车辆控制应用程序处理通过 MQTT 消息接收的每个命令,QoS 级别为 2。删除现有文本,然后在发布按钮下的文本框中输入 JSON 字符串的文本,然后单击发布按钮。MQTT.fx 将以 QoS 级别 2 将输入的文本发布到指定主题:

{"CMD": "TURN_OFF_PARKING_LIGHTS"} 

{"CMD": "SET_MAX_SPEED", "MPH": 10} 

{"CMD": "SET_MIN_SPEED", "MPH": 1} 

{"CMD": "TURN_ON_ENGINE"} 

{"CMD": "TURN_ON_HEADLIGHTS"} 

{"CMD": "ACCELERATE"} 

{"CMD": "ROTATE_RIGHT", "DEGREES": 45} 

{"CMD": "ACCELERATE"} 

{"CMD": "TURN_ON_PARKING_LIGHTS"} 

{"CMD": "BRAKE"} 

{"CMD": "TURN_OFF_ENGINE"} 

转到您可以看到由接收消息并控制车辆的 Python 代码生成的输出的窗口。您将看到以下输出,指示所有命令已被接收和处理:

    Result from connect: Connection Accepted.
    Received message payload: b'{"CMD": "LOCK_DOORS"}'
    vehiclepi01: Locking doors
    Received message payload: b'{"CMD": "TURN_OFF_PARKING_LIGHTS"}'
    vehiclepi01: Turning off parking lights
    Received message payload: b'{"CMD": "SET_MAX_SPEED", "MPH": 10}'
    vehiclepi01: Setting maximum speed to 10 MPH
    Received message payload: b'{"CMD": "SET_MIN_SPEED", "MPH": 1}'
    vehiclepi01: Setting minimum speed to 1 MPH
    Received message payload: b'{"CMD": "TURN_ON_ENGINE"}'
    vehiclepi01: Turning on the engine
    Received message payload: b'{"CMD": "TURN_ON_HEADLIGHTS"}'
    vehiclepi01: Turning on headlights
    Received message payload: b'{"CMD": "ACCELERATE"}'
    vehiclepi01: Accelerating
    Received message payload: b'{"CMD": "ROTATE_RIGHT", "DEGREES": 45}'
    vehiclepi01: Rotating right 45 degrees
    Received message payload: b'{"CMD": "ACCELERATE"}'
    vehiclepi01: Accelerating
    Received message payload: b'{"CMD": "TURN_ON_PARKING_LIGHTS"}'
    vehiclepi01: Turning on parking lights
    Received message payload: b'{"CMD": "BRAKE"}'
    vehiclepi01: Braking
    Received message payload: b'{"CMD": "TURN_OFF_ENGINE"}'
    vehiclepi01: Turning off the engine

返回到 MQTT.fx,单击订阅,您将看到已到达vehicles/vehiclepi01/executedcommands主题的共计 12 条消息。您可以通过单击窗口右侧代表每条消息的面板来轻松检查每条接收消息的有效负载的内容。以下屏幕截图显示了 MQTT.fx 中收到的最后一条消息:

现在,我们将使用 Mosquitto 命令行实用程序订阅vehicles/vehiclepi01/executedcommands主题,并发布许多带有命令的 JSON 字符串的 MQTT 消息到vehicles/vehiclepi01/commands主题。这次,我们将发布以下命令:

{"CMD": "UNLOCK_DOORS"} 
{"CMD": "LOCK_DOORS"} 
{"CMD": "SET_MAX_SPEED", "MPH": 20} 
{"CMD": "SET_MIN_SPEED", "MPH": 5} 
{"CMD": "TURN_ON_ENGINE"} 
{"CMD": "ACCELERATE"} 
{"CMD": "ROTATE_LEFT", "DEGREES": 15} 
{"CMD": "ROTATE_LEFT", "DEGREES": 20} 
{"CMD": "BRAKE"} 
{"CMD": "TURN_OFF_ENGINE"} 

在 macOS 或 Linux 中打开另一个终端,或者在 Windows 中打开另一个命令提示符,转到 Mosquitto 安装的目录,并运行以下命令。将192.168.1.1替换为 MQTT 服务器的 IP 或主机名。记得将ca.crtboard001.crtboard001.key替换为在board_certificates目录中创建的这些文件的完整路径。保持窗口打开,实用程序将显示在vehicles/vehiclepi01/executedcommands主题中接收的所有消息。示例的代码文件包含在mqtt_python_gaston_hillar_05_01文件夹中的script_01.txt文件中:

    mosquitto_sub -h 192.168.1.1 -V mqttv311 -p 8883 --cafile ca.crt --
    cert device001.crt --key device001.key -t 
    vehicles/vehiclepi01/executedcommands --tls-version tlsv1.2

在 macOS 或 Linux 中打开另一个终端,或者在 Windows 中打开另一个命令提示符,转到 Mosquitto 安装的目录,并运行以下命令以使用 QoS 级别 2 发布带有命令的消息到vehicles/vehiclepi01/commands主题。对于mosquitto_sub命令,进行与之前解释的相同替换。示例的代码文件包含在mqtt_python_gaston_hillar_05_01文件夹中的script_02.txt文件中:

    mosquitto_pub -h 192.168.1.1 -V mqttv311 -p 8883 --cafile ca.crt --
cert board001.crt --key board001.key -t vehicles/vehiclepi01/commands -m '{"CMD": "UNLOCK_DOORS"}' -q 2 --tls-version tlsv1.2

    mosquitto_pub -h 192.168.1.1 -V mqttv311 -p 8883 --cafile ca.crt --cert board001.crt --key board001.key -t vehicles/vehiclepi01/commands -m '{"CMD": "LOCK_DOORS"}' -q 2 --tls-version tlsv1.2

    mosquitto_pub -h 192.168.1.1 -V mqttv311 -p 8883 --cafile ca.crt --cert board001.crt --key board001.key -t vehicles/vehiclepi01/commands -m '{"CMD": "SET_MAX_SPEED", "MPH": 20}' -q 2 --tls-version tlsv1.2

    mosquitto_pub -h 192.168.1.1 -V mqttv311 -p 8883 --cafile ca.crt --cert board001.crt --key board001.key -t vehicles/vehiclepi01/commands -m '{"CMD": "SET_MIN_SPEED", "MPH": 5}' -q 2 --tls-version tlsv1.2

    mosquitto_pub -h 192.168.1.1 -V mqttv311 -p 8883 --cafile ca.crt --cert board001.crt --key board001.key -t vehicles/vehiclepi01/commands -m '{"CMD": "TURN_ON_ENGINE"}' -q 2 --tls-version tlsv1.2

    mosquitto_pub -h 192.168.1.1 -V mqttv311 -p 8883 --cafile ca.crt --cert board001.crt --key board001.key -t vehicles/vehiclepi01/commands -m '{"CMD": "ACCELERATE"}' -q 2 --tls-version tlsv1.2

    mosquitto_pub -h 192.168.1.1 -V mqttv311 -p 8883 --cafile ca.crt --cert board001.crt --key board001.key -t vehicles/vehiclepi01/commands -m '{"CMD": "ROTATE_LEFT", "DEGREES": 15}' -q 2 --tls-version tlsv1.2

    mosquitto_pub -h 192.168.1.1 -V mqttv311 -p 8883 --cafile ca.crt --cert board001.crt --key board001.key -t vehicles/vehiclepi01/commands -m '{"CMD": "ROTATE_LEFT", "DEGREES": 20}' -q 2 --tls-version tlsv1.2

    mosquitto_pub -h 192.168.1.1 -V mqttv311 -p 8883 --cafile ca.crt --cert board001.crt --key board001.key -t vehicles/vehiclepi01/commands -m '{"CMD": "BRAKE"}' -q 2 --tls-version tlsv1.2

    mosquitto_pub -h 192.168.1.1 -V mqttv311 -p 8883 --cafile ca.crt --cert board001.crt --key board001.key -t vehicles/vehiclepi01/commands -m '{"CMD": "TURN_OFF_ENGINE"}' -q 2 --tls-version tlsv1.2

运行上述命令后,VehicleCommandProcessor类将接收这些命令并处理它们。几秒钟后,您将在执行mosquitto_sub实用程序的窗口中看到以下输出:

    {"SUCCESSFULLY_PROCESSED_COMMAND": "UNLOCK_DOORS"}
    {"SUCCESSFULLY_PROCESSED_COMMAND": "LOCK_DOORS"}
    {"SUCCESSFULLY_PROCESSED_COMMAND": "SET_MAX_SPEED"}
    {"SUCCESSFULLY_PROCESSED_COMMAND": "SET_MIN_SPEED"}
    {"SUCCESSFULLY_PROCESSED_COMMAND": "TURN_ON_ENGINE"}
    {"SUCCESSFULLY_PROCESSED_COMMAND": "ACCELERATE"}
    {"SUCCESSFULLY_PROCESSED_COMMAND": "ROTATE_LEFT"}
    {"SUCCESSFULLY_PROCESSED_COMMAND": "ROTATE_LEFT"}
    {"SUCCESSFULLY_PROCESSED_COMMAND": "BRAKE"}
    {"SUCCESSFULLY_PROCESSED_COMMAND": "TURN_OFF_ENGINE"}

请注意,MQTT.fx 实用程序也将接收消息,因为它保持订阅vehicles/vehiclepi01/executedcommands主题。

转到您可以看到由接收消息并控制车辆的 Python 代码生成的输出的窗口。您将看到以下输出,指示所有命令已被接收和处理:

    Result from connect: Connection Accepted.
    Received message payload: b'{"CMD": "UNLOCK_DOORS"}'
    vehiclepi01: Unlocking doors
    Received message payload: b'{"CMD": "LOCK_DOORS"}'
    vehiclepi01: Locking doors
    Received message payload: b'{"CMD": "SET_MAX_SPEED", "MPH": 20}'
    vehiclepi01: Setting maximum speed to 20 MPH
    Received message payload: b'{"CMD": "SET_MIN_SPEED", "MPH": 5}'
    vehiclepi01: Setting minimum speed to 5 MPH
    Received message payload: b'{"CMD": "TURN_ON_ENGINE"}'
    vehiclepi01: Turning on the engine
    Received message payload: b'{"CMD": "ACCELERATE"}'
    vehiclepi01: Accelerating
    Received message payload: b'{"CMD": "ROTATE_LEFT", "DEGREES": 15}'
    vehiclepi01: Rotating left 15 degrees
    Received message payload: b'{"CMD": "ROTATE_LEFT", "DEGREES": 20}'
    vehiclepi01: Rotating left 20 degrees
    Received message payload: b'{"CMD": "BRAKE"}'
    vehiclepi01: Braking
    Received message payload: b'{"CMD": "TURN_OFF_ENGINE"}'
    vehiclepi01: Turning off the engine

使用 Python 发送消息

到目前为止,我们一直在使用 GUI 和命令行工具发布 MQTT 消息来控制车辆。现在,我们将编写 Python 代码来发布控制每辆车的命令,并检查执行这些命令的结果。当然,GUI 实用程序,如 MQTT.fx 和 Mosquitto 命令行实用程序,非常有用。但是,一旦我们知道事情正在按我们的期望进行,我们可以编写必要的代码以在与我们用于在 IoT 板上运行代码的相同编程语言中执行测试。

现在,我们将编写一个 Python 客户端,它将发布消息到vehicles/vehiclepi01/commands主题,并订阅到vehicles/vehiclepi01/executedcommands主题。我们将编写发布者和订阅者。这样,我们将能够设计能够通过 Python 代码与 MQTT 消息通信的应用程序,Python 将作为客户端应用程序的编程语言。具体来说,这些应用程序将能够通过 MQTT 服务器与所有发布者和订阅者设备中的 Python 代码进行通信。

我们可以在任何其他能够执行 Python 3.x 的计算机或物联网板上运行 Python 客户端。

在第四章中,使用 Python 和 MQTT 消息编写控制车辆的代码,我们在主虚拟环境文件夹中创建了一个名为config.py的 Python 文件。在这个文件中,我们定义了许多配置值,用于与 Mosquitto MQTT 服务器建立连接。这样,所有配置值都包含在一个特定的 Python 脚本中。如果您需要更改此文件以配置将组成并发送 MQTT 消息以控制车辆的应用程序,请确保您查看了第四章中包含的说明。

现在,我们将在主虚拟环境文件夹中创建一个名为vehicle_mqtt_remote_control.py的新的 Python 文件。我们将创建许多函数,并将它们分配为 MQTT 客户端中事件的回调函数。此外,我们将声明变量、一个辅助类和一个辅助函数,以便轻松地发布带有命令和所需值的消息。以下行显示了定义变量、辅助类和函数的代码。示例的代码文件包含在mqtt_python_gaston_hillar_05_01文件夹中的vehicle_mqtt_remote_control.py文件中:

from config import * 
from vehicle_commands import * 
import paho.mqtt.client as mqtt 
import time 
import json 

vehicle_name = "vehiclepi01" 
commands_topic = "vehicles/{}/commands".format(vehicle_name) 
processed_commands_topic = "vehicles/{}/executedcommands".format(vehicle_name) 

class LoopControl: 
    is_last_command_processed = False 

def on_connect(client, userdata, flags, rc): 
    print("Result from connect: {}".format( 
        mqtt.connack_string(rc))) 
    # Check whether the result form connect is the CONNACK_ACCEPTED 
      connack code 
    if rc == mqtt.CONNACK_ACCEPTED: 
        # Subscribe to the commands topic filter 
        client.subscribe( 
            processed_commands_topic,  
            qos=2) 

def on_message(client, userdata, msg): 
    if msg.topic == processed_commands_topic: 
        print(str(msg.payload)) 
        if str(msg.payload).count(CMD_TURN_OFF_ENGINE) > 0: 
            LoopControl.is_last_command_processed = True 

def on_subscribe(client, userdata, mid, granted_qos): 
    print("Subscribed with QoS: {}".format(granted_qos[0])) 

def build_command_message(command_name, key="", value=""): 
    if key: 
        # The command requires a key 
        command_message = json.dumps({ 
            COMMAND_KEY: command_name, 
            key: value}) 
    else: 
        # The command doesn't require a key 
        command_message = json.dumps({ 
            COMMAND_KEY: command_name}) 
    return command_message 

def publish_command(client, command_name, key="", value=""):
    command_message = build_command_message(
        command_name, key, value)
    result = client.publish(topic=commands_topic, payload=command_message, qos=2)
client.loop()
time.sleep(1)
return result

第一行导入了我们在著名的config.py文件中声明的变量。代码声明了vehicle_name变量,保存了一个字符串"vehiclepi01",我们可以轻松地用要控制的车辆的名称替换它。我们的主要目标是构建并发布命令消息到commands_topic变量中指定的主题。我们将订阅到processed_commands_topic变量中指定的主题。

LoopControl类声明了一个名为is_last_command_processed的类属性,初始化为False。我们将使用这个类属性作为控制网络循环的标志。

on_connect函数是一旦与 MQTT 服务器建立了成功的连接就会执行的回调函数。代码检查rc参数的值,该参数提供 MQTT 服务器返回的CONNACK代码。如果此值与mqtt.CONNACK_ACCEPTED匹配,则表示 MQTT 服务器接受了连接请求,因此,代码调用client.subscribe方法,为client参数中接收到的 MQTT 客户端订阅了保存在processed_commands_topic中的主题名称,QoS 级别为 0。

on_message函数将在每次新消息到达我们订阅的主题时执行。该函数只是打印接收到的消息的有效负载的原始字符串。如果有效负载包含在CMD_TURN_OFF_ENGINE常量中保存的字符串,则我们假定上一个命令已成功执行,并且代码将LoopControl.is_last_command_processed设置为True。这样,我们将根据车辆通过 MQTT 消息指示的已处理命令来控制网络循环。

on_subscribe函数将在订阅成功完成时调用。

下表总结了将根据从 MQTT 服务器接收到的响应调用的函数:

来自 MQTT 服务器的响应 将被调用的函数
CONNACK on_connnect
SUBACK on_subscribe
PUBLISH on_message

build_command_message函数接收命令名称、键和值,提供构建包含命令的 JSON 键值对字符串所需的信息。请注意,最后两个参数是可选的,它们的默认值是空字符串。该函数创建一个字典,并将字典序列化为 JSON 格式的字符串保存在command_message局部变量中。COMMAND_KEY常量是字典的第一个键,command_name作为参数接收,是组成第一个键值对的值。最后,函数返回command_message字符串。

publish_command函数接收 MQTT 客户端、命令名称、键和值,提供执行命令所需的信息。与build_command_message函数一样,键和值参数是可选的,它们的默认值是空字符串。该函数使用接收到的command_namekeyvalue参数调用先前解释的build_command_message函数,并将结果保存在command_message局部变量中。然后,代码调用client.publish方法,将command_message JSON 格式的字符串发布到commands_topic变量中保存的主题名称,QoS 级别为 2。

接下来的一行调用client.loop方法,以确保与 MQTT 服务器的通信进行,并休眠一秒。这样,消息将被发布,应用程序将等待一秒。

使用 Python 处理网络循环

现在,我们将在__main__方法中使用之前编写的functions,该方法将发布包含在 MQTT 消息中的许多命令,以便控制车辆的代码将处理这些命令。您必须将下面的代码添加到现有的vehicle_mqtt_remote_control.py Python 文件中。以下代码显示了__main__方法的代码块。示例的代码文件包含在mqtt_python_gaston_hillar_05_01文件夹中的vehicle_mqtt_remote_control.py文件中:

if __name__ == "__main__": 
    client = mqtt.Client(protocol=mqtt.MQTTv311) 
    client.on_connect = on_connect 
    client.on_subscribe = on_subscribe 
    client.on_message = on_message 
    client.tls_set(ca_certs = ca_certificate, 
        certfile=client_certificate, 
        keyfile=client_key) 
    client.connect(host=mqtt_server_host, 
        port=mqtt_server_port, 
        keepalive=mqtt_keepalive) 
    publish_command(client, CMD_SET_MAX_SPEED, KEY_MPH, 30) 
    publish_command(client, CMD_SET_MIN_SPEED, KEY_MPH, 8) 
    publish_command(client, CMD_LOCK_DOORS) 
    publish_command(client, CMD_TURN_ON_ENGINE) 
    publish_command(client, CMD_ROTATE_RIGHT, KEY_DEGREES, 15) 
    publish_command(client, CMD_ACCELERATE) 
    publish_command(client, CMD_ROTATE_RIGHT, KEY_DEGREES, 25) 
    publish_command(client, CMD_ACCELERATE) 
    publish_command(client, CMD_ROTATE_LEFT, KEY_DEGREES, 15) 
    publish_command(client, CMD_ACCELERATE) 
    publish_command(client, CMD_TURN_OFF_ENGINE) 
    while LoopControl.is_last_command_processed == False: 
        # Process messages and the commands every 500 milliseconds 
        client.loop() 
        time.sleep(0.5) 
    client.disconnect() 
    client.loop() 

代码的前几行与我们编写的第一个 Python 示例类似。调用client.connect方法后,代码多次调用publish_command命令来构建并发布带有命令的消息。

while循环调用client.loop方法,以确保与 MQTT 服务器的通信进行,并休眠 500 毫秒,即 0.5 秒。在最后一个命令被处理后,LoopControl.is_last_command_processed类变量被设置为Truewhile循环结束执行。当这发生时,代码调用client.disconnect方法,最后调用client.loop方法,以确保断开连接请求被处理。

如果在调用client.disconnect后不调用client.loop方法,程序可能会在不向 MQTT 服务器发送断开连接请求的情况下结束执行。在接下来的章节中,我们将使用遗嘱功能,并注意客户端断开连接的方式对该功能的使用产生重要影响。

在这种情况下,我们不希望循环永远运行,因为我们有一个特定的目标,即组合并发送一组命令。一旦我们确信最后一个命令已被处理,我们就会关闭与 MQTT 服务器的连接。

确保控制vehiclepi01的代码正在运行,也就是说,我们在第四章中编写的vehicle_mqtt_client.py Python 脚本正在运行。

然后,在任何您想要用作 MQTT 客户端并且使用 Linux 或 macOS 的计算机或设备上执行以下命令来启动车辆远程控制示例:

    python3 vehicle_mqtt_remote_control.py

在 Windows 中,您必须执行以下命令:

    python vehicle_mqtt_remote_control.py

保持代码在您选择用作此示例车辆远程控制的本地计算机或 IoT 板上运行。

转到执行先前的 Python 脚本vehicle_mqtt_remote_control.py的设备和窗口。您将看到以下输出。Python 代码将显示在vehicles/vehiclepi01/executedcommands主题中接收到的所有消息。在车辆成功处理TURN_OFF_ENGINE命令后,程序将结束执行:

    Result from connect: Connection Accepted.
    Subscribed with QoS: 2
    b'{"SUCCESSFULLY_PROCESSED_COMMAND": "SET_MAX_SPEED"}'
    b'{"SUCCESSFULLY_PROCESSED_COMMAND": "SET_MIN_SPEED"}'
    b'{"SUCCESSFULLY_PROCESSED_COMMAND": "LOCK_DOORS"}'
    b'{"SUCCESSFULLY_PROCESSED_COMMAND": "TURN_ON_ENGINE"}'
    b'{"SUCCESSFULLY_PROCESSED_COMMAND": "ROTATE_RIGHT"}'
    b'{"SUCCESSFULLY_PROCESSED_COMMAND": "ACCELERATE"}'
    b'{"SUCCESSFULLY_PROCESSED_COMMAND": "ROTATE_RIGHT"}'
    b'{"SUCCESSFULLY_PROCESSED_COMMAND": "ACCELERATE"}'
    b'{"SUCCESSFULLY_PROCESSED_COMMAND": "ROTATE_LEFT"}'
    b'{"SUCCESSFULLY_PROCESSED_COMMAND": "ACCELERATE"}'
    b'{"SUCCESSFULLY_PROCESSED_COMMAND": "TURN_OFF_ENGINE"}'

转到执行控制车辆并处理接收到的命令的 Python 脚本vehicle_mqtt_client.py的设备和窗口。您将看到以下输出:

    Received message payload: b'{"CMD": "SET_MAX_SPEED", "MPH": 30}'
    vehiclepi01: Setting maximum speed to 30 MPH
    Received message payload: b'{"CMD": "SET_MIN_SPEED", "MPH": 8}'
    vehiclepi01: Setting minimum speed to 8 MPH
    Received message payload: b'{"CMD": "LOCK_DOORS"}'
    vehiclepi01: Locking doors
    Received message payload: b'{"CMD": "TURN_ON_ENGINE"}'
    vehiclepi01: Turning on the engine
    Received message payload: b'{"CMD": "ROTATE_RIGHT", "DEGREES": 15}'
    vehiclepi01: Rotating right 15 degrees
    Received message payload: b'{"CMD": "ACCELERATE"}'
    vehiclepi01: Accelerating
    Received message payload: b'{"CMD": "ROTATE_RIGHT", "DEGREES": 25}'
    vehiclepi01: Rotating right 25 degrees
    Received message payload: b'{"CMD": "ACCELERATE"}'
    vehiclepi01: Accelerating
    Received message payload: b'{"CMD": "ROTATE_LEFT", "DEGREES": 15}'
    vehiclepi01: Rotating left 15 degrees
    Received message payload: b'{"CMD": "ACCELERATE"}'
    vehiclepi01: Accelerating
    Received message payload: b'{"CMD": "TURN_OFF_ENGINE"}'
    vehiclepi01: Turning off the engine

以下屏幕截图显示了在 macOS 计算机上运行的两个终端窗口。左侧的终端显示了由发布命令并作为车辆远程控制器的 Python 客户端显示的消息,即vehicle_mqtt_remote_control.py脚本。右侧的终端显示了控制车辆并处理接收到的命令的 Python 客户端代码的结果,即vehicle_mqtt_client.py脚本:

使用 Python 处理遗嘱

现在,我们将检查如果代表我们的车辆远程控制应用程序的 MQTT 客户端意外断开与我们迄今为止编写的代码所连接的 MQTT 服务器会发生什么。请注意所有步骤,因为我们将手动中断车辆远程控制程序的执行,以了解我们将利用遗嘱功能解决的特定问题。

在任何您想要用作 MQTT 客户端并且使用 Linux 或 macOS 的计算机或设备上执行以下命令来启动车辆远程控制示例:

    python3 vehicle_mqtt_remote_control.py

在 Windows 中,您必须执行以下命令:

    python vehicle_mqtt_remote_control.py

转到执行先前的 Python 脚本vehicle_mqtt_remote_control.py的设备和窗口。在看到以下输出后,按下Ctrl + C中断脚本的执行,直到所有命令都被处理:

    Result from connect: Connection Accepted.
    Subscribed with QoS: 2
    b'{"SUCCESSFULLY_PROCESSED_COMMAND": "SET_MAX_SPEED"}'
    b'{"SUCCESSFULLY_PROCESSED_COMMAND": "SET_MIN_SPEED"}'
    b'{"SUCCESSFULLY_PROCESSED_COMMAND": "LOCK_DOORS"}'
    b'{"SUCCESSFULLY_PROCESSED_COMMAND": "TURN_ON_ENGINE"}'
    b'{"SUCCESSFULLY_PROCESSED_COMMAND": "ROTATE_RIGHT"}'

按下Ctrl + C后,您将看到类似以下行的回溯输出:

    ^CTraceback (most recent call last):
      File "vehicle_mqtt_remote_control.py", line 86, in <module>
        publish_command(client, CMD_ACCELERATE)
      File "vehicle_mqtt_remote_control.py", line 57, in 
        publish_command
        time.sleep(1)
      KeyboardInterrupt

我们中断了作为车辆远程控制器的 MQTT 客户端与 MQTT 服务器之间的连接。我们没有等待所有命令被发布,而是意外地将 MQTT 客户端与 MQTT 服务器断开连接。车辆不知道远程控制应用程序已中断。

在这种情况下,我们使用了一个键盘快捷键来中断 Python 程序的执行。然而,网络故障可能是 MQTT 客户端意外与 MQTT 服务器断开连接的另一个原因。

当然,我们不希望网络故障使我们的车辆失去控制,因此,我们希望确保如果远程控制应用程序与 MQTT 服务器失去连接,车辆将停放在一个安全的地方。在这种情况下,我们希望确保车辆接收到一条指示车辆必须停放在为车辆配置的安全地点的命令的消息。

在第一章,安装 MQTT 3.1.1 Mosquitto 服务器中,我们分析了组成 MQTT 客户端发送到 MQTT 服务器以建立连接的CONNECT控制数据包的有效载荷的字段和标志。现在,我们将使用paho-mqtt中提供的适当方法来配置WillWillQoSWillRetainWillTopicWillMessage标志和字段的值,以使我们的 MQTT 客户端利用 MQTT 的遗嘱功能。

打开现有的vehicle_mqtt_remote_control.py Python 文件,并用以下代码替换定义__main__方法的行,以配置我们希望 MQTT 服务器在发生意外断开连接时发送给车辆的遗嘱消息。添加的行已经突出显示。示例的代码文件包含在mqtt_python_gaston_hillar_05_02文件夹中的vehicle_mqtt_remote_control.py文件中。

if __name__ == "__main__": 
    client = mqtt.Client(protocol=mqtt.MQTTv311) 
    client.on_connect = on_connect 
    client.on_subscribe = on_subscribe 
    client.on_message = on_message 
    client.tls_set(ca_certs = ca_certificate, 
        certfile=client_certificate, 
        keyfile=client_key) 
    # Set a will to be sent to the MQTT server in case the client 
    # disconnects unexpectedly 
    last_will_payload = build_command_message(CMD_PARK_IN_SAFE_PLACE) 
    client.will_set(topic=commands_topic,  
        payload=last_will_payload,  
        qos=2) 
    client.connect(host=mqtt_server_host, 
        port=mqtt_server_port, 
        keepalive=mqtt_keepalive) 
    publish_command(client, CMD_SET_MAX_SPEED, KEY_MPH, 30) 
    publish_command(client, CMD_SET_MIN_SPEED, KEY_MPH, 8) 
    publish_command(client, CMD_LOCK_DOORS) 
    publish_command(client, CMD_TURN_ON_ENGINE) 
    publish_command(client, CMD_ROTATE_RIGHT, KEY_DEGREES, 15) 
    publish_command(client, CMD_ACCELERATE) 
    publish_command(client, CMD_ROTATE_RIGHT, KEY_DEGREES, 25) 
    publish_command(client, CMD_ACCELERATE) 
    publish_command(client, CMD_ROTATE_LEFT, KEY_DEGREES, 15) 
    publish_command(client, CMD_ACCELERATE) 
    publish_command(client, CMD_TURN_OFF_ENGINE) 
    while LoopControl.is_last_command_processed == False: 
        # Process messages and the commands every 500 milliseconds 
        client.loop() 
        time.sleep(0.5) 
    client.disconnect() 
    client.loop() 

在代码调用client.connect方法之前,我们添加了两行代码,即在向 MQTT 服务器发送连接请求之前。第一行调用build_command_message函数,并将CMD_PARK_IN_SAFE_PLACE作为参数,以构建使车辆停放在安全地方的命令的 JSON 字符串,并将其存储在last_will_payload变量中。

下一行代码调用client.will_set方法,允许我们配置WillWillQoSWillRetainWillTopicWillMessage标志和字段的期望值,并将其用于 CONNECT 控制数据包。该代码使用commands_topiclast_will_payload2作为主题、有效载荷和 qos 参数的值来调用此方法。由于我们没有为retain参数指定值,该方法将使用其默认值False,这指定了遗嘱消息不会是保留消息。这样,当下一行代码调用client.connect方法请求 MQTT 客户端与 MQTT 服务器建立连接时,CONNECT控制数据包将包括用于配置遗嘱消息的字段和标志的适当值,QoS 级别为 2,commands_topic作为消息将被发布的主题,last_will_payload作为消息的有效载荷。

现在,在任何您想要用作 MQTT 客户端并使用 Linux 或 macOS 的计算机或设备上执行以下行以启动车辆远程控制示例:

    python3 vehicle_mqtt_remote_control.py

在 Windows 中,您必须执行以下行:

    python vehicle_mqtt_remote_control.py

转到您执行之前的 Python 脚本vehicle_mqtt_remote_control.py的设备和窗口。在看到以下输出后,按Ctrl + C中断脚本的执行,然后再处理所有命令:

    Result from connect: Connection Accepted.
    Subscribed with QoS: 2
    b'{"SUCCESSFULLY_PROCESSED_COMMAND": "SET_MAX_SPEED"}'
    b'{"SUCCESSFULLY_PROCESSED_COMMAND": "SET_MIN_SPEED"}'
    b'{"SUCCESSFULLY_PROCESSED_COMMAND": "LOCK_DOORS"}'
    b'{"SUCCESSFULLY_PROCESSED_COMMAND": "TURN_ON_ENGINE"}'

按下Ctrl + C后,您将看到类似以下行的输出:

^CTraceback (most recent call last):
 File "vehicle_mqtt_remote_control.py", line 87, in <module>
 publish_command(client, CMD_ROTATE_LEFT, KEY_DEGREES, 15)
 File "vehicle_mqtt_remote_control.py", line 57, in publish_command
 time.sleep(1)
 KeyboardInterrupt

我们中断了作为车辆远程控制器的 MQTT 客户端与 MQTT 服务器之间的连接。我们没有等待所有命令被发布,而是意外地从 MQTT 服务器断开了 MQTT 客户端的连接。因此,MQTT 服务器会发布配置的遗嘱消息,即当远程控制车辆的 MQTT 客户端与 MQTT 服务器建立连接时配置的遗嘱消息。这样,当远程控制应用程序与 MQTT 服务器之间的连接丢失时,车辆会收到一个命令,要求它停放在一个安全的地方。

转到您执行控制车辆并处理接收到的命令的 Python 脚本vehicle_mqtt_client.py的设备和窗口。您将看到类似以下行的输出。请注意,最后接收到的消息指示车辆停放在一个安全的地方。这个最后接收到的消息是我们在名为vehicle_mqtt_remote_control.py的 Python 脚本中添加的代码行配置的遗嘱消息。

Received message payload: b'{"CMD": "SET_MAX_SPEED", "MPH": 30}'
vehiclepi01: Setting maximum speed to 30 MPH
Received message payload: b'{"CMD": "SET_MIN_SPEED", "MPH": 8}'
vehiclepi01: Setting minimum speed to 8 MPH
Received message payload: b'{"CMD": "LOCK_DOORS"}'
vehiclepi01: Locking doors
Received message payload: b'{"CMD": "TURN_ON_ENGINE"}'
vehiclepi01: Turning on the engine
Received message payload: b'{"CMD": "ROTATE_RIGHT", "DEGREES": 15}'
vehiclepi01: Rotating right 15 degrees
Received message payload: b'{"CMD": "ACCELERATE"}'
vehiclepi01: Accelerating
Received message payload: b'{"CMD": "ROTATE_RIGHT", "DEGREES": 25}'
vehiclepi01: Rotating right 25 degrees
Received message payload: b'{"CMD": "ACCELERATE"}'
vehiclepi01: Accelerating
Received message payload: b'{"CMD": "PARK_IN_SAFE_PLACE"}'
vehiclepi01: Parking in safe place

以下屏幕截图显示了在 macOS 计算机上运行的两个终端窗口。左侧的终端显示了由发布命令并作为车辆远程控制器工作的 Python 客户端显示的消息,即vehicle_mqtt_remote_control.py脚本。右侧的终端显示了控制车辆并处理接收到的命令的 Python 客户端代码的结果,即vehicle_mqtt_client.py脚本。连接中断导致 MQTT 服务器发布了配置的最后遗嘱消息:

您可以利用最后遗嘱功能来指示感兴趣的客户端,特定的板、设备或传感器已离线。

现在,在任何您想要用作 MQTT 客户端并使用 Linux 或 macOS 的计算机或设备上执行以下命令以启动车辆远程控制示例:

    python3 vehicle_mqtt_remote_control.py

在 Windows 中,您必须执行以下命令:

    python vehicle_mqtt_remote_control.py

转到执行先前 Python 脚本的设备和窗口,名称为vehicle_mqtt_remote_control.py

这次,在您选择用作此示例车辆远程控制的本地计算机或 IoT 板上保持代码运行。

转到执行控制车辆并处理接收到的命令的 Python 脚本的设备和窗口,即vehicle_mqtt_client.py。您将在输出中看到以下最后几行:

    Received message payload: b'{"CMD": "ROTATE_LEFT", "DEGREES": 15}'
    vehiclepi01: Rotating left 15 degrees
    Received message payload: b'{"CMD": "ACCELERATE"}'
    vehiclepi01: Accelerating
    Received message payload: b'{"CMD": "TURN_OFF_ENGINE"}'
    vehiclepi01: Turning off the engine

在这种情况下,代码调用了client.disconnect方法,然后调用了client.loop方法。 MQTT 客户端以正常方式从 MQTT 服务器断开连接,因此,带有将车辆停放在安全位置的命令的最后遗嘱消息没有被发布。

非常重要的是要理解,当 MQTT 客户端通过调用client.disconnect方法断开与 MQTT 的连接并确保网络事件被处理时,配置的最后遗嘱消息不会被发布。如果我们希望在使用client.disconnect方法执行正常断开连接之前发布一条消息,我们必须在调用此方法之前编写必要的代码来执行此操作。此外,我们必须确保网络事件被处理。

使用保留的最后遗嘱消息

现在,我们将检查当控制车辆的 MQTT 客户端意外地与 MQTT 服务器断开连接时以及我们的车辆远程控制应用程序也意外断开连接时会发生什么。请注意所有步骤,因为我们将手动中断两个程序的执行,以了解我们将利用最后遗嘱功能结合保留标志值来解决的特定问题。

您必须迅速执行接下来的步骤。因此,请确保您阅读所有步骤,然后执行它们。

在任何您想要用作 MQTT 客户端并使用 Linux 或 macOS 的计算机或设备上执行以下命令以启动车辆远程控制示例:

    python3 vehicle_mqtt_remote_control.py

在 Windows 中,您必须执行以下命令:

    python vehicle_mqtt_remote_control.py

转到执行控制车辆并处理接收到的命令的 Python 脚本的设备和窗口,即vehicle_mqtt_client.py。在看到以下输出后,按Ctrl + C中断脚本的执行,然后再接收到所有命令之前:

    Received message payload: b'{"CMD": "PARK_IN_SAFE_PLACE"}'
    vehiclepi01: Parking in safe place
    Received message payload: b'{"CMD": "SET_MAX_SPEED", "MPH": 30}'
    vehiclepi01: Setting maximum speed to 30 MPH

按下Ctrl + C后,您将看到类似以下行的输出:

    ^CTraceback (most recent call last):
      File "vehicle_mqtt_client.py", line 198, in <module>
        time.sleep(1)
        KeyboardInterrupt

我们中断了控制车辆并处理接收到的命令的 MQTT 客户端与 MQTT 服务器之间的连接。我们没有等待所有命令被接收,而是意外地将 MQTT 客户端与 MQTT 服务器断开连接。车辆遥控应用程序不知道遥控应用程序已中断,它会等待直到它发送的最后一个命令被处理。

转到您执行先前的 Python 脚本vehicle_mqtt_remote_control.py的设备和窗口。按下Ctrl + C中断脚本的执行。按下Ctrl + C后,您将看到类似以下行的回溯输出:

    ^CTraceback (most recent call last):
      File "vehicle_mqtt_remote_control.py", line 93, in <module>
        client.loop()
      File "/Users/gaston/HillarMQTT/01/lib/python3.6/site-
        packages/paho/mqtt/client.py", line 988, in loop
        socklist = select.select(rlist, wlist, [], timeout)
        KeyboardInterrupt

返回到您执行控制车辆并处理接收到的命令的 Python 脚本的设备和窗口,即vehicle_mqtt_client.py。在任何您想要用作 MQTT 客户端并且使用 Linux 或 macOS 的计算机或设备上执行以下行以重新启动此脚本:

    python3 vehicle_mqtt_client.py 

在 Windows 中,您必须执行以下行:

    python vehicle_mqtt_client.py 

等待几秒钟,您将只看到以下指示已接受与 MQTT 服务器的连接的输出。没有接收到任何命令:

Result from connect: Connection Accepted.

以下屏幕截图显示了在 macOS 计算机上运行的两个终端窗口。左侧的终端显示了由发布命令并作为车辆遥控的 Python 客户端显示的消息,即vehicle_mqtt_remote_control.py脚本。右侧的终端显示了控制车辆并处理接收到的命令的 Python 客户端代码的运行结果,即先前解释的中断后的vehicle_mqtt_client.py脚本:

当我们启动vehicle_mqtt_client.py脚本时,代码生成了一个新的 MQTT 客户端,并与 MQTT 服务器建立了连接,并订阅了vehicles/vehiclepi01/commands。当我们中断vehicle_mqtt_remote_control.py脚本的执行时,发布到此主题的最后遗嘱消息已经设置为False,因此,消息没有被 MQTT 服务器保留,并且任何新的订阅匹配发送到保留的最后遗嘱消息的主题的主题过滤器的订阅都不会收到它。

打开现有的vehicle_mqtt_remote_control.py Python 文件,并用以下代码替换__main__方法中调用client.will_set方法的行。示例的代码文件包含在mqtt_python_gaston_hillar_05_03文件夹中的vehicle_mqtt_remote_control.py文件中:

    client.will_set(topic=commands_topic,  
        payload=last_will_payload,  
        qos=2, 
        retain=True) 

我们为retain参数指定了True值,而在代码的先前版本中使用了默认的False值。这样,最后遗嘱消息将成为保留消息。

在任何您想要用作 MQTT 客户端并且使用 Linux 或 macOS 的计算机或设备上执行以下行以启动车辆遥控示例:

    python3 vehicle_mqtt_remote_control.py

在 Windows 中,您必须执行以下行:

    python vehicle_mqtt_remote_control.py

转到您执行控制车辆并处理接收到的命令的 Python 脚本的设备和窗口,即vehicle_mqtt_client.py。在看到以下输出后,按下Ctrl + C中断脚本的执行,直到所有命令都被接收之前:

    Received message payload: b'{"CMD": "PARK_IN_SAFE_PLACE"}'
    vehiclepi01: Parking in safe place  

按下Ctrl + C后,您将看到类似以下行的回溯输出:

^CTraceback (most recent call last):
 File "vehicle_mqtt_client.py", line 198, in <module>
 time.sleep(1)
 KeyboardInterrupt

我们中断了控制车辆并处理接收到的命令的 MQTT 客户端与 MQTT 服务器之间的连接。我们没有等待所有命令被接收,而是突然断开了 MQTT 客户端与 MQTT 服务器的连接。车辆遥控应用程序不知道遥控应用程序已被中断,它会等到发送的最后一个命令被处理。

转到您执行先前的 Python 脚本vehicle_mqtt_remote_control.py的设备和窗口。按下Ctrl + C中断脚本的执行。按下Ctrl + C后,您将看到类似以下行的回溯输出:

    ^CTraceback (most recent call last):
      File "vehicle_mqtt_remote_control.py", line 93, in <module>
        client.loop()
      File "/Users/gaston/HillarMQTT/01/lib/python3.6/site-   
      packages/paho/mqtt/client.py", line 988, in loop
        socklist = select.select(rlist, wlist, [], timeout)
         KeyboardInterrupt

回到执行控制车辆并处理接收到的命令的 Python 脚本vehicle_mqtt_client.py的设备和窗口。在任何您想要用作 MQTT 客户端并且使用 Linux 或 macOS 的计算机或设备上再次执行以下命令来启动此脚本:

    python3 vehicle_mqtt_client.py

在 Windows 中,您必须执行以下命令:

    python vehicle_mqtt_client.py 

等待几秒钟,您将只会看到指示已接受与 MQTT 服务器的连接的输出,并且已接收和处理了指示车辆停放在安全位置的保留的遗嘱消息的输出。因此,车辆将停放在一个安全的地方:

Result from connect: Connection Accepted.
Received message payload: b'{"CMD": "PARK_IN_SAFE_PLACE"}'
vehiclepi01: Parking in safe place

以下屏幕截图显示了在 macOS 计算机上运行的两个终端窗口。左侧的终端显示了由发布命令并作为车辆远程控制器工作的 Python 客户端显示的消息,即vehicle_mqtt_remote_control.py脚本。右侧的终端显示了运行控制车辆并处理接收到的命令的 Python 客户端代码的结果,即在先前解释的中断之后的vehicle_mqtt_client.py脚本:

使用新代码时,当我们启动vehicle_mqtt_client.py脚本时,代码生成了一个新的 MQTT 客户端,与 MQTT 服务器建立了连接,并订阅了vehicles/vehiclepi01/commands。当我们中断vehicle_mqtt_remote_control.py脚本的执行时,最后一个遗嘱消息以Retained标志设置为True发布到此主题,因此,消息被 MQTT 服务器保留,并且任何新订阅与保留的遗嘱消息匹配的主题过滤器的连接都会接收到它。保留的遗嘱消息允许我们确保消息在新连接到 MQTT 服务器并订阅匹配主题时作为第一条消息到达。

在这种情况下,我们始终希望确保如果vehicle_mqtt_client.py脚本中创建的 MQTT 客户端与 MQTT 服务器失去连接,然后建立新连接,车辆会收到遗嘱消息。

理解阻塞和非阻塞代码

到目前为止,我们一直在处理与 MQTT 相关的网络流量和分发回调的阻塞调用。在以前的示例中,每当我们调用client.loop方法时,该方法都会使用两个可选参数的默认值:timeout1max_packets1。该方法最多阻塞一秒钟,即timeout参数的值,以处理传入或传出的数据。该方法以同步执行,因此,在此方法返回之前,下一行代码不会被执行。我们在主线程中调用了client.loop方法,因此,在client.loop方法阻塞时,此线程中无法执行其他代码。

在我们的第一个示例中,使用 Python 代码创建了一个 MQTT 客户端,我们调用了client.loop_forever方法。此方法会阻塞,直到客户端调用disconnect方法。该方法以同步执行,因此,在客户端调用disconnect方法之前,下一行代码不会被执行。我们还在主线程中调用了client.loop_forever,因此,在client.loop_forever方法阻塞时,此线程中无法执行其他代码。

循环方法和loop_forever方法之间的一个重要区别是,当我们使用循环方法时,需要手动处理重新连接。loop_forever方法会自动处理与 MQTT 服务器的重新连接。

paho-mqtt库为我们提供了一个用于网络循环的线程化客户端接口,启动另一个线程自动调用loop方法。这样,就可以释放主线程来运行其他代码。线程化接口是非阻塞的,我们不必担心重复调用loop方法。此外,线程化接口还会自动处理与 MQTT 服务器的重新连接。

使用线程化的客户端接口

现在,我们将编写车辆远程控制应用的新版本,以使用线程化接口,也称为线程循环。打开现有的vehicle_mqtt_remote_control.py Python 文件,并用以下行替换定义publish_command函数的行。示例的代码文件包含在mqtt_python_gaston_hillar_05_04文件夹中的vehicle_mqtt_remote_control.py文件中:

def publish_command(client, command_name, key="", value=""): 
    command_message = build_command_message( 
        command_name, key, value) 
    result = client.publish(topic=commands_topic, 
    payload=command_message, qos=2) 
    time.sleep(1) 
    return result 

在调用time.sleep(1)之前,我们移除了以下行:

    client.loop() 

线程循环将在另一个线程中自动调用client.loop,因此,我们不再需要在publish_command方法中包含对client.loop的调用。

打开现有的vehicle_mqtt_remote_control.py Python 文件,并用以下代码替换定义__main__方法的行,以使用线程循环。添加的行已经突出显示。示例的代码文件包含在mqtt_python_gaston_hillar_05_04文件夹中的vehicle_mqtt_remote_control.py文件中:

if __name__ == "__main__": 
    client = mqtt.Client(protocol=mqtt.MQTTv311) 
    client.on_connect = on_connect 
    client.on_subscribe = on_subscribe 
    client.on_message = on_message 
    client.tls_set(ca_certs = ca_certificate, 
         certfile=client_certificate, 
         keyfile=client_key) 
    # Set a will to be sent to the MQTT server in case the client 
    # disconnects unexpectedly 
    last_will_payload = build_command_message(CMD_PARK_IN_SAFE_PLACE) 
    client.will_set(topic=commands_topic,  
        payload=last_will_payload,  
        qos=2, 
        retain=True) 
    client.connect(host=mqtt_server_host, 
        port=mqtt_server_port, 
        keepalive=mqtt_keepalive) 
    client.loop_start() 
    publish_command(client, CMD_SET_MAX_SPEED, KEY_MPH, 30) 
    publish_command(client, CMD_SET_MIN_SPEED, KEY_MPH, 8) 
    publish_command(client, CMD_LOCK_DOORS) 
    publish_command(client, CMD_TURN_ON_ENGINE) 
    publish_command(client, CMD_ROTATE_RIGHT, KEY_DEGREES, 15) 
    publish_command(client, CMD_ACCELERATE) 
    publish_command(client, CMD_ROTATE_RIGHT, KEY_DEGREES, 25) 
    publish_command(client, CMD_ACCELERATE) 
    publish_command(client, CMD_ROTATE_LEFT, KEY_DEGREES, 15) 
    publish_command(client, CMD_ACCELERATE) 
    publish_command(client, CMD_TURN_OFF_ENGINE) 
    while LoopControl.is_last_command_processed == False: 
        # Check whether the last command has been processed or not  
        # every 500 milliseconds 
        time.sleep(0.5) 
       client.disconnect() 
       client.loop_stop() 

调用client.connect方法后,代码调用client.loop_start方法。该方法会启动一个新线程来处理 MQTT 网络流量,并释放主线程。

然后,编辑后的publish_command函数的调用不再调用client.loop,因为我们使用client.loop_start启动的线程化客户端接口将自动调用循环来处理传出消息。

每 500 毫秒检查最后一条命令是否已经被处理的while循环不再调用client.loop。现在,有另一个线程在为我们调用client.loop

当最后一条命令被处理时,代码调用client.disconnect方法,最后调用client.loop_stop方法来停止运行线程化客户端接口的线程。该方法将在线程完成时返回。

在任何您想要用作 MQTT 客户端并且使用 Linux 或 macOS 的计算机或设备上,执行以下行以启动车辆远程控制示例的新版本:

    python3 vehicle_mqtt_remote_control.py

在 Windows 中,您必须执行以下行:

    python vehicle_mqtt_remote_control.py 

您会注意到发送命令和处理命令之间的时间更清晰,因为新版本中处理网络事件的时间更准确。

测试您的知识

让我们看看您是否能正确回答以下问题:

  1. paho.mqtt.client.Client实例的以下哪种方法会阻塞执行并确保与 MQTT 服务器的通信进行?

  2. loop

  3. loop_start

  4. 阻塞循环

  5. paho.mqtt.client.Client实例的以下哪种方法会启动一个新线程,并确保与 MQTT 服务器的通信进行?

  6. loop

  7. loop_start

  8. non_blocking_loop

  9. paho.mqtt.client.Client实例的以下哪种方法配置了一个遗嘱消息,以便在客户端意外断开连接时发送到 MQTT 服务器?

  10. last_will_publish

  11. last_will_message

  12. will_set

  13. paho.mqtt.client.Client实例的以下哪种方法停止运行线程化客户端接口的线程?

  14. loop_end

  15. non_blocking_loop_stop

  16. loop_stop

  17. 以下哪种方法是非阻塞的?

  18. loop_start

  19. non_blocking_loop

  20. loop_forever

正确答案包含在附录中,解决方案

摘要

在本章中,我们使用 Python 代码处理接收的 JSON 字符串作为 MQTT 消息中的命令。然后,我们编写了一个 Python 客户端,用于组合和发布带有命令的消息,以作为车辆控制器的远程控制应用程序。

我们使用了阻塞网络循环,然后将应用程序转换为使用线程化的客户端接口,以避免阻塞主线程。我们利用了遗嘱功能,以确保在连接丢失时受控车辆停在安全位置。然后,我们处理了保留的遗嘱消息。

现在我们了解了如何使用 Python 来处理利用高级功能的多个 MQTT 应用程序,我们将使用基于云的实时 MQTT 提供程序来监视冲浪比赛,我们需要从多个传感器接收和处理数据,这就是我们将在第六章中讨论的内容,《使用基于云的实时 MQTT 提供程序和 Python 监视冲浪比赛》。

第六章:使用基于云的实时 MQTT 提供程序和 Python 监控冲浪比赛

在本章中,我们将编写 Python 代码,使用 PubNub 基于云的实时 MQTT 提供程序与 Mosquitto MQTT 服务器结合,监控冲浪比赛。我们将通过分析需求从头构建解决方案,并编写 Python 代码,该代码将在连接到冲浪板中的多个传感器的防水 IoT 板上运行。我们将定义主题和命令,并与基于云的 MQTT 服务器以及在先前章节中使用的 Mosquitto MQTT 服务器一起工作。我们将涵盖以下内容:

  • 理解要求

  • 定义主题和有效载荷

  • 编写冲浪板传感器仿真器

  • 配置 PubNub MQTT 接口

  • 将从传感器检索的数据发布到基于云的 MQTT 服务器

  • 使用多个 MQTT 服务器

  • 使用 freeboard 构建基于 Web 的仪表板

理解要求

许多为冲浪比赛训练的冲浪者希望我们构建一个实时基于 Web 的仪表板,该仪表板使用连接到冲浪板中的多个传感器的 IoT 板提供的数据。每个 IoT 板将提供以下数据:

  • 状态:每个冲浪者的潜水服中嵌入了许多可穿戴无线传感器,冲浪板中还包括其他传感器,它们将提供数据,而 IoT 板将进行实时分析以指示冲浪者的状态

  • 速度:传感器将以每小时英里mph)测量冲浪板的速度

  • 海拔:传感器将以英尺测量冲浪板的海拔

  • 水温:位于冲浪板鳍中的传感器将以华氏度测量水温

第三方软件正在 IoT 板上运行,我们无法更改发布不同主题数据的代码。我们可以提供必要的证书来配置与我们的 Mosquitto MQTT 服务器的安全连接,并指定其主机名和协议。此外,我们可以配置一个标识冲浪板并确定数据将被发布的主题的名称。

定义主题和有效载荷

IoT 板使用以下主题名称发布有关特定冲浪板的数据,其中sufboardname必须替换为分配给冲浪板的唯一名称:

变量 主题名称
状态 surfboards/surfboardname/status
速度(mph) surfboards/surfboardname/speedmph
海拔(英尺) surfboards/surfboardname/altitudefeet
水温(华氏度) surfboards/surfboardname/temperaturef

例如,如果我们将sufboard01指定为冲浪板的名称,那么想要接收冲浪板实际速度的客户端必须订阅sufboards/surfboard01/speedmph主题。

IoT 板及其连接的传感器能够区分冲浪者及其冲浪板的以下五种可能状态:

状态键 含义
0 空闲
1 划水
2 骑行
3 骑行结束
4 摔倒

IoT 板发布指定在状态键列中的整数值,指示冲浪者及其冲浪板的当前状态。例如,当冲浪者在冲浪时,板将在sufboards/surfboard01/status主题中发布2

该板将在先前解释的主题中发布速度、海拔和水温的浮点值。在这种情况下,IoT 板将只发布整数或浮点值作为 MQTT 消息的有效载荷。有效载荷不会是 JSON,就像我们之前的例子一样。有效载荷不会包含有关测量单位的任何其他信息。此信息包含在主题名称中。

IoT 板将在先前解释的主题中每秒发布数据。

在之前的例子中,我们是从零开始设计我们的解决方案。在这种情况下,我们必须与已经运行我们无法更改代码的物联网板进行交互。想象一下,我们必须在没有物联网板的情况下开始解决方案的工作;因此,我们将在 Python 中开发一个冲浪板传感器模拟器,以便为我们提供数据,以便我们可以接收发布的数据并开发所需的仪表板。在现实项目中,这是一个非常常见的情况。

正如我们在之前的章节中学到的,MQTT 已经成为物联网项目中非常流行的协议,其中许多传感器必须发布数据。由于其日益增长的流行度,许多基于云的消息基础设施已经包含了 MQTT 接口或桥接。例如,PubNub 数据流网络提供了可扩展的 MQTT 接口。我们可以利用到目前为止我们所学到的关于 MQTT 的一切来使用这个基于云的数据流网络。您可以在其网页上了解更多关于 PubNub 的信息:www.pubnub.com

一个 Python 程序将通过订阅四个主题来收集物联网板发布的数据,并且代码将每秒构建一个完整的冲浪者及其冲浪板状态。然后,代码将构建一个包含状态、速度、海拔和水温的 JSON 消息,并将其发布到 MQTT PubNub 接口的一个主题。

在我们的例子中,我们将利用 PubNub 及其 MQTT 接口提供的免费服务。我们不会使用一些可能增强我们的物联网项目连接需求的高级功能和附加服务,但这些功能也需要付费订阅。

我们将利用 freeboard.io 来可视化从传感器收集的数据,并在 PubNub MQTT 接口中发布,以多个表盘的形式呈现,并且可以在全球范围内的不同计算机和设备上使用。freeboard.io 允许我们通过选择数据源并拖放可定制的小部件来构建仪表板。freeboard.io 定义自己为一个允许我们可视化物联网的基于云的服务。您可以在其网页上了解更多关于 freeboard.io 的信息:freeboard.io

在我们的例子中,我们将利用 freeboard.io 提供的免费服务,并且我们不会使用一些提供我们仪表板隐私的高级功能,但这些功能也需要付费订阅。我们的仪表板将对任何拥有其唯一 URL 的人可用,因为我们不使用私人仪表板。

以下是提供冲浪者及其冲浪板状态的消息负载的示例。

{ 
    "Status": "Riding",  
    "Speed MPH": 15.0,  
    "Altitude Feet": 3.0,  
    "Water Temperature F": 56.0 
}

Freeboard.io 允许我们轻松地选择 PubNub MQTT 接口中接收的 JSON 消息的每个键作为仪表板的数据源。这样,我们将轻松地构建一个基于 Web 的仪表板,以提供给我们状态、速度、海拔和水温数值的表盘。

总之,我们的解决方案将由以下两个 Python 程序组成:

  • 冲浪板传感器模拟器:该程序将与我们的 Mosquitto MQTT 服务器建立安全连接,并且将从CSV(逗号分隔值)文件中读取的状态、速度、海拔和水温数值发布到适当的主题。该程序将工作得就像我们有一个穿着潜水服和冲浪板传感器的真实冲浪者在冲浪并发布数据一样。

  • 冲浪板监视器:该程序将与我们的 Mosquitto MQTT 服务器建立安全连接,并订阅冲浪板传感器模拟器发布的状态、速度、海拔和水温数值的主题。冲浪板监视器程序还将与 PubNub MQTT 接口建立连接。该程序将每秒向 PubNub MQTT 接口发布一个包含决定冲浪者及其冲浪板状态的键值对的单个消息。

编写冲浪板传感器模拟器

首先,我们将创建一个 CSV 文件,其中包含许多状态、速度(以英里/小时为单位)、海拔(以英尺为单位)和温度(以华氏度为单位)的值,这些值用逗号分隔。文件中的每一行将代表冲浪板传感器模拟器将发布到相应主题的一组值。在这种情况下,使用随机值并不方便,因为我们希望模拟冲浪者和他的冲浪板的真实场景。

现在,我们将在主虚拟环境文件夹中创建一个名为surfboard_sensors_data.csv的新文件。以下行显示了定义从冲浪者和他们的冲浪板中检索到的数据的代码。

从左到右用逗号分隔的值依次是:速度(以英里/小时为单位)、海拔(以英尺为单位)和温度(以华氏度为单位)。首先,冲浪者处于空闲状态,当划桨时增加速度,当冲浪时达到速度最大值,最后在状态设置为冲浪结束时减速。示例的代码文件包含在mqtt_python_gaston_hillar_06_01文件夹中的surfboard_sensors_data.csv文件中:

0, 1, 2, 58 
0, 1.1, 2, 58 
1, 2, 3, 57 
1, 3, 3, 57 
1, 3, 3, 57 
1, 3, 3, 57 
1, 4, 4, 57 
1, 5, 5, 57 
2, 8, 5, 57 
2, 10, 4, 57 
2, 12, 4, 56 
2, 15, 3, 56 
2, 15, 3, 56 
2, 15, 3, 56 
2, 15, 3, 56 
2, 15, 3, 56 
2, 12, 3, 56 
3, 3, 3, 55 
3, 2, 3, 55 
3, 1, 3, 55 
3, 0, 3, 55 

现在,我们将在主虚拟环境文件夹中创建一个名为surfboard_config.py的新 Python 文件。以下行显示了此文件的代码,它定义了许多配置值,这些值将用于配置冲浪板传感器模拟器将发布从传感器检索到的值的主题。冲浪板监视器也将需要这些主题来订阅它们,因此将所有配置值包含在一个特定的 Python 脚本中是方便的。示例的代码文件包含在mqtt_python_gaston_hillar_06_01文件夹中的surfboard_config.py文件中:

surfboard_name = "surfboard01" 
topic_format = "surfboards/{}/{}" 
status_topic = topic_format.format( 
    surfboard_name,  
    "status") 
speed_mph_topic = topic_format.format( 
    surfboard_name,  
    "speedmph") 
altitude_feet_topic = topic_format.format( 
    surfboard_name,  
    "altitudefeet") 
water_temperature_f_topic = topic_format.format( 
    surfboard_name,  
    "temperaturef")

该代码定义了冲浪板名称并将其存储在surfboard_name变量中。topic_format变量包含一个字符串,使得易于构建具有共同前缀的不同主题。以下表总结了四个变量的字符串值,这些变量定义了每个传感器的主题名称,基于一个名为surfboard01的定义的冲浪板:

变量
status_topic surfboards/surfboard01/status
speed_mph_topic surfboards/surfboard01/speedmph
altitude_feet_topic surfboards/surfboard01/altitudefeet
temperature_f_topic surfboards/surfboard01/temperaturef

现在,我们将在主虚拟环境文件夹中创建一个名为surfboard_sensors_emulator.py的新 Python 文件。以下行显示了此文件的代码,它与我们的 Mosquitto MQTT 服务器建立连接,读取先前创建的surfboard_sensors_data.csv CSV 文件,并持续发布从该文件中读取的值到先前枚举的主题。示例的代码文件包含在mqtt_python_gaston_hillar_06_01文件夹中的surfboard_sensors_emulator.py文件中:

from config import * 
from surfboard_config import * 
import paho.mqtt.client as mqtt 
import time 
import csv 

def on_connect(client, userdata, flags, rc): 
    print("Result from connect: {}".format( 
        mqtt.connack_string(rc))) 
    # Check whether the result form connect is the CONNACK_ACCEPTED connack code 
    if rc != mqtt.CONNACK_ACCEPTED: 
        raise IOError("I couldn't establish a connection with the MQTT server") 

def publish_value(client, topic, value): 
    result = client.publish(topic=topic, 
        payload=value, 
        qos=0) 
    return result 

if __name__ == "__main__": 
    client = mqtt.Client(protocol=mqtt.MQTTv311) 
    client.on_connect = on_connect 
    client.tls_set(ca_certs = ca_certificate, 
        certfile=client_certificate, 
        keyfile=client_key) 
    client.connect(host=mqtt_server_host, 
        port=mqtt_server_port, 
        keepalive=mqtt_keepalive) 
    client.loop_start() 
    publish_debug_message = "{}: {}" 
    try: 
        while True: 
            with open('surfboard_sensors_data.csv') as csvfile: 
                reader=csv.reader(csvfile) 
                for row in reader: 
                    status_value = int(row[0]) 
                    speed_mph_value = float(row[1]) 
                    altitude_feet_value = float(row[2]) 
                    water_temperature_f_value = float(row[3]) 
                    print(publish_debug_message.format( 
                        status_topic, 
                        status_value)) 
                    print(publish_debug_message.format( 
                        speed_mph_topic,  
                        speed_mph_value)) 
                    print(publish_debug_message.format( 
                        altitude_feet_topic,  
                        altitude_feet_value)) 
                    print(publish_debug_message.format( 
                        water_temperature_f_topic,  
                        water_temperature_f_value)) 
                    publish_value(client,  
                        status_topic,  
                        status_value) 
                    publish_value(client,  
                        speed_mph_topic,  
                        speed_mph_value) 
                    publish_value(client,  
                        altitude_feet_topic,  
                        altitude_feet_value) 
                    publish_value(client, 
                        water_temperature_f_topic,  
                        water_temperature_f_value)                    time.sleep(1) 
    except KeyboardInterrupt: 
        print("I'll disconnect from the MQTT server") 
        client.disconnect() 
        client.loop_stop() 

在第四章中,使用 Python 和 MQTT 消息编写控制车辆的代码,我们在主虚拟环境文件夹中创建了一个名为config.py的 Python 文件。在这个文件中,我们定义了许多配置值,用于与 Mosquitto MQTT 服务器建立连接。这样,所有配置值都包含在一个特定的 Python 脚本中。如果您需要更改此文件以配置冲浪板模拟器和未来的冲浪板监视器,请确保您查看该章节中包含的解释。

首先导入了我们在config.py文件和先前编码的surfboard_config.py文件中声明的变量。在这种情况下,我们还导入了csv模块,以便我们可以轻松地从包含模拟传感器值的 CSV 文件中读取。on_connect函数的代码与我们在先前的示例中使用的代码非常相似。

publish_value函数接收 MQTT 客户端、主题名称和我们要在clienttopicvalue参数中发布的值。该函数调用client.publish方法,将接收到的值作为有效载荷发布到topic参数中接收到的主题名称,QoS 级别为 0。

主要代码块使用我们非常熟悉的代码与 Mosquitto MQTT 服务器建立连接。调用client.connect方法后,代码调用client.loop_start方法启动一个处理 MQTT 网络流量并释放主线程的新线程。

然后,代码进入一个连续循环,打开surfboard_sensors_data.csv CSV 文件,并创建一个csv.reader来将逗号分隔的值的每一行读入row数组。代码检索row[0]中的字符串,该字符串代表状态值;将其转换为整数值;并将该值保存在status_value本地变量中。接下来的行检索row[1]row[2row[3]中的速度、海拔和水温的字符串。代码将这三个值转换为浮点数,并将它们保存在speed_mph_valuealtitude_feet_valuewater_temperature_f_value本地变量中。

接下来的行会打印调试消息,显示从 CSV 文件中读取的每个模拟传感器的值,并为每个值调用先前解释的publish_value函数。每次调用publish_value函数都会使用在surfboard_config.py文件中配置的主题名称的适当变量,因为每个值都会发布到不同的主题。

在代码发布了四个模拟传感器的值后,它会休眠一秒钟,然后重复 CSV 文件中下一行的过程。在读取了最后一行后,代码会再次开始循环,直到用户按下Ctrl + C并引发KeyboardInterrupt异常被捕获。在这种情况下,我们捕获此异常并调用client.disconnectclient.loop_stop方法,以适当地从 Mosquitto MQTT 服务器断开连接。在以前的示例中,我们并不关心这个异常。

配置 PubNub MQTT 接口

在使用 PubNub 的免费服务之前,PubNub 要求我们注册并创建一个带有有效电子邮件和密码的帐户,以便在 PubNub 中创建应用程序,包括设备的 PubNub MQTT 接口。我们不需要输入任何信用卡或付款信息。如果您已经在 PubNub 上有帐户,可以跳过下一步。

创建账户后,PubNub 将重定向您到列出 PubNub 应用程序的管理门户。为了在网络上发送和接收消息,需要生成 PubNub 的发布和订阅密钥。点击 CREATE NEW APP+,输入MQTT作为应用名称,然后点击 CREATE。

在管理门户中,一个新的窗格将代表应用程序。以下截图显示了 PubNub 管理门户中的 MQTT 应用程序窗格:

点击 MQTT 窗格,PubNub 将显示自动生成的 Demo Keyset 窗格。点击此窗格,PubNub 将显示 Publish Key、Subscribe Key 和 Secret key。我们必须复制并粘贴这些密钥,以便在使用 PubNub MQTT 接口发布消息和订阅这些消息的 freeboard.io 基于 Web 的仪表板的代码中使用。以下截图显示了密钥的前缀。请注意,图像中的其余字符已被删除:

为了复制 Secret key,您必须点击 Secret key 右侧的眼睛图标,PubNub 将使所有字符可见。

从传感器检索的数据发布到基于云的 MQTT 服务器

如果我们用数字显示冲浪者和他的冲浪板的状态,那么理解真实状态将会很困难。因此,我们必须将表示状态的整数映射到解释状态的字符串。

现在,我们将在主虚拟环境文件夹中创建一个名为surfboard_status.py的新 Python 文件。以下行显示了此文件的代码,其中定义了不同状态数字的常量和将这些常量与整数映射到状态描述字符串的字典。示例的代码文件包含在mqtt_python_gaston_hillar_06_01文件夹中的surfboard_status.py文件中:

SURFBOARD_STATUS_IDLE = 0 
SURFBOARD_STATUS_PADDLING = 1 
SURFBOARD_STATUS_RIDING = 2 
SURFBOARD_STATUS_RIDE_FINISHED = 3 
SURFBOARD_STATUS_WIPED_OUT = 4 

SURFBOARD_STATUS_DICTIONARY = { 
    SURFBOARD_STATUS_IDLE: 'Idle', 
    SURFBOARD_STATUS_PADDLING: 'Paddling', 
    SURFBOARD_STATUS_RIDING: 'Riding', 
    SURFBOARD_STATUS_RIDE_FINISHED: 'Ride finished', 
    SURFBOARD_STATUS_WIPED_OUT: 'Wiped out', 
    } 

现在,我们将编写冲浪板监视器的代码。我们将把代码分成许多代码片段,以便更容易理解每个代码部分。在主虚拟环境文件夹中创建一个名为surfboard_monitor.py的新 Python 文件。以下行声明了所有必要的导入和我们将用来与 PubNub MQTT 接口建立连接的变量。不要忘记用从先前解释的 PubNub 密钥生成过程中检索到的值替换分配给pubnub_publish_keypubnub_subscribe_key变量的字符串。示例的代码文件包含在mqtt_python_gaston_hillar_06_01文件夹中的surfboard_monitor.py文件中:

from config import * 
from surfboard_status import * 
from surfboard_config import * 
import paho.mqtt.client as mqtt 
import time 
import json 

# Publish key is the one that usually starts with the "pub-c-" prefix 
# Do not forget to replace the string with your publish key 
pubnub_publish_key = "pub-c-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" 
# Subscribe key is the one that usually starts with the "sub-c" prefix 
# Do not forget to replace the string with your subscribe key 
pubnub_subscribe_key = "sub-c-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" 
pubnub_mqtt_server_host = "mqtt.pndsn.com" 
pubnub_mqtt_server_port = 1883 
pubnub_mqtt_keepalive = 60 
device_id = surfboard_name 
pubnub_topic = surfboard_name 

首先导入了我们在config.py文件中声明的变量以及之前编写的surfboard_config.pysurfboard_status.py文件中的变量。然后,代码声明了以下变量,我们将使用这些变量与 PubNub MQTT 接口建立连接:

  • pubnub_publish_key:此字符串指定了 PubNub 发布密钥。

  • pubnub_subscribe_key:此字符串指定了 PubNub 订阅密钥。

  • pubnub_mqtt_server_host:此字符串指定了 PubNub MQTT 服务器地址。为了使用 PubNub MQTT 接口,我们必须始终与mqtt.pndsn.com主机建立连接。

  • pubnub_mqtt_server_port:此数字指定了 PubNub MQTT 服务器端口。在这种情况下,我们将与 PubNub MQTT 服务器建立一个不安全的连接,因此我们将使用端口号1883。我们希望保持 PubNub MQTT 接口配置简单,因此在此示例中不使用 TLS。

  • pubnub_mqtt_keepalive:此数字指定了与 PubNub MQTT 接口的连接的保持活动间隔配置。

  • device_id:此字符串指定了我们在创建Surfboard类的实例时要使用的设备标识符。代码分配了从surfboard_config.py文件导入的surfboard_name值。我们稍后将分析此类的代码。

  • Pubnub_topic:此字符串指定了冲浪板监视器将向其发布 JSON 有效载荷的主题,该有效载荷包含指定冲浪者和他们的冲浪板状态的键值对。代码分配了从surfboard_config.py文件导入的surfboard_name值。

冲浪板监视器将在端口1883上与mqtt.pndsn.com主机建立连接。因此,我们必须确保我们的防火墙配置具有适当的入站和出站规则配置,以允许在指定端口上建立连接。

将以下行添加到主虚拟环境文件夹中现有的surfboard_monitor.py中。以下行声明了Surfboard类。示例的代码文件包含在mqtt_python_gaston_hillar_06_01文件夹中的surfboard_monitor.py文件中:

class Surfboard: 
    active_instance = None 
    def __init__(self, device_id, status,  
        speed_mph, altitude_feet, water_temperature_f): 
        self.device_id = device_id 
        self.status = status 
        self.speed_mph = speed_mph 
        self.altitude_feet = altitude_feet 
        self.water_temperature_f = water_temperature_f 
        self.is_pubnub_connected = False 
        Surfboard.active_instance = self 

    def build_json_message(self): 
        # Build a message with the status for the surfboard 
        message = { 
            "Status": SURFBOARD_STATUS_DICTIONARY[self.status], 
            "Speed MPH": self.speed_mph, 
            "Altitude Feet": self.altitude_feet, 
            "Water Temperature F": self.water_temperature_f,  
        } 
        json_message = json.dumps(message) 
        return json_message

我们必须为传感器提供的数据的device_idstatusspeed_mphaltitude_feetwater_temperature_f参数指定一个device_id和初始值。构造函数,即__init__方法,将接收到的值保存在同名的属性中。

该代码还将引用保存在active_instance类属性中,因为我们必须在许多函数中访问该实例,这些函数将被指定为两个 MQTT 客户端触发的不同事件的回调:PubNub MQTT 客户端和 Mosquitto MQTT 客户端。在代码创建Surfboard实例后,我们将使用Surfboard.active_instance类属性访问活动实例。

该类声明了build_json_message方法,该方法构建了一个包含冲浪板状态的消息,并返回了由组成状态消息的键值对组成的 JSON 字符串。该代码使用SURFBOARD_STATUS_DICTIONARYsurfboard_status.py文件中声明的内容,将存储在status属性中的数字映射为解释状态的字符串。代码使用speed_mphaltitude_feetwater_temperature_f属性为其他键提供值。

在主虚拟环境文件夹中的现有surfboard_monitor.py中添加以下行。以下行声明了我们将用作回调的函数以及将由这些回调调用的其他函数。示例的代码文件包含在mqtt_python_gaston_hillar_06_01文件夹中的surfboard_monitor.py文件中:

def on_connect_mosquitto(client, userdata, flags, rc): 
    print("Result from Mosquitto connect: {}".format( 
        mqtt.connack_string(rc))) 
    # Check whether the result form connect is the CONNACK_ACCEPTED connack code 
    if rc == mqtt.CONNACK_ACCEPTED: 
        # Subscribe to a topic filter that provides all the sensors 
        sensors_topic_filter = topic_format.format( 
            surfboard_name, 
            "+") 
        client.subscribe(sensors_topic_filter, qos=0) 

def on_subscribe_mosquitto(client, userdata, mid, granted_qos): 
    print("I've subscribed with QoS: {}".format( 
        granted_qos[0])) 

def print_received_message_mosquitto(msg): 
    print("Message received. Topic: {}. Payload: {}".format( 
        msg.topic,  
        str(msg.payload))) 

def on_status_message_mosquitto(client, userdata, msg): 
    print_received_message_mosquitto(msg) 
    Surfboard.active_instance.status = int(msg.payload) 

def on_speed_mph_message_mosquitto(client, userdata, msg): 
    print_received_message_mosquitto(msg) 
    Surfboard.active_instance.speed_mph = float(msg.payload) 

def on_altitude_feet_message_mosquitto(client, userdata, msg): 
    print_received_message_mosquitto(msg) 
    Surfboard.active_instance.altitude_feet = float(msg.payload) 

def on_water_temperature_f_message_mosquitto(client, userdata, msg): 
    print_received_message_mosquitto(msg) 
    Surfboard.active_instance.water_temperature_f = float(msg.payload) 

def on_connect_pubnub(client, userdata, flags, rc): 
    print("Result from PubNub connect: {}".format( 
        mqtt.connack_string(rc))) 
    # Check whether the result form connect is the CONNACK_ACCEPTED connack code 
    if rc == mqtt.CONNACK_ACCEPTED: 
        Surfboard.active_instance.is_pubnub_connected = True 

def on_disconnect_pubnub(client, userdata, rc): 
    Surfboard.active_instance.is_pubnub_connected = False 
    print("Disconnected from PubNub")

该代码声明了以下以mosquitto前缀结尾的函数:

  • on_connect_mosquitto:这个函数是一旦与 Mosquitto MQTT 服务器建立了成功的连接,就会执行的回调。代码检查rc参数的值,该参数提供 Mosquitto MQTT 服务器返回的CONNACK代码。如果此值匹配mqtt.CONNACK_ACCEPTED,则意味着 Mosquitto MQTT 服务器接受了连接请求,因此代码调用client.subscribe方法,为client参数中接收的 MQTT 客户端订阅surfboards/surfboard01/+主题过滤器,QoS 级别为 0。这样,MQTT 客户端将接收从不同传感器检索的值发送到surfboards/surfboard01/statussurfboards/surfboard01/speedmphsurfboards/surfboard01/altitudefeetsurfboards/surfboard01/temperaturef主题的消息。

  • on_subscribe_mosquitto:当成功完成对surfboards/surfboard01/+主题过滤器的订阅时,将调用此函数。与之前的示例一样,该函数打印一条消息,指示订阅所授予的 QoS 级别。

  • print_received_message_mosquitto:此函数在msg参数中接收一个mqtt.MQTTMessage实例,并打印此消息的主题和负载,以帮助我们理解应用程序中发生的情况。

  • on_status_message_mosquitto:当来自 Mosquitto MQTT 服务器的消息到达surfboards/surfboard01/status主题时,将调用此函数。该函数使用接收到的mqtt.MQTTMessage实例作为参数调用print_received_message_mosquitto函数,并将Surfboard活动实例的status属性值设置为接收到的消息负载转换为int的值。

  • on_speed_mph_message_mosquitto:当来自 Mosquitto MQTT 服务器的消息到达surfboards/surfboard01/speedmph主题时,将调用此函数。该函数使用接收到的mqtt.MQTTMessage实例作为参数调用print_received_message_mosquitto函数,并将Surfboard活动实例的speed_mph属性值设置为接收到的消息负载转换为float的值。

  • on_altitude_feet_message_mosquitto:当从 Mosquitto MQTT 服务器接收到surfboards/surfboard01/altitudefeet主题的消息时,将调用此函数。 该函数使用接收到的mqtt.MQTTMessage实例作为参数调用print_received_message_mosquitto函数,并将Surfboard活动实例的altitude_feet属性值设置为接收到的消息负载的整数转换。

  • on_water_temperature_f_message_mosquitto:当从 Mosquitto MQTT 服务器接收到surfboards/surfboard01/watertemperaturef主题的消息时,将调用此函数。 该函数使用接收到的mqtt.MQTTMessage实例作为参数调用print_received_message_mosquitto函数,并将Surfboard活动实例的water_temperature_f属性值设置为接收到的消息负载的整数转换。

在这种情况下,我们没有一个单独的函数作为回调来处理来自 Mosquitto MQTT 服务器的所有传入消息。 我们为每个特定主题使用一个回调。 这样,我们就不必检查消息的主题以确定我们必须运行的代码。

代码声明了以下以pubnub前缀结尾的函数:

  • on_connect_pubnub:一旦与 PubNub MQTT 服务器建立成功连接,将执行此回调函数。 该代码检查提供 PubNub MQTT 服务器返回的CONNACK代码的rc参数的值。 如果此值与mqtt.CONNACK_ACCEPTED匹配,则表示 PubNub MQTT 服务器接受了连接请求,因此代码将 Surfboard 活动实例的is_pubnub_connected属性值设置为True

  • on_disconnect_pubnub:如果连接到 PubNub MQTT 服务器的客户端失去连接,将执行此回调函数。 该代码将 Surfboard 活动实例的is_pubnub_connected属性值设置为False,并打印一条消息。

使用多个 MQTT 服务器

在主虚拟环境文件夹中的现有surfboard_monitor.py中添加以下行。 以下行声明了主要块。 示例的代码文件包含在mqtt_python_gaston_hillar_06_01文件夹中的surfboard_monitor.py文件中:

if __name__ == "__main__": 
    surfboard = Surfboard(device_id=device_id, 
        status=SURFBOARD_STATUS_IDLE, 
        speed_mph=0,  
        altitude_feet=0,  
        water_temperature_f=0) 
    pubnub_client_id = "{}/{}/{}".format( 
        pubnub_publish_key, 
        pubnub_subscribe_key, 
        device_id) 
    pubnub_client = mqtt.Client(client_id=pubnub_client_id, 
        protocol=mqtt.MQTTv311) 
    pubnub_client.on_connect = on_connect_pubnub 
    pubnub_client.on_disconnect = on_disconnect_pubnub 
    pubnub_client.connect(host=pubnub_mqtt_server_host, 
        port=pubnub_mqtt_server_port, 
        keepalive=pubnub_mqtt_keepalive) 
    pubnub_client.loop_start() 
    mosquitto_client = mqtt.Client(protocol=mqtt.MQTTv311) 
    mosquitto_client.on_connect = on_connect_mosquitto 
    mosquitto_client.on_subscribe = on_subscribe_mosquitto 
    mosquitto_client.message_callback_add( 
        status_topic, 
        on_status_message_mosquitto) 
    mosquitto_client.message_callback_add( 
        speed_mph_topic, 
        on_speed_mph_message_mosquitto) 
    mosquitto_client.message_callback_add( 
        altitude_feet_topic, 
        on_altitude_feet_message_mosquitto) 
    mosquitto_client.message_callback_add( 
        water_temperature_f_topic, 
        on_water_temperature_f_message_mosquitto) 
    mosquitto_client.tls_set(ca_certs = ca_certificate, 
        certfile=client_certificate, 
        keyfile=client_key) 
    mosquitto_client.connect(host=mqtt_server_host, 
        port=mqtt_server_port, 
        keepalive=mqtt_keepalive) 
    mosquitto_client.loop_start() 
    try: 
        while True: 
            if Surfboard.active_instance.is_pubnub_connected: 
                payload = Surfboard.active_instance.build_json_message() 
                result = pubnub_client.publish(topic=pubnub_topic, 
                    payload=payload, 
                    qos=0) 
                print("Publishing: {}".format(payload)) 
            else: 
                print("Not connected") 
            time.sleep(1) 
    except KeyboardInterrupt: 
        print("I'll disconnect from both Mosquitto and PubNub") 
        pubnub_client.disconnect() 
        pubnub_client.loop_stop() 
        mosquitto_client.disconnect() 
        mosquitto_client.loop_stop() 

首先,主要块创建了Surfboard类的实例,并将其保存在surfboard本地变量中。 然后,代码生成了与 PubNub MQTT 接口建立连接所需的客户端 ID 字符串,并将其保存在pubnub_client_id本地变量中。 PubNub MQTT 接口要求我们使用以下组成的客户端 ID:

publish_key/subscribe_key/device_id 

代码使用pubnub_publish_keypubnub_subscribe_keydevice_id变量的值构建了一个符合 PubNub MQTT 接口要求的客户端 ID。 然后,代码创建了一个名为pubnub_clientmqtt.Client类(paho.mqtt.client.Client)的实例,该实例表示 PubNub MQTT 接口客户端。 我们使用此实例与 PubNub MQTT 服务器进行通信。

然后,代码将函数分配给属性。 以下表总结了这些分配:

属性 分配的函数
pubnub_client.on_connect on_connect_pubnub
pubnub_client.on_disconnect on_disconnect_pubnub

然后,代码调用pubnub_client.connect方法,并指定hostportkeepalive参数的值。 这样,代码要求 MQTT 客户端与指定的 PubNub MQTT 服务器建立连接。 调用pubnub_client.connect方法后,代码调用pubnub_client.loop_start方法。 此方法启动一个处理与 PubNub MQTT 接口相关的 MQTT 网络流量的新线程,并释放主线程。

然后,主要块创建了mqtt.Client类(paho.mqtt.client.Client)的另一个实例mosquitto_client,代表 Mosquitto MQTT 服务器客户端。我们使用此实例与本地 Mosquitto MQTT 服务器进行通信。

然后,代码将函数分配给属性。以下表总结了这些分配:

属性 分配的函数
mosquitto_client.on_connect on_connect_mosquitto
mosquitto_client.on_subscribe on_subscribe_mosquitto

请注意,在这种情况下,代码没有将函数分配给mosquitto_client.on_message。接下来的行调用mosquitto_client.message_callback_add方法,以指定客户端在特定主题接收到消息时必须调用的回调函数。以下表总结了根据定义消息到达的主题的变量调用的函数:

主题变量 分配的函数
status_topic on_status_message_mosquitto
speed_mph_topic on_speed_mph_message_mosquitto
altitude_feet_topic on_altitude_feet_message_mosquitto
water_temperature_f_topic on_water_temperature_f_message_mosquitto

每当客户端从任何传感器接收到消息时,它将更新Surfboard活动实例的适当属性。这些分配的函数负责更新Surfboard活动实例的状态。

然后,代码调用了众所周知的mosquitto_client.tls_setmosquitto_client.connect方法。这样,代码要求 MQTT 客户端与指定的 Mosquitto MQTT 服务器建立连接。调用mosquitto_client.connect方法后,代码调用mosquitto_client.loop_start方法。此方法启动一个处理与 Mosquitto MQTT 服务器相关的 MQTT 网络流量的新线程,并释放主线程。

请注意,我们对loop_start进行了两次调用,因此我们将有两个线程处理 MQTT 网络流量:一个用于 PubNub MQTT 服务器,另一个用于 Mosquitto MQTT 服务器。

接下来的行声明了一个while循环,该循环将一直运行,直到发生KeyboardInterrupt异常。循环检查Surfboard.active_instance.is_pubnub_connected属性的值,以确保与 PubNub MQTT 服务器的连接没有中断。如果连接是活动的,代码将调用Surfboard.active_instance.build_json_message方法,根据Surfboard属性的当前值构建 JSON 字符串,这些值在传感器传来具有新值的消息时被更新。

代码将 JSON 字符串保存在payload本地变量中,并调用pubnub_client.publish方法将payload JSON 格式的字符串发布到pubnub_topic变量中保存的主题名称,QoS 级别为 0。这样,负责处理 PubNub MQTT 客户端的 MQTT 网络事件的线程将发布消息,并使用 PubNub MQTT 服务器作为数据源的基于 Web 的仪表板将被更新。下一行打印了正在发布到 PubNub MQTT 服务器的负载的消息。

运行多个客户端

现在,我们将运行最近编写的冲浪板传感器模拟器和冲浪板监视器。确保在运行这些 Python 程序之前,您已经按照必要的步骤激活了我们一直在其中工作的虚拟环境。

在任何您想要用作冲浪板传感器模拟器并使用 Linux 或 macOS 的 MQTT 客户端的计算机或设备上执行以下行以启动冲浪板传感器模拟器示例:

    python3 surfboard_sensors_emulator.py  

在 Windows 中,您必须执行以下行:

    python surfboard_sensors_emulator.py

几秒钟后,您将看到下面显示的输出:

 Result from connect: Connection Accepted.
    surfboards/surfboard01/status: 0
    surfboards/surfboard01/speedmph: 1.0
    surfboards/surfboard01/altitudefeet: 2.0
    surfboards/surfboard01/temperaturef: 58.0
    surfboards/surfboard01/status: 0
    surfboards/surfboard01/speedmph: 1.1
    surfboards/surfboard01/altitudefeet: 2.0
    surfboards/surfboard01/temperaturef: 58.0
    surfboards/surfboard01/status: 1
    surfboards/surfboard01/speedmph: 2.0
    surfboards/surfboard01/altitudefeet: 3.0
    surfboards/surfboard01/temperaturef: 57.0

程序将继续为主题发布消息到 Mosquitto MQTT 服务器。保持代码在您的本地计算机上运行,或者在您选择用作本示例冲浪板传感器模拟器的物联网板上运行。

然后,在任何您想要用作 MQTT 客户端的计算机或设备上执行以下命令,该客户端接收来自 Mosquitto MQTT 服务器的消息并发布消息到 PubNub MQTT 服务器,并使用 Linux 或 macOS:

    python3 surfboard_monitor.py  

在 Windows 中,您必须执行以下命令:

    python surfboard_monitor.py

几秒钟后,您将看到类似下面几行的消息输出。请注意,值将不同,因为您开始运行程序的时间将使值变化:

    Not connected
    Result from Mosquitto connect: Connection Accepted.
    I've subscribed with QoS: 0
    Result from PubNub connect: Connection Accepted.
    Message received. Topic: surfboards/surfboard01/status. Payload: 
    b'3'
    Message received. Topic: surfboards/surfboard01/speedmph. Payload: 
    b'0.0'
    Message received. Topic: surfboards/surfboard01/altitudefeet. 
    Payload: b'3.0'
    Message received. Topic: surfboards/surfboard01/temperaturef. 
    Payload: b'55.0'
    Publishing: {"Status": "Ride finished", "Speed MPH": 0.0, "Altitude 
    Feet": 3.0, "Water Temperature F": 55.0}
    Message received. Topic: surfboards/surfboard01/status. Payload: 
    b'0'
    Message received. Topic: surfboards/surfboard01/speedmph. Payload: 
    b'1.0'
    Message received. Topic: surfboards/surfboard01/altitudefeet. 
    Payload: b'2.0'
    Message received. Topic: surfboards/surfboard01/temperaturef. 
    Payload: b'58.0'
    Publishing: {"Status": "Idle", "Speed MPH": 1.0, "Altitude Feet": 
    2.0, "Water Temperature F": 58.0}
    Message received. Topic: surfboards/surfboard01/status. Payload: 
    b'0'
    Message received. Topic: surfboards/surfboard01/speedmph. Payload: 
    b'1.1'
    Message received. Topic: surfboards/surfboard01/altitudefeet. 
    Payload: b'2.0'
    Message received. Topic: surfboards/surfboard01/temperaturef. 
    Payload: b'58.0'
    Publishing: {"Status": "Idle", "Speed MPH": 1.1, "Altitude Feet": 
    2.0, "Water Temperature F": 58.0}

程序将继续接收来自冲浪板传感器模拟器的消息,并将消息发布到 PubNub MQTT 服务器。保持代码在您的本地计算机上运行,或者在您选择用作本示例冲浪板监视器的物联网板上运行。

下面的屏幕截图显示了在 macOS 计算机上运行的两个终端窗口。左侧的终端显示了作为冲浪板传感器模拟器的 Python 客户端显示的消息,即surfboard_sensors_emulator.py脚本。右侧的终端显示了作为冲浪板监视器的 Python 客户端运行代码的结果,即surfboard_monitor.py脚本:

使用 freeboard 构建基于网络的仪表板

现在,我们准备使用 PubNub MQTT 服务器作为数据源来构建实时的基于网络的仪表板。如前所述,我们将利用 freeboard.io 来在许多表盘中可视化冲浪者和冲浪板的数据。

freeboard.io 要求我们注册并创建一个带有有效电子邮件和密码的账户,然后我们才能构建基于网络的仪表板。我们不需要输入任何信用卡或付款信息。如果您已经在 freeboard.io 上有账户,可以跳过下一步。

在您的网络浏览器中转到freeboard.io,然后点击立即开始。您也可以直接转到freeboard.io/signup。在选择用户名中输入您想要的用户名,在输入您的电子邮件中输入您的电子邮件,在创建密码中输入所需的密码。填写完所有字段后,点击创建我的账户。

创建完账户后,您可以在您的网络浏览器中转到freeboard.io,然后点击登录。您也可以通过访问freeboard.io/login来实现相同的目标。然后,输入您的用户名或电子邮件和密码,然后点击登录。freeboard 将显示您的 freeboard,也称为仪表板。

在创建新按钮的左侧的输入名称文本框中输入Surfboard01,然后单击此按钮。freeboard.io 将显示一个空的仪表板,其中有许多按钮,可以让我们添加窗格和数据源等。下面的屏幕截图显示了空的仪表板:

点击数据源下方的添加,网站将打开数据源对话框。在类型下拉菜单中选择 PubNub,对话框将显示定义 PubNub 数据源所需的字段。

请注意,也可以使用 MQTT 作为 freeboard.io 的数据源。但是,这将要求我们将我们的 Mosquitto MQTT 服务器公开可用。相反,我们利用 PubNub MQTT 接口,它允许我们轻松地在 PubNub 网络上提供消息。但是,在需要 freeboard.io 提供所需功能的项目中,您绝对可以使用 MQTT 服务器作为数据源来工作。

在名称中输入surfboard01

输入你从 PubNub 设置中复制的订阅密钥。请记住,订阅密钥通常以sub-c前缀开头。

在频道中输入surfboard01

如果之前的任何数值名称错误,数据源将无法获得适当的数据。下面的截图显示了 PubNub 数据源的配置,订阅仅显示sub-c前缀:

点击保存,数据源将显示在数据源下方。由于冲浪板传感器模拟器和冲浪板监视器正在运行,所以下方的“最后更新”时间将每秒变化一次。如果时间没有每秒变化,这意味着数据源配置错误,或者 Python 程序中的任何一个未按预期运行。

点击“添加窗格”以在仪表板上添加一个新的空窗格。然后,点击新空窗格右上角的加号(+),freeboard 将显示小部件对话框。

在类型下拉菜单中选择文本,并且对话框将显示添加文本小部件到仪表板窗格所需的字段。在标题中输入Status

在值文本框的右侧点击+数据源,选择 surfboard01,然后选择状态。做出选择后,值文本框中将出现以下文本:datasources ["surfboard01"] ["Status"],如下一截图所示:

然后,点击保存,freeboard 将关闭对话框,并将新的仪表添加到仪表板中之前创建的窗格中。表盘将显示冲浪板监视器最后一次发布到 PubNub MQTT 接口的状态的最新数值,即代码上次发布的 JSON 数据中Status键的数值。下面的截图显示了 surfboard01 数据源显示的最后更新时间,以及仪表显示了状态的最新数值。

点击“添加窗格”以在仪表板上添加另一个新的空窗格。然后,点击新空窗格右上角的加号(+),freeboard 将显示小部件对话框。

在类型下拉菜单中选择仪表,并且对话框将显示添加仪表小部件到仪表板窗格所需的字段。在标题中输入Speed

在值文本框的右侧点击+数据源,选择 surfboard01,然后选择速度 MPH。做出选择后,值文本框中将出现以下文本:datasources ["surfboard01"] ["Speed MPH"]

在单位中输入MPH,最小值为0,最大值为40。然后,点击保存,freeboard 将关闭对话框,并将新的表盘添加到仪表板上之前创建的窗格中。表盘将显示冲浪板监视器最后一次发布到 PubNub MQTT 接口的速度的最新数值,即代码上次发布的 JSON 数据中Speed MPH键的数值。

下面的截图显示了 surfboard01 数据源显示的最后更新时间,以及添加的仪表显示了 mph 速度的最新数值。

点击“添加窗格”以在仪表板上添加另一个新的空窗格。然后,点击新空窗格右上角的加号(+),freeboard 将显示小部件对话框。

在类型下拉菜单中选择仪表,并且对话框将显示添加仪表小部件到仪表板窗格所需的字段。在标题中输入Altitude

在值文本框的右侧点击+数据源,选择 surfboard01,然后选择海拔英尺。做出选择后,值文本框中将出现以下文本:datasources ["surfboard01"] ["Altitude Feet"]

在单位中输入“英尺”,在最小值中输入0,在最大值中输入30。然后,单击“保存”,freeboard 将关闭对话框,并将新的仪表添加到仪表板上以前创建的窗格中。仪表将显示冲浪板监视器最后一次发布到 PubNub MQTT 接口的海拔值,即代码为Altitude Feet键发布的 JSON 数据的最新值。

现在,我们将添加最后一个窗格。单击“添加窗格”以在仪表板上添加另一个新的空窗格。然后,单击新空窗格右上角的加号(+),freeboard 将显示小部件对话框。

在类型下拉菜单中选择仪表,对话框将显示添加仪表小部件到仪表板上的窗格所需的字段。在标题中输入“水温”。

在值文本框的右侧点击+数据源,选择 surfboard01,然后选择 Water Temperature F。在进行选择后,值文本框中将显示以下文本:datasources ["surfboard01"] ["Water Temperature F"]

在单位中输入“ºF”,在最小值中输入0,在最大值中输入80。然后,单击“保存”,freeboard 将关闭对话框,并将新的仪表添加到仪表板上以前创建的窗格中。仪表将显示冲浪板监视器最后一次发布到 PubNub MQTT 接口的水温,即代码为Water Temperature F键发布的 JSON 数据的最新值。

拖放窗格以找到布局中显示的窗格。屏幕截图显示了我们使用四个窗格和三个仪表构建的仪表板,当我们的冲浪板监视器向 PubNub MQTT 接口发布数据时,这些仪表会每秒自动刷新数据。

我们可以通过输入 Web 浏览器在我们使用仪表板时显示的 URL 来访问最近构建的仪表板。该 URL 由https://freeboard.io/board/前缀后跟字母和数字组成。例如,如果 URL 是https://freeboard.io/board/EXAMPLE,我们只需在任何连接到互联网的设备或计算机上的任何 Web 浏览器中输入它,我们就可以观看仪表,并且当新数据从我们的冲浪板监视器发布时,它们将被刷新。

将 PubNub 作为我们的数据源,将 freeboard.io 作为我们的基于 Web 的仪表板,使我们能够轻松监视从冲浪者潜水服和冲浪板传感器检索的数据。我们可以在任何提供 Web 浏览器的设备上监视数据。这两个基于云的 IoT 服务的组合只是我们如何可以轻松地将不同的服务与 MQTT 结合在我们的解决方案中的一个例子。

测试你的知识

让我们看看你是否能正确回答以下问题:

  1. PubNub MQTT 接口要求我们使用以下格式组成的客户端 ID:

  2. publish_key/subscribe_key/device_id

  3. device_id/publish_key/subscribe_key

  4. publish_key/device_id

  5. 当我们向 PubNub MQTT 接口发布消息时:

  6. 它仅在 PubNub MQTT 子网络上可用

  7. 它在 PubNub 网络上可用

  8. 需要特定的有效负载前缀才能在 PubNub 网络上使用

  9. 以下paho.mqtt.client.Client实例的哪种方法允许我们指定客户端在特定主题接收消息时必须调用的回调函数:

  10. message_callback_add

  11. message_arrived_to_topic_callback

  12. message_on_topic

摘要

在本章中,我们将前几章学到的知识结合起来,使用 freeboard 构建了一个基于 Web 的仪表板,每秒显示仪表中的数据。我们从头开始构建了解决方案。首先,我们分析了要求,了解了嵌入在冲浪板中的 IoT 板将如何为我们提供必要的数据。

我们编写了一个冲浪板传感器模拟器,以与物联网板相同的方式工作。然后,我们配置了 PubNub MQTT 接口,并编写了一个冲浪板监视器,收集来自冲浪板传感器模拟器的数据,并将数据发布到基于云的 PubNub MQTT 接口。我们编写了一个 Python 程序,与两个 MQTT 客户端一起使用两个线程循环接口。

最后,我们可以利用这样一个事实:发布到 PubNub MQTT 接口的消息也可以在 PubNub 网络上轻松构建一个基于 web 的仪表板,使用 freeboard。

我们能够创建能够在最流行和强大的物联网板上运行的代码。我们准备在各种项目中使用 MQTT,使用最流行和多功能的编程语言之一:Python 3。

第七章:解决方案

第一章:安装 MQTT 3.1.1 Mosquitto 服务器

问题 答案
Q1 2
Q2 1
Q3 3
Q4 3
Q5 2

第二章:使用命令行和 GUI 工具学习 MQTT 的工作原理

问题 答案
Q1 2
Q2 3
Q3 1
Q4 3
Q5 3

第三章:保护 MQTT 3.1.1 Mosquitto 服务器

问题 答案
Q1 3
Q2 3
Q3 1
Q4 2
Q5 1

第四章:使用 Python 和 MQTT 消息编写控制车辆的代码

问题 答案
Q1 1
Q2 2
Q3 2
Q4 2
Q5 3

第五章:在 Python 中测试和改进我们的车辆控制解决方案

问题 答案
Q1 1
Q2 2
Q3 3
Q4 3
Q5 1

第六章:使用基于云的实时 MQTT 提供者和 Python 监控冲浪比赛

问题 答案
Q1 1
Q2 2
Q3 1
posted @ 2024-05-04 21:27  绝不原创的飞龙  阅读(582)  评论(0编辑  收藏  举报