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

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

MetaTrader 5Integração |
129 0
Jocimar Lopes
Jocimar Lopes

"A otimização prematura é a raiz de todo mal" (Donald Knuth)

Introdução

O MQTT é um protocolo de troca de mensagens baseado em publicação e assinatura. Dito isso, podemos esperar que seu núcleo sejam os pacotes PUBLISH (publicação) e SUBSCRIBE (assinatura). Todos os outros tipos de pacotes são auxiliares.

Além de criar pacotes PUBLISH, também precisamos ser capazes de lê-los, pois as mensagens que nosso cliente receberá de outros clientes também são pacotes PUBLISH. Isso ocorre porque o protocolo de entrega é simétrico.

"Um pacote PUBLISH é enviado do cliente para o servidor ou do servidor para o cliente para transportar uma mensagem de aplicativo (Application Message)".

Os pacotes PUBLISH têm um cabeçalho fixo diferente com flags de publicação e um cabeçalho variável com o nome do tópico obrigatório, codificado como uma string UTF-8, e um identificador de pacote obrigatório (se QoS > 0). Além disso, com o tempo, ele pode usar quase todas as propriedades (incluindo as personalizadas) apresentadas no MQTT 5.0, incluindo propriedades relacionadas ao modo de interação "Request/Response".

No artigo, analisaremos a estrutura dos cabeçalhos, bem como o teste e a implementação das flags de publicação, nomes de tópicos e identificadores de pacotes. 

Nas descrições seguintes, usamos as palavras DEVE (MUST) e PODE (MAY) conforme usadas no padrão OASIS, que, por sua vez, as utiliza como descrito no documento IETF RFC 2119.

Além disso, a menos que indicado de outra forma, todas as citações são retiradas do padrão OASIS.


Estrutura do cabeçalho fixo do pacote MQTT 5.0 PUBLISH

O cabeçalho fixo do pacote PUBLISH tem a mesma estrutura básica de dois bytes que todos os outros tipos de pacotes de controle. O primeiro byte é destinado ao tipo de pacote. O segundo byte é o host do comprimento restante do pacote, codificado como um número inteiro de byte variável.

Mas, enquanto todos os outros tipos de pacotes têm os primeiros quatro bits do primeiro byte no estado RESERVADO, o pacote PUBLISH usa esses quatro bits para codificar três funções: RETAIN, Nível QoS e DUP.

Pacote de Controle MQTT Flags do cabeçalho fixo Bit 3 Bit 2 Bit 1 Bit 0
CONNECT Reservado 0 0 0 0
CONNACK Reservado
0 0 0 0
PUBLISH Usado no MQTT v5.0 DUP QoS 2 QoS 1 RETAIN
PUBACK Reservado
0 0 0 0
PUBREC Reservado
0 0 0 0
PUBREL Reservado
0 0 1 0
PUBCOMP Reservado
0 0 0 0
SUBSCRIBE Reservado
0 0 1 0
SUBACK Reservado 0 0 0 0
UNSUBSCRIBE Reservado
0 0 1 0
UNSUBACK Reservado
0 0 0 0
PINGREQ Reservado
0 0 0 0
PINGRESP Reservado
0 0 0 0
DISCONNECT Reservado
0 0 0 0
AUTH Reservado
0 0 0 0

Tabela 1. Reprodução de bits das flags da tabela 2-3 do Padrão MQTT Oasis

"Se o bit da flag estiver marcada como Reservado, ele será mantido em reserva para uso futuro e DEVE ser definido com o valor especificado."

Devido a essa diferença fixa nos cabeçalhos entre os pacotes PUBLISH e todos os outros pacotes de controle, a função que utilizamos para gerar cabeçalhos fixos não pode ser aplicada aqui.

//+------------------------------------------------------------------+
//|                     SetFixedHeader                               |
//+------------------------------------------------------------------+
void SetFixedHeader(ENUM_PKT_TYPE pkt_type, uchar& buf[], uchar& dest_buf[])
  {
   dest_buf[0] = (uchar)pkt_type << 4;
   dest_buf[1] = EncodeVariableByteInteger(buf);
  }

Como você pode ver, os parâmetros da função incluem apenas o tipo de pacote e referências a dois arrays, um que serve como fonte e outro como destino do array com o cabeçalho fixo. A primeira linha obtém o valor inteiro do tipo de pacote a partir de um Enum e o desloca para a esquerda em quatro bits, atribuindo o resultado da operação bit a bit ao primeiro byte do array do cabeçalho fixo (dest_buf[0]). Essa operação bit a bit garante que os primeiros quatro bits permaneçam não atribuídos ou "reservados", de acordo com o padrão.

A segunda linha chama uma função que calcula o comprimento restante do pacote, atribuindo o valor ao segundo byte do array do cabeçalho fixo (dest_buf[1]), codificado como um número inteiro de byte variável.

Mas essa função não oferece meios para configurar as flags de publicação.

