English Русский 中文 Deutsch 日本語 Português
preview
Desarrollando un cliente MQTT para MetaTrader 5: metodología TDD (Parte 6)

Desarrollando un cliente MQTT para MetaTrader 5: metodología TDD (Parte 6)

MetaTrader 5Integración | 11 septiembre 2024, 15:54
169 0
Jocimar Lopes
Jocimar Lopes

 

"El optimismo supone una fuente de riesgos laborales para el programador: la opinión del cliente puede aplastarlo" (Kent Beck)

Introducción

La metodología Test-Driven Development (TDD) ofrece muchas ventajas y tiene un inconveniente importante. Nos ayuda a escribir módulos bien definidos y variables con nombres adecuados para lograr una amplia cobertura de las pruebas, comprender mejor el tema, evitar una complicación excesiva y centrarnos en la tarea que tenemos entre manos. El principal inconveniente es consecuencia directa de este estrecho enfoque en la tarea que nos ocupa: para evitar sentirnos intimidados por la complejidad global del proyecto, los desarrolladores seguimos abordando la tarea más pequeña posible cada vez. Si un genio es alguien que elimina la complejidad resolviéndola, entonces podemos decir que un desarrollador TDD es alguien que ignora deliberadamente la complejidad. 

Puede compararse con un caballo con anteojeras o un burro persiguiendo una zanahoria.

La complejidad no va a desaparecer porque la ignoremos. Ignorando el bosque y escudriñando la hoja, seguiremos teniendo, digamos, una deuda técnica. Así que dejaremos a un lado las funciones redundantes, los miembros duplicados, las pruebas inútiles, las clases innecesarias y el código ilegible e inaccesible. Esta deuda técnica que se acumula durante el desarrollo puede resultar perjudicial para nuestra productividad. Por este motivo, la refactorización forma parte integrante de la práctica del TDD. El siguiente esquema muestra los pasos típicos del TDD.

Etapas típicas de TDD: rojo, verde, refactorización

Figura 01. Etapas típicas de TDD: Rojo, Verde, Refactorización (fuente: IBM Developer)

En las secciones siguientes, describiremos la reorganización de las clases escritas anteriormente y comentaremos algunas de las mejoras. También mostraremos la creación de paquetes PUBLISH tras las mejoras, así como la obtención de un proyecto viable para las clases de construcción de paquetes. La primera clase que seguirá al nuevo patrón es la clase PUBACK. Como los paquetes PUBACK son el análogo de los paquetes PUBLISH con QoS 1, necesitaremos ocuparnos de la gestión del estado de la sesión (Session State). Nuestro cliente necesitará algún tipo de capa de persistencia para guardar y actualizar el estado. 

La capa de persistencia va más allá de la norma OASIS, y dependerá de la aplicación. Puede tratarse de un simple archivo en un sistema de archivos local o de un sistema de base de datos de alta disponibilidad completamente distribuido en la nube. Para nuestros objetivos, bastará con una base de datos como un servidor PostgreSQL que se ejecute localmente en Windows o a través de WSL. No obstante, como disponemos de integración incorporada entre MQL y SQLite, la elección de este SGBD sin servidor de archivo único resultará obvia en este caso. SQLite es sencillo, escalable, fiable y no necesita mantenimiento del servidor. Incluso podemos tener una base de datos exclusivamente de memoria, que resulta ideal para las pruebas y la depuración. 

Pero en esta fase no implementaré la capa de persistencia, ya que hemos decidido probar a fondo la escritura y lectura de paquetes antes de proceder con la gestión del estado de la sesión. Antes de pasar a la capa de persistencia, deberemos asegurarnos de que codificamos y descodificamos correctamente los distintos tipos de datos utilizados por el protocolo MQTT. Para lograr este objetivo, hemos escrito extensas pruebas modulares y pronto empezaremos a ejecutar pequeñas pruebas funcionales con un bróker real que se ejecutará de manera local (bróker mosquitto con código abierto de la Eclipse Foundation).

Por lo tanto, para probar la interacción PUBLISH/PUBACK, utilizaremos una base de datos de imitación, es decir, un conjunto de funciones para generar los datos controlados que necesitemos para las pruebas. Lo presentaremos más adelante cuando describamos la clase CPUback.

En las siguientes descripciones, utilizaremos las palabras MUST y MAY tal y como se utilizan en el estándar OASIS, que a su vez las utiliza tal y como se describen en IETF RFC 2119.

