English 中文 Español Deutsch 日本語 Português
preview
Разработка MQTT-клиента для MetaTrader 5: методология TDD (Часть 2)

Разработка MQTT-клиента для MetaTrader 5: методология TDD (Часть 2)

MetaTrader 5Интеграция | 25 января 2024, 13:53
521 0
Jocimar Lopes
Jocimar Lopes

Введение

"Преждевременная оптимизация — корень всех зол" (Дональд Кнут)

В предыдущей статье мы рассмотрели MQTT, высокоэффективный двоичный протокол обмена сообщениями типа "публикация/подписка". Мы рассказали о том, что такое MQTT, почему его разработка началась двадцать пять лет назад и для чего он используется сегодня во многих отраслях: от автомобилестроения до Интернета вещей, от авиакосмической промышленности до простых чат-приложений. Мы увидели, что MQTT может быть полезен в любом контексте, где требуется протокол обмена сообщениями, независимый от типа контента, включая контекст торговых приложений. Мы отметили преимущества включения в нашу базу кодов нативного клиента MQL5 для MQTT и установили минимально необходимую среду разработки с использованием брокера MQTT с открытым исходным кодом Mosquitto, работающего на WSL (подсистема Windows для Linux).

Мы начали разработку нашего собственного клиента с жестко запрограммированной функцией генератора фиксированного заголовка и дошли до того, что смогли подключиться к локальному брокеру Mosquitto, но соединение было немедленно сброшено сервером из-за ошибки протокола.

Цитата из конца предыдущей статьи:

"Наш фиксированный заголовок CONNECT был распознан Mosquitto, но неизвестный (<unknown>) клиент был немедленно отключен "из-за ошибки протокола". Ошибка произошла из-за того, что мы еще не включили заголовок переменной с именем протокола, уровнем протокола и другими связанными метаданными. Мы исправим это на следующем шаге".

Рис. 01. Ошибка соединения

Рис. 01. Вкладка советников в MetaEditor, отображающая ошибку соединения


Поскольку мы применяем принцип разработки через тестирование (TDD), мы начнем с написания теста для конструктора пакетов CONNECT, который включает соответствующие метаданные и не отклоняется "из-за ошибки протокола".

Написание теста до написания тестируемого кода многим может показаться нелогичным. Но если мы будем думать о тестах как об объективном описании требований к проекту и рассматривать их как наиболее объективное определение цели, которую может иметь разработчик, то всё встанет на свои места.

"Юнит-тесты — это документы. Они описывают проект системы самого низкого уровня. Они недвусмысленны, точны, написаны на языке, понятном аудитории, и настолько формальны, что исполняются. Это лучший вид низкоуровневой документации, которая только может существовать. Какой профессионал не предоставит такую документацию?” (Роберт Мартин "Чистый код", 2011)


Организация кода: ООП и файлы заголовков

Как было сказано выше, мы начали создавать фиксированный заголовок нашего пакета подключения с жесткого кодирования массива байтов с "правильными" значениями. Затем мы попытались подключить нашего клиента, отправив этот жестко закодированный массив байтов нашему локальному брокеру. Наши попытки с треском провалились "из-за ошибки протокола". Но одновременно мы узнали кое-что о нашей среде разработки, о нашем журнале Mosquito, написали наши первые тесты и, прежде всего, начали делать что-то, что работает.

Как видите, эта неудача была намеренной. Но мы знаем, что такой способ разработки сложного приложения не является устойчивым в долгосрочной перспективе.

Создание соответствующих пакетов MQTT — это только первый (и самый легкий) шаг в процессе написания надежного и удобного в обслуживании клиента. Когда дело доходит до спецификации рабочих характеристик, всплывают все сложности протокола. Эта задача потребует от нас большей работы как разработчиков. Помимо отправки подходящих пакетов, нам придется иметь дело с большим количеством различных ответов сервера и различными состояниями приложений. На этом этапе жестко закодированных байтовых массивов (или чего-либо еще, запрограммированного в данном случае) будет недостаточно.

К счастью, MQL5 является объектно-ориентированным языком программирования, и мы не работаем в среде с ограничениями по памяти/процессору, для которых MQTT изначально был разработан. Таким образом, мы можем использовать все преимущества объектно-ориентированной (ООП) парадигмы, чтобы:

  • Легко принимать решения по протоколу за счет выбора правильного уровня абстракции
  • Легко читать код (помните, что код читается гораздо больше раз, чем пишется)
  • Обслуживать код без особых затруднений
  • И легко тестировать

В разделе "Объектно-ориентированное программирование" справочника MQL5 целый раздел посвящен этой теме. 


Определения протоколов

Протокол обмена сообщениями — это набор правил, который устанавливает общую основу взаимодействия между двумя или более объектами. В нашем случае - между двумя и более устройствами. Многие из этих правил касаются того, что делать, принимая во внимание то, что было сделано раньше. Они статичны. Чтобы выбрать следующее действие, наш код должен оценить текущее состояние приложения. В протоколе MQTT это делается по правилам рабочих характеристик (Operational behavior rules).

Помимо правил с отслеживанием состояния (фактически предшествующих им) существуют определения терминов, значений и вычислений, которые не зависят от состояния приложения. Обычно это константы, перечисления и алгоритмы оценки, как и в случае с именем протокола MQTT, типами пакетов управления и значением байта оставшейся длины фиксированного заголовка соответственно.