Figura 1. Cabeçalho fixo do pacote PUBLISH MQTT 5.0 RETAIN, flags de nível de QoS e DUP

Fig. 1. Figura 1. Cabeçalho fixo do pacote PUBLISH MQTT 5.0 RETAIN, flags de nível de QoS e DUP

Assim sendo, adicionamos um switch para lidar com os pacotes PUBLISH e um último parâmetro para receber as flags de publicação. Poderíamos sobrecarregar a função para receber as flags de publicação, modificando ligeiramente seu corpo para implementar as particularidades dos pacotes PUBLISH. Mas este é um caso ideal para usar um switch, pois temos apenas uma exceção (PUBLISH) e, em todos os outros casos, a implementação anterior é utilizada por padrão.

O último parâmetro é, por padrão, zero, o que significa que pode ser ignorado ao configurar todos os cabeçalhos fixos dos pacotes. O dest_buf só será modificado se algum flag de publicação estiver definida.

//+------------------------------------------------------------------+
//|                     SetFixedHeader                               |
//+------------------------------------------------------------------+
void SetFixedHeader(ENUM_PKT_TYPE pkt_type,
                    uchar& buf[], uchar& dest_buf[], uchar publish_flags = 0)
  {
   switch(pkt_type)
     {
      case PUBLISH:
         dest_buf[0] = (uchar)pkt_type << 4;
         dest_buf[0] |= publish_flags;
         dest_buf[1] = EncodeVariableByteInteger(buf);
         break;
      default:
         dest_buf[0] = (uchar)pkt_type << 4;
         dest_buf[1] = EncodeVariableByteInteger(buf);
         break;
     }
  }

Como você pode ver, o buffer de destino que contém um cabeçalho fixo é modificado por uma operação OR de bit a bit combinada com a atribuição ao primeiro byte. Usamos amplamente esse padrão para alternar as flags de conexão e agora usamos o mesmo padrão para alternar as flags de publicação.

Por exemplo, a flag RETAIN é definida/redefinida com o seguinte código. 

//+------------------------------------------------------------------+
//|               CPktPublish::SetRetain                             |
//+------------------------------------------------------------------+
void CPktPublish::SetRetain(const bool retain)
  {
   retain ? m_publish_flags |= RETAIN_FLAG : m_publish_flags &= ~RETAIN_FLAG;
   SetFixedHeader(PUBLISH, m_buf, ByteArray, m_publish_flags);
  }

Flag de nível QoS_1 (sem uma assinatura funcional semelhante).

QoS_1 ? m_publish_flags |= QoS_1_FLAG : m_publish_flags &= ~QoS_1_FLAG;   
SetFixedHeader(PUBLISH, m_buf, ByteArray, m_publish_flags);

Flag de nível QoS_2.

QoS_2 ? m_publish_flags |= QoS_2_FLAG : m_publish_flags &= ~QoS_2_FLAG;
SetFixedHeader(PUBLISH, m_buf, ByteArray, m_publish_flags);

Flag DUP.

dup ? m_publish_flags |= DUP_FLAG : m_publish_flags &= ~DUP_FLAG;
SetFixedHeader(PUBLISH, m_buf, ByteArray, m_publish_flags);

Os valores das flags (máscaras de flags) são constantes definidas em um Enum como valores de potências de dois, de acordo com a posição do bit correspondente no byte alternado.

//+------------------------------------------------------------------+
//|             PUBLISH - FIXED HEADER - PUBLISH FLAGS               |
//+------------------------------------------------------------------+
enum ENUM_PUBLISH_FLAGS
  {
   RETAIN_FLAG  	= 0x01,
   QoS_1_FLAG           = 0x02,
   QoS_2_FLAG           = 0x04,
   DUP_FLAG             = 0x08
  };

Assim, as flags têm os seguintes valores binários e posições no byte.

RETAIN

Decimal 1 0 0 0 0 0 0 0 1

QoS 1

Decimal 2 0 0 0 0 0 0 1 0

QoS 2

Decimal 4 0 0 0 0 0 1 0 0

DUP

Decimal 8 0 0 0 0 1 0 0 0

O valor decimal do pacote PUBLISH é 3.

Decimal 3 0 0 0 0 0 0 1 1

Deslocamos o valor do tipo de pacote para a esquerda em quatro bits (dest_buf[0] = (uchar)pkt_type << 4).

Decimal 48 0 0 1 1 0 0 0 0

Quando aplicamos a operação bit a bit OR ( dest_buf[0] |= publish_flags; ) à representação binária do valor do tipo de pacote e das flags, basicamente estamos combinando os bits. Assim, a representação binária do valor deslocado para a esquerda do pacote PUBLISH com a flag DUP configurada se torna a seguinte.

Decimal 56 0 0 1 1 1 0 0 0

Se as flags RETAIN e QoS 2 estiverem definidas, os bits do primeiro byte do cabeçalho fixo serão assim:

