
为 MetaTrader 5 开发MQTT客户端:TDD方法——第3部分
“如果你不知道所有的代码都能工作,你怎么能认为自己是专业人士?如果每次进行更改时都不进行测试,你怎么能知道所有的代码都有效呢?如果没有覆盖率很高的自动化单元测试,如何在每次进行更改时都对其进行测试?在不练习TDD的情况下,如何获得覆盖率非常高的自动化单元测试?”(Robert‘Uncle Bob’Martin,The Clean Coder,2011)
概述
到目前为止,在第1部分和本系列的第2部分中,我们一直在处理MQTT协议的非操作部分的一小部分。我们在两个独立的头文件中组织了所有的协议定义、枚举和一些将在类之间共享的公共函数。此外,我们编写了一个接口作为对象层次结构的根,并在一个类中实现了它,其唯一目的是构建一个一致的MQTT CONNECT数据包。同时,我们一直在为构建数据包所涉及的每个函数编写单元测试。尽管我们将生成的数据包发送到本地MQTT代理,以检查它是否会被识别为格式良好的MQTT数据包,但从技术上讲,这一步骤是不需要的。由于我们使用固定数据来提供函数参数,所以我们知道我们是在孤立地测试它们,我们知道我们以独立于状态的方式测试它们。这很好,我们将努力继续以这种方式编写我们的测试,从而编写我们的函数。这将使我们的代码更加灵活,允许我们在不更改测试代码的情况下更改函数实现,只要我们有相同的函数签名。
从现在起,我们将处理MQTT协议的操作部分。毫不奇怪,它在OASIS标准中被命名为Operational Behavior(操作行为)。也就是说,从现在起,我们需要处理从服务器发送的数据包。我们的客户端必须能够识别服务器数据包类型及其语义,并且必须能够在给定的上下文中选择适当的行为,在每个可能的客户端状态中选择合适的行为。
为了能够处理此任务,我们必须在响应的第一个字节中识别服务器数据包类型。如果它是CONNACK数据包,我们必须读取其连接原因代码(Connect Reason Code),并做出相应的反应。
(CONNECT)设置客户端连接标志
当我们的客户端请求与服务器连接时,它必须通知服务器
- 代理的一些期望的能力,
- 是否它将需要使用用户名和密码进行身份验证,
- 以及该连接是否是新会话,还是它正在恢复先前打开的会话。
这是通过在“Protocol Name(协议名称)”和“Protocol Version(协议版本)”之后的 Variable Header (变量头)的开头设置一些位标志来实现的。CONNECT 数据包上的这些位标志被命名为 CONNECT Flags (连接标志)。
请记住,位标志是布尔值。它们可能被赋予不同的名称或表示,但布尔值只有两个可能的值,通常是true或false。
图01-用于表示布尔值的常用术语
OASIS标准一致使用1(一)和0(零)。我们将在这里大部分时间使用true和false,最终,我们将使用 set 和 unset。这将使文本更具可读性。此外,我们的公共API始终使用true和false来设置这些值,因此使用这些术语应该会使关注库开发的读者更容易理解本文。
图02 - OASIS连接标志位
正如您在上图中的OASIS表中所看到的,第一个位(bit_0)是保留的,我们必须将其单独保留:置零、未检查、布尔值为false、未设置。如果我们设置它,我们将有一个格式不正确的数据包(Malformed Packet)。
Clean Start (bit_1)
我们可以设置的第一个位是第二个位。它用于设置“Clean Start”标志——如果为true,服务器将执行“干净启动(Clean Start)”并丢弃与我们的客户端标识符相关联的任何现有会话。服务器将启动一个新会话。如果未设置,服务器将恢复我们以前的会话(如果有),或者如果没有与我们的客户端标识符关联的现有会话,则启动新会话。
这就是我们现在设置/取消设置此标志的函数。
void CPktConnect::SetCleanStart(const bool cleanStart) { cleanStart ? m_connect_flags |= CLEAN_START : m_connect_flags &= ~CLEAN_START; ArrayFill(ByteArray, ArraySize(ByteArray) - 1, 1, m_connect_flags); }
我们通过逐位运算来切换值。我们使用三元运算符来切换布尔赋值和复合赋值,以使代码更加紧凑。然后我们将结果存储在m_connect_flags私有类成员中。最后,我们通过调用内置函数ArrayFill,用新值更新表示CONNECT数据包的字节数组。(请注意,使用数组填充的最后一步是我们稍后可能会更改的实现细节。)
我们的一个测试中的这一行显示了它是如何被调用的。
CPktConnect *cut = new CPktConnect(buf); //--- Act cut.SetCleanStart(true);
Bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
User Name Flag | Password Flag | Will Retain | Will QoS 2 | Will QoS 1 | Will Flag | Clean Start |
| |
X | X | X | X | X | X | 1 | 0 |
表01-位标志Clean Start(bit_1)设置为true–MQTT v5.0
我们将广泛使用此模式来切换布尔标志:三元运算符和带复合赋值的逐位运算。
以下三个名为 Will ‘Something’ 的标志 旨在表达我们对服务器具有某些功能的愿望。他们通知服务器,我们“愿意”服务器能够
- 存储 Will 消息,并将其与我们的客户端会话相关联(稍后将对此进行详细介绍);
- 提供特定的QoS级别,通常高于QoS 0,如果没有设置,则为默认值;
- 保留 Will 信息,并在 Will 信息设置为true的情况下将其发布为“retained(保留)”(见下文)
Will Flag (bit_2)
第三位用于设置Will Flag –如果设置为true,我们的客户端必须提供“在网络连接未正常关闭的情况下”发布的Will 消息。人们可以将Will 消息视为一种客户在面对订阅者时死去后的“遗言”。
void CPktConnect::SetWillFlag(const bool willFlag) { willFlag ? m_connect_flags |= WILL_FLAG : m_connect_flags &= ~WILL_FLAG; ArrayFill(ByteArray, ArraySize(ByteArray) - 1, 1, m_connect_flags); }
它的调用方式与上一个函数相同。
//--- Act CPktConnect *cut = new CPktConnect(buf); cut.SetWillFlag(true);
Bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
User Name Flag | Password Flag | Will Retain | Will QoS 2 | Will QoS 1 | Will Flag | Clean Start |
| |
X | X | X | X | X | 1 | X | 0 |
表02-位标志 Will Flag(bit_2)设置为true–MQTT v5.0
Will QoS (bit_3, bit_4)
与前面的两个标志不同,如果客户端请求QoS级别2,则此功能需要设置两个位,即第四和第五位。QoS代表服务质量(Quality of Service),可以是三者之一。
图 03 - OASIS QoS 定义
从最不可靠到最可靠的交付系统,它们是:
QoS 0
QoS 0最多设置一次交付。这是一种“发射就结束”,发送人将尝试一次,消息可能会丢失。没有来自服务器的确认。这是默认值,也就是说,如果在位3和4上没有设置任何内容,则客户端请求的QoS级别为QoS 0。
QoS 1
QoS 1 设置至少传递一次。它有一个确认收到的 PUBACK。
相同的函数定义模式。
void CPktConnect::SetWillQoS_1(const bool willQoS_1) { willQoS_1 ? m_connect_flags |= WILL_QOS_1 : m_connect_flags &= ~WILL_QOS_1; ArrayFill(ByteArray, ArraySize(ByteArray) - 1, 1, m_connect_flags); }
相同的函数调用模式。
//--- Act CPktConnect *cut = new CPktConnect(buf); cut.SetWillQoS_1(true);
Bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
User Name Flag | Password Flag | Will Retain | Will QoS 2 | Will QoS 1 | Will Flag | Clean Start |
| |
X | X | X | X | 1 | X | X | 0 |
表03 - 位标志 Will QoS 1(bit_3)设置为true–MQTT v5.0
QoS 2
QoS 2设置精确传递一次。这种QoS要求没有丢失或重复。发送者将使用PUBREC确认消息,并使用PUBREL确认发送。
人们可以把这个级别看作是发送一个注册的包裹。当你把包裹转交给他们时,邮政系统会给你一张收据,确认从现在起,他们有责任把包裹送到正确的地址。当这种情况发生时,当他们递送包裹时,他们会向你发送一张收件人签名的收据,确认包裹已送达。
Idem.
void CPktConnect::SetWillQoS_2(const bool willQoS_2) { willQoS_2 ? m_connect_flags |= WILL_QOS_2 : m_connect_flags &= ~WILL_QOS_2; ArrayFill(ByteArray, ArraySize(ByteArray) - 1, 1, m_connect_flags); }
Ibidem.
//--- Act CPktConnect *cut = new CPktConnect(buf); cut.SetWillQoS_2(true);
Bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
User Name Flag | Password Flag | Will Retain | Will QoS 2 | Will QoS 1 | Will Flag | Clean Start |
| |
X | X | X | 1 | X | X | X | 0 |
表04-位标志 Will QoS 2(bit_4)设置为true–MQTT v5.0
服务器将在CONNACK原因代码和CONNACK属性中告诉我们其接受的最大QoS级别。客户端可以请求,但服务器功能是必需的。如果我们收到具有最大QoS的CONNACK,我们必须遵守此服务器限制,并且不能发送具有更高QoS的PUBLISH。否则,服务器将断开连接(DISCONNECT)。
QoS 2是MQTT v5.0上可用的最高QoS级别,并且由于传递协议是对称的,这意味着任何一方(服务器和客户端)都可以同时作为发送方或接收方,因此与此相关的开销很大。
注意:从用户的角度来看,我们可以说QoS是协议的核心。它定义了应用程序配置文件,并影响了协议的数十个其他方面。因此,我们将在PUBLISH数据包实现的上下文中深入研究QoS级别及其设置。
值得注意的是,QoS 1和QoS 2对于客户端实现是可选的。正如OASIS在非规范性评论中所说:
“客户端不需要支持QoS 1或QoS 2 PUBLISH数据包。如果是这种情况,客户端只需将其发送的任何SUBSCRIBE命令中的最大QoS字段限制为其可以支持的值。”
Will RETAIN (bit_5)
在第六位中,我们设置了Will Retain标志。这个标志与上面的 Will Flag 是联系在一起的,
- 如果 Will Flag 未设置,那么 Will Retain 也必须未设置。
- 如果设置了Will Flag且未设置Will Retain,则服务器会将Will Message发布为非保留消息。
- 如果两者都设置了,服务器将把 Will 消息作为保留消息发布。
由于函数定义和函数调用模式与前面的两个标志完全相同,为了简洁起见,省略了这两个标志和后面两个标志中的代码。有关详细信息和测试,请参阅附件。
Bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
User Name Flag | Password Flag | Will Retain | Will QoS 2 | Will QoS 1 | Will Flag | Clean Start |
| |
X | X | 1 | X | X | X | X | 0 |
表05 - 位标志 Will Retain(bit_5)设置为true–MQTT v5.0
在开始发送PUBLISH数据包之前,我们必须等待CONNACK数据包检查此标志。如果服务器接收到“Will Return”设置为1的PUBLISH数据包,并且不支持保留消息,则服务器将断开连接。你可能在想:但等等?是否可以在收到CONNACK数据包之前就开始发布?是的,这是可能的。标准允许这种行为。但它们也对此发表了评论:
“在接收CONNACK之前发送MQTT控制数据包的客户端将不知道服务器约束”
因此,在发送任何将 Will Retain 设置为1(一)的PUBLISH数据包之前,我们必须检查CONNACK数据包上的此标志。
Password flag (bit_6)
在第七位中,我们通知服务器我们是否将在Payload中发送密码。如果设置了此标志,Payload 中必须存在密码字段。如果未设置,则Payload中不得存在密码字段。
“此版本的协议允许发送不带用户名的密码,而MQTT v3.1.1没有。这反映了密码对密码以外的凭据的常见使用。”(OASIS标准,3.1.2.9)
Bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
User Name Flag | Password Flag | Will Retain | Will QoS 2 | Will QoS 1 | Will Flag | Clean Start |
| |
X | 1 | X | X | X | X | X | 0 |
表06-位标志 Password Flag(bit_6)设置为true–MQTT v5.0
User Name flag (bit_7)
最后,在8位上,我们通知服务器我们是否要在Payload中发送用户名(User Name)。与上面的密码标志一样,如果设置了此标志,则 Payload 中必须存在用户名字段。否则,Payload 中不得存在用户名字段。
Bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
User Name Flag | Password Flag | Will Retain | Will QoS 2 | Will QoS 1 | Will Flag | Clean Start |
| |
1 | X | X | X | X | X | X | 0 |
表07-位标志 User Name(bit_7)设置为true–MQTT v5.0
因此,一个具有以下位序列的连接标志字节…
Bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
User Name Flag | Password Flag | Will Retain | Will QoS 2 | Will QoS 1 | Will Flag | Clean Start |
| |
X | X | 1 | 1 | X | 1 | 1 | 0 |
表08-为Clean Start、Will Flag、Will QoS2和Will Retain设置的位标志–MQTT v5.0
…可以翻译为:为QoS级别为2的新会话打开连接,并准备存储我的 Will 消息并将其作为保留消息发布。顺便说一句,服务器先生,我不需要使用用户名和密码进行身份验证。
如果它能满足我们的意愿,服务器会很乐意回答。它可能能够完全、部分或根本不实现它们。服务器将以CONNACK数据包中的连接原因代码的形式发送其答案。
(CONNACK)获取与连接标志相关的原因代码
MQTT v5.0中有四十四个原因代码(Reason Codes),我们已经在Defines.mqh标题中收集了它们。CONNACK(和其他数据包类型)有一个单独的原因代码作为变量头的一部分,它们被命名为“连接原因代码”(Connect Reason Codes)。
数值 | 十六进制值 | 原因代码名称 | 描述 |
---|---|---|---|
0 | 0x00 | Success | 已接受连接。 |
128 | 0x80 | Unspecified error | 服务器不希望透露失败的原因,或者其他原因代码都不适用。 |
129 | 0x81 | Malformed Packet | CONNECT数据包中的数据无法正确解析。 |
130 | 0x82 | Protocol Error | CONNECT数据包中的数据不符合此规范。 |
131 | 0x83 | Implementation specific error | CONNECT有效,但不被此服务器接受。 |
132 | 0x84 | Unsupported Protocol Version | 服务器不支持客户端请求的MQTT协议版本。 |
133 | 0x85 | Client Identifier not valid | 客户端标识符是一个有效的字符串,但服务器不允许使用。 |
134 | 0x86 | Bad User Name or Password | 服务器不接受客户端指定的用户名或密码 |
135 | 0x87 | Not authorized | 客户端无权连接。 |
136 | 0x88 | Server unavailable | MQTT服务器不可用。 |
137 | 0x89 | Server busy | 服务器正忙,请稍后再试。 |
138 | 0x8A | Banned | 此客户端已被管理方禁止行动,请与服务器管理员联系。 |
140 | 0x8C | Bad authentication method | 不支持该身份验证方法,或者该方法与当前使用的身份验证方法不匹配。 |
144 | 0x90 | Topic Name invalid | Will 主题名称格式正确,但此服务器不接受该名称。 |
149 | 0x95 | Packet too large | CONNECT数据包超出了允许的最大大小。 |
151 | 0x97 | Quota exceeded | 已超过执行或管理方强制限制。 |
153 | 0x99 | Payload format invalid | Will Payload与指定的Payload Format Indicator不匹配。 |
154 | 0x9A | Retain not supported | 服务器不支持保留邮件,而“Will Retain”设置为1。 |
155 | 0x9B | QoS not supported | 服务器不支持Will QoS中设置的QoS。 |
156 | 0x9C | Use another server | 客户端应暂时使用另一台服务器。 |
157 | 0x9D | Server moved | 客户端应永久使用另一台服务器。 |
159 | 0x9F | Connection rate exceeded | 已超过连接速率限制。 |
表08- 连接原因代码值
该标准明确规定了服务器需要在CONNACK上发送连接原因代码:
“发送CONNACK数据包的服务器必须使用其中一个连接原因代码值[MQTT-3.2.2-8]。”
在这一点上,我们对连接原因代码特别感兴趣。因为在进行沟通之前,我们需要对它们进行检查。它们将告知我们一些服务器功能和限制,如可用的QoS级别和保留消息的可用性。此外,正如您在上表中他们的姓名和描述中所看到的,他们会通知我们CONNECT尝试是否成功。
为了获得原因码,首先,我们需要识别数据包类型,因为我们只对CONNACK数据包感兴趣。
我们将利用这样一个事实,即我们需要一个非常简单的函数来获取数据包类型,来描述我们如何使用测试驱动开发,对该技术进行一些推理,并提供几个简短的示例。您可以在附件中获取所有详细信息。
(CONNACK)识别服务器数据包类型
我们确信,任何MQTT控制数据包的第一个字节都会对数据包的类型进行编码。因此,我们将尽快读取第一个字节,并获得服务器数据包类型。
uchar pkt_type = server_response_buffer[0];
停下,完成,下一个问题。对吗?
当然,这并没有错。代码很清楚,变量的名称也很好,而且它应该是高性能的和轻量级的。
但是等等!将使用我们库的代码应该如何调用此语句?数据包类型将由公共函数调用返回?或者这些信息可以作为实现细节隐藏在私有成员后面?如果它是由函数调用返回的,那么这个函数应该放在哪里?在CPktConnect类里面吗?或者它应该托管在我们的任何头文件中,因为它将被许多不同的类使用?如果它存储在一个私有成员中,它应该放在在哪个类中?
“有不止一种方法可以做到这一点”(TMTOWTD*,There is more than one way to do it)是一个非常流行的缩写词。TDD是另一个非常流行的缩写词,原因各不相同。正如过去一样,这两个缩写词都被滥用和炒作,其中一些甚至成为了“时尚”,TDD就是这样:
__ “I’m tddying, mom! It’s cool.”
但是,创造这些代码的团队是在多年努力解决同一个基本问题后才做到这一点的:如何在提高开发人员生产力的同时编写更高性能、更惯用、更健壮的代码?如何让开发人员专注于必须做的事情,而不是徘徊在可能做的事情上?如何让他们每个人一次只专注于一项任务?如何确保他们所做的不会引入回归错误并破坏系统?
总之,这些缩写词、它们所承载的思想以及它们所推荐的技术,巩固了数百名在软件开发方面具有专业知识的不同个人多年的实践。TDD不是一种理论,而是一种实践。我们可以说,TDD是一种通过将问题分解为其组成部分来缩小范围来解决问题的技术。我们必须确定将使我们前进一步的单一任务。只需一步,经常是一小步。
那么,我们现在的问题是什么?我们需要确定服务器响应是否是CONNACK数据包,就这么简单。因为,根据规范,我们需要阅读CONNACK响应代码来决定下一步要做什么。我的意思是,识别我们从服务器接收的数据包类型作为响应是我们从连接状态前进到发布状态所必需的。
我们如何识别服务器响应是否是CONNACK数据包?嗯,这很容易。它有一个特定的类型,在我们的MQTT.mqh头中编码为枚举,即ENUM_PKT_TYPE。
//+------------------------------------------------------------------+ //| MQTT - CONTROL PACKET - TYPES | //+------------------------------------------------------------------+ /* Position: byte 1, bits 7-4. Represented as a 4-bit unsigned value, the values are shown below. */ enum ENUM_PKT_TYPE { CONNECT = 0x01, // Connection request CONNACK = 0x02, // Connection Acknowledgment PUBLISH = 0x03, // Publish message ...
因此,也许我们可以从一个函数开始,该函数在传递来自MQTT代理的网络字节数组时返回数据包的类型。
听起来不错。让我们为这个函数写一个测试。
bool TEST_GetPktType_FAIL() { Print(__FUNCTION__); //--- Arrange uchar expected[] = {(uchar)CONNACK}; uchar result[1] = {}; uchar wrong_first_byte[] = {'X'}; //--- Act CSrvResponse *cut = new CSrvResponse(); ENUM_PKT_TYPE pkt_type = cut.GetPktType(wrong_first_byte); ArrayFill(result,0,1,(uchar)pkt_type); //--- Assert bool isTrue = AssertNotEqual(expected, result); //--- cleanup delete cut; ZeroMemory(result); return isTrue ? true : false; }
让我们试着理解测试函数,因为我们将在所有测试中使用此模式,为了简洁起见,我们不会在这里详细说明所有测试。
在评论行中,我们描述了模式的每个步骤:
Arrange(安排)
首先,我们用一个元素初始化数组,该元素是我们期望函数返回的字节值。
其次,我们初始化一个大小为1的空char缓冲区,以接收函数调用的结果。
我们初始化我们的fixture以传递给我们正在测试的函数。它代表从服务器响应固定头中读取的第一个单字节,以识别数据包类型。在这种情况下,它有一个wrong_first_byte,我们通过相应地命名变量来使其更明显。
Act(行动)
我们实例化测试中的类(cut)并调用我们的函数。
Assert(断言)
我们使用MQL5 ArrayCompare函数断言预期数组和结果数组在内容和大小上的不等式(请参阅所附的测试文件)。
Clean-Up (清理)
最后,我们通过删除'cut'实例并将'result'缓冲区传递给ZeroMemory来清理资源。这样可以避免内存泄漏和测试污染。
图04-TEST_CSrvResponse-FAIL-未声明的标识符
它在编译时失败,因为函数还不存在。我们需要把它写出来,但是应该放置在哪里呢?
我们已经知道,我们将需要始终识别响应数据包类型。每当我们向经纪商发送数据包时,它都会向我们发送其中一个“响应内容”。这个“事物”是一种MQTT控制数据包。因此,既然它是一种“事物”,那么它应该在类似的“事物”的组下有自己的类,这似乎是很自然的。比方说一个类来表示控制数据包组下的所有服务器响应。
比方说,一个实现IControlPacket接口的CSrvResponse类。
我们可能会想让它成为我们已经存在的CPktConnect类中的另一个函数。但是我们违反了面向对象编程的一个重要原则:单一责任原则(SRP,Single Responsibility Principle )。
“你应该把那些因不同原因而改变的事物分开,把那些因相同原因而改变了的事物组合在一起。”(R. Martin, The Clean Coder, 2011).
一方面,每当我们改变构建CONNECT数据包的方式时,我们的CPktConnect类就会发生变化;另一方面,当我们改变读取CONNACK、PUBACK、SUBACK和其他服务器响应的方式时(不存在的)CSrvResponse类就会发生变化。因此,它们有非常明确的不同责任,在这种情况下很容易看出这一点。但有时,决定我们正在建模的域的实体是否应该在适当的类中声明可能会很棘手。通过应用SRP,您有一个客观的指导方针来决定这些“事物”。
所以让我们来写吧,刚好可以通过测试。
ENUM_PKT_TYPE CSrvResponse::GetPktType(uchar &resp_buf[]) { return (ENUM_PKT_TYPE)resp_buf[0]; }
测试编译,但如预期的那样失败,因为我们传递了一个“错误”的服务器响应以使其失败。
图05-TEST_CSrvResponse-FAIL-错误数据包
让“正确”的CONNACK数据包类型作为服务器响应传递。再次注意,我们显式地命名fixture:<s0>right_first_byte</s0>。这个名字本身只是一个标签。重要的是,任何阅读我们代码的人都应该清楚它的含义。包括六个月或六年后的我们自己。
bool TEST_GetPktType() { Print(__FUNCTION__); //--- Arrange uchar expected[] = {(uchar)CONNACK}; uchar result[1] = {}; uchar right_first_byte[] = {2}; //--- Act CSrvResponse *cut = new CSrvResponse(); ENUM_PKT_TYPE pkt_type = cut.GetPktType(right_first_byte); ArrayFill(result,0,1,(uchar)pkt_type); //--- Assert bool isTrue = AssertEqual(expected, result); //--- cleanup delete cut; ZeroMemory(result); return isTrue ? true : false; }
图06-TEST_CSrv响应-通过
好吧,现在测试正在通过,我们知道,至少对于这两个论点,一个错误,一个正确,它分别是失败的或通过的。稍后,如果需要,我们可能会对其进行更广泛的测试。
在这些简单的步骤中嵌入了R.Martin总结的TDD的三个基本“定律”。
- 在第一次编写失败的单元测试之前,不允许编写任何生产代码。
- 不允许编写超过足以失败的单元测试,不编译就是失败。
- 不允许您编写超过通过当前失败的单元测试所需数量的生产代码。
好吧,TDD目前已经足够了。让我们回到手头的任务,阅读从服务器到达的CONNACK数据包上的连接原因代码(Connect Reason Codes)。
(连接原因代码)如何处理服务器上不可用的功能?
在这一点上,有两个连接原因代码值得我们关注。
- QoS not supported
- Retain not supported
它们有些特殊,因为它们并不表示错误,而是表示服务器限制。如果我们的CONNACK有这些连接原因代码中的任何一个,则服务器表示我们的网络连接成功,我们的Connect是一个格式良好的数据包,并且服务器处于联机状态并正在工作,但无法满足我们的要求。我们需要采取行动。我们需要选择下一步做什么。
如果我们用Will QoS 2发送CONNECT,而服务器用QoS Maximum 1回复,我们该怎么办?我们是否应该重新发送带有降级QoS标志的CONNECT?还是应该在降级前断开连接?如果RETAIN特性就是这种情况,我们是否可以忽略它,认为它无关紧要,然后开始发布?还是应该在发布之前重新发送带有降级标志的CONNECT?
在我们收到成功的CONNACK后,我们应该怎么办,这意味着服务器接受了我们的连接并具有我们要求的所有功能?我们必须立即开始发送PUBLISH数据包吗?或者,我们可以通过发送连续的PINGREQ数据包来保持连接打开,直到我们准备好发布消息为止?顺便问一下,我们必须在发布之前订阅某个主题吗?
这些问题大多要跟随标准回答。它们需要实现AS-IS,以便拥有符合MQTT v5.0的客户端。许多选择留给了应用程序开发人员。目前,我们将只处理所需的内容,因此我们可以尽快拥有一个一致的客户端。
根据该标准,如果Will Flag也设置为1,则客户端只能请求QoS级别>0,这意味着只有当我们还在CONNECT数据包中发送Will Message时,才允许我们请求QoS级别>0。但我们不希望,或者更好的是,我们现在不需要处理 Will 信息。因此,我们的决定是在只了解我们现在需要了解的内容和尝试了解标准的所有复杂性之间做出妥协,最终编写出以后可能毫无用处的代码。
我们只需要知道如果服务器上没有请求的QoS级别或Retain,我们的客户端会做什么。一旦新的CONNACK到来,我们就需要知道这一点。因此,我们在CSrvResponse的构造函数中检查它。如果响应是CONNACK,则构造函数调用受保护的方法GetConnectReasonCode。
CSrvResponse::CSrvResponse(uchar &resp_buf[]) { if(GetPktType(resp_buf) == CONNACK && GetConnectReasonCode(resp_buf) == (MQTT_REASON_CODE_QOS_NOT_SUPPORTED || MQTT_REASON_CODE_RETAIN_NOT_SUPPORTED)) { CSrvProfile *serverProfile = new CSrvProfile(); serverProfile.Update("000.000.00.00", resp_buf); } }
如果连接原因代码是MQTT_Reason_Code_QOS_NOT_SUPPORTED或MQTT_REAS on_Code_RETAIN_NOT_SUPPORTED之一,它将在服务器配置文件中存储此信息。目前,我们将只存储有关服务器的信息,并等待断开连接。稍后,我们将在同一服务器上请求新连接时使用它。请注意,这“稍后”可能是在第一次连接尝试后的几毫秒,或者可能是几个星期后。关键是,我们将把这些信息存储在服务器配置文件中。
我们如何测试受保护的方法
为了测试受保护的方法,我们在测试脚本上创建了一个类,该类派生自测试中的类,在本例中为CSrvResponse。然后,我们通过这个名为TestProtectedMethods的“仅用于测试目的”派生类调用CSrvResponse上的受保护方法。
class TestProtectedMethods: public CSrvResponse { public: TestProtectedMethods() {}; ~TestProtectedMethods() {}; bool TEST_GetConnectReasonCode_FAIL(); bool TEST_GetConnectReasonCode(); }; //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ bool TestProtectedMethods::TEST_GetConnectReasonCode_FAIL() { Print(__FUNCTION__); //--- Arrange uchar expected = MQTT_REASON_CODE_SUCCESS; uchar reason_code_banned[4]; reason_code_banned[0] = B'00100000'; // packet type reason_code_banned[1] = 2; // remaining length reason_code_banned[2] = 0; // connect acknowledge flags reason_code_banned[3] = MQTT_REASON_CODE_BANNED; //--- Act CSrvResponse *cut = new CSrvResponse(); uchar result = this.GetConnectReasonCode(reason_code_banned); //--- Assert bool isTrue = AssertNotEqual(expected, result); //--- cleanup delete cut; ZeroMemory(result); return isTrue ? true : false; } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ bool TestProtectedMethods::TEST_GetConnectReasonCode() { Print(__FUNCTION__); //--- Arrange uchar expected = MQTT_REASON_CODE_SUCCESS; uchar reason_code_success[4]; reason_code_success[0] = B'00100000'; // packet type reason_code_success[1] = 2; // remaining length reason_code_success[2] = 0; // connect acknowledge flags reason_code_success[3] = MQTT_REASON_CODE_SUCCESS; //--- Act CSrvResponse *cut = new CSrvResponse(); uchar result = this.GetConnectReasonCode(reason_code_success); //--- Assert bool isTrue = AssertEqual(expected, result); //--- cleanup delete cut; ZeroMemory(result); return isTrue ? true : false; }
请注意,我们没有在服务器配置文件(Server Profile)中存储任何内容。事实上,服务器配置文件甚至还不存在。我们只是打印一条消息,表示正在更新服务器配置文件。这是因为服务器配置文件将在客户端的会话之间持久化,而我们还没有处理持久化问题。稍后,在实现持久性时,我们可以更改此存根函数,以将服务器配置文件持久化到SQLite数据库中,例如,甚至不必删除打印(或记录)的消息。这只是目前没有实现的情况。如上所述,在这一点上,我们只需要知道如果服务器与我们请求的功能不匹配该怎么办:我们存储信息以供以后重用。
结论
在本文中,我们描述了如何根据OASIS标准的要求,开始处理MQTT v5.0协议的操作行为部分,以便使一致的客户端尽快工作。我们描述了如何实现CSrvResponse类来标识服务器响应类型及其相关的原因代码。我们还描述了我们的客户端将如何对不可用的服务器功能做出反应。
在下一步中,我们将实现PUBLISH,更好地了解QoS级别的操作行为,并处理会话及其近乎所需的持久性。
**其他有用的缩写词:DRY、KISS、YAGNI。他们每个人都嵌入了一些实践智慧,但YMMV:)
本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/13388



