反应式系统实现MQTT客户机
反应式系统实现MQTT客户机
Implementing an MQTT client for reactive systems
MQTT Reactive是从LiamBindle的MQTT-C库派生的MQTT v3.1.1客户机。MQTT-Reactive的目的是提供一个用C语言编写的可移植、无阻塞的MQTT客户机,以便在反应式嵌入式系统中使用。首先,本文解释了什么是反应式系统。然后,介绍了如何设计适合该类系统的软件结构。最后,本文展示了如何通过使用状态机和事件驱动范式在反应式系统中使用MQTT反应式库。为了做到这一点,本文以一个实际的物联网设备为例,通过使用UML图(如状态机、交互和结构)来解释其软件结构和基于状态的行为。本文还提供了用C语言实现IoT设备的MQTT反应式客户端的指南。
许多嵌入式系统是被动的,即它们对内部或外部事件做出反应。一旦这些反应完成,软件就会返回等待下一个事件。这就是为什么事件驱动系统被称为反应系统。
事件驱动编程,或称反应式编程,是实现反应式系统灵活、可预测和可维护软件的最合适的编程范式之一。在这个范例中,程序的流程是由事件决定的。通常,反应式软件的结构是由多个并发单元(称为活动对象)组成,这些单元等待并处理不同类型的事件。每个活动对象都拥有一个控制线程和一个事件队列,通过它来处理传入的事件。在反应式系统中,活动对象通常具有状态图中定义的基于状态的行为。
为了探索如何在具有多个并发任务的反应式系统中使用MQTT反应库,同时使用状态机和事件驱动范式,我们以物联网设备为例。
使用MQTT协议的想法是在一家铁路公司开发物联网设备时产生的。该装置是一个清晰的反应系统,能够:
检测并存储多个数字输入的变化
采集、滤波和存储多个模拟信号
定期将存储的信息发送到远程服务器
在GSM网络上通过MQTT协议发送和接收信息
之所以选择MQTT,是因为它是一种基于发布者和订户的轻量级消息传递协议,通常用于物联网和网络应用中,在这些应用中,预期会出现高延迟和低数据速率链路,例如GSM网络。
上述物联网设备的MQTT功能是通过使用LiamBindle的MQTT-C的修改版本实现的。由于该设备的软件设计为反应式软件,因此必须修改MQTT-C,以便通过交换异步事件与系统的其余部分进行通信。这些事件用于通过网络接收和发送流量,以及连接敏感信息并将其发布到服务器。生成的软件库称为MQTT Reactive。
状态机
MQTT Reactive是通过一个状态机使用的,如图1所示,该状态机为MQTT反应式客户机的基本行为建模。它是一个名为MqttMgr(MQTT管理器)的活动对象。图1中的状态机操作演示了如何从状态机使用MQTT反应库。即使在图1中使用C语言作为动作语言,也可以使用任何计算机或形式语言。
Figure 1. State machine of an MQTT-Reactive client
图1中的状态机以WaitingForNetConnection状态启动。当网络连接建立到等待状态后,服务器将接收到等待连接状态。只有在这种状态下,状态机才能分别通过CONNECT和PUBLISH事件将MQTT消息发送到代理,例如CONNECT或PUBLISH。同步状态使用UML的特殊机制来延迟发布事件,发布事件由同步状态的内部分区中包含的defer关键字指定。如果发布事件在Sync为当前状态时发生,则它将被保存(延迟)以供将来处理,直到SM进入发布事件不在其延迟事件列表(例如WaitingForSync或WaitingForNetConnection)中的状态。进入这些状态后,状态机将自动调用任何保存的发布事件,然后根据转换目标状态使用或丢弃此事件。
每同步一毫秒,状态机就转换到Sync composite状态,该状态通过向网络管理器发布接收和发送事件来实际发送和接收来自网络的流量。它是一个处理网络问题的并发实体。
尽管引入的MqttMgr只支持CONNECT和PUBLISH包,但它可以通过相当简单的更改来支持SUBSCRIBE包。
状态机操作使用params关键字访问已消费事件的参数。例如,在以下转换中,Connect事件携带两个参数clientId和keepAlive,它们的值用于更新相应的MqttMgr对象的属性:
Connect(clientId, keepAlive)/
me->clientId = params->clientId;
me->keepAlive = params->keepAlive;
me->operRes = mqtt_connect(&me->client, me->clientId, NULL, NULL, 0,
NULL, NULL, 0, me->keepAlive);
在本例中,Connect(clientId,keepAlive)事件是转换的触发器,mqtt_Connect()调用是作为结果执行的操作的一部分。换句话说,当MqttMgr对象接收到参数为“publishing_client”和“400”,Connect(“publishing_client”,400)的Connect(clientId,keepAlive)事件时,MqttMgr的clientId和keepAlive属性会相应地更新为值“publishing\u client”和“400”。
为了创建和发送事件,状态机的操作使用GEN()宏。例如,以下语句将接收事件发送到收集器对象,该对象被其收集器指针引用为MqttMgr对象的属性:
GEN(me->itsCollector, Receive());
GEN()语句的第一个参数是接收事件的对象,而第二个参数是要发送的事件,包括事件参数(如果有)。参数必须与事件参数一致。例如,以下语句生成一个ConnRefused(code)事件,并将其发送到收集器对象,将代理返回的代码作为事件参数传递:
GEN(me->itsCollector, ConRefused(code));
使用params关键字访问所消费事件的参数和GEN()宏从操作中生成事件的想法是从Rational Rhapsody Developer’s code开发人员的代码生成器中纯粹出于说明目的而采用的。
图1中状态机的默认操作设置每当从代理接收到连接接受时MQTT Reactive调用的回调。此回调应在MqttMgr代码中实现。此回调必须生成ConnAccepted or ConnRefused(code)事件,以便发送到收集器对象,如下所示。
static void
connack_response_callback(enum MQTTConnackReturnCode return_code)
{
/*...*/
if (return_code == MQTT_CONNACK_ACCEPTED)
{
GEN(me->itsCollector, ConnAccepted());
}
else
{
GEN(me->itsCollector, ConnRefused(return_code));
}
}
模型实施
图1中的模型可以用C或C++实现,或者使用您最喜欢的软件工具,或者只使用自己的状态机实现。在因特网上有不同的工具可以实现这一点,例如RKH框架、QP框架、Yakindu Statechart工具或RationalRhapsody Developer等等。它们都支持状态图和C/C++语言。此外,其中一些还包括绘制状态图并从中生成代码的工具。
此状态机是从名为MqttMgr(MQTT管理器)的活动对象执行的,它提供了MQTT反应式代码的严格封装,并且它是唯一允许调用任何MQTT反应式函数或访问MQTT反应式数据的实体。系统中的其他并发实体以及任何isr只能通过与MqttMgr交换事件来间接使用MQTT Reactive。使用这种机制来同步并发实体并在它们之间共享数据避免了传统阻塞机制(如信号量、互斥体、延迟或事件标志)的危险。这些机制可能会导致难以诊断和修复的意外故障。 MqttMgr活动对象将其属性封装为一组数据项。数据项用名称和类型指定变量,其中类型实际上是数据类型。MqttMgr对象的数据项映射到对象结构的成员。成员的名称和类型与对象数据的名称和类型相同。例如,MqttMgr对象类型的client属性作为数据成员嵌入到MqttMgr结构中:
struct MqttMgr
{
/* ... */
struct mqtt_client client; /* attribute client */
LocalRecvAll localRecv; /* attribute localRecv */
};
直接访问和修改MqttMgr对象的数据,而不使用访问器或赋值器操作。例如,客户机和localRecv通过me指针访问,该指针指向MqttMgr的实例。
mqtt_recvMsgError(&me->client, &me->localRecv);
MqttMgr具有表1中显示的属性列表。
图2中的结构有助于记住相关参与者之间的关系。它们是:Collector对象,它希望向代理发送信息;NetMgr对象,它处理网络;以及MqttMgr对象。
Figure 2. Draft of IoT system structure
图3中的序列图显示了当需要MqttMgr对象打开与MQTT服务器的会话时,它是如何与系统其余部分交互的。在此图中,MqttMgr状态和交换的异步消息显示在收集器、MqttMgr和NetMgr参与者之间。
NetMgr对象建立到代理的网络连接后,从MqttMgr发送到MQTT服务器的第一个数据包必须是CONNECT数据包。因此,收集器actor向MqttMgr actor发送一个Connect(clientId,keepAlive)事件。此事件必须携带客户端标识符和保持活动时间间隔。如果服务器接受连接请求,MqttMgr actor将向收集器actor发送ConnAccepted事件以通知此情况。从那时起,收集器参与者可以向该代理发布信息消息。
如果服务器拒绝连接请求,MqttMgr actor将向收集器actor发送一个conndrekend事件。此事件附带一个代码,用于通知拒绝原因,如图4所示。
Figure 4. Broker rejects a connection request
图5显示了消息发布时的交互流。为了做到这一点,收集器参与者发送一个发布(data,size,topic,qos)事件,该事件携带要发布的信息(data)、信息的长度(以字节为单位)、信息将被发布到的主题名称(topic)以及传递该消息的保证级别(qos)。在前面提到的IoT设备中,发布的信息是使用JSON规范格式化的。它是一种开放的标准格式,在人类可读文本中包含具有属性值对的数据对象。这种格式是使用jWrite实现的,jWrite是一个用C编写的简单而轻量级的库。
Figure 5. Publishing data to a broker
图6显示了一个场景,其中MQTT消息的接收和发送失败。如果网络管理器无法从网络接收流量,它将向MqttMgr actor发送ReceiveFail。类似地,如果网络管理器不能向网络发送数据,它将向MqttMgr actor发送SendFail。
Figure 6. Failures in network
表2总结了所示场景中涉及的事件。
结论
通过避免传统阻塞机制(如信号量、互斥、延迟或事件标志)的危险,本文提出的MQTT反应库、状态机和软件体系结构允许反应式嵌入式系统以新颖的方式实现MQTT客户机。它是通过将MQTT反应式代码封装在称为active object的并发单元中实现的,active object基于状态的行为在建议的状态机中定义。此活动对象通过交换异步事件与系统的其余部分进行通信:不仅用于在网络上接收和发送流量,还用于将信息连接并发布到用于物联网应用程序的服务器。