Decimal 53 0 0 1 1 0 1 0 1

Por outro lado, a operação bit a bit AND entre o valor do tipo de pacote e o complemento (~) binário das flags remove a flag (m_publish_flags &= ~RETAIN_FLAG ).

Assim, se o byte estivesse definido com QoS 1 sem DUP ou RETAIN, ele ficaria assim:

Decimal 50 0 0 1 1 0 0 1 0

O complemento da flag QoS 1 acima é o valor de todos os seus bits invertidos.

Flag QoS_1 0 0 1 0
Flag ~QoS_1 1 1 0 1

Como qualquer valor AND zero é igual a zero, removemos efetivamente a flag.

Agora, observe que o valor binário do byte obviamente muda conforme as flags são definidas. Se todas as flags não estiverem definidas, ele terá o valor decimal 48 após o deslocamento à esquerda do valor decimal 3 em quatro bits. Quando configuramos a flag RETAIN, ele tem o valor decimal 49. O valor se torna 51 com RETAIN e QoS 1. E assim por diante.

Esses valores decimais são os que procuramos ao estudar todas as combinações possíveis de configuração/remoção de flags em nossos testes.

//+------------------------------------------------------------------+
//|              TEST_SetFixedHeader_DUP_QoS2_RETAIN                 |
//+------------------------------------------------------------------+
bool TEST_SetFixedHeader_DUP_QoS2_RETAIN()
  {
   Print(__FUNCTION__);
//--- Arrange
   static uchar expected[] = {61, 0};
   uchar buf[] = {};
//--- Act
   CPktPublish *cut = new CPktPublish(buf);
   cut.SetDup(true);
   cut.SetQoS_2(true);
   cut.SetRetain(true);
   uchar result[];
   ArrayCopy(result, cut.ByteArray);
//--- Assert
   bool isTrue = AssertEqual(expected, result);
//--- cleanup
   delete cut;
   ZeroMemory(result);
   return isTrue;
  }

Esses testes relativamente simples (assim como outros mais complexos), escritos antes da implementação, permitem que nos concentremos na tarefa em questão e também funcionam como uma "rede de segurança" quando precisamos modificar ou reorganizar o código. Muitos deles estão nos arquivos anexados. 

Ao rodar os testes, você deve ver algo assim.

Figura 2. Cabeçalho fixo da exibição de teste PUBLISH do MQTT 5.0

Fig. 2. Cabeçalho fixo da exibição de teste PUBLISH do MQTT 5.0

Se o ciclo de publicação/assinatura é o núcleo do protocolo, essas três funções (RETAIN, DUP e QoS) são o coração do comportamento operacional do protocolo. Elas podem ter um grande impacto no gerenciamento do estado da sessão. Portanto, vamos além da especificação estrita do protocolo e tentemos entender sua semântica.

RETAIN

Como vimos na primeira parte desta série, o padrão de publicação/assinatura está vinculado a um nome de tópico específico: um cliente publica uma mensagem com um nome de tópico ou assina um nome de tópico, e todos os clientes recebem mensagens publicadas sob o nome do tópico ao qual estão assinados. 

Ao publicar, podemos usar a flag RETAIN definida como 1 (true) para indicar ao servidor que mantenha a mensagem e a entregue como uma "mensagem retida" para novos assinantes. Sempre existe apenas uma mensagem retida, e configuramos RETAIN para 1 para armazenar/substituir mensagens retidas existentes. Enviamos uma carga útil de zero bytes com essa flag definida em 1 para limpar as mensagens retidas. Configuramos para 0 para indicar ao servidor que não deve fazer nada com as mensagens retidas sob esse nome de tópico — não armazenar, substituir ou limpar.

Ao assinar um nome de tópico, recebemos a mensagem retida. No caso de assinaturas compartilhadas (Shared Subscriptions), a mensagem retida será enviada apenas a um dos clientes do filtro de tópico compartilhado (Topic Filter). Vamos nos aprofundar nas assinaturas compartilhadas ao trabalhar com pacotes SUBSCRIBE.

Essa função funciona em conjunto com as flags Retain Available (retenção disponível) e Retain Not Supported (retenção não suportada) nos pacotes CONNACK enviados pelo servidor. 

A validade das mensagens retidas expira como qualquer outra mensagem, de acordo com o intervalo de expiração da mensagem estabelecido no parâmetro PUBLISH ou nas propriedades Will da carga útil do CONNECT.

Devemos ter em mente que RETAIN é uma função da corretora dinâmica. Isso significa que ela pode mudar de "disponível" para "não suportada" e vice-versa dentro de uma única sessão.

Nível QoS

Já falamos sobre o nível QoS (Quality of Service) no artigo introdutório desta série, listando algumas decisões de design tomadas pelos criadores do protocolo.

