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

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

MetaTrader 5Integração | 5 março 2024, 16:23
169 0
Jocimar Lopes
Jocimar Lopes

"Como você pode se considerar um profissional se não tem certeza de que todo seu código funciona? E como pode ter certeza de que todo o seu código funciona se você não o testa toda vez que faz uma alteração? E como pode testar seu código toda vez que fizer uma alteração sem ter testes unitários automatizados com uma cobertura muito alta? Mas é possível criar testes unitários automatizados com uma cobertura muito alta sem a aplicação do TDD?" (Robert "Tio Bob" Martin, "Código Limpo", 2011)

Introdução

Nas duas primeiras partes desta série, trabalhamos com uma pequena parte da seção não funcional do protocolo MQTT. Ali programamos em dois arquivos de cabeçalho separados todas as definições de protocolos, enumerações e algumas funções comuns usadas por todas as nossas classes. Além disso, escrevemos uma interface que atua como a raiz da hierarquia de objetos e implementamos em uma classe, com o único objetivo de criar o pacote MQTT CONNECT correspondente. Simultaneamente, escrevemos testes unitários para cada função envolvida na criação de pacotes. Embora tenhamos enviado o pacote gerado ao nosso broker MQTT local para verificar se ele seria reconhecido como um pacote MQTT corretamente formado, tecnicamente esse passo não era necessário. Como usamos dados específicos para passar parâmetros para nossa função, sabíamos que estávamos testando-os de forma isolada e independente do estado. Isso é bom, e vamos nos esforçar para continuar escrevendo nossos testes – e, consequentemente, nossas funções – dessa maneira. Isso tornará nosso código mais flexível e nos permitirá alterar a implementação da função, mesmo sem mudar o código de teste, desde que tenhamos a mesma assinatura de função.

A partir deste momento, estaremos trabalhando com a parte operacional do protocolo MQTT. No padrão OASIS, ela é chamada de comportamento operacional (Operational Behavior). Isso significa que agora precisamos trabalhar com pacotes enviados desde o servidor. Nosso cliente deve ser capaz de identificar o tipo de pacote do servidor e sua semântica, bem como escolher o comportamento apropriado neste contexto e em cada possível estado do cliente.

Para trabalhar com essa tarefa, devemos determinar o tipo de pacote do servidor no primeiro byte da resposta. Se for um pacote CONNACK, precisamos ler seu código de motivo de conexão (Connect Reason Code) e reagir apropriadamente.


(CONNECT) Definição das flags de conexão do cliente

Quando nosso cliente solicita uma conexão com o servidor, ele deve informar ao servidor sobre:

  • algumas capacidades desejadas pelo broker,
  • se a autenticação será necessária usando nome de usuário e senha,
  • e se esta conexão é destinada a uma nova sessão ou à retomada de uma sessão previamente aberta.

Isso é feito com a definição de alguns flags de bits no início do cabeçalho da variável, logo após o nome e a versão do protocolo. Essas flags de bits no pacote CONNECT são chamadas de flags de conexão (Connect Flags).

Lembre-se de que as flags de bits são valores lógicos. Elas podem ter diferentes nomes ou representações, mas os valores lógicos têm apenas dois possíveis valores, geralmente verdadeiro ou falso.

Fig. 01. Termos comuns usados para representar valores lógicos

Fig. 01. Termos comuns usados para representar valores lógicos

O padrão OASIS usa consistentemente 1 (um) e 0 (zero). Na maioria das vezes, usaremos verdadeiro e falso e, eventualmente, definido e não definido. Isso deve tornar o texto mais legível. Além disso, nossa API pública usa consistentemente verdadeiro e falso para definir esses valores, portanto, usar esses termos deve tornar este artigo mais compreensível para aqueles leitores que estão acompanhando o desenvolvimento da biblioteca.

Fig. 02. Bits dos sinalizadores de conexão OASIS

Fig. 02. Bits dos sinalizadores de conexão OASIS

Como você pode ver na tabela OASIS na imagem acima, o primeiro bit (bit_0) é reservado, e devemos deixá-lo inalterado: zerado, não marcado, valor lógico falso, não definido. Se o definirmos, teremos um pacote distorcido.


Clean Start (bit_1)

O primeiro bit que podemos definir é o segundo bit. Ele é usado para definir a flag Clean Start - se for verdadeiro, o servidor executará o Clean Start e cancelará qualquer sessão existente associada ao nosso ID de cliente. O servidor iniciará uma nova sessão. Se não estiver definido, o servidor retomará nossa conversa anterior, se houver, ou iniciará uma nova sessão se não houver nenhuma sessão existente associada ao nosso ID de cliente.

Aqui está como nossa função de definição/não-definição desta flag atualmente parece.

void CPktConnect::SetCleanStart(const bool cleanStart)
  {
   cleanStart ? m_connect_flags |= CLEAN_START : m_connect_flags &= ~CLEAN_START;
   ArrayFill(ByteArray, ArraySize(ByteArray) - 1, 1, m_connect_flags);
  }