Además, salvo que se indique lo contrario, todas las citas procederán del Estándar OASIS.


Creación de paquetes PUBLISH

Durante la reescritura de la clase CPublish, hemos eliminado algunos miembros de la clase. También hemos combinado la construcción de encabezados fijos y variables en un constructor de un solo paso. Estos cambios se repetirán en otras clases de paquetes de control.

Actualmente, nuestra clase CPublish tiene los siguientes miembros y métodos.

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

Además de la simplificación, el proceso de establecimiento de banderas de publicación, nombres de temas y propiedades es ahora independiente, lo cual significa que cada uno de ellos podrá establecerse en cualquier orden, siempre que el método Build() se llame en último lugar.
Esta prueba formalizará este comportamiento. Asimismo, comprobará el constructor de la clase con las dos banderas RETAIN y QoS1 establecidas y el nombre del tema requerido.
bool TEST_Ctor_Retain_QoS1_TopicName1Char()
  {
   Print(__FUNCTION__);
   CPublish *cut = new CPublish();
   uchar expected[] = {51, 6, 0, 1, 'a', 0, 1, 0}; // QoS > 0 require packet ID
   uchar result[];
   cut.SetTopicName("a");
   cut.SetRetain(true);
   cut.SetQoS_1(true);
   cut.Build(result);
   bool isTrue = AssertEqual(expected, result);
   delete(cut);
   ZeroMemory(result);
   return isTrue;
  }

Ahora los métodos SetTopicName(), SetRetain() y SetQos1() pueden llamarse en cualquier orden y el paquete recibido seguirá siendo válido. Como hemos mencionado antes, este comportamiento se reproducirá en todas las clases de paquetes de control, y tendremos una prueba para cada combinación de banderas de publicación. Todas las pruebas se hallan en los archivos adjuntos.

Corregido el encabezamiento del paquete PUBLISH

Los encabezados fijos de los paquetes PUBLISH son diferentes del resto de paquetes de control MQTT 5.0 en la versión actual del protocolo. Tienen tres banderas que NO están reservadas para uso futuro: las banderas RETAIN, QoS y DUP. Encontrará una descripción detallada de las banderas PUBLISH en el artículo anterior (el quinto) de la serie.

Banderas RETAIN, QoS Level y DUP del encabezado fijo del paquete MQTT 5.0 PUBLISH

Figura 02. Encabezado fijo del paquete PUBLISH MQTT 5.0 RETAIN, bandera QoS Level y DUP

Usaremos el mismo patrón para cambiar cualquiera de las banderas de publicación, pero ahora, después de la refactorización, ya no llamaremos a SetFixedHeader() en cada una de ellas. En primer lugar, definiremos un interruptor como un valor booleano que se transmitirá como argumento a una función.

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

A continuación, comprobaremos si el valor booleano es verdadero o falso.

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

Si el valor lógico es verdadero, aplicaremos una asignación OR a nivel de bits entre el valor de la bandera y el elemento uchar (un byte) para activar la bandera.

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

Si el valor lógico es falso, realizaremos una asignación AND a nivel de bits entre el valor de la bandera y el mismo elemento uchar para borrar la bandera.

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

Así, la variable m_pubflags contendrá todas las banderas establecidas/borradas al configurar el paquete. Más tarde, cuando el método Build() sea llamado, realizaremos nuevamente una asignación OR a nivel de bits, esta vez entre m_pubflags y el primer byte del paquete (byte 0).

pkt[0] |= m_pubflags;


Encabezado variable del paquete PUBLISH

La encabezado variable del paquete PUBLISH contendrá los siguientes campos en el siguiente orden: nombre del tema, ID del paquete y propiedades.

Nombre del tema

Como todas las relaciones editor-suscriptor están vinculadas al título del tema de publicación, este campo será obligatorio en los paquetes PUBLISH y no podrá contener comodines. Al establecer este campo, tendremos dos condiciones de protección: para los comodines y para una cadena de longitud cero que se retornarán inmediatamente y registrarán un error si cualquiera de estas condiciones es verdadera.

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

Si no se cumple ninguna de las condiciones de protección, codificaremos la cadena como UTF-8 y almacenaremos el array de caracteres en el elemento protegido m_topname para su posterior inclusión en el paquete final al llamar a Build().

Identificador del paquete

