English Русский 中文 Español Deutsch 日本語
preview
Desenvolvimento de um Cliente MQTT para o MetaTrader 5: Metodologia TDD (Parte 2)

Desenvolvimento de um Cliente MQTT para o MetaTrader 5: Metodologia TDD (Parte 2)

MetaTrader 5Integração | 19 fevereiro 2024, 16:05
180 0
Jocimar Lopes
Jocimar Lopes

Introdução

"A otimização prematura é a raiz de todos os males" (Donald Knuth)

Na parte anterior, examinamos o MQTT, um protocolo binário de troca de mensagens altamente eficiente do tipo "publicar/assinar". Falamos sobre o que é o MQTT, por que seu desenvolvimento começou vinte e cinco anos atrás e para que é usado hoje em muitos setores: desde a indústria automobilística até a Internet das Coisas, desde a indústria aeroespacial até simples aplicativos de chat. Vimos que o MQTT pode ser útil em qualquer contexto onde um protocolo de troca de mensagens independente do tipo de conteúdo é necessário, incluindo o contexto de aplicativos de negociação. Destacamos as vantagens de incluir em nossa base de códigos um cliente MQL5 nativo para MQTT e estabelecemos o ambiente mínimo necessário para o desenvolvimento, usando o broker MQTT de código aberto Mosquitto, operando no WSL (Subsistema Windows para Linux).

Começamos o desenvolvimento do nosso próprio cliente com uma função geradora de cabeçalho fixo rigidamente programada e chegamos ao ponto de conseguir nos conectar ao broker local Mosquitto, mas a conexão foi imediatamente resetada pelo servidor devido a um erro de protocolo.

Citação retirada do final do artigo anterior:

"Nosso cabeçalho CONNECT de dois bytes foi reconhecido pelo Mosquitto, mas um cliente desconhecido (<unknown>) foi desconectado imediatamente 'devido a um erro de protocolo'. Isso ocorreu porque ainda não incluímos o cabeçalho variável com o nome do protocolo, o nível do protocolo e outros metadados relacionados. Vamos corrigir isso no próximo passo."

Fig. 01. Erro de Conexão

Fig. 01. Aba de EAs no MetaEditor, mostrando o erro de conexão


Uma vez que aplicamos o princípio do desenvolvimento orientado por testes (TDD), começaremos escrevendo um teste para o construtor de pacotes CONNECT, que inclui os metadados apropriados e não é rejeitado "devido a um erro de protocolo".

Escrever o teste antes do código a ser testado pode parecer ilógico para muitos. No entanto, se pensarmos nos testes como uma descrição objetiva dos requisitos do projeto e os considerarmos a definição mais objetiva de um objetivo que um desenvolvedor pode ter, tudo se encaixa.

"Os testes unitários são documentos. Eles descrevem o design do sistema no nível mais baixo. Eles são inequívocos, precisos, escritos em uma linguagem compreensível para a audiência, e são tão formais que podem ser executados. Este é o melhor tipo de documentação de baixo nível que pode existir. Que profissional não forneceria tal documentação?" (Robert Martin "Código Limpo", 2011)


Organização do código: OOP e arquivos de cabeçalho

Como mencionado anteriormente, começamos a criar o cabeçalho fixo do nosso pacote de conexão codificando rigidamente um array de bytes com os "valores corretos". Então, tentamos conectar nosso cliente enviando esse array de bytes codificado rigidamente ao nosso broker local. Nossas tentativas falharam "devido a um erro de protocolo". Mas, ao mesmo tempo, aprendemos algo sobre nosso ambiente de desenvolvimento, especialmente sobre nosso log do Mosquitto - assim escrevemos nossos primeiros testes e, acima de tudo, começamos a fazer algo que funciona.

Como você pode ver, esse fracasso foi intencional. Mas sabemos que essa maneira de desenvolver um aplicativo complexo não é sustentável a longo prazo.

