English Русский Deutsch 日本語
preview
Desenvolvendo um cliente MQTT para Metatrader 5: uma abordagem TDD — Parte 6

Desenvolvendo um cliente MQTT para Metatrader 5: uma abordagem TDD — Parte 6

MetaTrader 5Integração | 26 julho 2024, 17:25
21 0
Jocimar Lopes
Jocimar Lopes

 

"O otimismo é um risco ocupacional da programação; o feedback é o tratamento." (Kent Beck)

Introdução

A metodologia de Desenvolvimento Orientado a Testes oferece muitos benefícios e tem uma grande desvantagem. Entre os benefícios, ela nos ajuda a escrever unidades bem definidas e variáveis bem nomeadas, alcançar alta cobertura de testes, ter uma melhor compreensão do domínio, evitar superengenharia e manter o foco na tarefa em mãos. A principal desvantagem é uma consequência direta desse foco estreito na tarefa em mãos, ou seja, para evitar ser assustado pela complexidade geral do projeto, nós, como desenvolvedores, continuamos resolvendo o menor desafio possível de cada vez, e apenas um desafio de cada vez. Se o gênio é a pessoa que remove a complexidade resolvendo-a, o desenvolvedor TDD é a pessoa que deliberadamente ignora a complexidade. 

Sim, você entendeu: muito parecido com cavalos usando antolhos, muito parecido com aquele burro seguindo a cenoura.

Mas a complexidade não desaparece porque a ignoramos. Ela fica lá, esperando que a enfrentemos. Ao ignorar a floresta para olhar de perto a folha, continuamos deixando uma dívida técnica para trás. Continuamos deixando funções redundantes, membros duplicados, testes inúteis, classes desnecessárias, código ilegível e inacessível, você sabe. Essa dívida técnica acumulada durante o desenvolvimento pode ser prejudicial à nossa produtividade. É por isso que o refatoramento é uma parte integrante da prática de TDD. O diagrama abaixo mostra as etapas típicas de uma prática de TDD.

As Etapas Típicas de uma Prática de TDD: Vermelho, Verde, Refatoração

Fig. 01 - As Etapas Típicas de uma Prática de TDD: Vermelho, Verde, Refatoração (Fonte: IBM Developer)

Nas seções seguintes, descrevemos como refatoramos nossas classes anteriormente escritas e comentamos algumas melhorias. Mostramos como estamos construindo nossos pacotes PUBLISH após essas melhorias e como chegamos a um modelo viável para nossas classes de construção de pacotes. A primeira classe seguindo o novo padrão é a classe PUBACK. Como os pacotes PUBACK são a contraparte dos pacotes PUBLISH com QoS 1, precisamos começar a lidar com o gerenciamento do Estado da Sessão. Nosso cliente precisará ter algum tipo de camada de persistência para preservar e atualizar o estado. 

A camada de persistência está fora do escopo do Padrão OASIS. Ele é específico da aplicação. Pode ser um simples arquivo no sistema de arquivos local ou um sistema de banco de dados totalmente distribuído e de alta disponibilidade na nuvem. Para nossos propósitos, um banco de dados como um servidor PostgreSQL rodando localmente no Windows ou via WSL seria suficiente. No entanto, como temos uma integração nativa entre MQL e SQLite, este RDBMS de arquivo único, sem servidor, é a escolha óbvia aqui. O SQLite é leve, escalável, confiável e livre de manutenção de servidor. Podemos até ter um banco de dados apenas em memória, o que é bastante conveniente para testes e depuração. 

Mas não implementaremos a camada de persistência neste momento, porque escolhemos testar bem a escrita e leitura de pacotes antes de lidar com o gerenciamento do Estado da Sessão. Precisamos ter certeza de que estamos codificando e decodificando corretamente os diferentes tipos de dados usados pelo protocolo MQTT antes de avançar para a camada de persistência. Para alcançar esse objetivo, estamos escrevendo testes unitários extensivos e em breve começaremos com pequenos testes funcionais contra um corretor real rodando localmente (o corretor mosquitto de código aberto, da Eclipse Foundation).

Então, para testar nossas interações PUBLISH/PUBACK, usaremos um banco de dados falso, uma coleção de funções para gerar os dados controlados que precisamos para testar, uma espécie de fixture. Nós o apresentaremos abaixo ao descrever a classe CPuback.

Nas descrições que se seguem, usamos os termos DEVE e PODE como são usados pelo Padrão OASIS, que por sua vez os usa conforme descrito no IETF RFC 2119.

Além disso, salvo indicação em contrário, todas as citações são do Padrão OASIS.


Como Estamos Construindo Pacotes PUBLISH

No processo de reescrita da classe CPublish, removemos alguns membros da classe. Também mesclamos a construção do cabeçalho fixo/variável em um construtor de uma etapa. Essas mudanças estão sendo replicadas em outras classes de Pacotes de Controle.

Atualmente, nossa classe CPublish possui os seguintes membros e métodos.