Alternamos valores usando operações bit a bit. Usamos o operador ternário para alternar o valor lógico e operações de atribuição para tornar o código mais compacto. Em seguida, armazenamos o resultado no membro privado da classe m_connect_flags. Finalmente, atualizamos o array de bytes que representa nosso pacote CONNECT com novos valores, chamando a função embutida ArrayFill (provavelmente mudaremos mais tarde este último uso — preenchimento do array).

Uma linha de um dos nossos testes mostra como ela é chamada.

   CPktConnect *cut = new CPktConnect(buf);
//--- Act
   cut.SetCleanStart(true);


Bit 7 6 5 4 3 2 1 0

User Name Flag (sinalizador de nome de usuário)
Password Flag (sinalizador de senha)
Will Retain (retenção de Will)
Will QoS 2
Will QoS 1

Will Flag (sinalizador Will)

Clean Start (início limpo)


Reserved (reservado)


X X X X X X 1 0

Tabela 01. Sinalizador de bits Clean Start (bit_1) definido como true – MQTT v5.0

Vamos usar amplamente este padrão para alternar flags lógicas: operadores ternários e operações bit a bit com operações de atribuição.

As próximas três flags, com nomes começando com Will, são destinadas a adicionar ao servidor certas capacidades. Elas informam ao servidor que "desejamos" que o servidor possa:

  1. salvar mensagens Will e associá-las à nossa sessão de cliente (mais sobre isso mais tarde);
  2. fornecer um determinado nível de QoS, geralmente acima do QoS 0, que é o valor padrão se nada for definido;
  3. salvar a(s) mensagem(ns) e publicá-las como "salvas" (veja abaixo), se o Will message for definido como true.


Will Flag (bit_2)

O terceiro bit é usado para configurar a flag Will. Se definido como verdadeiro, nosso cliente deve fornecer uma mensagem Will para ser publicada "em casos em que a conexão de rede não é fechada normalmente". A mensagem Will é uma espécie de "últimas palavras" do cliente quando enfrenta os assinantes.

void CPktConnect::SetWillFlag(const bool willFlag)
  {
   willFlag ? m_connect_flags |= WILL_FLAG : m_connect_flags &= ~WILL_FLAG;
   ArrayFill(ByteArray, ArraySize(ByteArray) - 1, 1, m_connect_flags);
  }

Ela é chamada da mesma forma que a função anterior.

//--- Act
   CPktConnect *cut = new CPktConnect(buf);
   cut.SetWillFlag(true);


Bit 7 6 5 4 3 2 1 0

User Name Flag (sinalizador de nome de usuário)
Password Flag (sinalizador de senha)
Will Retain (retenção de Will)
Will QoS 2
Will QoS 1

Will Flag (sinalizador Will)

Clean Start (início limpo)


Reserved (reservado)


X X X X X 1 X 0

Tabela 02. Sinalizador de bits Will (bit_2) com o valor true – MQTT v5.0

Will QoS (bit_3, bit_4) 

Ao contrário das dois flags anteriores, essa função requer a configuração de dois bits, se o cliente solicita o nível QoS 2, o quarto e o quinto bits. QoS significa qualidade de serviço (Quality of Service) e pode ser um de três.

Fig. 03. Definições OASIS QoS

Fig. 03. Definições OASIS QoS

Do sistema de entrega menos confiável ao mais confiável:

QoS 0

QoS 0 é definido principalmente no momento da entrega. Uma espécie de "atire e esqueça". O remetente faz uma tentativa. A mensagem pode ser perdida. Não há confirmação do servidor. Este é o valor padrão, ou seja, se nada estiver definido nos bits 3 e 4, o nível de QoS solicitado pelo cliente é QoS 0.

QoS 1

QoS 1 é definido ao menos no momento da entrega. Há um PUBACK confirmando a entrega.

O mesmo padrão para definição de função.

void CPktConnect::SetWillQoS_1(const bool willQoS_1)
  {
   willQoS_1 ? m_connect_flags |= WILL_QOS_1 : m_connect_flags &= ~WILL_QOS_1;
   ArrayFill(ByteArray, ArraySize(ByteArray) - 1, 1, m_connect_flags);
  }

O mesmo padrão para chamada de função.

//--- Act
   CPktConnect *cut = new CPktConnect(buf);
   cut.SetWillQoS_1(true);

Bit 7 6 5 4 3 2 1 0

User Name Flag (sinalizador de nome de usuário)
Password Flag (sinalizador de senha)
Will Retain (retenção de Will)
Will QoS 2
Will QoS 1

Will Flag (sinalizador Will)

Clean Start (início limpo)


Reserved (reservado)


X X X X 1 X X 0

Tabela 03. Sinalizador de bits Will QoS 1 (bit_3) definido como true – MQTT v5.0

QoS 2 

O QoS 2 é definido exatamente no momento da entrega. Requer a ausência de perdas e duplicações. O remetente confirmará a mensagem com o PUBREC, e a entrega, com o PUBREL.

Este nível pode ser comparado ao envio de um pacote registrado. O sistema postal lhe fornece um recibo quando você entrega o pacote em suas mãos, reconhecendo que, a partir desse momento, eles são responsáveis pela entrega ao endereço correto. E, quando isso acontece, quando entregam o pacote, eles enviam a você um recibo assinado pelo destinatário, confirmando a entrega do pacote.