Мы соберем эти два разных набора правил в двух отдельных заголовочных файлах. Первый из них предназначен только для определений терминов и значений, общих для наших файлов. Назовем его Defines.mqh. Эти термины и значения обычно являются константами, и этот файл практически не должен изменяться.

Другой заголовочный файл будет содержать некоторые общие перечисления, структуры и функции. Назовем его MQTT.mqh. Эти перечисления, структуры и функции будут часо меняться, и не только пока мы разрабатываем первую версию. Файл будет меняться всякий раз, когда мы проводим улучшение, оптимизацию и исправление ошибок. Вероятно, этот файл будет разделен на другие, более специфичные файлы.

Практика использования заголовочных файлов для организации кода не связана с объектно-ориентированным программированием. Полезные заметки о таких файлах можно найти в уже ставшей классической книге Брайана Кернигана и Денниса Ритчи "Язык программирования Си". 

"(…) определения и декларации, общие для всех файлов. Насколько это возможно, мы хотим централизовать процесс, чтобы была только одна копия, которую можно получить и хранить по мере развития программы. (…) Вероятно, вплоть до некоторого умеренного размера программы лучше иметь один заголовочный файл, содержащий все, что должно совместно использоваться любыми двумя частями программы (...). Для гораздо более крупной программы потребуется больше организации и больше заголовков".

Но именно в объектно-ориентированном программировании практика организации кода в небольших модулях компиляции заметна особенно отчетливо. Более того, поскольку мы создаем библиотеку, почти весь наш код будет находиться в заголовочных файлах.

Заголовок Defines

На этом этапе определения имени протокола и уровня протокола используются только в пакетах CONNECT. Итак, при желании мы можем поместить их в конкретный класс CPktConnect (см. ниже). Но мы оставим их в заголовке Defines для единообразия. Хотя на данный момент они используются только в пакетах CONNECT, позже их можно будет использовать и в других файлах.

Комментарии к протоколу представляют собой цитаты из официального описания стандарта.