//+------------------------------------------------------------------+
//|                                                      Publish.mqh |
//|            ********* WORK IN PROGRESS **********                 |
//| **** PART OF ARTICLE https://www.mql5.com/en/articles/14391 **** |
//+------------------------------------------------------------------+
#include "IControlPacket.mqh"
//+------------------------------------------------------------------+
//|        PUBLISH VARIABLE HEADER                                   |
//+------------------------------------------------------------------+
/*
The Variable Header of the PUBLISH Packet contains the following fields in the order: Topic Name,
Packet Identifier, and Properties.
*/
//+------------------------------------------------------------------+
//| Class CPublish.                                                  |
//| Purpose: Class of MQTT Publish Control Packets.                  |
//|          Implements IControlPacket                               |
//+------------------------------------------------------------------+
class CPublish : public IControlPacket
  {
private:
   bool              IsControlPacket() {return true;}
   bool              HasWildcardChar(const string str);
protected:
   uchar             m_pubflags;
   uint              m_remlen;
   uchar             m_topname[];
   uchar             m_props[];
   uint              m_payload[];
public:
                     CPublish();
                    ~CPublish();
   //--- methods for setting Publish flags
   void              SetRetain(const bool retain);
   void              SetQoS_1(const bool QoS_1);
   void              SetQoS_2(const bool QoS_2);
   void              SetDup(const bool dup);
   //--- method for setting Topic Name
   void              SetTopicName(const string topic_name);
   //--- methods for setting Properties
   void              SetPayloadFormatIndicator(PAYLOAD_FORMAT_INDICATOR format);
   void              SetMessageExpiryInterval(uint msg_expiry_interval);
   void              SetTopicAlias(ushort topic_alias);
   void              SetResponseTopic(const string response_topic);
   void              SetCorrelationData(uchar &binary_data[]);
   void              SetUserProperty(const string key, const string val);
   void              SetSubscriptionIdentifier(uint subscript_id);
   void              SetContentType(const string content_type);
   //--- method for setting the payload
   void              SetPayload(const string payload);
   //--- method for building the final packet
   void              Build(uchar &result[]);
  };

Além da simplificação, agora o processo de configuração das flags de publicação, nomes de tópicos e propriedades são todos independentes, o que significa que cada um deles pode ser definido em qualquer ordem, desde que o método Build() seja o último a ser invocado.
Este teste formaliza esse comportamento. Ele testa o construtor da classe com duas flags definidas, RETAIN e QoS1, e o Nome do Tópico necessário.
bool TEST_Ctor_Retain_QoS1_TopicName1Char()
  {
   Print(__FUNCTION__);
   CPublish *cut = new CPublish();
   uchar expected[] = {51, 6, 0, 1, 'a', 0, 1, 0}; // QoS > 0 require packet ID
   uchar result[];
   cut.SetTopicName("a");
   cut.SetRetain(true);
   cut.SetQoS_1(true);
   cut.Build(result);
   bool isTrue = AssertEqual(expected, result);
   delete(cut);
   ZeroMemory(result);
   return isTrue;
  }

Agora, os métodos SetTopicName(), SetRetain() e SetQos1() podem ser chamados em qualquer ordem e o pacote resultante ainda é válido. Como dito, esse comportamento está sendo replicado em todas as classes de pacotes de controle, e temos um teste para cada combinação de flags de publicação. Por favor, veja os arquivos anexados para obter todos os testes.

O cabeçalho fixo do pacote PUBLISH

Os cabeçalhos fixos dos pacotes PUBLISH são diferentes de todos os outros Pacotes de Controle MQTT 5.0 na versão atual do protocolo. Eles têm três flags que NÃO estão reservadas para uso futuro: RETAIN, QoS e DUP. No artigo anterior, parte 5 desta série, você pode ver uma descrição detalhada sobre essas flags PUBLISH.

Cabeçalho Fixo do Pacote MQTT 5.0 PUBLISH: Flags RETAIN, Nível QoS e DUP

Fig. 02 - Cabeçalho Fixo do Pacote MQTT 5.0 PUBLISH: Flags RETAIN, Nível QoS e DUP

Estamos usando o mesmo padrão para alternar qualquer uma das flags de publicação, mas agora, após o refatoramento, não estamos mais chamando SetFixedHeader() em cada uma delas. Primeiro, definimos a alternância como um valor booleano que é passado como argumento para a função.

void CPktPublish::SetRetain(const bool retain)
  {
   retain ? m_pubflags |= RETAIN_FLAG : m_pubflags &= ~RETAIN_FLAG;
  }

Em seguida, verificamos se o valor booleano é verdadeiro ou falso.

void CPktPublish::SetQoS_1(const bool QoS_1)
  {
   QoS_1 ? m_pubflags |= QoS_1_FLAG : m_pubflags &= ~QoS_1_FLAG;
  }

Se o valor booleano for verdadeiro, realizamos uma atribuição OR bit a bit entre o valor da flag e um membro uchar (um byte) para definir a flag.

void CPktPublish::SetQoS_2(const bool QoS_2)
  {
   QoS_2 ? m_pubflags |= QoS_2_FLAG : m_pubflags &= ~QoS_2_FLAG;
  }

Se o valor booleano for falso, realizamos uma atribuição AND bit a bit entre o valor da flag e o mesmo membro uchar para desmarcar a flag.

void CPktPublish::SetDup(const bool dup)
  {
   dup ? m_pubflags |= DUP_FLAG : m_pubflags &= ~DUP_FLAG;
  }