"Apesar de o protocolo ter que ser confiável, rápido e barato devido a limitações tecnológicas e altos custos de rede, ele precisava garantir a entrega de dados com qualidade e consciência contínua da sessão (session awareness), permitindo lidar com conexões de Internet não confiáveis ou mesmo intermitentes".

No contexto das flags de conexão, analisamos uma tabela definindo cada nível QoS.

Valor QoS Bit 2 Bit 1 Descrição
0 0 0 Basicamente no momento da entrega
1 0 1 Pelo menos uma vez na entrega
2 1 0 Exatamente uma vez na entrega
- 1 1 Reservado – não pode ser usado

Tabela 2. Reprodução da tabela 3-9 de definições QoS do Padrão MQTT 5.0 Oasis

Ao descrever o uso dos níveis QoS e outras funções, usamos os termos "servidor" e "corretora" para nos referirmos ao serviço que distribuirá nossas mensagens. Mas segundo o Padrão:

"O protocolo de entrega é simétrico, na descrição abaixo tanto o Cliente quanto o Servidor podem atuar como remetente ou destinatário. O protocolo de entrega trata exclusivamente da entrega da mensagem do aplicativo de um remetente a um destinatário. Quando o Servidor entrega a Mensagem do Aplicativo a mais de um Cliente, cada Cliente é tratado de forma independente. O nível QoS usado para entregar a mensagem de saída do aplicativo ao cliente pode diferir do nível da mensagem de entrada do aplicativo." (destaque meu)

Assim, o uso dos termos "servidor" e "corretora" no sentido que utilizamos até agora é justificado, pois estamos falando do ponto de vista do Cliente em um sentido amplo, mas devemos lembrar dessa simetria no protocolo de entrega.

O nível QoS padrão é 0. Isso significa que, se não configurarmos esse flag, estaremos informando ao servidor que 0 (zero) é o nível máximo de QoS que estamos dispostos a aceitar. Qualquer corretora compatível aceita esse nível. Isso é publicação no princípio "fire and forget" (disparar e esquecer), onde o remetente reconhece que, na entrega, podem ocorrer perdas ou duplicações.

Figura 3. MQTT 5.0 - diagrama de blocos da camada 0 do cliente-servidor QoS

Fig. 3. MQTT 5.0 - diagrama de bloco do cliente-servidor QoS camada 0

O nível QoS 1 permite a possibilidade de duplicação na entrega, mas não permite perdas. O servidor confirma a mensagem com PUBACK.

Figura 4. MQTT 5.0 - Diagrama de blocos cliente-servidor QoS da camada 1

Fig. 4. MQTT 5.0 - Diagrama de blocos cliente-servidor QoS da camada 1

O nível QoS 2 não permite perdas nem duplicações. Nesse nível, participam quatro pacotes. O servidor reconhece o início da entrega com PUBREC. Em seguida, o cliente solicitará a liberação desse identificador de pacote específico com PUBREL, e, finalmente, o servidor notificará a conclusão da entrega com PUBCOMP.

Figura 4. MQTT 5.0 - Diagrama de blocos cliente-servidor QoS da camada 2

Fig. 5. Figura 4. MQTT 5.0 - Diagrama de blocos cliente-servidor QoS da camada 2

A analogia foi tirada do artigo anterior, onde falamos sobre as flags de conexão:

"Esse nível [QoS 2] pode ser comparado ao envio de uma encomenda registrada. O sistema postal lhe dá um recibo quando você entrega a encomenda a eles, reconhecendo que a partir desse momento eles são responsáveis por entregá-la ao endereço desejado. E quando isso ocorre, quando eles entregam a encomenda, eles enviam a você um recibo assinado pelo destinatário, confirmando a entrega da encomenda".

A Qualidade de Serviço (Quality of Service) pode ser necessária para uma mensagem Will, para uma assinatura (incluindo assinaturas compartilhadas) ou para uma mensagem específica. 

Mensagem Will Assinatura Mensagem
CONNECT Will QoS Opções de assinatura SUBSCRIBE Flag do nível QoS do PUBLISH

Tabela 3. Pacotes MQTT 5.0 e flags, para as quais pode ser definido o nível QoS

O leitor atento talvez tenha notado que QoS 1 e QoS 2 envolvem algum estado de sessão (Session state). Falaremos sobre o estado da Sessão e o nível de persistência correspondente em um artigo separado.

DUP

Se a flag DUP estiver definida, isso significa que estamos tentando novamente enviar um pacote PUBLISH anterior que falhou. Ela DEVE ser definida como 0 (zero) para todas as mensagens QoS 0. A duplicação se refere ao próprio pacote, não à mensagem.


Cabeçalho variável do pacote MQTT 5.0 PUBLISH: nome do tópico, identificador do pacote e propriedades.