//+------------------------------------------------------------------+
//|                                                      Defines.mqh |
//|            ********* WORK IN PROGRESS **********                 |
//| **** PART OF ARTICLE https://www.mql5.com/en/articles/13334 **** |
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//|              PROTOCOL NAME AND VERSION                           |
//+------------------------------------------------------------------+
#define MQTT_PROTOCOL_NAME_LENGTH_MSB           0x00
#define MQTT_PROTOCOL_NAME_LENGTH_LSB           0x04
#define MQTT_PROTOCOL_NAME_BYTE_3               'M'
#define MQTT_PROTOCOL_NAME_BYTE_4               'Q'
#define MQTT_PROTOCOL_NAME_BYTE_5               'T'
#define MQTT_PROTOCOL_NAME_BYTE_6               'T'
#define MQTT_PROTOCOL_VERSION                   0x05
//+------------------------------------------------------------------+
//|              PROPERTIES                                          |
//+------------------------------------------------------------------+
/*
The last field in the Variable Header of the CONNECT, CONNACK, PUBLISH, PUBACK, PUBREC,
PUBREL, PUBCOMP, SUBSCRIBE, SUBACK, UNSUBSCRIBE, UNSUBACK, DISCONNECT, and
AUTH packet is a set of Properties. In the CONNECT packet there is also an optional set of Properties in
the Will Properties field with the Payload
*/
#define MQTT_PROPERTY_PAYLOAD_FORMAT_INDICATOR          0x01 // (1) Byte                  
#define MQTT_PROPERTY_MESSAGE_EXPIRY_INTERVAL           0x02 // (2) Four Byte Integer     
#define MQTT_PROPERTY_CONTENT_TYPE                      0x03 // (3) UTF-8 Encoded String  
#define MQTT_PROPERTY_RESPONSE_TOPIC                    0x08 // (8) UTF-8 Encoded String  
#define MQTT_PROPERTY_CORRELATION_DATA                  0x09 // (9) Binary Data           
#define MQTT_PROPERTY_SUBSCRIPTION_IDENTIFIER           0x0B // (11) Variable Byte Integer
#define MQTT_PROPERTY_SESSION_EXPIRY_INTERVAL           0x11 // (17) Four Byte Integer    
#define MQTT_PROPERTY_ASSIGNED_CLIENT_IDENTIFIER        0x12 // (18) UTF-8 Encoded String  
#define MQTT_PROPERTY_SERVER_KEEP_ALIVE                 0x13 // (19) Two Byte Integer      
#define MQTT_PROPERTY_AUTHENTICATION_METHOD             0x15 // (21) UTF-8 Encoded String 
#define MQTT_PROPERTY_AUTHENTICATION_DATA               0x16 // (22) Binary Data          
#define MQTT_PROPERTY_REQUEST_PROBLEM_INFORMATION       0x17 // (23) Byte                  
#define MQTT_PROPERTY_WILL_DELAY_INTERVAL               0x18 // (24) Four Byte Integer    
#define MQTT_PROPERTY_REQUEST_RESPONSE_INFORMATION      0x19 // (25) Byte                  
#define MQTT_PROPERTY_RESPONSE_INFORMATION              0x1A // (26) UTF-8 Encoded String  
#define MQTT_PROPERTY_SERVER_REFERENCE                  0x1C // (28) UTF-8 Encoded String 
#define MQTT_PROPERTY_REASON_STRING                     0x1F // (31) UTF-8 Encoded String
#define MQTT_PROPERTY_RECEIVE_MAXIMUM                   0x21 // (33) Two Byte Integer     
#define MQTT_PROPERTY_TOPIC_ALIAS_MAXIMUM               0x22 // (34) Two Byte Integer     
#define MQTT_PROPERTY_TOPIC_ALIAS                       0x23 // (35) Two Byte Integer     
#define MQTT_PROPERTY_MAXIMUM_QOS                       0x24 // (36) Byte                 
#define MQTT_PROPERTY_RETAIN_AVAILABLE                  0x25 // (37) Byte                 
#define MQTT_PROPERTY_USER_PROPERTY                     0x26 // (38) UTF-8 String Pair   
#define MQTT_PROPERTY_MAXIMUM_PACKET_SIZE               0x27 // (39) Four Byte Integer    
#define MQTT_PROPERTY_WILDCARD_SUBSCRIPTION_AVAILABLE   0x28 // (40) Byte                  
#define MQTT_PROPERTY_SUBSCRIPTION_IDENTIFIER_AVAILABLE 0x29 // (41) Byte                  
#define MQTT_PROPERTY_SHARED_SUBSCRIPTION_AVAILABLE     0x2A // (42) Byte 
//+------------------------------------------------------------------+
//|              REASON CODES                                        |
//+------------------------------------------------------------------+
/*
A Reason Code is a one byte unsigned value that indicates the result of an operation. Reason Codes less
than 0x80 indicate successful completion of an operation. The normal Reason Code for success is 0.
Reason Code values of 0x80 or greater indicate failure.

The CONNACK, PUBACK, PUBREC, PUBREL, PUBCOMP, DISCONNECT and AUTH Control Packets
have a single Reason Code as part of the Variable Header. The SUBACK and UNSUBACK packets
contain a list of one or more Reason Codes in the Payload.
*/
#define MQTT_REASON_CODE_SUCCESS                                0x00 // (0)
#define MQTT_REASON_CODE_NORMAL_DISCONNECTION                   0x00 // (0)
#define MQTT_REASON_CODE_GRANTED_QOS_0                          0x00 // (0)
#define MQTT_REASON_CODE_GRANTED_QOS_1                          0x01 // (1)
#define MQTT_REASON_CODE_GRANTED_QOS_2                          0x02 // (2)
#define MQTT_REASON_CODE_DISCONNECT_WITH_WILL_MESSAGE           0x04 // (4)
#define MQTT_REASON_CODE_NO_MATCHING_SUBSCRIBERS                0x10 // (16)
#define MQTT_REASON_CODE_NO_SUBSCRIPTION_EXISTED                0x11 // (17)
#define MQTT_REASON_CODE_CONTINUE_AUTHENTICATION                0x18 // (24)
#define MQTT_REASON_CODE_RE_AUTHENTICATE                        0x19 // (25)
#define MQTT_REASON_CODE_UNSPECIFIED_ERROR                      0x80 // (128)
#define MQTT_REASON_CODE_MALFORMED_PACKET                       0x81 // (129)
#define MQTT_REASON_CODE_PROTOCOL_ERROR                         0x82 // (130)
#define MQTT_REASON_CODE_IMPLEMENTATION_SPECIFIC_ERROR          0x83 // (131)
#define MQTT_REASON_CODE_UNSUPPORTED_PROTOCOL_VERSION           0x84 // (132)
#define MQTT_REASON_CODE_CLIENT_IDENTIFIER_NOT_VALID            0x85 // (133)
#define MQTT_REASON_CODE_BAD_USER_NAME_OR_PASSWORD              0x86 // (134)
#define MQTT_REASON_CODE_NOT_AUTHORIZED                         0x87 // (135)
#define MQTT_REASON_CODE_SERVER_UNAVAILABLE                     0x88 // (136)
#define MQTT_REASON_CODE_SERVER_BUSY                            0x89 // (137)
#define MQTT_REASON_CODE_BANNED                                 0x8A // (138)
#define MQTT_REASON_CODE_SERVER_SHUTTING_DOWN                   0x8B // (139)
#define MQTT_REASON_CODE_BAD_AUTHENTICATION_METHOD              0x8C // (140)
#define MQTT_REASON_CODE_KEEP_ALIVE_TIMEOUT                     0x8D // (141)
#define MQTT_REASON_CODE_SESSION_TAKEN_OVER                     0x8E // (142)
#define MQTT_REASON_CODE_TOPIC_FILTER_INVALID                   0x8F // (143)
#define MQTT_REASON_CODE_TOPIC_NAME_INVALID                     0x90 // (144)
#define MQTT_REASON_CODE_PACKET_IDENTIFIER_IN_USE               0x91 // (145)
#define MQTT_REASON_CODE_PACKET_IDENTIFIER_NOT_FOUND            0x92 // (146)
#define MQTT_REASON_CODE_RECEIVE_MAXIMUM_EXCEEDED               0x93 // (147)
#define MQTT_REASON_CODE_TOPIC_ALIAS_INVALID                    0x94 // (148)
#define MQTT_REASON_CODE_PACKET_TOO_LARGE                       0x95 // (149)
#define MQTT_REASON_CODE_MESSAGE_RATE_TOO_HIGH                  0x96 // (150)
#define MQTT_REASON_CODE_QUOTA_EXCEEDED                         0x97 // (151)
#define MQTT_REASON_CODE_ADMINISTRATIVE_ACTION                  0x98 // (152)
#define MQTT_REASON_CODE_PAYLOAD_FORMAT_INVALID                 0x99 // (153)
#define MQTT_REASON_CODE_RETAIN_NOT_SUPPORTED                   0x9A // (154)
#define MQTT_REASON_CODE_QOS_NOT_SUPPORTED                      0x9B // (155)
#define MQTT_REASON_CODE_USE_ANOTHER_SERVER                     0x9C // (156)
#define MQTT_REASON_CODE_SERVER_MOVED                           0x9D // (157)
#define MQTT_REASON_CODE_SHARED_SUBSCRIPTIONS_NOT_SUPPORTED     0x9E // (158)
#define MQTT_REASON_CODE_CONNECTION_RATE_EXCEEDED               0x9F // (159)
#define MQTT_REASON_CODE_MAXIMUM_CONNECT_TIME                   0xA0 // (160)
#define MQTT_REASON_CODE_SUBSCRIPTION_IDENTIFIERS_NOT_SUPPORTED 0xA1 // (161)
#define MQTT_REASON_CODE_WILDCARD_SUBSCRIPTIONS_NOT_SUPPORTED   0xA2 // (162)