O mesmo princípio.

void CPktConnect::SetWillQoS_2(const bool willQoS_2)
  {
   willQoS_2 ? m_connect_flags |= WILL_QOS_2 : m_connect_flags &= ~WILL_QOS_2;
   ArrayFill(ByteArray, ArraySize(ByteArray) - 1, 1, m_connect_flags);
  }

A mesma coisa.

//--- Act
   CPktConnect *cut = new CPktConnect(buf);
   cut.SetWillQoS_2(true);
Bit 7 6 5 4 3 2 1 0

User Name Flag (sinalizador de nome de usuário)
Password Flag (sinalizador de senha)
Will Retain (retenção de Will)
Will QoS 2
Will QoS 1

Will Flag (sinalizador Will)

Clean Start (início limpo)


Reserved (reservado)


X X X 1 X X X 0

Tabela 04. Sinalizador de bits Will QoS 2 (bit_4) definido como true – MQTT v5.0

O servidor nos informará sobre o nível máximo de QoS aceito nos códigos de motivo e propriedades do CONNACK. O cliente pode solicitar, mas as capacidades do servidor são obrigatórias. Se recebermos um CONNACK com o máximo QoS, devemos respeitar essa limitação do servidor e não enviar um PUBLISH com QoS superior. Caso contrário, o servidor se desconectará (DISCONNECT).

O QoS 2 é o nível mais alto de QoS disponível no MQTT v5.0, e está associado a certas dificuldades, uma vez que o protocolo de entrega é simétrico, significando que qualquer uma das partes (servidor e cliente) pode atuar tanto como remetente quanto receptor

NOTA: Pode-se dizer que o QoS é o núcleo do protocolo do ponto de vista do usuário. Ele define o perfil da aplicação e afeta dezenas de outros aspectos do protocolo. Portanto, vamos explorar mais profundamente o nível de QoS e suas configurações no contexto da implementação do pacote PUBLISH.

É importante destacar que QoS 1 e QoS 2 são opcionais para implementações do cliente. Como diz a OASIS em um comentário não normativo:

"Não é necessário que o cliente suporte os pacotes PUBLISH QoS 1 ou QoS 2. Neste caso, o Cliente simplesmente limita o campo máximo de QoS em quaisquer comandos SUBSCRIBE enviados ao valor que ele pode suportar".


Will RETAIN (bit_5)

No sexto byte, definimos o sinalizador Will Retain. Esse sinalizador está relacionado ao sinalizador Will mencionado anteriormente: 

  • Se o sinalizador Will não estiver definido (unset), então Will Retain também deve ser desativado.
  • Se o sinalizador Will estiver definido, mas o parâmetro Will Retain não, o servidor publicará a mensagem Will como uma mensagem não salva.
  • Se ambos os parâmetros estiverem definidos, o servidor publicará a mensagem Will como uma mensagem salva.

O código é omitido por brevidade neste e nos dois últimos sinalizadores restantes, pois a definição da função e os padrões de chamada são exatamente os mesmos dos sinalizadores anteriores. Mais informações estão disponíveis nos arquivos anexados. Eles também podem ser testados.

Bit 7 6 5 4 3 2 1 0

User Name Flag (sinalizador de nome de usuário)
Password Flag (sinalizador de senha)
Will Retain (retenção de Will)
Will QoS 2
Will QoS 1

Will Flag (sinalizador Will)

Clean Start (início limpo)


Reserved (reservado)


X X 1 X X X X 0

Tabela 05. Sinalizador de bits Will Retain (bit_5) definido como true – MQTT v5.0

Devemos esperar até que o pacote CONNACK verifique essa flag antes de começarmos a enviar pacotes PUBLISH. Se o servidor receber um pacote PUBLISH com o parâmetro Will Retain definido como 1, e ele não suportar mensagens salvas, o servidor se DESCONECTARÁ. É possível iniciar a publicação antes de receber o pacote CONNACK? Sim, é possível. O padrão permite tal comportamento. Mas também faz a seguinte observação:

"Clientes que enviam pacotes de controle MQTT antes de receber o CONNACK não estarão cientes das restrições do servidor"

Portanto, devemos verificar essa flag nos pacotes CONNACK antes de enviar quaisquer pacotes PUBLISH com o parâmetro Will Retain definido como 1 (um). 


Password flag (bit_6)

No sétimo bit, informamos ao servidor se vamos enviar a senha no payload ou não. Se essa flag estiver definida, o campo de senha deve estar presente no payload. Se não estiver definida, o campo de senha não deve estar presente no payload.

"Esta versão do protocolo permite o envio de senha sem nome de usuário, o que não é o caso no MQTT v3.1.1. Isso reflete o uso comum de senha para credenciais diferentes de senha.” (Padrão OASIS, 3.1.2.9)

Bit 7 6 5 4 3 2 1 0

User Name Flag (sinalizador de nome de usuário)
Password Flag (sinalizador de senha)
Will Retain (retenção de Will)
Will QoS 2
Will QoS 1

Will Flag (sinalizador Will)

Clean Start (início limpo)


Reserved (reservado)