O cabeçalho variável do pacote MQTT 5.0 PUBLISH DEVE ter um nome de tópico e, se o QoS for maior que 0 (zero), também DEVE ter um identificador de pacote. Após esses dois campos, geralmente seguem um conjunto de propriedades e dados úteis, mas um pacote PUBLISH sem propriedades e com dados úteis de comprimento zero é um pacote válido. Em outras palavras, o pacote PUBLISH mais simples e válido é um pacote com cabeçalho fixo com QoS 0, sem flags DUP e RETAIN, e um cabeçalho variável contendo apenas o nome do tópico.

Nome do tópico

Como todas as interações entre clientes e servidor — e, consequentemente, todas as interações entre usuários/dispositivos — no protocolo de mensagens de publicação/assinatura giram em torno da publicação em um tópico e assinatura em um tópico, podemos dizer que o campo "Nome do tópico" merece atenção especial. Em muitos serviços em tempo real, em vez do nome do tópico, encontramos o termo "canal". Isso faz sentido, pois o nome do tópico representa um canal de informações ao qual os clientes estão assinados.

O nome do tópico é uma string codificada em UTF-8, organizada em uma estrutura hierárquica de árvore. A barra ( / U+002F ) é usada como delimitador no nível do tópico. 

broker1/account12345/EURUSD

O nome é sensível a maiúsculas e minúsculas. Ou seja, abaixo temos dois tópicos diferentes.

  • broker1/account12345/EURUSD
  • broker1/account12345/eurusd

Esses delimitadores de nível só são significativos se houver algum caractere curinga no filtro de tópicos do cliente (ver abaixo). Não há limite para o número de níveis, além da limitação da própria string UTF-8. Eventualmente, o nome do tópico pode ser substituído por um pseudônimo de tópico (Topic Alias).

"O pseudônimo do tópico é um valor inteiro usado para identificar o tópico em vez do nome do tópico. Isso reduz o tamanho do pacote PUBLISH e é útil quando os nomes dos tópicos são longos e os mesmos nomes de tópicos são reutilizados na conexão de rede".

Identificador do pacote

O identificador do pacote é um campo de dois bytes que é necessário para pacotes PUBLISH com QoS > 0. Ele é usado em todos os pacotes que participam diretamente do ciclo de publicação/assinatura para gerenciar o estado da sessão. O identificador do pacote NÃO DEVE ser usado em PUBLISH com QoS 0.

Ele é usado para associar o PUBLISH aos ACK correspondentes.

Lembre-se de que, como o protocolo de entrega é simétrico, ao usar QoS 1, nosso cliente pode receber PUBLISH do servidor com o mesmo identificador de pacote antes de receber o PUBACK relacionado ao PUBLISH enviado anteriormente.

"O cliente pode enviar um pacote PUBLISH com um identificador de pacote 0x1234 e, em seguida, receber outro pacote PUBLISH com um identificador de pacote 0x1234 do seu servidor antes de receber o PUBACK para o pacote PUBLISH que ele enviou."

Vale notar que o identificador do pacote também é usado para associar os reconhecimentos (ACK) correspondentes nos pacotes SUBSCRIBE e UNSUBSCRIBE.


Como escrevemos o(s) nome(s) do(s) tópico(s)?

O nome do tópico é o primeiro campo no cabeçalho variável. Ele é codificado como uma string UTF-8 com alguns pontos de código Unicode inválidos, e aqui há um detalhe importante. Por favor, observe essas três afirmações com alguns requisitos para codificação da string UTF-8 para MQTT 5.0.

"[…] os dados de caracteres NÃO DEVEM incluir codificações de pontos de código entre U+D800 e U+DFFF. Se o cliente ou servidor receber um pacote de controle MQTT contendo um código malformado, é um pacote malformado (Malformed)".

"A string codificada em UTF-8 NÃO DEVE incluir a codificação do caractere nulo U+0000. Se o destinatário (cliente ou servidor) receber um pacote de controle MQTT contendo U+0000, é um pacote malformado".

"Os dados NÃO DEVEM incluir codificações de pontos de código Unicode [Unicode] listados abaixo. Se o destinatário (cliente ou servidor) receber um pacote de controle MQTT contendo qualquer um deles, ele PODE tratar o pacote como malformado. Abaixo estão os pontos de código Unicode inválidos.

Caracteres de controle U+0001..U+001F

Caracteres de controle U+007F..U+009F

Pontos de código definidos na especificação [Unicode] que não são caracteres"

Como você pode ver, tanto a primeira quanto a segunda afirmação são estritas (NÃO DEVEM), o que significa que qualquer implementação compatível verificará a presença de pontos de código proibidos, enquanto a terceira afirmação é uma recomendação (NÃO DEVE), o que significa que a implementação pode não verificar a presença de pontos de código proibidos e ainda assim ser considerada compatível.

Como um pacote malformado é uma causa de DISCONNECT, se permitirmos esses pontos de código em nosso Cliente e nossa corretora decidir não tratá-los como um pacote malformado, podemos causar a desconexão de outros clientes que garantem o cumprimento da recomendação. Portanto, embora a exclusão de elementos de controle e não-caracteres do Unicode seja apenas uma recomendação, não permitimos seu uso em nossa implementação.