El ID del paquete NO lo definirá el usuario y no será necesario para QoS 0. En su lugar, se establecerá automáticamente en el método Build() si la calidad de servicio requerida > 0. 

// QoS > 0 requires packet ID
   if((m_pubflags & 0x06) != 0)
     {
      SetPacketID(pkt, pkt.Size());
     }
Al construir el paquete final, comprobaremos el miembro m_pubflags usando AND a nivel de bits con el valor binario 0110 (0x06). Si el resultado no es cero, sabremos que el paquete tiene QoS_1 o QoS_2 y estableceremos el ID del paquete.
La función SetPacketID generará un entero pseudoaleatorio utilizando TimeLocal() para generar el estado inicial. Para facilitarnos la vida a la hora de realizar las pruebas, hemos definido una variable lógica TEST. Si es igual a true, la función establecerá el valor 1 como identificador del paquete.
//+------------------------------------------------------------------+
//|            SetPacketID                                           |
//+------------------------------------------------------------------+
#define TEST true

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

Como podemos ver, también tendremos una advertencia (WARN) por si acaso.

Propiedades

En la cuarta parte de la serie, analizamos en profundidad las propiedades y su papel en los mecanismos de extensión de MQTT 5.0. A continuación describiremos su aplicación, prestando especial atención a la codificación de los distintostipos de datos

En el paquete de control MQTT 5.0, existen seis tipos de representación de datos para codificar los valores de las propiedades:

  1. Entero de un byte que representa enteros sin signo de 8 bits
  2. Enteros de dos bytes, que son enteros sin signo de 16 bits clasificados de mayor a menor (orden de red).
  3. Enteros de cuatro bytes, que son enteros sin signo de 32 bits, también en orden de red
  4. Enteros de bytes variables que usan un mínimo de hasta cuatro bytes para representar un valor de 0 a 268 435 455
  5. Datos binarios de 0 a 65 535
  6. Cadenas codificadas en UTF-8 que también pueden usarse para codificar el par clave:valor en las propiedades de usuario.

La siguiente tabla muestra las propiedades PUBLISH disponibles y la representación de datos correspondiente.

Propiedad Presentación de los datos
Indicador de formato de carga útil (Payload Format Indicator) Número entero de un byte
Intervalo de expiración del mensaje (Message Expiry Interval) Número entero de cuatro bytes
Alias de tema (Topic Alias) Número entero de dos bytes 
Tema de respuesta (Response Topic) Cadena codificada en UTF-8
Datos de correlación (Correlation Data)  Datos binarios 
Propiedad del usuario (User Property)  Un par de líneas en codificación UTF-8
Identificador de suscripción (Subscription Identifier)  Número entero con número variable de bytes
Tipo de contenido (Content Type) Cadena codificada en UTF-8

Tabla 01. Propiedades PUBLISH y su correspondiente representación de datos en MQTT 5.0

Los identificadores de las propiedades se han incluido en el encabezado Defines.mqh.

//+------------------------------------------------------------------+
//|              PROPERTIES                                          |
//+------------------------------------------------------------------+
/*
The last field in the Variable Header of the CONNECT, CONNACK, PUBLISH, PUBACK, PUBREC,
PUBREL, PUBCOMP, SUBSCRIBE, SUBACK, UNSUBSCRIBE, UNSUBACK, DISCONNECT, and
AUTH packet is a set of Properties. In the CONNECT packet there is also an optional set of Properties in
the Will Properties field with the Payload
*/
#define MQTT_PROP_IDENTIFIER_PAYLOAD_FORMAT_INDICATOR          0x01 // (1) Byte                  
#define MQTT_PROP_IDENTIFIER_MESSAGE_EXPIRY_INTERVAL           0x02 // (2) Four Byte Integer     
#define MQTT_PROP_IDENTIFIER_CONTENT_TYPE                      0x03 // (3) UTF-8 Encoded String  
#define MQTT_PROP_IDENTIFIER_RESPONSE_TOPIC                    0x08 // (8) UTF-8 Encoded String  
#define MQTT_PROP_IDENTIFIER_CORRELATION_DATA                  0x09 // (9) Binary Data           
#define MQTT_PROP_IDENTIFIER_SUBSCRIPTION_IDENTIFIER           0x0B // (11) Variable Byte Integer
#define MQTT_PROP_IDENTIFIER_SESSION_EXPIRY_INTERVAL           0x11 // (17) Four Byte Integer   
.
.
. 