Dessa forma, a variável m_pubflags mantém todas as flags definidas/desmarcadas enquanto configuramos o pacote. Mais tarde, quando o método Build() é chamado, realizamos novamente uma atribuição OR bit a bit, desta vez entre m_pubflags e o primeiro byte do pacote (byte 0).

pkt[0] |= m_pubflags;


O cabeçalho variável do pacote PUBLISH

O Cabeçalho Variável do Pacote PUBLISH contém os seguintes campos na ordem: Nome do Tópico, Identificador do Pacote e Propriedades.

Nome do Tópico

Como todas as relações entre publicadores e assinantes estão ligadas ao Nome do Tópico da publicação, este campo é obrigatório nos pacotes PUBLISH e não pode conter caracteres coringa. Ao definir este campo, temos duas condições de guarda: para caracteres coringa e para uma string com comprimento zero. Se qualquer uma dessas condições for verdadeira, retornamos imediatamente e registramos o erro.

void CPktPublish::SetTopicName(const string topic_name)
  {
   if(HasWildcardChar(topic_name) || StringLen(topic_name) == 0)
     {
      ArrayFree(m_topname);
      return;
     }
   EncodeUTF8String(topic_name, m_topname);
  }

Se nenhuma das condições de guarda for atendida, codificamos a string como UTF-8 e armazenamos o array de caracteres no membro protegido m_topname para ser incluído no pacote final quando o Build() for chamado.

Identificador do Pacote

O Identificador do Pacote NÃO é definido pelo usuário e não é necessário para QoS 0. Em vez disso, ele é definido automaticamente no método Build(), se a QoS necessária for > 0. 

// QoS > 0 requires packet ID
   if((m_pubflags & 0x06) != 0)
     {
      SetPacketID(pkt, pkt.Size());
     }
Ao construir o pacote final, verificamos o membro m_pubflags através de uma operação AND bit a bit com o valor binário 0110 (0x06). Se o resultado não for igual a zero, sabemos que o pacote possui QoS_1 ou QoS_2 e definimos o Identificador do Pacote.
A função SetPacketID gera um número inteiro pseudorrandômico usando TimeLocal() para gerar o estado inicial. Para facilitar nossos testes, definimos uma variável booleana TEST. Quando essa variável está verdadeira, a função define o valor 1 como o ID do pacote.
//+------------------------------------------------------------------+
//|            SetPacketID                                           |
//+------------------------------------------------------------------+
#define TEST true

void SetPacketID(uchar& buf[], int start_idx)
  {
// MathRand - Before the first call of the function, it's necessary to call
// MathSrand to set the generator of pseudorandom numbers to the initial state.
   MathSrand((int)TimeLocal());
   int packet_id = MathRand();
   if(ArrayResize(buf, buf.Size() + 2) < 0)
     {
      printf("ERROR: failed to resize array at %s", __FUNCTION__);
      return;
     }
   buf[start_idx] = (uchar)packet_id >> 8; // MSB
   buf[start_idx + 1] = (uchar)(packet_id % 256) & 0xff; //LSB
//--- if testing, set packet ID to 1
   if(TEST)
     {
      Print("WARN: SetPacketID TEST true fixed ID = 1");
      buf[start_idx] = 0; // MSB
      buf[start_idx + 1] = 1; //LSB
     }
  }

Como você pode ver, também temos um aviso WARNing, apenas por precaução.

Propriedades

Em parte 4 desta série de artigos, vimos em detalhes o que são as Propriedades e seu papel como parte dos Mecanismos de Extensibilidade do MQTT 5.0. Aqui, descreveremos como estamos implementando-as, com especial atenção para os diferentes tipos de dados e sua codificação.

Existem seis tipos de representação de dados usados para codificar os valores das Propriedades em um Pacote de Controle MQTT 5.0:

  1. Inteiro de Um Byte, que são inteiros sem sinal de 8 bits
  2. Inteiros de Dois Bytes, que são inteiros sem sinal de 16 bits em ordem big-endian, também chamada de ordem de rede
  3. Inteiros de Quatro Bytes, que são inteiros sem sinal de 32 bits também em ordem big-endian
  4. Inteiros de Byte Variável, que usam o número mínimo de até quatro bytes para representar um valor entre 0 e 268.435.455
  5. Dados Binários entre 0 e 65.535 de comprimentos
  6. Strings codificadas em UTF-8, que também podem ser usadas para codificar um par chave nas Propriedades do Usuário

A tabela a seguir mostra as propriedades PUBLISH disponíveis e sua respectiva representação de dados.

Propriedades Representação de Dados
Indicador de Formato de Payload Inteiro de Um Byte
Intervalo de Expiração da Mensagem Inteiro de Quatro Bytes
Alias de Tópico Inteiro de Dois Bytes 
Tópico de Resposta String Codificada em UTF-8
Dados de Correlação  Dados Binários 
Propriedade do Usuário  Par de Strings Codificado em UTF-8
Identificador de Inscrição  Inteiro de Byte Variável
Tipo de Conteúdo String Codificada em UTF-8

Tabela 01 - Propriedades PUBLISH e sua Representação de Dados no MQTT 5.0