Criar pacotes MQTT apropriados é apenas o primeiro (e mais fácil) passo no processo de escrever um cliente confiável e fácil de manter. Quando se trata de especificar as características de desempenho, todas as complexidades do protocolo vêm à tona. Esta tarefa exigirá mais de nós como desenvolvedores. Além de enviar os pacotes apropriados, teremos que lidar com uma grande variedade de respostas do servidor e os diferentes estados dos aplicativos. Nesta fase, arrays de bytes codificados de forma rígida (ou qualquer coisa programada neste caso) não serão suficientes.

Felizmente, o MQL5 é uma linguagem de programação orientada a objetos, e não estamos trabalhando em um ambiente com limitações de memória/processador para as quais o MQTT foi originalmente projetado. Assim, podemos aproveitar todos os benefícios do paradigma de Programação Orientada a Objetos (POO) para:

  • Facilitar a tomada de decisões sobre o protocolo através da escolha do nível de abstração correto
  • Facilitar a leitura do código (lembre-se de que o código é lido muito mais vezes do que é escrito)
  • Manter o código sem muita dificuldade
  • E testar facilmente

Na seção "Programação Orientada a Objetos" do guia MQL5, há uma seção inteira dedicada a este tema. 


Definições de protocolos

O protocolo de troca de mensagens é um conjunto de regras que estabelece uma base comum de interação entre dois ou mais objetos. No nosso caso, entre dois ou mais dispositivos. Muitas dessas regras dizem respeito ao que fazer, levando em consideração o que foi feito anteriormente. Elas são estáticas. Para escolher a próxima ação, nosso código deve avaliar o estado atual do aplicativo. No protocolo MQTT, isso é feito seguindo as regras de comportamento operacional (Operational behavior rules).

Além das regras com rastreamento de estado (de fato, antecedendo-as), existem definições de termos, valores e cálculos que não dependem do estado do aplicativo. Geralmente, são constantes, enumerações e algoritmos de avaliação, como no caso do nome do protocolo MQTT, tipos de pacotes de controle e o valor do byte de comprimento restante do cabeçalho fixo, respectivamente.

Vamos compilar esses dois diferentes conjuntos de regras em dois arquivos de cabeçalho separados. O primeiro deles é destinado apenas para definições de termos e valores comuns aos nossos arquivos. Vamos chamá-lo de Defines.mqh. Esses termos e valores geralmente são constantes, e esse arquivo praticamente não deve mudar.

O outro arquivo de cabeçalho conterá algumas enumerações, estruturas e funções comuns. Vamos chamá-lo de MQTT.mqh. Essas enumerações, estruturas e funções vão mudar frequentemente, e não apenas enquanto desenvolvemos a primeira versão. O arquivo mudará sempre que realizarmos melhorias, otimizações e correções de erros. Provavelmente, esse arquivo será dividido em outros arquivos mais específicos.

A prática de usar arquivos de cabeçalho para organizar o código não está diretamente relacionada à programação orientada a objetos. Notas úteis sobre tais arquivos podem ser encontradas no livro clássico de Brian Kernighan e Dennis Ritchie, "The C Programming Language". 

"(…) definições e declarações comuns a todos os arquivos. Na medida do possível, queremos centralizar o processo para ter apenas uma cópia, que pode ser obtida e mantida à medida que o programa se desenvolve. (…) Provavelmente, até certo tamanho moderado do programa, é melhor ter um arquivo de cabeçalho contendo tudo o que precisa ser compartilhado por qualquer duas partes do programa (...)." Para programas muito maiores, será necessário mais organização e mais cabeçalhos.

Mas é na programação orientada a objetos que a prática de organizar o código em pequenos módulos de compilação se torna particularmente evidente. Além disso, como estamos criando uma biblioteca, quase todo o nosso código estará em arquivos de cabeçalho.

Cabeçalho Defines

Neste estágio, as definições do nome do protocolo e do nível do protocolo são usadas apenas nos pacotes CONNECT. Assim, se desejado, podemos colocá-los em uma classe específica CPktConnect (veja abaixo). Mas vamos deixá-los no cabeçalho Defines para uniformidade. Embora no momento sejam usados apenas nos pacotes CONNECT, mais tarde eles podem ser usados em outros arquivos.