Indicador de formato de carga útil (Payload Format Indicator)

El indicador de formato de la carga útil puede tener un valor de 0 o 1, indicando bytes sin procesar o una cadena codificada en UTF-8, respectivamente. Si está ausente, se asumirá que es 0 (bytes no procesados).

Aunque este campo puede establecerse directamente en el array de elementos m_props, hemos decidido utilizar un búfer local auxiliar como intermediario para garantizar la coherencia con la mayoría de las propiedades que requieren alguna manipulación antes de copiarse al array de propiedades final.

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

Si bien solo hay dos valores posibles para esta propiedad, hemos decidido asignarles un valor condicional para una mayor legibilidad.

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

El uso de un valor condicional hace que la llamada al método sea explícita para el usuario final de la biblioteca.

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

Intervalo de expiración del mensaje (Message Expiry Interval)

El intervalo de validez del mensaje se representará como un número entero de cuatro bytes. Conviene recordar que esta representación es distinta de la representación en bytes enteros de una variable. Aunque en este último caso se usará el número mínimo de bytes necesario para representar el valor, en el primero siempre se utilizarán cuatro bytes.

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

Nuestra función para codificar un entero de cuatro bytes seguirá el conocido patrón de dos desplazamientos a la derecha para posibilitar la clasificación de red necesaria.

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

Alias de tema (Topic Alias)

Esta propiedad puede usarse para reducir el tamaño de los paquetes. Está restringido a cada conexión de red y forma parte del estado de la sesión MQTT. Así que, en este punto, nuestra función para establecer un alias de tema podría considerarse un stub. Deberá rellenarse al trabajar con el estado de la sesión.

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

Nuestra función para codificar un entero de dos bytes seguirá el mismo patrón bien conocido de dos desplazamientos a la derecha para posibilitar la clasificación de red necesaria.

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

Tema de respuesta (Response Topic)

La propiedad no forma parte de la plantilla de publicación/suscripción, sino de la interacción solicitud/respuesta a través de MQTT. Como podemos ver, nuestra función utiliza dos búferes auxiliares: uno para guardar el ID de la propiedad y otro para guardar la cadena codificada en UTF-8. Lo mismo ocurrirá con otras cadenas codificadas en UTF-8 porque nuestra función de codificación de cadenas no tiene un tercer parámetro para especificar el índice de inicio del búfer objetivo. La solución podría ser sobrecargar en las siguientes versiones.

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

Datos de correlación (Correlation Data)

También forma parte de la interacción "solicitud/respuesta" a través de MQTT, pero no de la interacción "publicación/suscripción". Como su valor son datos binarios, nuestra función simplemente copiará los datos transmitidos como argumento en el array de bytes m_props después de establecer el ID de la propiedad.

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

Propiedad del usuario (User Property)

Esta es la propiedad más flexible de MQTT 5.0, ya que se puede usar para transmitir pares clave:valor en codificación UTF-8 con semántica dependiente de la aplicación.

"Comentario no normativo.

Esta propiedad está pensada para ofrecer un medio de transmisión de etiquetas nombre-valor a nivel de aplicación cuyo significado e interpretación solo conozcan los programas de aplicación responsables de enviarlas y recibirlas."

Nuestra función usa tres búferes auxiliares para codificar esta propiedad, ya que nuestro codificador de cadenas UTF-8 no dispone actualmente de un tercer parámetro para especificar el índice de inicio del búfer objetivo. La solución podría ser la sobrecarga en las versiones siguientes. (Véase "Tema de respuesta" más arriba).

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

Identificador de suscripción (Subscription Identifier)

La función para establecer el ID de suscripción comprueba si el argumento transmitido se encuentra en el rango de 1 a 268 435 455, que es un valor válido para esta propiedad. Si no, imprimiremos/registraremos un mensaje de error y regresaremos de inmediato.

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

Tipo de contenido (Content Type)

El valor de la propiedad lo define la aplicación. "MQTT no realiza ninguna comprobación de una cadena salvo para verificar que es una cadena codificada en UTF-8 válida".

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

Carga útil (Payload)

Último campo del encabezado de la variable PUBLISH. Una carga útil de longitud cero es permisible. Nuestra función no supone más que la cáscara de nuestro codificador de cadenas UTF-8, siguiendo el mismo patrón de uso de un búfer auxiliar para su posterior copiado al elemento m_payload.

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

El último método Build