Nossos identificadores de Propriedade foram incluídos no nosso cabeçalho Defines.mqh.

//+------------------------------------------------------------------+
//|              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_PROP_IDENTIFIER_PAYLOAD_FORMAT_INDICATOR          0x01 // (1) Byte                  
#define MQTT_PROP_IDENTIFIER_MESSAGE_EXPIRY_INTERVAL           0x02 // (2) Four Byte Integer     
#define MQTT_PROP_IDENTIFIER_CONTENT_TYPE                      0x03 // (3) UTF-8 Encoded String  
#define MQTT_PROP_IDENTIFIER_RESPONSE_TOPIC                    0x08 // (8) UTF-8 Encoded String  
#define MQTT_PROP_IDENTIFIER_CORRELATION_DATA                  0x09 // (9) Binary Data           
#define MQTT_PROP_IDENTIFIER_SUBSCRIPTION_IDENTIFIER           0x0B // (11) Variable Byte Integer
#define MQTT_PROP_IDENTIFIER_SESSION_EXPIRY_INTERVAL           0x11 // (17) Four Byte Integer   
.
.
. 

Indicador de Formato de Payload

O Indicador de Formato de Payload pode ser um valor 0 ou 1, significando bytes brutos ou string codificada em UTF-8, respectivamente. Se não estiver presente, assume-se que é 0 (bytes brutos).

Embora esse campo pudesse ser definido diretamente no array membro m_props, optamos por usar um buffer local auxiliar como intermediário para ser consistente com a maioria das propriedades que requerem algum tipo de manipulação antes de serem copiadas para o array final de propriedades.

void CPktPublish::SetPayloadFormatIndicator(PAYLOAD_FORMAT_INDICATOR format)
  {
   uchar aux[2];
   aux[0] = MQTT_PROP_IDENTIFIER_PAYLOAD_FORMAT_INDICATOR;
   aux[1] = (uchar)format;
   ArrayCopy(m_props, aux, m_props.Size());
  }

Embora existam apenas dois valores possíveis para esta propriedade, escolhemos atribuir um valor simbólico para eles por questões de legibilidade.

enum PAYLOAD_FORMAT_INDICATOR
  {
   RAW_BYTES   = 0x00,
   UTF8        = 0x01
  };

O uso desse valor simbólico torna a chamada do método explícita para o usuário da biblioteca.

cut.SetPayloadFormatIndicator(RAW_BYTES);
cut.SetPayloadFormatIndicator(UTF8);

Intervalo de Expiração da Mensagem

O Intervalo de Expiração da Mensagem é representado como um inteiro de quatro bytes. Vale lembrar que essa representação é diferente da de um inteiro de byte variável. Enquanto o último usará o número mínimo de bytes necessários para representar o valor, o primeiro será sempre representado usando os quatro bytes completos.

void CPktPublish::SetMessageExpiryInterval(uint msg_expiry_interval)
  {
   uchar aux[4];
   aux[0] = MQTT_PROP_IDENTIFIER_MESSAGE_EXPIRY_INTERVAL;
   ArrayCopy(m_props, aux, m_props.Size(), 0, 1);
   EncodeFourByteInteger(msg_expiry_interval, aux);
   ArrayCopy(m_props, aux, m_props.Size());
  }

Nossa função para codificar o inteiro de quatro bytes segue um padrão bem conhecido de deslocamento para a direita por potência de dois para garantir a ordem big-endian (ou ordem de rede) necessária.

void EncodeFourByteInteger(uint val, uchar &dest_buf[])
  {
   ArrayResize(dest_buf, 4);
   dest_buf[0] = (uchar)(val >> 24) & 0xff;
   dest_buf[1] = (uchar)(val >> 16) & 0xff;
   dest_buf[2] = (uchar)(val >> 8) & 0xff;
   dest_buf[3] = (uchar)val & 0xff;
  }

Alias de Tópico

A propriedade Alias de Tópico pode ser usada para reduzir o tamanho do pacote. Ela é restrita a cada conexão de rede e faz parte do estado da sessão MQTT. Portanto, nossa função para definir o Alias de Tópico pode ser considerada um stub por enquanto. Ela deve ser completada ao lidar com o Estado da Sessão.

void CPktPublish::SetTopicAlias(ushort topic_alias)
  {
   uchar aux[2];
   aux[0] = MQTT_PROP_IDENTIFIER_TOPIC_ALIAS;
   ArrayCopy(m_props, aux, m_props.Size(), 0, 1);
   EncodeTwoByteInteger(topic_alias, aux);
   ArrayCopy(m_props, aux, m_props.Size());
  }

Nossa função para codificar o inteiro de dois bytes segue o mesmo padrão bem conhecido que usamos para codificar inteiros de quatro bytes, ou seja, deslocamentos para a direita por potências de dois para garantir a ordem big-endian necessária.

void EncodeTwoByteInteger(uint val, uchar &dest_buf[])
  {
   ArrayResize(dest_buf, 2);
   dest_buf[0] = (uchar)(val >> 8) & 0xff;
   dest_buf[1] = (uchar)val & 0xff;
  }

Tópico de Resposta