Обратите внимание, что мы добавляем префикс MQTT ко всем определениям, специфичным для протокола. Это сделано для того, чтобы отличать их от наших собственных определений, которые будут добавлены позже. Также обратите внимание в начале нашего файла Defines.mqh, в разделе PROTOCOL NAME AND VERSION, мы стараемся указать наименования идентификаторов как можно более явно. Это сделано для того, чтобы соответствовать принципам так называемого "чистого кода". Эти принципы должны помочь сделать наш код более удобным для чтения, более легким для отладки и дружественным к IDE, то есть более доступным для поиска и хорошо подходящим для использования функции автозаполнения современных IDE.


Заголовок MQTT

//+------------------------------------------------------------------+
//|                                                         MQTT.mqh |
//|            ********* WORK IN PROGRESS **********                 |
//| **** PART OF ARTICLE https://www.mql5.com/en/articles/13334 **** |
//+------------------------------------------------------------------+
#include "Defines.mqh"
//+------------------------------------------------------------------+
//|              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
   PUBACK      =  0x04, // Publish acknowledgment (QoS 1)
   PUBREC      =  0x05, // Publish received (QoS 2 delivery part 1)
   PUBREL      =  0x06, // Publish release (QoS 2 delivery part 2)
   PUBCOMP     =  0x07, // Publish complete (QoS 2 delivery part 3)
   SUBSCRIBE   =  0x08, // Subscribe request
   SUBACK      =  0x09, // Subscribe acknowledgment
   UNSUBSCRIBE =  0x0A, // Unsubscribe request
   UNSUBACK    =  0x0B, // Unsubscribe acknowledgment
   PINGREQ     =  0x0C, // PING request
   PINGRESP    =  0x0D, // PING response
   DISCONNECT  =  0x0E, // Disconnect notification
   AUTH        =  0x0F, // Authentication exchange
  };
//+------------------------------------------------------------------+
//|             CONNECT - VARIABLE HEADER - CONNECT FLAGS            |
//+------------------------------------------------------------------+
/*
The Connect Flags byte contains several parameters specifying the behavior of the MQTT connection. It
also indicates the presence or absence of fields in the Payload.
*/
enum ENUM_CONNECT_FLAGS
  {
   RESERVED       = 0x00,
   CLEAN_START    = 0x02,
   WILL_FLAG      = 0x04,
   WILL_QOS_1     = 0x08,
   WILL_QOS_2     = 0x10,
   WILL_RETAIN    = 0x20,
   PASSWORD_FLAG  = 0x40,
   USER_NAME_FLAG = 0x80
  };
//+------------------------------------------------------------------+
//|             CONNECT - VARIABLE HEADER - QoS LEVELS               |
//+------------------------------------------------------------------+
/*
Position: bits 4 and 3 of the Connect Flags.
These two bits specify the QoS level to be used when publishing the Will Message.
If the Will Flag is set to 0, then the Will QoS MUST be set to 0 (0x00) [MQTT-3.1.2-11].
If the Will Flag is set to 1, the value of Will QoS can be 0 (0x00), 1 (0x01), or 2 (0x02) [MQTT-3.1.2-12].
*/
enum ENUM_QOS_LEVEL
  {
   AT_MOST_ONCE   = 0x00,
   AT_LEAST_ONCE  = 0x01,
   EXACTLY_ONCE   = 0x02
  };
//+------------------------------------------------------------------+
//|                   SetProtocolVersion                             |
//+------------------------------------------------------------------+
void SetProtocolVersion(uchar& dest_buf[])
  {
   dest_buf[8] = MQTT_PROTOCOL_VERSION;
  }
//+------------------------------------------------------------------+
//|                     SetProtocolName                              |
//+------------------------------------------------------------------+
void SetProtocolName(uchar& dest_buf[])
  {
   dest_buf[2] = MQTT_PROTOCOL_NAME_LENGTH_MSB;
   dest_buf[3] = MQTT_PROTOCOL_NAME_LENGTH_LSB;
   dest_buf[4] = MQTT_PROTOCOL_NAME_BYTE_3;
   dest_buf[5] = MQTT_PROTOCOL_NAME_BYTE_4;
   dest_buf[6] = MQTT_PROTOCOL_NAME_BYTE_5;
   dest_buf[7] = MQTT_PROTOCOL_NAME_BYTE_6;
  }