X 1 X X X X X 0

Tabela 06. Sinalizador de bits Password Flag (bit_6) definido como true – MQTT v5.0


User Name flag (bit_7)

E, por fim, no nível de oito bits, informamos ao servidor se vamos enviar o nome de usuário no payload ou não. Assim como no caso da flag de senha descrita acima, se essa flag estiver definida, o campo de nome de usuário deve estar presente no payload. Caso contrário, o campo de nome de usuário não deve estar presente no payload.

Bit 7 6 5 4 3 2 1 0

User Name Flag (sinalizador de nome de usuário)
Password Flag (sinalizador de senha)
Will Retain (retenção de Will)
Will QoS 2
Will QoS 1

Will Flag (sinalizador Will)

Clean Start (início limpo)


Reserved (reservado)


1 X X X X X X 0

Tabela 07. Sinalizador de bits User Name (bit_7) definido como true – MQTT v5.0

Portanto, o byte de falgs de conexão com a seguinte sequência de bits...

Bit 7 6 5 4 3 2 1 0

User Name Flag (sinalizador de nome de usuário)
Password Flag (sinalizador de senha)
Will Retain (retenção de Will)
Will QoS 2
Will QoS 1

Will Flag (sinalizador Will)

Clean Start (início limpo)


Reserved (reservado)


X X 1 1 X 1 1 0

Table 08. Definidas as flags de bits para Clean Start, Will Flag, Will QoS2 e Will Retain – MQTT v5.0

... pode ser traduzido aproximadamente como: abra uma conexão para uma nova sessão com nível de QoS 2 e esteja pronto para reter minha mensagem Will e publicá-la como salva. E, a propósito, senhor Servidor, não precisarei passar por autenticação com nome de usuário e senha.

O Servidor gentilmente responderá se pode atender à nossa solicitação. Ele pode ser capaz de atender completamente, parcialmente ou não ser capaz de atender de todo. O Servidor enviará sua resposta na forma de códigos de motivo de conexão (Connect Reason Code) nos pacotes CONNACK.


(CONNACK) Recebimento de códigos de motivo relacionados às flags de conexão

No MQTT v5.0, existem quarenta e quatro códigos de motivo. Nós os compilamos no cabeçalho Defines.mqh. CONNACK (e outros tipos de pacotes) têm um código de motivo como parte do cabeçalho variável. São eles que são chamados códigos de motivo de conexão.

Valor Hex Nome do código de motivo Descrição
0 0x00 Success (sucesso) A conexão foi aceita.
128 0x80 Unspecified error (erro não especificado) O servidor não está disposto a revelar o motivo da falha ou nenhum dos outros códigos de motivo se aplica.
129 0x81 Malformed Packet (pacote malformado) Não foi possível analisar corretamente os dados no pacote CONNECT. 
130 0x82 Protocol Error (erro de protocolo) Os dados no pacote CONNECT não estão de acordo com a especificação.
131 0x83 Implementation specific error (erro específico de implementação) CONNECT é válido, mas não é aceito pelo servidor.
132 0x84 Unsupported Protocol Version (versão do protocolo não suportada) O servidor não suporta a versão do protocolo MQTT solicitada pelo Cliente.
133 0x85 Client Identifier not valid (identificador do cliente inválido) O identificador do cliente é uma string válida, mas não é permitido pelo servidor.
134 0x86 Bad User Name or Password (nome de usuário ou senha incorretos) O servidor não aceita o nome de usuário ou a senha fornecidos pelo Cliente.
135 0x87 Not authorized (não autorizado) O Cliente não está autorizado a se conectar.
136 0x88 Server unavailable (servidor indisponível) O servidor MQTT está indisponível.
137 0x89 Server busy (servidor ocupado) O servidor está ocupado. Tente mais tarde.
138 0x8A Banned (proibido) O Cliente foi bloqueado pelo administrador. Contate o administrador do servidor.
140 0x8C Bad authentication method (método de autenticação incorreto) O método de autenticação não é suportado ou não corresponde ao método de autenticação atualmente em uso.
144 0x90 Topic Name invalid (nome do tópico inválido) O nome do tópico Will não está malformado, mas não é aceito pelo servidor.
149 0x95 Packet too large (pacote muito grande) O pacote CONNECT excedeu o tamanho máximo permitido.
151 0x97 Quota exceeded (cota excedida) O limite definido durante a implementação ou pelo administrador foi excedido.
153 0x99 Payload format invalid (formato da carga inválido) A carga útil Will não corresponde ao indicador de formato da carga útil especificado (Payload Format Indicator).
154 0x9A Retain not supported (retenção não suportada) O servidor não suporta mensagens salvas, e o Will Retain está definido como 1.
155 0x9B QoS not supported (QoS não suportado) O servidor não suporta o QoS definido em Will QoS.
156 0x9C Use outro servidor O Cliente deve usar temporariamente outro servidor.
157 0x9D Server moved (servidor mudou) O Cliente deve usar outro servidor.
159 0x9F Connection rate exceeded (taxa de conexão excedida) A taxa de conexão limite foi excedida.

Tabela 08. Valores dos códigos de motivo de conexão