Os comentários ao protocolo são citações da descrição oficial do padrão.

//+------------------------------------------------------------------+
//|                                                      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)

Note que adicionamos o prefixo MQTT a todas as definições específicas do protocolo. Isso é feito para distingui-las de nossas próprias definições, que serão adicionadas mais tarde. Também observe que, no início do nosso arquivo Defines.mqh, na seção PROTOCOL NAME AND VERSION, tentamos especificar os nomes dos identificadores de forma tão explícita quanto possível. Isso é feito para aderir aos princípios do chamado "código limpo". Esses princípios devem ajudar a tornar nosso código mais fácil de ler, mais simples de depurar e amigável ao IDE, ou seja, mais acessível para busca e bem adequado para o uso da função de preenchimento automático dos IDEs modernos.


Cabeçalho 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;
  };

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


Classes e estruturas

Interface dos pacotes de controle MQTT

Aqui precisamos fazer uma escolha: iniciar a hierarquia de objetos dos pacotes de controle (Control Packets) com uma classe abstrata ou com uma interface. Poderíamos começar com uma classe base comum, adequada para qualquer pacote de controle. Esta classe abstrata poderia ser especializada em classes derivadas mais específicas de pacotes de controle. Ou poderíamos começar com uma interface simples, que seria implementada por essas classes de pacotes de controle.

Vamos começar com a interface IcontrolPacket. Ela terá um método simples. Esta escolha pode mudar na implementação das características de trabalho do protocolo. Provavelmente, mudaremos essa interface para uma classe abstrata com algumas funções virtuais.

//+------------------------------------------------------------------+
//|                                               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();

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

Como já foi dito, no momento, o único objetivo desta interface é atuar como a raiz da hierarquia dos objetos de pacotes MQTT. No momento, não é nada mais do que um stub.

Classe de conexão dos pacotes de controle MQTT

O pacote de controle CONNECT é o mais trabalhoso de desenvolver. Além de ainda termos que nos familiarizar com o protocolo, é este pacote específico que recebeu as melhorias mais significativas na versão 5.0, nomeadamente as propriedades de conexão (Connect Properties) e as propriedades do usuário (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()
  {
  }
//+------------------------------------------------------------------+


Testando nossa primeira classe

O único objetivo da classe CPktConnect é criar um pacote MQTT CONNECT corretamente formatado. Assim, para testá-lo, precisamos começar criando um exemplo de array de bytes, que representará um pacote CONNECT corretamente formatado. Mas como podemos ter certeza de que nosso array de bytes formado representa um pacote CONNECT corretamente formatado? Afinal, este é o teste que usaremos para criar nossa classe do zero. Muitos, se não todos, os nossos esforços podem ser desperdiçados se nosso "arranjo" representar um pacote mal formado.

Aqui, o desenvolvedor do protocolo, OASIS, veio em nosso auxílio. Na seção 3.1.2.12, pode-se encontrar um exemplo -fora da norma- do cabeçalho variável para o pacote CONNECT. Como já testamos nosso gerador de cabeçalho fixo (veja o artigo anterior), este exemplo da OASIS será suficiente para começarmos. Isso nos permitirá ter certeza de que nossa classe gera um pacote corretamente formatado com algumas configurações variadas, como a opção CleanSession lógica e o intervalo de tempo de atividade solicitado para a conexão (Keep-Alive).

Este array de bytes codificado de forma rígida, gerado manualmente, será então comparado com o pacote gerado por 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;
  }
//+------------------------------------------------------------------+