//+------------------------------------------------------------------+
//|                     SetFixedHeader                               |
//+------------------------------------------------------------------+
void SetFixedHeader(ENUM_PKT_TYPE pkt_type, uchar& buf[], uchar& dest_buf[])
  {
   dest_buf[0] = (uchar)pkt_type << 4;
   dest_buf[1] = GetRemainingLength(buf);
  }
//+------------------------------------------------------------------+
//|                    GetRemainingLength                            |
//+------------------------------------------------------------------+
/*
Position: starts at byte 2.
The Remaining Length is a Variable Byte Integer that represents the number of bytes remaining within the
current Control Packet, including data in the Variable Header and the Payload. The Remaining Length
does not include the bytes used to encode the Remaining Length. The packet size is the total number of
bytes in an MQTT Control Packet, this is equal to the length of the Fixed Header plus the Remaining
Length.
*/
uchar GetRemainingLength(uchar &buf[])
  {
   uint x;
   x = ArraySize(buf);
   uint rem_len;
   do
     {
      rem_len = x % 128;
      x = (x / 128);
      if(x > 0)
        {
         rem_len = rem_len | 128;
        }
     }
   while(x > 0);
   return (uchar)rem_len;
  };

//+------------------------------------------------------------------+


Классы и структуры

Интерфейс пакетов управления MQTT

Здесь нам нужно сделать выбор: начать иерархию объектов пакетов управления (Control Packets) с абстрактного класса или с интерфейса. Мы могли бы начать с общего базового класса, хорошо подходящего для любого пакета управления. Этот абстрактный класс может специализироваться на более специфических производных классах пакетов управления. Или мы могли бы начать с простого интерфейса, который будет реализован этими классами пакетов управления.

Начнем с интерфейса IcontrolPacket. Он будет иметь простой метод. Этот выбор может измениться при реализации рабочих характеристик протокола. Вероятно, мы изменим этот интерфейс на абстрактный класс с некоторыми виртуальными функциями.

//+------------------------------------------------------------------+
//|                                               IControlPacket.mqh |
//|            ********* WORK IN PROGRESS **********                 |
//| **** PART OF ARTICLE https://www.mql5.com/en/articles/13334 **** |
//+------------------------------------------------------------------+
#include "MQTT.mqh"
//+------------------------------------------------------------------+
//|       Interface IControlPacket                                   |
//|       The root of object hierarchy                               |
//+------------------------------------------------------------------+
interface IControlPacket
  {

   bool              IsControlPacket();

  };
//+------------------------------------------------------------------+

Как уже было сказано, на данный момент единственная цель этого интерфейса — выступать в качестве корня иерархии объектов пакетов MQTT. На данный момент это не более чем заглушка.

Класс подключения управляющих пакетов MQTT

Пакет управления CONNECT является наиболее трудоемким в разработке. Помимо того, что нам еще предстоит познакомиться с протоколом, именно этот конкретный пакет получил в версии 5.0 наиболее существенные улучшения, а именно свойства подключения (Connect Properties) и свойства пользователя (User Properties).

//+------------------------------------------------------------------+
//|                                                   PktConnect.mqh |
//|            ********* WORK IN PROGRESS **********                 |
//| **** PART OF ARTICLE https://www.mql5.com/en/articles/13334 **** |
//+------------------------------------------------------------------+
#include "MQTT.mqh"
#include "Defines.mqh"
#include "IControlPacket.mqh"
//+------------------------------------------------------------------+
//|        CONNECT VARIABLE HEADER                                   |
//+------------------------------------------------------------------+
/*
The Variable Header for the CONNECT Packet contains the following fields in this order:
Protocol Name,Protocol Level, Connect Flags, Keep Alive, and Properties.
*/
struct MqttClientIdentifierLength
  {
   uchar             msb;
   uchar             lsb;
  } clientIdLen;
//---
struct MqttKeepAlive
  {
   uchar             msb;
   uchar             lsb;
  } keepAlive;
//---
struct MqttConnectProperties
  {
   uint              prop_len;
   uchar             session_expiry_interval_id;
   uint              session_expiry_interval;
   uchar             receive_maximum_id;
   ushort            receive_maximum;
   uchar             maximum_packet_size_id;
   ushort            maximum_packet_size;
   uchar             topic_alias_maximum_id;
   ushort            topic_alias_maximum;
   uchar             request_response_information_id;
   uchar             request_response_information;
   uchar             request_problem_information_id;
   uchar             request_problem_information;
   uchar             user_property_id;
   string            user_property_key;
   string            user_property_value;
   uchar             authentication_method_id;
   string            authentication_method;
   uchar             authentication_data_id;
  } connectProps;
//---
struct MqttConnectPayload
  {
   uchar             client_id_len;
   string            client_id;
   ushort            will_properties_len;
   uchar             will_delay_interval_id;
   uint              will_delay_interval;
   uchar             payload_format_indicator_id;
   uchar             payload_format_indicator;
   uchar             message_expiry_interval_id;
   uint              message_expiry_interval;
   uchar             content_type_id;
   string            content_type;
   uchar             response_topic_id; // for request/response
   string            response_topic;
   uchar             correlation_data_id; // for request/response
   ulong             correlation_data[]; // binary data
   uchar             user_property_id;
   string            user_property_key;
   string            user_property_value;
   uchar             will_topic_len;
   string            will_topic;
   uchar             will_payload_len;
   ulong             will_payload[]; // binary data
   uchar             user_name_len;
   string            user_name;
   uchar             password_len;
   ulong             password; // binary data
  } connectPayload;