El propósito del método Build() es combinar un encabezado fijo, el nombre del tema, el ID del paquete, las propiedades y la carga útil en un paquete final, codificando al mismo tiempo tanto la longitud de las propiedades como la longitud restante del paquete como un entero de bytes variables.

En primer lugar, comprobaremos si existe un nombre de tema obligatorio. Si la longitud es cero, imprimiremos/registraremos un error y regresaremos inmediatamente.

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


A continuación, estableceremos el primer byte del encabezado fijo con el tipo de paquete de control y las banderas PUBLISH correspondientes.

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

Luego copiaremos el array m_topname al último paquete y estableceremos/copiaremos el ID del paquete si QoS > 0.

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

A continuación, codificaremos la longitud de la propiedad como un entero de byte variable.

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

Después copiaremos las propiedades y cargas útiles de los miembros de la clase al array final del paquete.

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

Por último, estableceremos la longitud restante del paquete, codificada como un entero de byte variable.

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


Paquete de control PUBACK

Como hemos visto antes con la implementación de nuestra clase CPublish, cualquier paquete PUBLISH con QoS 1 necesita un identificador de paquete distinto de cero. Este identificador de paquete se retornará en el paquete PUBACK correspondiente. Es este identificador el que permitirá a nuestro cliente saber si un paquete PUBLISH enviado previamente ha sido entregado o si se ha producido un error. Tanto si la entrega tiene éxito como si falla, PUBACK es el disparador que usaremos para actualizar el estado de la sesión. Actualizaremos el estado de la sesión en función del código o códigos de motivo.

El paquete PUBACK retornará uno de los nueve códigos de motivo.

SUCCESS - el mensaje está en orden. Ha sido aceptado, su publicación está en curso. El destinatario ha aceptado la propiedad del mensaje. Este es el único código de motivo que puede ser implícito, es decir, que puede omitirse. Un PUBACK que solo tenga un identificador de paquete DEBERÁ ser interpretado como una entrega QoS 1 exitosa.

"Un cliente o servidor que envíe un paquete PUBACK DEBERÁ utilizar uno de los códigos de motivo PUBACK [MQTT-3.4.2-1]. El código de motivo y la longitud de la propiedad podrán omitirse si el código de motivo es 0x00 (Success) y no hay propiedades."

NO MATCHING SUBSCRIBERS - el mensaje está en orden. Ha sido aceptado, la publicación está en marcha, pero nadie está suscrito al título del hilo. Este código de motivo solo lo enviará el bróker y es opcional, lo cual significa que el bróker PUEDE enviar este código de motivo en lugar de SUCCESS.

UNSPECIFIED ERROR - el mensaje ha sido rechazado, pero el editor no quiere revelar la razón, o ninguno de los otros códigos de motivo más específicos son apropiados para describir la razón.

IMPLEMENTATION SPECIFIC ERROR - el mensaje no tiene nada de malo, pero el editor no quiere publicarlo. La norma no ofrece detalles adicionales sobre la semántica de este código de motivo, pero podremos concluir que el motivo de la no publicación no entra dentro del ámbito del protocolo, es decir, dependerá de la aplicación.

NOT AUTHORIZED - sin derechos de acceso.

TOPIC NAME INVALID - todo está correcto en el mensaje, incluido el nombre del tema, que es una cadena codificada UTF-8 correctamente formada, pero el editor, ya sea cliente o bróker, no acepta ese nombre de tema. Una vez más, podemos concluir que el motivo de la no publicación dependerá de la aplicación.

PACKET IDENTIFIER IN USE - no hay nada malo en el mensaje, pero puede existir un desajuste en el estado de la sesión entre el cliente y el bróker porque el ID del paquete que hemos enviado en PUBLISH ya está en uso.

QUOTA EXCEEDED - cuota superada. El motivo del rechazo, una vez más, no constará en el protocolo. Dependerá de la aplicación.

PAYLOAD FORMAT INVALID - no hay nada incorrecto en el mensaje, pero la propiedad del indicador de formato de carga útil que hemos enviado en nuestra publicación es diferente del formato de carga útil real.

Además del código de motivo, el paquete PUBACK puede contener una cadena de motivo y una propiedad de usuario.