Atualmente, nossa função para codificar strings em UTF-8 funciona da seguinte forma:

//+------------------------------------------------------------------+
//|                    Encode UTF-8 String                           |
//+------------------------------------------------------------------+
void EncodeUTF8String(string str, ushort& dest_buf[])
  {
   uint str_len = StringLen(str);
// check for disallowed Unicode code points
   uint iter_pos = 0;
   while(iter_pos < str_len)
     {
      Print("Checking disallowed code points");
      ushort code_point = StringGetCharacter(str, iter_pos);
      if(IsDisallowedCodePoint(code_point))
        {
         printf("Found disallowed code point at position %d", iter_pos);
         ZeroMemory(dest_buf);
         return;
        }
      printf("Iter position %d", iter_pos);
      iter_pos++;
     }
   if(str_len == 0)
     {
      Print("Cleaning buffer: string empty");
      ZeroMemory(dest_buf);
      return;
     }
// we have no disallowed code points and the string is not empty: encode it.
   printf("Encoding %d bytes ", str_len);
   ArrayResize(dest_buf, str_len + 2);
   dest_buf[0] = (char)str_len >> 8; // MSB
   dest_buf[1] = (char)str_len % 256; // LSB
   ushort char_array[];
   StringToShortArray(str, char_array, 0, str_len);// to Unicode
   ArrayCopy(dest_buf, char_array, 2);
   ZeroMemory(char_array);
  }

Se a string passada para essa função contiver um código inválido, registramos sua posição na string, passamos o buffer de destino para ZeroMemory e retornamos imediatamente. Como o nome do tópico tem um comprimento mínimo exigido de 1, se a string estiver vazia, fazemos o mesmo: registramos no log, limpamos o buffer e retornamos.

A propósito, observe que usamos StringToShortArray para converter a string em um array Unicode. Se estivéssemos convertendo para um array ASCII, usaríamos StringToCharArray. Uma explicação detalhada e muito mais podem ser encontradas no livro recentemente adicionado à documentação ou neste artigo abrangente sobre strings no MQL5.

Também observe que, nesta chamada StringToShortArray, usamos o comprimento da string como o último parâmetro em vez da função padrão. Isso porque não precisamos do caractere nulo (0x00) em nosso array, e, conforme a documentação da função: 

"O valor padrão é menos 1, o que significa copiar até o final do array ou até o 0 final. O 0 final também será copiado para o array do destinatário",

Enquanto o valor retornado por StringLen é

"O número de caracteres em uma string sem um zero final".

A função de verificação de pontos de código inválidos é bastante familiar.

//+------------------------------------------------------------------+
//|              IsDisallowedCodePoint                               |
//|   https://unicode.org/faq/utf_bom.html#utf16-2                   |
//+------------------------------------------------------------------+
bool IsDisallowedCodePoint(ushort code_point)
  {
   if((code_point >= 0xD800 && code_point <= 0xDFFF) // Surrogates
      || (code_point > 0x00 && code_point <= 0x1F) // C0 - Control Characters
      || (code_point >= 0x7F && code_point <= 0x9F) // C0 - Control Characters
      || (code_point == 0xFFF0 || code_point == 0xFFFF)) // Specials - non-characters
     {
      return true;
     }
   return false;
  };

Além dos pontos de código proibidos, também precisamos verificar a presença de dois caracteres curinga que são usados em filtros de tópicos de assinaturas, mas são proibidos no nome do tópico: o sinal de mais ('+' U+002B) e o sinal de número ('#' U+0023).

A função de verificação de pontos de código inválidos será amplamente usada para codificar qualquer string, por isso é colocada no cabeçalho MQTT.mqh, enquanto a função de verificação de caracteres curinga é específica para o nome do tópico, então faz parte da nossa classe CPktPublish.

//+------------------------------------------------------------------+
//|            CPktPublish::HasWildcardChar                          |
//+------------------------------------------------------------------+
bool CPktPublish::HasWildcardChar(const string str)
  {
   if(StringFind(str, "#") > -1 || StringFind(str, "+") > -1)
     {
      printf("Wildcard char not allowed in Topic Names");
      return true;
     }
   return false;
  }

A função embutida StringFind retorna a posição inicial da substring correspondente e -1 se a substring correspondente não for encontrada. Portanto, verificamos qualquer valor maior que -1. Em seguida, chamamos isso na função principal.

//+------------------------------------------------------------------+
//|            CPktPublish::SetTopicName                             |
//+------------------------------------------------------------------+
void CPktPublish::SetTopicName(const string topic_name)
  {
   if(HasWildcardChar(topic_name) || StringLen(topic_name) == 0)
     {
      ArrayFree(ByteArray);
      return;
     }
   ushort encoded_string[];
   EncodeUTF8String(topic_name, encoded_string);
   ArrayCopy(ByteArray, encoded_string, 2);
   ByteArray[1] = EncodeVariableByteInteger(encoded_string);
  }