//+------------------------------------------------------------------+
//| Class CPktConnect.                                               |
//| Purpose: Class of MQTT Connect Control Packets.                  |
//|          Implements IControlPacket                               |
//+------------------------------------------------------------------+
class CPktConnect : public IControlPacket
  {
private:
   bool              IsControlPacket() {return true;}
protected:
   void              InitConnectFlags() {ByteArray[9] = 0;}
   void              InitKeepAlive() {ByteArray[10] = 0; ByteArray[11] = 0;}
   void              InitPropertiesLength() {ByteArray[12] = 0;}
   uchar             m_connect_flags;

public:
                     CPktConnect();
                     CPktConnect(uchar &buf[]);
                    ~CPktConnect();
   //--- methods for setting Connect Flags
   void              SetCleanStart(const bool cleanStart);
   void              SetWillFlag(const bool willFlag);
   void              SetWillQoS_1(const bool willQoS_1);
   void              SetWillQoS_2(const bool willQoS_2);
   void              SetWillRetain(const bool willRetain);
   void              SetPasswordFlag(const bool passwordFlag);
   void              SetUserNameFlag(const bool userNameFlag);
   void              SetKeepAlive(ushort seconds);
   void              SetClientIdentifierLength(string clientId);
   void              SetClientIdentifier(string clientId);

   //--- member for getting the byte array
   uchar             ByteArray[];
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CPktConnect::CPktConnect(uchar &buf[])
  {
   ArrayFree(ByteArray);
   ArrayResize(ByteArray, buf.Size() + 2, UCHAR_MAX);
   SetFixedHeader(CONNECT, buf, ByteArray);
   SetProtocolName(ByteArray);
   SetProtocolVersion(ByteArray);
   InitConnectFlags();
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void CPktConnect::SetClientIdentifier(string clientId)
  {
   SetClientIdentifierLength(clientId);
   StringToCharArray(clientId, ByteArray,
                     ByteArray.Size() - StringLen(clientId), StringLen(clientId));
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void CPktConnect::SetClientIdentifierLength(string clientId)
  {
   clientIdLen.msb = (char)StringLen(clientId) >> 8;
   clientIdLen.lsb = (char)StringLen(clientId) % 256;
   ByteArray[12] = clientIdLen.msb;
   ByteArray[13] = clientIdLen.lsb;
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void CPktConnect::SetKeepAlive(ushort seconds) // MQTT max is 65,535 sec
  {
   keepAlive.msb = (uchar)(seconds >> 8) & 255;
   keepAlive.lsb = (uchar)seconds & 255;
   ByteArray[10] = keepAlive.msb;
   ByteArray[11] = keepAlive.lsb;
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void CPktConnect::SetPasswordFlag(const bool passwordFlag)
  {
   passwordFlag ? m_connect_flags |= PASSWORD_FLAG : m_connect_flags &= ~PASSWORD_FLAG;
   ArrayFill(ByteArray, sizeof(ByteArray), 1, m_connect_flags);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void CPktConnect::SetUserNameFlag(const bool userNameFlag)
  {
   userNameFlag ? m_connect_flags |= USER_NAME_FLAG : m_connect_flags &= (uchar) ~USER_NAME_FLAG;
   ArrayFill(ByteArray, sizeof(ByteArray), 1, m_connect_flags);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void CPktConnect::SetWillRetain(const bool willRetain)
  {
   willRetain ? m_connect_flags |= WILL_RETAIN : m_connect_flags &= ~WILL_RETAIN;
   ArrayFill(ByteArray, sizeof(ByteArray), 1, m_connect_flags);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void CPktConnect::SetWillQoS_2(const bool willQoS_2)
  {
   willQoS_2 ? m_connect_flags |= WILL_QOS_2 : m_connect_flags &= ~WILL_QOS_2;
   ArrayFill(ByteArray, sizeof(ByteArray), 1, m_connect_flags);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void CPktConnect::SetWillQoS_1(const bool willQoS_1)
  {
   willQoS_1 ? m_connect_flags |= WILL_QOS_1 : m_connect_flags &= ~WILL_QOS_1;
   ArrayFill(ByteArray, sizeof(ByteArray), 1, m_connect_flags);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void CPktConnect::SetWillFlag(const bool willFlag)
  {
   willFlag ? m_connect_flags |= WILL_FLAG : m_connect_flags &= ~WILL_FLAG;
   ArrayFill(ByteArray, sizeof(ByteArray), 1, m_connect_flags);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void CPktConnect::SetCleanStart(const bool cleanStart)
  {
   cleanStart ? m_connect_flags |= CLEAN_START : m_connect_flags &= ~CLEAN_START;
   ArrayFill(ByteArray, 9, 1, m_connect_flags);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CPktConnect::CPktConnect()
  {
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CPktConnect::~CPktConnect()
  {
  }
//+------------------------------------------------------------------+


Тестируем наш первый класс

Единственная цель класса CPktConnect - создание правильно сформированного пакета MQTT CONNECT. Итак, чтобы протестировать его, нам нужно начать с создания образца массива байтов, который будет представлять правильно сформированный пакет CONNECT. Но как мы можем быть уверены, что наш сформированный массив байтов представляет собой правильно сформированный пакет CONNECT? В конце концов, это тест, который мы будем использовать, чтобы создать наш класс с нуля. Многие, если не все, наши усилия могут быть потрачены впустую, если наше "приспособление" представляет собой плохой пакет.

Здесь нам на помощь пришел разработчик протокола, OASIS. В разделе 3.1.2.12 можно найти ненормативный пример заголовка переменной для пакета CONNECT. Поскольку мы уже протестировали наш генератор фиксированного заголовка (см. предыдущую статью), этого примера OASIS будет достаточно для начала. Это позволит нам быть уверенными, что наш класс генерирует правильно сформированный пакет с некоторыми различными конфигурациями, такими как логический CleanSession и запрошенный интервал времени активности соединения (Keep-Alive).

Этот жестко закодированный массив байтов, сгенерированный вручную, затем будет сравниваться со сгенерированным пакетом CpktConnect.

//+------------------------------------------------------------------+
//|                                  TEST_CControlPacket_Connect.mq5 |
//|                                                                  |
//|            ********* WORK IN PROGRESS **********                 |
//| **** PART OF ARTICLE https://www.mql5.com/en/articles/13334 **** |
//+------------------------------------------------------------------+
#include <MQTT\CPktConnect.mqh>

//+------------------------------------------------------------------+
//| Tests for CControlPacketConnect class                            |
//+------------------------------------------------------------------+
void OnStart()
  {
   Print(TEST_SetCleanStart_KeepAlive_ClientIdentifier());
   Print(TEST_SetClientIdentifier());
   Print(TEST_SetClientIdentifierLength());
   Print(TEST_SetCleanStart_and_SetKeepAlive());
   Print(TEST_SetKeepAlive());
   Print(TEST_SetCleanStart());
  }
/* REFERENCE ARRAY (FIXTURE)
{16, 24, 0, 4, 77, 81, 84, 84, 5, 2, 0, 10, 0, 4, 7, 17, 0, 0, 0, 10, 25, 1, 77, 81, 76, 53}
*/
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool TEST_SetCleanStart_KeepAlive_ClientIdentifier()
  {
   Print(__FUNCTION__);
//--- Arrange
   static uchar expected[] =
     {16, 16, 0, 4, 77, 81, 84, 84, 5, 2, 0, 10, 0, 4, 77, 81, 76, 53};
   uchar buf[expected.Size() - 2];
   CPktConnect *cut = new CPktConnect(buf);
//--- Act
   cut.SetCleanStart(true);
   cut.SetKeepAlive(10);//10 sec
   cut.SetClientIdentifier("MQL5");
   uchar result[];
   ArrayCopy(result, cut.ByteArray);
//--- Assert
   bool isTrue = Assert(expected, result);
//--- cleanup
   delete cut;
   ZeroMemory(result);
   return  isTrue ? true : false;
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool TEST_SetClientIdentifier()
  {
   Print(__FUNCTION__);
//--- Arrange
   static uchar expected[] =
     {16, 16, 0, 4, 77, 81, 84, 84, 5, 0, 0, 0, 0, 4, 77, 81, 76, 53};
   uchar buf[expected.Size() - 2];
   CPktConnect *cut = new CPktConnect(buf);
//--- Act
   cut.SetClientIdentifier("MQL5");
   uchar result[];
   ArrayCopy(result, cut.ByteArray);
//--- Assert
   bool isTrue = Assert(expected, result);
//--- cleanup
   delete cut;
   ZeroMemory(result);
   return  isTrue ? true : false;
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool TEST_SetClientIdentifierLength()
  {
   Print(__FUNCTION__);
//--- Arrange
   static uchar expected[] =
     {16, 12, 0, 4, 77, 81, 84, 84, 5, 0, 0, 0, 0, 4};
   uchar buf[expected.Size() - 2];
   CPktConnect *cut = new CPktConnect(buf);
//--- Act
   cut.SetClientIdentifierLength("MQL5");
   uchar result[];
   ArrayCopy(result, cut.ByteArray);
//--- Assert
   bool isTrue = Assert(expected, result);
//--- cleanup
   delete cut;
   ZeroMemory(result);
   return  isTrue ? true : false;
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool TEST_SetCleanStart_and_SetKeepAlive()
  {
   Print(__FUNCTION__);
//--- Arrange
   static uchar expected[] =
     {16, 10, 0, 4, 77, 81, 84, 84, 5, 2, 0, 10};
   uchar buf[expected.Size() - 2];
   CPktConnect *cut = new CPktConnect(buf);
//--- Act
   cut.SetCleanStart(true);
   cut.SetKeepAlive(10); //10 secs
   uchar result[];
   ArrayCopy(result, cut.ByteArray);
//--- Assert
   bool isTrue = Assert(expected, result);
//--- cleanup
   delete cut;
   ZeroMemory(result);
   return  isTrue ? true : false;
  }
//+------------------------------------------------------------------+
bool TEST_SetKeepAlive()
  {
   Print(__FUNCTION__);
//--- Arrange
   static uchar expected[] =
     {16, 10, 0, 4, 77, 81, 84, 84, 5, 0, 0, 10};
   uchar buf[expected.Size() - 2];
   CPktConnect *cut = new CPktConnect(buf);
//--- Act
   cut.SetKeepAlive(10); //10 secs
   uchar result[];
   ArrayCopy(result, cut.ByteArray);
//--- Assert
   bool isTrue = Assert(expected, result);
//--- cleanup
   delete cut;
   ZeroMemory(result);
   return  isTrue ? true : false;
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool TEST_SetCleanStart()
  {
   Print(__FUNCTION__);
//--- Arrange
   static uchar expected[] =
     {16, 8, 0, 4, 77, 81, 84, 84, 5, 2};
   uchar buf[expected.Size() - 2];
   CPktConnect *cut = new CPktConnect(buf);
//--- Act
   cut.SetCleanStart(true);
   uchar result[];
   ArrayCopy(result, cut.ByteArray);
//--- Assert
   bool isTrue = Assert(expected, result);
//--- cleanup
   delete cut;
//ZeroMemory(result);
   return  isTrue ? true : false;
  }
//+------------------------------------------------------------------+
bool Assert(uchar& expected[], uchar& result[])
  {
   if(!ArrayCompare(expected, result) == 0)
     {
      for(uint i = 0; i < expected.Size(); i++)
        {
         printf("expected\t%d\t\t%d result", expected[i], result[i]);
        }
      printf("expected size %d <=> %d result size", expected.Size(), result.Size());
      Print("Expected");
      ArrayPrint(expected);
      Print("Result");
      ArrayPrint(result);
      return false;
     }
   return true;
  }
//+------------------------------------------------------------------+

Примечание: Как вы знаете, написание тестов по принципу "давайте посмотрим, есть ли в массиве заголовков данные, которые я только что в него поместил" может показаться пустой тратой времени. Но это не так. Этот набор "очевидных" тестов будет всегда сопровождать наш код. Их можно рассматривать как непрерывный и автоматизированный инструмент отладки, который докажет свою ценность, когда вы обнаружите какую-то регрессионную ошибку или даже какую-то элементарную оплошность, вызванную, например, неправильным копированием. По этой причине мы не проверяем только то, работает ли действие подключения методом «черного ящика». Мы хотим убедиться, что наш заголовок правильно сформирован, прежде чем тестировать действие подключения. Стоит помнить, что TDD — это процесс. Многие, если не все, эти тесты будут переписаны или даже удалены, прежде чем мы получим первую рабочую версию нашего кода. Но те, что останутся, вероятно, останутся навсегда.

Этот тест будет пройден, только когда массив байтов, сгенерированный CPktConnect, вернет 0 (ноль) при ArrayCompare(d) с нашим эталонным массивом байтов.​

После того, как мы протестировали некоторые комбинации основных свойств подключения, пакет будет отправлен брокеру. На этот раз он не должен отклоняться "из-за ошибки протокола".

Рис. 02. Результаты тестирования класса CPktConnect

Рис. 02. Результаты тестирования класса CPktConnect на вкладке советников MetaEditor


Проверка с локальным MQTT-брокером

Теперь мы можем запустить нашего локального брокера Mosquitto на WSL, чтобы проверить, было ли наше MQTT-соединение успешным.

Если вы выполнили установку по умолчанию, Mosquito должен работать в Linux как служба. Таким образом, вам нужно только "перенаправить" (redir) порты (80 → 1883) и включить имя хоста для разрешенных URL-адресов в настройках MetaTrader 5.

Рис. 03. Журнал WSL Mosquitto об успешном подключении/отключении

Рис. 03. Журнал Mosquitto в WSL, показывающий статус подключения/отключения: Success (успех)


 Ура! Наша попытка подключения не возвращает ошибку протокола. Теперь мы можем попробовать обмениваться сообщениями между клиентом и сервером.


Заключение

На следующем этапе мы рассмотрим ответы CONNACK. На этом этапе у нас будет прочная основа для публикации нашего первого сообщения. И, конечно же, мы начнем писать для него тест!  :)  Ждите новых статей!


Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/13334

Прикрепленные файлы |
Defines.mqh (8.03 KB)
MQTT.mqh (5.01 KB)
CPktConnect.mqh (9.97 KB)
Популяционные алгоритмы оптимизации: Бинарный генетический алгоритм (Binary Genetic Algorithm, BGA). Часть II Популяционные алгоритмы оптимизации: Бинарный генетический алгоритм (Binary Genetic Algorithm, BGA). Часть II
В этой статье мы рассмотрим бинарный генетический алгоритм (BGA), который моделирует естественные процессы, происходящие в генетическом материале у живых существ в природе.
Тип рисования DRAW_ARROW в мультисимвольных мультипериодных индикаторах Тип рисования DRAW_ARROW в мультисимвольных мультипериодных индикаторах
В статье рассмотрим рисование стрелочных мультисимвольных мультипериодных индикаторов. Доработаем методы класса для корректного отображения стрелок, отображающих данные стрелочных индикаторов, рассчитанных на символе/периоде, не соответствующих символу/периоду текущего графика.
Нейросети — это просто (Часть 74): Адаптивное прогнозирование траекторий Нейросети — это просто (Часть 74): Адаптивное прогнозирование траекторий
Предлагаю Вам познакомиться с довольно эффективным методом многоагентного прогнозирования траекторий, который способен адаптироваться к различным состояниям окружающей среды.
Разрабатываем мультивалютный советник (Часть 1): Совместная работа нескольких торговых стратегий Разрабатываем мультивалютный советник (Часть 1): Совместная работа нескольких торговых стратегий
Различных торговых стратегий существует довольно много. С точки зрения диверсификации рисков и повышения устойчивости торговых результатов может оказаться полезным использовать несколько параллельно работающих стратегий. Но если каждая стратегия будет реализована в виде отдельного советника, то управлять их совместной работой на одном торговом счёте становится гораздо сложнее. Для решения этой проблемы желательно реализовать работу разных торговых стратегий в одном советнике.