La cadena del motivo es una cadena codificada en UTF-8 legible y diseñada para ayudar en el diagnóstico. Su objetivo no es ser analizada por el destinatario. En su lugar, portará información adicional que podrá ser registrada, impresa, adjuntada a informes, etc. Cabe señalar que cualquier servidor o cliente compatible no enviará una cadena de motivo si su inclusión aumenta el tamaño del paquete por encima del tamaño máximo especificado en el momento de la conexión (paquete CONNECT).

PUBACK también puede contener cualquier número de pares clave:valor codificados como propiedad(es) de usuario. Estos pares pueden usarse para proporcionar información adicional sobre el error. También dependerán de la aplicación. El protocolo no definirá su semántica. 

Nuestro cliente "DEBERÁ tratar el paquete PUBLISH como "no reconocido" hasta que reciba el correspondiente paquete PUBACK del destinatario".


Clase CPuback

Nuestra clase CPuback seguirá el mismo patrón que la clase CPublish. También implementará la interfaz IControlPacket, que es una interfaz raíz e inferior para la jerarquía de objetos.

Se enviará un paquete PUBACK en respuesta a paquetes PUBLISH con QoS 1. Su encabezado fijo de dos bytes solo contendrá el identificador del paquete de control en el primer byte y la longitud restante del paquete en el segundo byte. En esta versión del protocolo, todas las banderas de bits se establecerán en RESERVED.

Estructura del encabezado fijo del paquete MQTT-5.0 PUBACK

Figura 03. Estructura del encabezado fijo del paquete MQTT-5.0 PUBACK

"El encabezado variable del paquete PUBACK contendrá los siguientes campos en el siguiente orden: el identificador de paquete del paquete PUBLISH que se está validando, el código de motivo PUBACK, la longitud de las propiedades y las propiedades propiamente dichas."

Estructura del encabezado variable del paquete MQTT-5.0 PUBACK

Figura 04. Estructura del encabezado variable del paquete MQTT-5.0 PUBACK

Hasta ahora solo hemos trabajado con nuestro Cliente como emisor. A partir de aquí, deberemos considerar también su papel como receptor.

"El protocolo de entrega es simétrico, [...] El Cliente y el Servidor pueden actuar como emisor o receptor".

Vamos a escribir una prueba para una función que obtenga el ID del paquete de confirmación

  1. del paquete devuelto enviado por el bróker al recibir un PUBACK
  2. o de nuestro sistema de guardado al enviar un PUBACK

Un paquete PUBLISH con QoS 1 no tendrá sentido sin el PUBACK correspondiente, que a su vez requerirá que se almacene el identificador del paquete PUBLISH correspondiente. Pero aunque ya sabemos que en algún momento necesitaremos utilizar una base de datos real como capa de persistencia, por ahora no la necesitaremos. Para probar y desarrollar nuestra función, necesitaremos algo que funcione como una base de datos, es decir, que cuando se consulte, retorne el ID de los paquetes PUBLISH que esperan ser reconocidos. Para evitar sorpresas, crearemos la función GetPendingPublishIDs(ushort &result[]) y la almacenaremos en el archivo DB.mqh.

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

Con nuestra "capa de persistencia" podremos centrarnos en la tarea que nos ocupa: escribir una función que, cuando se transmita un array de bytes (de paquete) PUBACK enviado por un bróker, recupere el identificador PUBLISH confirmado y lo coteje con los identificadores PUBLISH pendientes almacenados en nuestra capa de persistencia. Si los identificadores coinciden, se retornará True. Más adelante, cuando implementemos el Comportamiento Operacional (Operational Behavior) del protocolo, liberaremos este identificador del almacenamiento real.

Dada la estructura anterior del encabezado de la variable PUBACK, todo lo que necesitamos hacer ahora será leer los dos primeros bytes para obtener el ID del paquete que está siendo confirmado.

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

Como recordará, el identificador del paquete se codifica como un entero de dos bytes en orden de red, con el byte más significativo (MSB) en primer lugar. Hemos usado una operación de desplazamiento a la izquierda (<<) para la codificación. Para desencriptar, multiplicaremos el valor del byte más significativo por 256 y añadiremos el byte menos significativo.

Por el momento, la función anterior será suficiente. Más adelante, cuando probemos un bróker real en una red abierta, posiblemente tengamos que hacer frente a problemas de órdenes de bytes, pero no los probaremos en esta fase. Sigamos con la tarea.

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