Nesse ponto, se um caractere curinga for encontrado, fazemos o mesmo "tratamento de erro" de antes: registramos a informação, limpamos o buffer e retornamos imediatamente. Mais tarde, podemos aprimorar isso, como emitindo alertas.

A última linha da função atribui o comprimento restante do pacote ao segundo byte do nosso cabeçalho fixo, usando o algoritmo proposto pelo Padrão. Eu escrevi sobre isso no primeiro artigo da série.

Nossos testes também têm a mesma estrutura.

//+------------------------------------------------------------------+
//|           TEST_SetTopicName_WildcardChar_NumberSign              |
//+------------------------------------------------------------------+
bool TEST_SetTopicName_WildcardChar_NumberSign()
  {
   Print(__FUNCTION__);
//--- Arrange
   static uchar expected[] = {};
   uchar payload[] = {};
//--- Act
   CPktPublish *cut = new CPktPublish(payload);
   cut.SetTopicName("a#");
   uchar result[];
   ArrayCopy(result, cut.ByteArray);
//--- Assert
   bool isTrue = AssertEqual(expected, result);
//--- cleanup
   delete cut;
   ZeroMemory(result);
   return isTrue;
  }

Se você executar os testes, deverá ver algo assim:

Figura 6. MQTT 5.0 - Nome do tópico correspondente à saída do teste PUBLISH

Fig. 6. MQTT 5.0 - Nome do tópico correspondente à saída do teste PUBLISH


Como escrevemos o(s) identificador(es) do(s) pacote(s)?

O identificador do pacote NÃO é destinado à atribuição pelo usuário. Em vez disso, ele DEVE ser atribuído pelo Cliente a qualquer pacote PUBLISH onde o nível QoS > 0, e NÃO DEVE ser atribuído de outra forma. Em outras palavras, sempre que criamos um pacote PUBLISH com QoS 1 ou QoS 2, devemos definir seu identificador de pacote. 

Podemos realizar o teste necessário agora. Tudo o que precisamos fazer é criar uma instância do pacote e definir o nome do tópico e o QoS igual a 1 ou 2. O array de bytes resultante do pacote deve conter um identificador de pacote.

//+------------------------------------------------------------------+
//|            TEST_SetPacketID_QoS2_TopicName1Char                  |
//+------------------------------------------------------------------+
bool TEST_SetPacketID_QoS2_TopicName5Char()
  {
   Print(__FUNCTION__);
// Arrange
   uchar payload[] = {};
   uchar result[]; // expected {52, 9, 0, 1, 'a', 'b', 'c', 'd', 'e', pktID MSB, pktID LSB}
// Act
   CPktPublish *cut = new CPktPublish(payload);
// FIX: if we call SetQoS first this test breaks
   cut.SetTopicName("abcde");
   cut.SetQoS_2(true);
   ArrayCopy(result, cut.ByteArray);
// Assert
   ArrayPrint(result);
   bool is_true = result[9] > 0 || result[10] > 0;
// cleanup
   delete cut;
   ZeroMemory(result);
   return is_true;
  }

Observe que não podemos verificar o valor do identificador de pacote gerado, pois é um número gerado (pseudo)aleatoriamente, como você pode ver abaixo na implementação de exemplo. Em vez disso, verificamos sua presença. Também observe que precisamos corrigir um problema (FIX). A ordem das chamadas das funções SetTopicName e SetQoS_X afeta inesperadamente o array de bytes resultante. Não é recomendável ter uma dependência da ordem de chamadas entre as funções. Isso seria um erro, mas, como se diz, um erro é um teste não escrito. Desse modo, escreveremos um teste para garantir a ausência dessa dependência de ordem de chamada na próxima iteração. Por enquanto, estamos interessados apenas em passar este teste.

Claro, o teste nem compilará enquanto não tivermos a implementação da função para definir identificadores de pacotes. Como os identificadores de pacotes são necessários em vários pacotes de controle, a função para escrevê-los NÃO deve ser um membro da classe CPktPublish. O cabeçalho MQTT.mqh parece ser o arquivo mais adequado para colocá-la.

//+------------------------------------------------------------------+
//|            SetPacketID                                           |
//+------------------------------------------------------------------+
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; //LSB
  }

Usaremos a função embutida MathRand para gerar os identificadores de pacotes. Para isso, é necessário chamar MathSrand previamente. Precisamos passar um "seed" para essa função como inicialização do gerador de números aleatórios. Escolhemos TimeLocal como valor de inicialização, seguindo a recomendação sobre geração de números pseudoaleatórios no MQL5 conforme indicado no livro recentemente adicionado à documentação.

Para definir o identificador de pacote, ajustamos o tamanho do array de bytes original para liberar espaço para o identificador de pacote (um número inteiro de dois bytes) e configuramos os valores do byte mais significativo e do byte menos significativo, começando pela posição passada como argumento (start_idx). O último passo é chamar a função da nossa classe CPktPublish para os métodos SetQoS_1 e SetQoS_2.