Nota: Como você sabe, escrever testes sob o princípio "vamos ver se os dados que acabei de colocar no array de cabeçalhos estão lá" pode parecer uma perda de tempo. Mas não é. Este conjunto de testes "óbvios" sempre acompanhará nosso código. Eles podem ser vistos como uma ferramenta de depuração contínua e automatizada que provará seu valor quando você encontrar algum erro de regressão ou mesmo um erro básico causado, por exemplo, por uma cópia incorreta. Por esta razão, não estamos apenas verificando se a ação de conexão funciona como um "caixa preta". Queremos ter certeza de que nosso cabeçalho está corretamente formatado antes de testar a ação de conexão. Vale lembrar que TDD (Desenvolvimento Guiado por Testes) é um processo. Muitos, se não todos, desses testes serão reescritos ou mesmo removidos antes de obtermos a primeira versão funcional do nosso código. Mas aqueles que permanecerem, provavelmente permanecerão para sempre.

Este teste será considerado aprovado apenas quando o array de bytes gerado por CPktConnect retornar 0 (zero) no ArrayCompare(d) com nosso array de bytes de referência.​

Após termos testado algumas combinações das propriedades básicas de conexão, o pacote será enviado ao broker. Desta vez, ele não deve ser rejeitado "devido a um erro de protocolo".

Fig. 02. Resultados do teste da classe CPktConnect

Fig. 02. Resultados do teste da classe CPktConnect na aba de EAs do MetaEditor


Teste com o broker MQTT local

Agora, podemos iniciar nosso broker local Mosquitto no WSL para verificar se nossa conexão MQTT foi bem-sucedida.

Se você realizou a instalação padrão, o Mosquitto deve funcionar no Linux como um serviço. Assim, você só precisa "redirecionar" (redir) as portas (80 → 1883) e habilitar o nome do host para URLs permitidos nas configurações do MetaTrader 5.

Fig. 03. Log do Mosquitto no WSL sobre a conexão/desconexão bem-sucedida

Fig. 03. Log do Mosquitto no WSL, mostrando o status de conexão/desconexão: Sucesso


 Viva! Nossa tentativa de conexão não retorna um erro de protocolo. Agora podemos tentar trocar mensagens entre o cliente e o servidor.


Considerações finais

Na próxima etapa, examinaremos as respostas CONNACK. Neste ponto, teremos uma base sólida para publicar nossa primeira mensagem. E, claro, começaremos a escrever um teste para isso! :)  Aguarde novos artigos!


Traduzido do Inglês pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/en/articles/13334

Arquivos anexados |
Defines.mqh (8.03 KB)
MQTT.mqh (5.01 KB)
CPktConnect.mqh (9.97 KB)
Modelos prontos para integrar indicadores nos Expert Advisors (Parte 2): Indicadores de volume e Bill Williams Modelos prontos para integrar indicadores nos Expert Advisors (Parte 2): Indicadores de volume e Bill Williams
Neste artigo, examinaremos os indicadores padrão das categorias Volumes e Bill Williams. Criaremos modelos prontos a serem usados em Expert Advisors, modelos esses que incluirão: declaração e configuração de parâmetros, inicialização/desinicialização de indicadores e recuperação de dados/sinais a partir de buffers de indicador em EAs.
Regressão rede elástica usando descida de coordenadas no MQL5 Regressão rede elástica usando descida de coordenadas no MQL5
Neste artigo, exploraremos a implementação prática da regressão rede elástica (elastic net regularization) para minimizar o sobreajuste e, ao mesmo tempo, separar automaticamente preditores úteis daqueles que possuem pouca força preditiva.
Pairs Trade Pairs Trade
Neste artigo, examinaremos o pairs trade, ou negociação de pares, principalmente seus princípios e perspectivas quanto à sua aplicação prática. Além disso, tentaremos criar uma estratégia baseada nele.
Desenvolvendo um sistema de Replay (Parte 43): Projeto do Chart Trade (II) Desenvolvendo um sistema de Replay (Parte 43): Projeto do Chart Trade (II)
Grande parte das pessoas que querem, ou desejam aprender a programar, não fazem de fato ideia, do que estão fazendo. O que elas fazem é tentar criar as coisas de uma determinada maneira. No entanto, quando programamos não estamos de fato tentando criar um solução. Se você tentar fazer isto, desta forma irá gerar mais problemas do que realmente uma solução. Aqui iremos fazer algo um pouco mais avançado, e por consequência diferente.