A propriedade de Tópico de Resposta não faz parte do padrão de publicação/inscrição. Em vez disso, ela faz parte da interação de solicitação/resposta sobre o MQTT. Como você pode ver, nossa função usa dois buffers auxiliares, um para hospedar o identificador da propriedade e o outro buffer para hospedar a string codificada em UTF-8. O mesmo ocorrerá com outras strings codificadas em UTF-8 porque nossa função de codificação de strings não tem um terceiro parâmetro para endereçar o índice de início do buffer de destino. Isso pode ser resolvido com uma sobrecarga nas próximas versões (veja acima Tópico de Resposta).

void CPktPublish::SetResponseTopic(const string response_topic)
  {
   uchar aux[1];
   aux[0] = MQTT_PROP_IDENTIFIER_RESPONSE_TOPIC;
   ArrayCopy(m_props, aux, m_props.Size());
   uchar buf[];
   EncodeUTF8String(response_topic, buf);
   ArrayCopy(m_props, buf, m_props.Size());
  }

Dados de Correlação

A propriedade de Dados de Correlação também faz parte da interação de solicitação/resposta sobre o MQTT, e não do padrão de publicação/inscrição. Como seu valor é dado em dados binários, nossa função está simplesmente copiando os dados passados como argumento para o array de bytes m_props após definir o identificador da propriedade.

void CPktPublish::SetCorrelationData(uchar &binary_data[])
  {
   uchar aux[1];
   aux[0] = MQTT_PROP_IDENTIFIER_CORRELATION_DATA;
   ArrayCopy(m_props, aux, m_props.Size());
   ArrayCopy(m_props, binary_data, m_props.Size());
  }

Propriedade do Usuário

A Propriedade do Usuário é a propriedade mais flexível do MQTT 5.0 porque pode ser usada para transmitir pares chave codificados em UTF-8 com semântica específica da aplicação.

“Comentário não normativo

Esta propriedade tem o objetivo de fornecer um meio de transferir tags nome-valor da camada de aplicação, cujo significado e interpretação são conhecidos apenas pelos programas de aplicação responsáveis pelo envio e recebimento delas.”

Nossa função está usando três buffers auxiliares para codificar essa propriedade porque, atualmente, nosso codificador de strings UTF-8 não tem um terceiro parâmetro para endereçar o índice de início do buffer de destino. Isso pode ser resolvido com uma sobrecarga nas próximas versões (veja acima Tópico de Resposta). (veja o tópico de resposta acima.)

void CPktPublish::SetUserProperty(const string key, const string val)
  {
   uchar aux[1];
   aux[0] = MQTT_PROP_IDENTIFIER_USER_PROPERTY;
   ArrayCopy(m_props, aux, m_props.Size());
   uchar key_buf[];
   EncodeUTF8String(key, key_buf);
   ArrayCopy(m_props, key_buf, m_props.Size());
   uchar val_buf[];
   EncodeUTF8String(val, val_buf);
   ArrayCopy(m_props, val_buf, m_props.Size());
  }

Identificador de Inscrição

Nossa função para definir a propriedade de Identificador de Inscrição começa verificando se o argumento passado está entre 1 e 268.435.455, que são os valores aceitos para essa propriedade. Se não estiver, imprimimos/registramos uma mensagem de erro e retornamos imediatamente.

void CPktPublish::SetSubscriptionIdentifier(uint subscript_id)
  {
   if(subscript_id < 1 || subscript_id > 0xfffffff)
     {
      printf("Error: " + __FUNCTION__ +  "Subscription Identifier must be between 1 and 268,435,455");
      return;
     }
   uchar aux[1];
   aux[0] = MQTT_PROP_IDENTIFIER_SUBSCRIPTION_IDENTIFIER;
   ArrayCopy(m_props, aux, m_props.Size());
   uchar buf[];
   EncodeVariableByteInteger(subscript_id, buf);
   ArrayCopy(m_props, buf, m_props.Size());
  }

Tipo de Conteúdo

O valor da propriedade Tipo de Conteúdo é definido pela aplicação. “O MQTT não realiza validação da string, exceto para garantir que seja uma String Codificada em UTF-8 válida.”

void CPktPublish::SetContentType(const string content_type)
  {
   uchar aux[1];
   aux[0] = MQTT_PROP_IDENTIFIER_CONTENT_TYPE;
   ArrayCopy(m_props, aux, m_props.Size());
   uchar buf[];
   EncodeUTF8String(content_type, buf);
   ArrayCopy(m_props, buf, m_props.Size());
  };

Payload

O último campo no cabeçalho variável do PUBLISH é o payload propriamente dito. Um payload de comprimento zero é válido. Nossa função é nada mais do que uma camada ao redor do nosso codificador de strings UTF-8, seguindo o mesmo padrão de usar um buffer auxiliar para ser copiado para o membro m_payload.

void CPktPublish::SetPayload(const string payload)
  {
   uchar aux[];
   EncodeUTF8String(payload, aux);
   ArrayCopy(m_payload, aux, m_props.Size());
  }

O método Build final

O objetivo do método Build() é combinar o Cabeçalho Fixo, o Nome do Tópico, o Identificador do Pacote, as Propriedades e o Payload no pacote final, enquanto codifica tanto o Comprimento da(s) Propriedade(s) quanto o Comprimento Restante do pacote como inteiro de byte variável.