La función anterior recibirá un array de bytes como argumento. Este array de bytes será el encabezado variable del paquete PUBACK. A continuación, almacenaremos en una variable local (pending_ids) un array con los IDs de paquetes de nuestro almacenamiento/base de datos que aún no han sido reconocidos. Por último, leerá el ID del paquete del array de bytes enviado por el bróker y lo comparará con el array de IDs pendientes. Si el paquete se encuentra en el array, nuestra función retornará True y podremos liberar el identificador.

La misma lógica nos permitirá liberar los identificadores de los paquetes PUBREC, PUBREL y PUBCOMP para PUBLISH con QoS 2. Además, más adelante sustituiremos nuestro falso "nivel de almacenamiento" por una única función en el archivo con una base de datos real, pero la lógica básica de la función se mantendrá. En este punto, otro desarrollador podría trabajar en la capa de persistencia, mientras nosotros desarrollamos las clases de nuestro paquete de forma totalmente independiente.

También necesitaremos poder leer los códigos de motivo del encabezado de la variable PUBACK. Como este campo tiene una posición y un tamaño fijos, lo único que tendremos que hacer es leer ese byte en concreto.

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


Como solo estamos trabajando en el lado receptor de nuestro cliente, es decir, todavía no estamos enviando un PUBACK, las características anteriores son suficientes para nuestras próximas pruebas funcionales. Ahora tendremos que probarlo en un bróker real.


Conclusión

La refactorización continua forma parte de la metodología TDD. Su objetivo es conseguir un código no solo plenamente funcional, sino también limpio: unidades de responsabilidad y funciones unificadas (en nuestro caso, clases y métodos), identificadores legibles (clases, métodos y nombres de variables) y también evitar la redundancia ("no te repitas"). Es un proceso, no una tarea de un solo paso. Por lo tanto, ya sabemos con seguridad que estaremos refactorizando constantemente hasta que tengamos un cliente MQTT 5.0 totalmente funcional.

Ahora estamos listos para empezar a escribir nuestra primera prueba funcional en un bróker MQTT real para ver si nuestros paquetes CONNECT, CONNACK, PUBLISH y PUBACK funcionan correctamente. 

Los paquetes PUBACK son el análogo de los paquetes PUBLISH con QoS 1. Los paquetes PUBLISH con QoS 2 necesitarán paquetes PUBREC, PUBCOMP y PUBREL como análogos. Nuestro próximo artículo estará dedicado a ellos.

Si conoce bien MQL5 y puede contribuir al desarrollo de este cliente MQTT de código abierto, publíquelo en los comentarios más abajo o en el chat de la comunidad. 


Traducción del inglés realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/en/articles/14391

Archivos adjuntos |
MQTT.zip (20.83 KB)
Tests.zip (16.88 KB)
Características del Wizard MQL5 que debe conocer (Parte 17): Negociación con multidivisas Características del Wizard MQL5 que debe conocer (Parte 17): Negociación con multidivisas
La negociación con varias divisas no está disponible por defecto cuando se crea un asesor experto mediante el asistente. Examinamos dos posibles trucos que los operadores pueden utilizar para poner a prueba sus ideas con más de un símbolo a la vez.
El método de manejo de datos en grupo: implementación del algoritmo combinatorio en MQL5 El método de manejo de datos en grupo: implementación del algoritmo combinatorio en MQL5
En este artículo continuamos nuestra exploración de la familia de algoritmos del método de manejo de datos en grupo, con la implementación del algoritmo combinatorio junto con su encarnación refinada, el algoritmo combinatorio selectivo en MQL5.
Variables y tipos de datos extendidos en MQL5 Variables y tipos de datos extendidos en MQL5
Las variables y los tipos de datos son temas muy importantes no solo en la programación MQL5, sino también en cualquier lenguaje de programación. Las variables y los tipos de datos de MQL5 pueden dividirse en simples y extendidos. Aquí veremos las variables y los tipos de datos extendidos. Ya analizamos los sencillos en un artículo anterior.
Introducción a MQL5 (Parte 5): Funciones de trabajo con arrays para principiantes Introducción a MQL5 (Parte 5): Funciones de trabajo con arrays para principiantes
En el quinto artículo de nuestra serie, nos familiarizaremos con el mundo de los arrays en MQL5. Este artículo ha sido pensado para principiantes. En este artículo intentaremos repasar conceptos complejos de programación de manera simplificada para que el material resulte comprensible para todos. Asimismo, exploraremos conceptos básicos, discutiremos diferentes cuestiones y compartiremos conocimientos.