//+------------------------------------------------------------------+
//|            CPktPublish::SetQoS_2                                 |
//+------------------------------------------------------------------+
void CPktPublish::SetQoS_2(const bool QoS_2)
  {
   QoS_2 ? m_publish_flags |= QoS_2_FLAG : m_publish_flags &= ~QoS_2_FLAG;
   SetFixedHeader(PUBLISH, m_buf, ByteArray, m_publish_flags);
   SetPacketID(ByteArray, ByteArray.Size());
  }

Ao executar os testes incluídos nos arquivos anexados, você verá algo parecido com isto (algumas das linhas foram cortadas para abreviar):

Figura 7. MQTT 5.0 - Identificador do pacote de saída do teste PUBLISH

Fig. 7. MQTT 5.0 - Identificador do pacote de saída do teste PUBLISH

Considerações finais

Como os pacotes PUBLISH estão no centro do protocolo, sua implementação é um pouco mais complexa: eles têm diferentes cabeçalhos fixos, exigem um cabeçalho variável com um nome de tópico codificado como UTF-8 e protegido contra alguns pontos de código inválidos, exigem um identificador de pacote se QoS > 0 e podem usar quase todas as propriedades personalizadas disponíveis no MQTT 5.0.

Neste artigo, explicamos como criamos cabeçalhos PUBLISH válidos com flags de publicação, nome do tópico e identificador de pacote. No próximo artigo desta série, examinaremos a criação de propriedades.

Uma nota sobre as mudanças recentes: se você está acompanhando o desenvolvimento deste cliente MQTT, talvez tenha notado que alterei várias assinaturas de funções, nomes de variáveis, níveis de acesso a campos, dispositivos de teste, etc. Algumas dessas mudanças são esperadas em qualquer desenvolvimento de software, mas a maioria delas está relacionada ao fato de usarmos a abordagem TDD e nos esforçarmos para aderir ao máximo a essa metodologia. Podemos esperar grandes mudanças antes de obtermos o primeiro resultado.

Como você sabe, nenhum desenvolvedor sozinho sabe tudo o que é necessário para desenvolver um Cliente como este para nossa base de código. O TDD ajuda muito na implementação gradual de funcionalidades extensas. Se você puder ajudar, deixe uma mensagem no chat da comunidade ou nos comentários abaixo. Qualquer ajuda é bem-vinda. Obrigado!

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

Arquivos anexados |
Ciência de dados e aprendizado de máquina (Parte 18): Comparando a eficácia do TruncatedSVD e NMF no tratamento de dados complexos de mercado Ciência de dados e aprendizado de máquina (Parte 18): Comparando a eficácia do TruncatedSVD e NMF no tratamento de dados complexos de mercado
A decomposição em valores singulares truncada (TruncatedSVD) e a fatoração de matriz não negativa (NMF) são métodos de redução de dimensionalidade. Ambos podem ser bastante úteis ao trabalhar com estratégias de negociação baseadas na análise de dados. Neste artigo, analisamos a aplicabilidade desses métodos no processamento de dados complexos de mercado, incluindo suas capacidades de redução de dimensionalidade para otimizar a análise quantitativa nos mercados financeiros.
Anotação de dados na análise de série temporal (Parte 5): Aplicação e teste de um EA usando Socket Anotação de dados na análise de série temporal (Parte 5): Aplicação e teste de um EA usando Socket
Nesta série de artigos, apresentamos vários métodos de anotação de séries temporais que podem criar dados compatíveis com a maioria dos modelos de inteligência artificial (IA). A anotação precisa dos dados pode tornar o modelo de IA treinado mais alinhado com os objetivos e tarefas dos usuários, aumentar a precisão do modelo e até ajudar a alcançar uma melhoria significativa na qualidade!
Negociação algorítmica com MetaTrader 5 e R para iniciantes Negociação algorítmica com MetaTrader 5 e R para iniciantes
Neste artigo, vamos combinar análise financeira com negociação algorítmica, além de ver como integrar R e MetaTrader 5. Este artigo é um guia para unir a flexibilidade analítica do R com as enormes possibilidades de negociação do MetaTrader 5.
Rede neural na prática: Pseudo Inversa (I) Rede neural na prática: Pseudo Inversa (I)
Aqui, vamos começar a ver como podermos implementar, usando MQL5 puro, o cálculo de pseudo inversa. Apesar do código que será visto, será de fato bem mais complicado, para os iniciantes, do que eu de fato gostaria de apresentar. Ainda estou pensando em como o explicar de forma simples. Veja isto como uma oportunidade de estudar um o código pouco comum. Então vá com calma. Sem pressa e correria. Mesmo que ele não vise ser eficiente e de rápida execução. O objetivo é ser o mais didático possível.