Primeiro, verificamos a presença do Nome do Tópico obrigatório. Se o seu comprimento for zero, imprimimos/registramos o erro e retornamos imediatamente.

void CPktPublish::Build(uchar &pkt[])
  {
   if(m_topname.Size() == 0)
     {
      printf("Error: " + __FUNCTION__ + " topic name is mandatory");
      return;
     }
   ArrayResize(pkt, 2);


Em seguida, definimos o primeiro byte do cabeçalho fixo com o tipo de pacote de controle e os respectivos flags PUBLISH.

// pkt type with publish flags
   pkt[0] = (uchar)PUBLISH << 4;
   pkt[0] |= m_pubflags;

Em seguida, copiamos o array m_topname para o pacote final e definimos/copiamos o identificador do pacote se QoS > 0.

// topic name
   ArrayCopy(pkt, m_topname, pkt.Size());
// QoS > 0 require packet ID
   if((m_pubflags & 0x06) != 0)
     {
      SetPacketID(pkt, pkt.Size());
     }

Em seguida, codificamos o Comprimento da(s) Propriedade(s) como um inteiro de byte variável.

// properties length
   uchar buf[];
   EncodeVariableByteInteger(m_props.Size(), buf);
   ArrayCopy(pkt, buf, pkt.Size());

Copiamos as propriedades e o payload de seus membros da classe para o array final do pacote.

// properties
   ArrayCopy(pkt, m_props, pkt.Size());
// payload
   ArrayCopy(pkt, m_payload, pkt.Size());

Finalmente, definimos o Comprimento Restante do pacote codificado como um inteiro de byte variável

// remaining length
   m_remlen += pkt.Size() - 2;
   uchar aux[];
   EncodeVariableByteInteger(m_remlen, aux);
   ArrayCopy(pkt, aux, 1);
  }


O Pacote de Controle PUBACK

Como vimos acima ao implementar nossa classe CPublish, qualquer pacote PUBLISH com QoS 1 requer um Identificador de Pacote não-zero. Esse ID de pacote será retornado no pacote PUBACK correspondente. É esse ID que permite ao nosso cliente saber se o pacote PUBLISH enviado anteriormente foi entregue ou se houve um erro. Seja uma entrega bem-sucedida ou uma falha, o PUBACK é o gatilho que usaremos para atualizar o Estado da Sessão. Atualizaremos o Estado da Sessão com base no(s) Código(s) de Razão.

O pacote PUBACK retornará um de nove Códigos de Razão.

SUCCESS - Tudo está bem com a mensagem. Ela foi aceita e a publicação está em andamento. "Sucesso" aqui significa que o receptor aceitou a propriedade da mensagem. Este é o único Código de Razão que pode ser implícito, ou seja, é o único Código de Razão que pode ser omitido. Um PUBACK com apenas um ID de pacote DEVE ser interpretado como uma entrega bem-sucedida de QoS 1.

“O Cliente ou Servidor que envia o pacote PUBACK DEVE usar um dos Códigos de Razão PUBACK [MQTT-3.4.2-1]. O Código de Razão e o Comprimento da Propriedade podem ser omitidos se o Código de Razão for 0x00 (Sucesso) e não houver Propriedades.”

NO MATCHING SUBSCRIBERS - Tudo está bem com a mensagem. Ela foi aceita e a publicação está em andamento, mas ninguém está inscrito no nome do seu tópico. Este Código de Razão é enviado apenas pelo broker e é opcional, ou seja, o broker PODE enviar este Código de Razão em vez de SUCCESS.

UNSPECIFIED ERROR - A mensagem é rejeitada, mas o publicador não quer revelar o motivo ou nenhum dos outros Códigos de Razão mais específicos é adequado para descrever o motivo.

IMPLEMENTATION SPECIFIC ERROR - Tudo está bem com a mensagem, mas o publicador não quer publicá-la. O Padrão não oferece detalhes adicionais sobre a semântica deste Código de Razão, mas podemos inferir que o motivo para não publicar não está no escopo do protocolo, ou seja, é específico da aplicação.

NOT AUTHORIZED - Autoexplicativo.

TOPIC NAME INVALID - Tudo está bem com a mensagem, incluindo o Nome do Tópico, que é uma string UTF-8 bem formada e bem codificada. Mas o publicador, seja o cliente ou o broker, não aceita este Nome de Tópico. Novamente, podemos inferir que o motivo para não publicar é específico da aplicação.

PACKET IDENTIFIER IN USE - Tudo está bem com a mensagem, mas há uma possível incompatibilidade no Estado da Sessão entre o cliente e o broker porque o ID do pacote que enviamos no PUBLISH já está em uso.

QUOTA EXCEEDED - Autoexplicativo. Mais uma vez, o motivo da rejeição não está no escopo do protocolo. Ele é específico da aplicação.

PAYLOAD FORMAT INVALID - Tudo está bem com a mensagem, mas a propriedade do Indicador de Formato do Payload que enviamos no nosso PUBLISH é diferente do formato real do payload.

Além do Código de Razão, o pacote PUBACK pode ter uma String de Razão e uma Propriedade de Usuário.

