
Desenvolvimento de um Cliente MQTT para o MetaTrader 5: Metodologia TDD (Parte 5)
"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.
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.
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.
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.
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.
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."
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:
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):
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
Aviso: Todos os direitos sobre esses materiais pertencem à MetaQuotes Ltd. É proibida a reimpressão total ou parcial.
Esse artigo foi escrito por um usuário do site e reflete seu ponto de vista pessoal. A MetaQuotes Ltd. não se responsabiliza pela precisão das informações apresentadas nem pelas possíveis consequências decorrentes do uso das soluções, estratégias ou recomendações descritas.





- Aplicativos de negociação gratuitos
- 8 000+ sinais para cópia
- Notícias econômicas para análise dos mercados financeiros
Você concorda com a política do site e com os termos de uso