É claramente especificado no padrão que o servidor deve enviar códigos de motivo de conexão através do CONNACK:

"O servidor, ao enviar o pacote CONNACK, DEVE usar um dos valores de código de motivo de conexão [MQTT-3.2.2-8]."

Neste ponto, os códigos de motivo de conexão são de interesse particular para nós. Precisamos verificar esses antes de continuar a comunicação. Eles nos informarão sobre algumas capacidades e limitações do servidor, como o nível de QoS disponível e a disponibilidade de mensagens salvas. Além disso, como você pode ver pelos seus nomes e descrições na tabela acima, eles nos informarão se nossa tentativa de conexão (CONNECT) foi bem-sucedida ou não.

Para obter os códigos de motivo, primeiro precisamos identificar o tipo de pacote, já que estamos interessados apenas nos pacotes CONNACK.

Vamos aproveitar o fato de que precisamos de uma função muito simples para obter o tipo de pacote, para descrever como usamos o desenvolvimento orientado por testes, refletir um pouco sobre este método e fornecer alguns exemplos curtos. Toda a informação detalhada pode ser obtida nos arquivos anexados.


(CONNACK) Determinação do tipo de pacote do servidor

Sabemos com certeza que o primeiro byte de qualquer pacote de controle MQTT codifica o tipo de pacote. Então, simplesmente leremos este primeiro byte o mais rápido possível, e teremos o tipo de pacote do servidor.

uchar pkt_type = server_response_buffer[0];

O código é claro, as variáveis estão corretamente nomeadas. Não são esperados problemas.

Mas espere! Como o código que vai usar nossa biblioteca deve chamar esse operador? O tipo de pacote será retornado por uma chamada a uma função pública? Ou essa informação pode ser ocultada por um membro privado como um detalhe de implementação? Se for retornada por uma chamada de função, onde essa função deve ser localizada? Na classe CPktConnect? Ou deve ser colocada em qualquer um dos nossos arquivos de cabeçalho, já que será usada por várias classes diferentes? Se estiver armazenada em um membro privado, em qual classe ela deve estar?

Existe um acrônimo conhecido - TMTOWTDI* (there is more than one way to do it, há mais de uma maneira de fazer isso). TDD (Desenvolvimento Orientado por Testes) é outra abreviação que se tornou muito popular por várias razões. Ambos os acrônimos foram ativamente promovidos e até se tornaram moda:

__ "I’m tddying, mom! It’s cool" ("Mãe, estou fazendo TDD! É legal!")

Isso foi feito após muitos anos de trabalho árduo na mesma questão fundamental: como escrever código mais eficiente, idiomático e confiável, ao mesmo tempo em que se aumenta a produtividade dos desenvolvedores? Como fazer com que os desenvolvedores se concentrem no que precisa ser feito, ao invés de divagar sobre o que poderia ser feito? Como fazer com que cada um deles se concentre em uma tarefa - e apenas uma tarefa - de cada vez? Como ter certeza de que suas ações não levarão ao surgimento de erros de regressão e a falhas do sistema? 

Em suma, essas abreviações, as ideias que carregam e os métodos que recomendam são o resultado de muitos anos de trabalho de centenas de pessoas diferentes com experiência em desenvolvimento de software. TDD não é uma teoria, é uma prática. Pode-se dizer que o TDD é um método de solução de problemas através da delimitação do escopo, dividindo o problema em partes constituintes. Precisamos definir uma única tarefa que nos moverá um passo à frente. Apenas um passo. Muitas vezes, até mesmo um pequeno passo.

Então, qual é o nosso problema agora? Precisamos determinar se a resposta do servidor é um pacote CONNACK. É simples. Porque, de acordo com a especificação, precisamos ler os códigos de resposta do CONNACK para decidir o que fazer a seguir. Quero dizer, a identificação do tipo de pacote que recebemos do servidor em resposta é necessária para que possamos passar do estado de conexão para o estado de publicação.

Como podemos determinar se a resposta do servidor é um pacote CONNACK? Fácil. Ele tem um tipo específico, codificado no cabeçalho MQTT.mqh como uma enumeração (Enumeration), especificamente ENUM_PKT_TYPE.