String de Razão é uma string codificada em UTF-8 legível por humanos destinada a ajudar no diagnóstico. Ela não é destinada a ser analisada pelo receptor. Em vez disso, seu propósito é carregar informações adicionais que podem ser registradas, impressas, anexadas a relatórios, etc. Vale a pena notar que qualquer servidor ou cliente compatível não enviará a String de Razão se sua inclusão aumentar o tamanho do pacote além do Tamanho Máximo do Pacote especificado no momento da conexão (pacote CONNECT).

O PUBACK também pode ter qualquer número de pares chave codificados como Propriedade(s) de Usuário. Esses pares podem ser usados para fornecer informações adicionais sobre o erro e também são específicos da aplicação. Ou seja, o protocolo não define suas semânticas. 

Nosso cliente "DEVE tratar o pacote PUBLISH como 'não reconhecido' até que tenha recebido o pacote PUBACK correspondente do receptor."


A Classe CPuback

Nossa classe CPuback segue o mesmo modelo da classe CPublish. Ela também implementa a interface IControlPacket que está funcionando como nosso stub root para a hierarquia de objetos.

Um pacote PUBACK é enviado como uma resposta para pacotes PUBLISH com QoS 1. Seu cabeçalho fixo de dois bytes tem apenas o identificador do pacote de controle no primeiro byte e o comprimento restante do pacote no segundo byte. Seus flags de bits estão todos definidos como RESERVADOS nesta versão do protocolo.

Estrutura do Cabeçalho Fixo de um pacote PUBACK MQTT-5.0

Fig. 03 - Estrutura-do-Cabeçalho-Fixo-de-um-pacote-PUBACK-MQTT-5.0

“O Cabeçalho Variável do Pacote PUBACK contém os seguintes campos na ordem: Identificador de Pacote do pacote PUBLISH que está sendo reconhecido, Código de Razão PUBACK, Comprimento da Propriedade e as Propriedades.”

Estrutura do Cabeçalho Variável de um pacote PUBACK MQTT-5.0

Fig. 04 - Estrutura-do-Cabeçalho-Variável-de-um-pacote-PUBACK-MQTT-5.0

Até agora, lidamos com nosso Cliente apenas como um remetente; de agora em diante, precisamos levar em conta o papel do receptor também. Isso porque

“O protocolo de entrega é simétrico, [...] o Cliente e o Servidor podem assumir o papel de remetente ou receptor.“

Precisamos escrever um teste para uma função que obtém o identificador do pacote que está sendo reconhecido

  1. do pacote retornado enviado pelo broker ao receber um PUBACK
  2. ou do nosso sistema de persistência ao enviar um PUBACK

Um pacote PUBLISH com QoS 1 não tem sentido sem seu PUBACK correspondente, que por sua vez requer algum tipo de persistência para armazenar o ID do pacote de seu pacote PUBLISH correspondente. Mas, embora já saibamos que em algum momento precisaremos usar um banco de dados real como camada de persistência, neste ponto ainda não precisamos disso. Para testar e desenvolver nossa função, tudo o que precisamos é de algo que aja como um banco de dados, algo que quando consultado retorne o que seria o identificador dos pacotes PUBLISH pendentes de reconhecimento. Para evitar surpresas, vamos criar uma única função chamada GetPendingPublishIDs(ushort &result[]) e salvá-la em um arquivo chamado DB.mqh.

void GetPendingPublishIDs(ushort &result[])
  {
   ArrayResize(result, 3);
   result[0] = 1;
   result[1] = 255; // one byte
   result[2] = 65535; // two bytes
  }

Com nossa “camada de persistência” em vigor, podemos nos concentrar na tarefa em mãos: escrever uma função que, quando passado um array de bytes PUBACK (pacote) enviado pelo broker, obtenha o identificador do PUBLISH sendo reconhecido e o verifique em relação aos IDs de PUBLISH pendentes armazenados em nossa camada de persistência. Se houver uma correspondência de ID, retorna ‘True’. Mais tarde, ao implementar o Comportamento Operacional do protocolo, liberaremos esse ID correspondente do armazenamento real.

Dada a estrutura do cabeçalho variável do PUBACK acima, tudo o que precisamos por enquanto é ler os dois primeiros bytes para obter o ID do pacote sendo reconhecido.

ushort CPuback::GetPacketID(uchar &pkt[])
  {
   return (pkt[0] * 256) + pkt[1];
  }

Vamos lembrar que o identificador do pacote é codificado como um inteiro de dois bytes em ordem big-endian (ou de rede), com o byte mais significativo (MSB) apresentado primeiro. Para codificá-lo, usamos uma operação de deslocamento à esquerda bit a bit (<<). Para decodificá-lo, estamos multiplicando o valor do byte mais significativo por 256 e adicionando o byte menos significativo.

A função acima é suficiente por enquanto. Mais tarde, ao testar contra um broker real na rede aberta, talvez tenhamos que lidar com problemas de endianness, mas não testaremos isso neste momento. Vamos continuar avançando em direção à nossa atraente cenoura, a tarefa em mãos.

bool CPuback::IsPendingPkt(uchar &pkt[])
{
   ushort pending_ids[];
   GetPendingPublishIDs(pending_ids);
   ushort packet_id = GetPacketID(pkt);
   for(uint i = 0; i < pending_ids.Size(); i++)
     {
      if(pending_ids[i] == packet_id)
        {
         return true;
        }
     }
   return false;
}

A função acima recebe um array de bytes como argumento. Este array de bytes é o cabeçalho variável do pacote PUBACK. Ele então armazena em uma variável local (pending_ids) um array de identificadores de pacotes do nosso armazenamento/banco de dados que ainda não foram reconhecidos. Finalmente, ele lê o ID do pacote no array de bytes enviado pelo broker e o compara com aquele array de IDs pendentes. Se o pacote estiver no array, nossa função retorna ‘True’ e podemos liberar o ID.

A mesma lógica nos permitirá liberar os identificadores dos pacotes PUBREC, PUBREL e PUBCOMP para PUBLISH com QoS 2. Além disso, mais tarde substituiremos nossa “camada de persistência” falsa de uma função em um arquivo por um banco de dados real, mas a lógica principal da função permanecerá. Neste ponto, outro desenvolvedor pode estar trabalhando na camada de persistência enquanto desenvolvemos nossas classes de pacotes de forma totalmente independente.

Também precisamos ser capazes de ler o(s) Código(s) de Razão do cabeçalho variável do PUBACK. Como este campo tem uma posição e tamanho fixos, tudo o que precisamos é ler esse byte específico.

uchar CPuback::GetReasonCode(uchar &pkt[])
  {
   return pkt[2];
  }


Como estamos trabalhando apenas no lado do receptor do nosso cliente - ou seja, ainda não enviando PUBACKs - as funções acima são suficientes para nossos próximos testes funcionais. Agora, contra um broker real.


Conclusão

O refatoramento contínuo faz parte da prática de TDD. Ele visa alcançar não apenas um código totalmente funcional, mas também um código limpo: unidades e funções de responsabilidade única (classes e métodos aqui), identificadores legíveis (nomes de classes, métodos e variáveis) e evitar redundância ("não se repita"). É um processo, não uma tarefa de um único passo. Então, já sabemos com certeza que estaremos refatorando continuamente até termos um cliente MQTT 5.0 totalmente funcional.

Agora estamos prontos para começar a escrever nosso primeiro teste funcional contra um broker MQTT real para ver se nossos pacotes CONNECT, CONNACK, PUBLISH e PUBACK estão funcionando como esperado. 

Pacotes PUBACK são a contraparte dos pacotes PUBLISH com QoS 1. Pacotes PUBLISH com QoS 2 requererão os pacotes PUBREC, PUBCOMP e PUBREL como sua contraparte. Eles são o assunto do nosso próximo artigo.

Se você tem um bom entendimento de MQL5 e pode contribuir para o desenvolvimento deste cliente MQTT de código aberto, por favor, deixe uma nota nos comentários abaixo ou em nosso Chat Comunitário. 


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

Arquivos anexados |
MQTT.zip (20.83 KB)
Tests.zip (16.88 KB)
Caminhe em novos trilhos: Personalize indicadores no MQL5 Caminhe em novos trilhos: Personalize indicadores no MQL5
Vou agora listar todas as possibilidades novas e recursos do novo terminal e linguagem. Elas são várias, e algumas novidades valem a discussão em um artigo separado. Além disso, não há códigos aqui escritos com programação orientada ao objeto, é um tópico muito importante para ser simplesmente mencionado em um contexto como vantagens adicionais para os desenvolvedores. Neste artigo vamos considerar os indicadores, sua estrutura, desenho, tipos e seus detalhes de programação em comparação com o MQL4. Espero que este artigo seja útil tanto para desenvolvedores iniciantes quanto para experientes, talvez alguns deles encontrem algo novo.
Trabalho com modelos ONNX nos formatos float16 e float8 Trabalho com modelos ONNX nos formatos float16 e float8
Os formatos de dados utilizados para representar modelos de aprendizado de máquina desempenham um papel fundamental em sua eficiência. Nos últimos anos, surgiram vários novos tipos de dados desenvolvidos especificamente para trabalhar com modelos de aprendizado profundo. Neste artigo, vamos focar em dois novos formatos de dados que se tornaram amplamente utilizados nos modelos modernos.
Está chegando o novo MetaTrader 5 e MQL5 Está chegando o novo MetaTrader 5 e MQL5
Esta é apenas uma breve resenha do MetaTrader 5. Eu não posso descrever todos os novos recursos do sistema por um período tão curto de tempo - os testes começaram em 09.09.2009. Esta é uma data simbólica, e tenho certeza que será um número de sorte. Alguns dias passaram-se desde que eu obtive a versão beta do terminal MetaTrader 5 e MQL5. Eu ainda não consegui testar todos os seus recursos, mas já estou impressionado.
Desenvolvendo um EA multimoeda (Parte 4): Ordens virtuais pendentes e salvamento de estado Desenvolvendo um EA multimoeda (Parte 4): Ordens virtuais pendentes e salvamento de estado
Ao começar a desenvolver um EA multimoeda, já alcançamos alguns resultados e realizamos várias iterações de melhoria do código. No entanto, nosso EA não podia trabalhar com ordens pendentes e retomar o trabalho após reiniciar o terminal. Vamos adicionar essas funcionalidades.