//+------------------------------------------------------------------+
//|              MQTT - CONTROL PACKET - TYPES                       |
//+------------------------------------------------------------------+
/*
Position: byte 1, bits 7-4.
Represented as a 4-bit unsigned value, the values are shown below.
*/
enum ENUM_PKT_TYPE
  {
   CONNECT     =  0x01, // Connection request
   CONNACK     =  0x02, // Connection Acknowledgment
   PUBLISH     =  0x03, // Publish message
...

Vamos começar com uma função que, ao receber uma matriz de bytes de rede vinda do broker MQTT, retorna o tipo de pacote.

Parece bom. Vamos escrever um teste para essa função.

bool TEST_GetPktType_FAIL()
  {
   Print(__FUNCTION__);
//--- Arrange
   uchar expected[] = {(uchar)CONNACK};
   uchar result[1] = {};
   uchar wrong_first_byte[] = {'X'};
//--- Act
   CSrvResponse *cut = new CSrvResponse();
   ENUM_PKT_TYPE pkt_type = cut.GetPktType(wrong_first_byte);
   ArrayFill(result,0,1,(uchar)pkt_type);
//--- Assert
   bool isTrue = AssertNotEqual(expected, result);
//--- cleanup
   delete cut;
   ZeroMemory(result);
   return  isTrue ? true : false;
  }

Vamos tentar entender a função de teste, já que usaremos esse padrão em todos os nossos testes e não vamos descrever todos eles em detalhes aqui por brevidade.

Nos comentários, descrevemos cada passo do padrão: 

Arrange (organização)

Primeiro, inicializamos um array com um único elemento, que é o valor do byte que esperamos ser retornado pela nossa função.

Em segundo lugar, inicializamos um buffer de caracteres vazio de tamanho um para receber o resultado da chamada da nossa função.

Inicializamos nosso acessório para passar para a função sob teste. Isso representa o primeiro byte único que deve ser lido no cabeçalho fixo correspondente à resposta do servidor para determinar o tipo de pacote. Nesse caso, ele tem o valor wrong_first_byte. Tornamos isso explícito ao nomear a variável de forma apropriada.

Act (ação)

Criamos uma instância da classe em teste (cut) e chamamos nossa função.

Assert (afirmação)

Afirmamos desigualdade entre os arrays esperado e resultante, tanto em conteúdo quanto em tamanho, usando a função MQL5 ArrayCompare (veja o arquivo de teste anexado).

Clean-Up (limpeza)

Por fim, limpamos os recursos, deletando a instância "cortada" e passando nosso buffer de "resultado" para ZeroMemory. Isso evitará vazamentos de memória e contaminação dos testes.

Fig. 04. TEST_CSrvResponse - FAIL - identificador não declarado

Fig. 04. TEST_CSrvResponse - FAIL - identificador não declarado

O teste falha na compilação, pois a função ainda não existe. Precisamos escrevê-la. Mas onde ela deve ser colocada?

Já sabemos que precisaremos constantemente determinar o tipo de pacote de resposta. Sempre que enviamos um pacote ao nosso broker, ele nos envia uma dessas "respostas". E essa "resposta" é um tipo de pacote de controle MQTT. Portanto, sendo um tipo de "resposta", ela deve ter sua própria classe no grupo de "respostas" similares. Suponhamos que temos uma classe para representar todas as respostas do servidor no grupo de pacotes de controle.

Essa é a classe CSrvResponse, que implementa a interface IControlPacket.

Poderíamos ser tentados a torná-la mais uma função em nossa classe CPktConnect existente. Mas estaríamos violando um princípio importante da programação orientada a objetos: o Princípio da Responsabilidade Única (Single Responsibility Principle, SRP).

"Você deve separar as coisas que mudam por razões diferentes e agrupar as coisas que mudam pelas mesmas razões" (R. Martin, "Código Limpo", 2011).

Por um lado, nossa classe CPktConnect mudará sempre que alterarmos a maneira de construir pacotes CONNECT, e por outro lado, nossa (inexistente) classe CSrvResponse mudará sempre que alterarmos a maneira de ler nossos CONNACKs, PUBACKs, SUBACKs e outras respostas do servidor. Assim, eles têm responsabilidades completamente diferentes, e neste caso, isso é bastante fácil de ver. Mas, às vezes, pode ser difícil decidir se um objeto do domínio que estamos modelando deve ser declarado na classe correspondente. Aplicando o SRP, você obtém uma orientação objetiva para tomar essa decisão.

Então, vamos tentar fazer as mínimas alterações necessárias para passar no teste.

ENUM_PKT_TYPE CSrvResponse::GetPktType(uchar &resp_buf[])
  {
   return (ENUM_PKT_TYPE)resp_buf[0];
  }

O teste compila, mas, como esperado, falha porque passamos intencionalmente uma "resposta errada" do servidor.

Fig. 05. TEST_CSrvResponse - FAIL - pacote incorreto

Fig. 05. TEST_CSrvResponse - FAIL - pacote incorreto

Vamos passar o "tipo correto" de pacote CONNACK como resposta do servidor. Note que nós novamente atribuímos um nome explicitamente: right_first_byte. O nome em si é apenas um rótulo. O importante é que seu significado seja claro para todos que leem nosso código. Incluindo nós mesmos, seis meses ou seis anos depois.

bool TEST_GetPktType()
  {
   Print(__FUNCTION__);
//--- Arrange
   uchar expected[] = {(uchar)CONNACK};
   uchar result[1] = {};
   uchar right_first_byte[] = {2};
//--- Act
   CSrvResponse *cut = new CSrvResponse();
   ENUM_PKT_TYPE pkt_type = cut.GetPktType(right_first_byte);
   ArrayFill(result,0,1,(uchar)pkt_type);
//--- Assert
   bool isTrue = AssertEqual(expected, result);
//--- cleanup
   delete cut;
   ZeroMemory(result);
   return  isTrue ? true : false;
  }

Fig. 06. TEST_CSrvResponse - PASS

Fig. 06. TEST_CSrvResponse - PASS

Ótimo. Agora que o teste foi aprovado, sabemos que, pelo menos para esses dois argumentos, um incorreto e um correto, ele falhou ou passou, respectivamente. Mais tarde, se necessário, poderemos testá-lo mais a fundo.

Esses passos simples encapsulam os três "leis" fundamentais do TDD, resumidas por R. Martin.

  1. Você não pode escrever nenhum código finalizado até escrever um teste unitário que falhe.
  2. Você não está permitido a escrever mais teste unitário do que o necessário para falhar, e a falha de compilação conta como falha.
  3. Você não está permitido a escrever mais código finalizado do que o necessário para passar no teste unitário atual que falha.

Ótimo. Por agora, deixemos o tópico de TDD de lado. Vamos voltar à nossa tarefa e ler os códigos de motivo de conexão nos pacotes CONNACK vindos do servidor.


(Connect Reason Codes) O que fazer com as capacidades indisponíveis no servidor?

Neste estágio, merecem nossa atenção dois códigos de motivo de conexão:

  1. QoS not supported (QoS não suportado)
  2. Retain not supported (retenção não suportada)

Eles são especiais porque não indicam um erro, mas uma limitação do servidor. Se nosso CONNACK apresentar algum desses códigos de motivo de conexão, o servidor está informando que nossa conexão de rede foi bem-sucedida, que nosso CONNECT é um pacote bem formado, e que o servidor está online e operacional, mas não pode cumprir nossos requisitos. Precisamos tomar medidas. Temos que escolher o que fazer a seguir.

O que devemos fazer se enviarmos um CONNECT com QoS 2 e o servidor responder com QoS máximo 1? Devemos reenviar o CONNECT com um QoS rebaixado? Ou devemos nos desconectar (DISCONNECT) antes de reduzir o QoS? Se o mesmo acontecer com a função RETAIN, podemos simplesmente ignorá-la como irrelevante e ainda iniciar a publicação? Ou devemos reenviar o CONNECT com flags rebaixadas antes da publicação?

Após recebermos uma mensagem CONNACK bem-sucedida, indicando que o servidor aceitou nossa conexão e possui todas as capacidades que solicitamos, o que devemos fazer? Devemos começar a enviar pacotes PUBLISH imediatamente? Ou podemos manter a conexão aberta, enviando pacotes PINGREQ sequenciais, até estarmos prontos para publicar uma mensagem? Além disso, é necessário se inscrever (SUBSCRIBE) em um tópico antes de publicar?

A maioria dessas questões é respondida pelo Padrão. É necessário implementar AS-IS para ter um cliente compatível com o MQTT v5.0. Os desenvolvedores de aplicativos têm muitas opções para escolher. Por agora, trabalharemos apenas com o que é necessário para obter um cliente compatível o mais rápido possível.

De acordo com o padrão, um cliente só pode solicitar um nível de QoS > 0 se a flag Will também estiver definida como 1, o que significa que só podemos solicitar um nível de QoS > 0 se também estivermos enviando uma mensagem Will no pacote CONNECT. Mas nós não queremos, ou melhor, não precisamos trabalhar com mensagens Will agora. Portanto, nossa decisão é um compromisso entre entender o que precisamos saber agora e tentar desvendar todas as nuances do Padrão, eventualmente escrevendo código que pode não ser necessário mais tarde. 

Precisamos apenas saber o que nosso cliente fará se o nível de QoS solicitado ou salvo não estiver disponível no servidor. E precisamos saber isso assim que um novo CONNACK chegar. Por isso, fazemos essa verificação no construtor de CSrvResponse. Se a resposta for um CONNACK, o construtor chama um método protegido GetConnectReasonCode.

CSrvResponse::CSrvResponse(uchar &resp_buf[])
  {
   if(GetPktType(resp_buf) == CONNACK
      && GetConnectReasonCode(resp_buf)
      == (MQTT_REASON_CODE_QOS_NOT_SUPPORTED || MQTT_REASON_CODE_RETAIN_NOT_SUPPORTED))
     {
      CSrvProfile *serverProfile = new CSrvProfile();
      serverProfile.Update("000.000.00.00", resp_buf);
     }
  }

Se o código de motivo da conexão for MQTT_REASON_CODE_QOS_NOT_SUPPORTED ou MQTT_REASON_CODE_RETAIN_NOT_SUPPORTED, essa informação será armazenada no perfil do servidor. Por enquanto, vamos apenas armazenar essa informação sobre o servidor e esperar pelo desligamento (DISCONNECT). Mais tarde, usaremos essa informação ao solicitar uma nova conexão neste mesmo servidor. Observe que esse "mais tarde" pode acontecer alguns milissegundos após a primeira tentativa de conexão. Ou talvez algumas semanas depois. A questão é que essas informações serão armazenadas no perfil do servidor.


Como testamos métodos protegidos?

Para testar métodos protegidos, criamos uma classe em nosso cenário de teste, derivada de nossa classe sob teste, neste caso, CSrvResponse. Então, chamamos os métodos protegidos do CSrvResponse por meio dessa classe derivada, criada "para fins de teste", que chamamos de TestProtectedMethods.

class TestProtectedMethods: public CSrvResponse
  {
public:
                     TestProtectedMethods() {};
                    ~TestProtectedMethods() {};
   bool              TEST_GetConnectReasonCode_FAIL();
   bool              TEST_GetConnectReasonCode();
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool TestProtectedMethods::TEST_GetConnectReasonCode_FAIL()
  {
   Print(__FUNCTION__);
//--- Arrange
   uchar expected = MQTT_REASON_CODE_SUCCESS;
   uchar reason_code_banned[4];
   reason_code_banned[0] = B'00100000'; // packet type
   reason_code_banned[1] = 2; // remaining length
   reason_code_banned[2] = 0; // connect acknowledge flags
   reason_code_banned[3] = MQTT_REASON_CODE_BANNED;
//--- Act
   CSrvResponse *cut = new CSrvResponse();
   uchar result = this.GetConnectReasonCode(reason_code_banned);
//--- Assert
   bool isTrue = AssertNotEqual(expected, result);
//--- cleanup
   delete cut;
   ZeroMemory(result);
   return  isTrue ? true : false;
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool TestProtectedMethods::TEST_GetConnectReasonCode()
  {
   Print(__FUNCTION__);
//--- Arrange
   uchar expected = MQTT_REASON_CODE_SUCCESS;
   uchar reason_code_success[4];
   reason_code_success[0] = B'00100000'; // packet type
   reason_code_success[1] = 2; // remaining length
   reason_code_success[2] = 0; // connect acknowledge flags
   reason_code_success[3] = MQTT_REASON_CODE_SUCCESS;
//--- Act
   CSrvResponse *cut = new CSrvResponse();
   uchar result = this.GetConnectReasonCode(reason_code_success);
//--- Assert
   bool isTrue = AssertEqual(expected, result);
//--- cleanup
   delete cut;
   ZeroMemory(result);
   return  isTrue ? true : false;
  }

Observe que não armazenamos nada no perfil do servidor. Ele nem mesmo existe ainda. Simplesmente imprimimos uma mensagem indicando que o perfil do servidor está sendo atualizado. Isso porque o perfil do servidor deve ser mantido entre as sessões de nosso cliente, e ainda não estamos lidando com a persistência. Mais tarde, ao implementar a persistência, podemos alterar essa função stub para realmente salvar o perfil do servidor, por exemplo, em um banco de dados SQLite, mesmo sem precisar remover a mensagem impressa (ou registrada). Simplesmente ainda não está implementado. Como mencionado anteriormente, neste estágio, precisamos apenas saber o que fazer se o servidor não atender às nossas capacidades solicitadas: armazenamos as informações para reutilização futura.


Considerações finais

Neste artigo, descrevemos como começar a trabalhar com a parte operacional do protocolo MQTT v5.0, conforme exigido pelo padrão OASIS, para rodar um cliente compatível o mais rápido possível. Explicamos como implementamos a classe CSrvResponse para determinar o tipo de resposta do servidor e os códigos de motivo associados. Também descrevemos como nosso cliente reagirá às capacidades indisponíveis do servidor.

Na próxima fase, implementaremos PUBLISH, entenderemos melhor o comportamento de trabalho para os níveis de QoS e abordaremos as sessões (Sessions) e a persistência necessária.

** Outros acrônimos úteis: DRY (don’t repeat yourself - não se repita), KISS (keep it simple, stupid - mantenha simples, estúpido), YAGNI (you aren't gonna need it - você não vai precisar disso). Cada um deles carrega sua própria sabedoria prática, mas YMMV (your mileage may vary - gosto não se discute) :) 

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

Arquivos anexados |
headers.zip (7.16 KB)
tests.zip (3 KB)
Avaliando o desempenho futuro com intervalos de confiança Avaliando o desempenho futuro com intervalos de confiança
Neste artigo, vamos explorar o uso do bootstrapping como um meio de avaliar a eficácia futura de uma estratégia automatizada.
Teoria das Categorias em MQL5 (Parte 21): Transformações naturais com LDA Teoria das Categorias em MQL5 (Parte 21): Transformações naturais com LDA
Este artigo, o 21º de nossa série, continua nossa análise das transformações naturais e de como elas podem ser implementadas usando a análise discriminante linear. Assim como no artigo anterior, a implementação é apresentada no formato de uma classe de sinal.
Modelos prontos para integrar indicadores nos Expert Advisors (Parte 3): Indicadores de tendência Modelos prontos para integrar indicadores nos Expert Advisors (Parte 3): Indicadores de tendência
Neste artigo de referência, vamos dar uma olhada nos indicadores padrão da categoria Indicadores de tendência. 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.
Algoritmos de otimização populacionais: salto de sapo embaralhado Algoritmos de otimização populacionais: salto de sapo embaralhado
O artigo apresenta uma descrição detalhada do algoritmo salto de sapo embaralhado (Shuffled Frog Leaping Algorithm, SFL) e suas capacidades na solução de problemas de otimização. O algoritmo SFL é inspirado no comportamento dos sapos em seu ambiente natural e oferece uma nova abordagem para a otimização de funções. O algoritmo SFL é uma ferramenta eficaz e flexível, capaz de lidar com diversos tipos de dados e alcançar soluções ótimas.