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

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

MetaTrader 5Integración | 11 julio 2024, 14:01
138 0
Jocimar Lopes
Jocimar Lopes

"La optimización prematura es la raíz de todos los males" (Donald Knuth)

Introducción

MQTT es un protocolo de intercambio de mensajes de publicación-suscripción. Así, podemos esperar que nuestros principales paquetes sean PUBLISH (publicar) y SUBSCRIBE (suscribir). Todos los demás tipos de paquetes serán auxiliares.

Además de poder crear paquetes PUBLISH, también necesitaremos poder leerlos, ya que los mensajes que nuestro Cliente recibirá de otros Clientes también serán paquetes PUBLISH. Esto se debe a que el protocolo de entrega es simétrico.

"Un paquete PUBLISH se envía del cliente al servidor o del servidor al cliente para transportar un mensaje de aplicación (Application Message)".

Los paquetes PUBLISH tienen otro encabezado fijo con banderas de publicación y un encabezado variable con un nombre de tema obligatorio codificado como cadena UTF-8 y un ID de paquete obligatorio (si QoS > 0). Además, con el tiempo podremos utilizar casi todas las propiedades (incluidas las propiedades personalizadas) introducidas en MQTT 5.0, incluidas las propiedades relacionadas con el modo de interacción "Solicitud/Respuesta" (Request/Response).

En este artículo, veremos la estructura de los encabezados y la prueba e implementación de las banderas de publicación, los nombres de los temas y los identificadores de paquetes. 

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.


Estructura fija de encabezado del paquete MQTT 5.0 PUBLISH

El encabezado fijo del paquete PUBLISH tiene la misma estructura básica de dos bytes que todos los demás tipos de paquetes de control. El primer byte sirve para indicar el tipo de paquete, mientras que el segundo es el host de la longitud restante (Remaining Length) del paquete, codificado como un entero de byte variable (Variable Byte Integer).

Pero mientras que todos los demás tipos de paquetes tienen los cuatro primeros bits del primer byte en estado RESERVED, el paquete PUBLISH utiliza estos cuatro bits para codificar tres funciones: RETAIN, QoS Level y DUP.

Paquete de control MQTT Banderas de encabezado fijo Bit 3 Bit 2 Bit 1 Bit 0
CONNECT Reservado 0 0 0 0
CONNACK Reservado
0 0 0 0
PUBLISH Se utiliza en 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

Tabla 1. Reproducción de los bits de las banderas de la Tabla 2-3 del Estándar MQTT 5.0 Oasis

"Si un bit de bandera se marca como Reservado, se mantendrá en reserva para un uso futuro y DEBERÁ establecerse en el valor especificado".

Debido a esta diferencia de encabezado fijo entre los paquetes PUBLISH y el resto de paquetes de control, la función que utilizaremos para generar los encabezados fijos no se puede utilizar aquí.

//+------------------------------------------------------------------+
//|                     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 podemos ver, los parámetros de la función solo tienen el tipo de paquete y las referencias a dos arrays, siendo uno el origen y el otro el destino del array de encabezado fijo. A continuación, la primera línea toma el valor entero del tipo de paquete de Enum y lo desplaza cuatro bits a la izquierda, asignando el resultado de la operación bit a bit al primer byte del array de encabezado fijo (dest_buf[0]). Esta operación bit a bit garantiza que los cuatro primeros bits permanezcan sin asignar, o "reservados", según el Estándar.

La segunda línea llama a una función que calcula la longitud restante del paquete asignando un valor al segundo byte del array de encabezado fijo (dest_buf[1]), codificado como un entero variable.

Pero esta función no ofrece ningún medio para establecer banderas de publicación.

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

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

Así que añadiremos un interruptor (Switch) para colocar paquetes PUBLISH y un parámetro final para recuperar las banderas de publicación. Podríamos sobrecargar la función para recuperar las banderas de publicación modificando ligeramente su cuerpo para implementar las características de los paquetes PUBLISH, pero este es el caso de uso ideal para Switch, ya que solo tenemos una excepción (PUBLISH), y en todos los demás casos se utilizará por defecto la implementación anterior.

El último parámetro por defecto es cero, lo cual significa que podrá ser ignorado al configurar todas las encabezados de paquetes fijos. dest_buf solo cambiará si se establece alguna bandera de publicación.

//+------------------------------------------------------------------+
//|                     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 podemos ver, el búfer de destino que contiene un encabezado fijo se modifica usando una operación OR a nivel de bits combinada con la asignación de la misma al primer byte. Hemos usado ampliamente este patrón para cambiar las banderas de conexión, y ahora utilizaremos el mismo patrón para cambiar los banderas de publicación.

Por ejemplo, la bandera RETAIN se activa/desactiva con el código siguiente. 

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

Bandera de nivel QoS_1 (carece de una firma funcional similar).

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

Bandera de nivel QoS_2.

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

Bandera DUP.

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

Los valores de bandera (máscaras de bandera) son constantes definidas en Enum como valores del grado del número dos según la posición del bit correspondiente en el 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
  };

Así, las banderas tienen los siguientes valores binarios y posiciones de 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

El valor decimal del paquete PUBLISH es 3.

Decimal 3 0 0 0 0 0 0 1 1

Hemos desplazado el valor del tipo de paquete cuatro bits a la izquierda (dest_buf[0] = (uchar)pkt_type << 4).

Decimal 48 0 0 1 1 0 0 0 0

Al aplicar una operación bitwise OR ( dest_buf[0] |= publish_flags; ) a la representación binaria del valor del tipo de paquete y las banderas, estamos esencialmente combinando bits. Así, la representación binaria del valor desplazado a la izquierda de un paquete PUBLISH con la bandera DUP activada será la siguiente.

Decimal 56 0 0 1 1 1 0 0 0

Si las banderas RETAIN y QoS 2 están activadas, los bits del primer byte del encabezado fijo serán los siguientes.

Decimal 53 0 0 1 1 0 1 0 1

A la inversa, una operación AND a nivel de bits entre el valor del tipo de paquete y su complemento (~) de la representación binaria de banderas borrará la bandera ( m_publish_flags &= ~RETAIN_FLAG ).

Así, si el byte se ha fijado con QoS 1 sin DUP o RETAIN, se vería así.

Decimal 50 0 0 1 1 0 0 1 0

El complemento de la bandera QoS 1 anterior será el valor de todos sus bits invertidos.

Bandera QoS_1 0 0 1 0
Bandera ~QoS_1 1 1 0 1

Como cualquier valor AND cero es igual a cero, eliminaremos efectivamente la bandera.

Ahora observe que el valor binario del byte obviamente cambiará a medida que se activen las banderas. Si ninguna bandera ha sido establecida, el valor será igual al valor decimal 48 después de desplazar a la izquierda el valor decimal 3 en cuatro bits. Cuando activamos la bandera RETAIN, esta tiene un valor decimal de 49. El valor pasa a ser 51 cuando RETAIN y QoS 1. Y así sucesivamente.

Estos valores decimales son los que buscaremos al examinar todas las combinaciones posibles de ajuste y desbloqueo de banderas en nuestras pruebas.

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

Estas pruebas, un tanto ingenuas (así como otras más complejas) escritas antes de la implementación nos permiten centrarnos en la tarea que tenemos entre manos, y también actúan como "red de seguridad" cuando necesitamos cambiar o reorganizar el código. Encontrará muchas de ellas en los archivos adjuntos. 

Al ejecutar las pruebas, deberíamos ver aproximadamente lo siguiente.

Figura 2. Encabezado fijo de la muestra de prueba MQTT 5.0 PUBLISH

Figura 2. Encabezado fijo de la muestra de prueba MQTT 5.0 PUBLISH

Si el ciclo publicar/suscribir supone el núcleo del protocolo, estas tres funciones (RETAIN, DUP y QoS) será el núcleo del comportamiento operativo del protocolo. Probablemente tendrán un gran impacto en la gestión del estado de la sesión. Así pues, vamos a ir un poco más allá de la estricta especificación del protocolo e intentaremos comprender su semántica.

RETAIN

Como hemos visto en la parte 1 de esta serie, el patrón de publicación/suscripción está vinculado a un nombre de tema específico: un cliente publica un mensaje con un nombre de tema o se suscribe a un nombre de tema, y todos los clientes reciben los mensajes publicados con el nombre del tema al que se han suscrito. 

Al publicar, podemos utilizar la bandera RETAIN establecida en 1 (true) para indicar al servidor que guarde el mensaje y lo entregue como "mensaje guardado" a los nuevos suscriptores. Siempre hay un solo mensaje guardado, y estableceremos RETAIN en 1 para almacenar/reemplazar los mensajes guardados existentes. Con esta bandera enviaremos una carga útil de cero bytes a 1 para borrar los mensajes almacenados. Lo resetearemos a 0 para decirle al servidor que no haga nada con los mensajes guardados bajo este nombre de hilo: ni guardar, ni reemplazar, ni borrar.

Cuando nos suscribimos al título de un hilo, recibimos un mensaje guardado. En el caso de las suscripciones compartidas, el mensaje guardado solo se enviará a uno de los clientes del filtro de temas. Profundizaremos en las suscripciones más comunes al trabajar con los paquetes SUBSCRIBE.

Esta función actúa en conjunto con los banderas Retain Available y Retain Not Supported en los paquetes CONNACK enviados desde el servidor. 

Los mensajes almacenados caducan como cualquier otro mensaje según el intervalo de caducidad del mensaje establecido en el parámetro PUBLISH, o bien en las propiedades Will de la carga útil CONNECT.

Hay que tener en cuenta que RETAIN es una función de bróker dinámico. Esto significa que puede cambiar de "disponible" a "no compatible" y viceversa dentro de la misma sesión.

Nivel QoS

Ya hablamos del nivel de QoS en el artículo introductorio de esta serie, enumerando entonces algunas de las decisiones de diseño tomadas por los creadores del protocolo.

"Aunque el protocolo debía ser fiable, rápido y económico debido a las limitaciones tecnológicas y los altos costos de la red, debía ofrecer una entrega de datos de alta calidad con un conocimiento continuo de la sesión que le permitiera hacer frente a conexiones a Internet poco fiables o incluso intermitentes".

En el contexto de los banderas de conectividad, observamos una tabla que define cada nivel de QoS.

Valor QoS Bit 2 Bit 1 Descripción
0 0 0 Principalmente en el momento de entrega
1 0 1 Al menos en el momento de entrega
2 1 0 Exactamente en el momento de entrega
- 1 1 Reservado - no se puede usar

Tabla 2. Reproducción de la Tabla 3-9 de definiciones de QoS del Estándar MQTT 5.0 Oasis

Al describir el uso de los niveles de calidad de servicio y otras características, hemos usado los términos "servidor" y "bróker" para referirnos al servicio que distribuirá nuestros mensajes. Pero según el Estándar,

"El protocolo de entrega es simétrico, en la siguiente descripción tanto el Cliente como el Servidor pueden actuar como emisor o receptor. El protocolo de entrega se encarga exclusivamente de la entrega de un mensaje de aplicación de un emisor a un receptor. Cuando un Servidor entrega un Mensaje de Aplicación a más de un Cliente, cada Cliente se procesa de manera independiente. El nivel de QoS utilizado para entregar un mensaje de aplicación saliente a un cliente puede resultar diferente del de un mensaje de aplicación entrante." (el subrayado es mío)

Por tanto, utilizar los términos "servidor" y "bróker" en el sentido en que lo hemos hecho hasta ahora está justificado porque estamos hablando desde la perspectiva del Cliente en un sentido amplio, pero debemos considerar esta simetría en el protocolo de entrega.

El nivel QoS por defecto es 0. Esto significa que si no establecemos dicha bandera, le diremos al servidor que 0 (cero) es el máximo nivel de QoS que estamos dispuestos a aceptar. Cualquier bróker que cumpla las normas aceptará este nivel. Se trata de una publicación "fire and forget" (dispara y olvida) en la que el remitente reconoce que pueden producirse tanto pérdidas como duplicaciones en la entrega.

Figura 3 MQTT 5.0 - diagrama de bloques de la capa 0 de QoS cliente-servidor

Figura 3. MQTT 5.0 - diagrama de bloques de la capa 0 de QoS cliente-servidor

El nivel 1 de QoS permite la posibilidad de duplicación en la entrega, pero no tolera pérdidas. El servidor acusa recibo del mensaje PUBACK.

Figura 4. MQTT 5.0 - diagrama de bloques de la capa 1 de QoS cliente-servidor

Figura 4. Figura 4. MQTT 5.0 - diagrama de bloques de la capa 1 de QoS cliente-servidor

El nivel 2 de QoS no requiere pérdidas ni redundancia. A este nivel intervienen cuatro paquetes. El servidor reconoce el inicio de la entrega con la ayuda de PUBREC. A continuación, el cliente solicitará la liberación de ese identificador de paquete concreto usando PUBREL y, por último, el servidor notificará la finalización de la entrega mediante PUBCOMP.

Figura 5. MQTT 5.0 - diagrama de bloques de la capa 2 de QoS cliente-servidor

Figura 5. MQTT 5.0 - diagrama de bloques de la capa 2 de QoS cliente-servidor

La analogía está tomada de un artículo anterior en el que hablábamos de las banderas de conexión:

"Este nivel [QoS 2] puede considerarse como el envío certificado de un paquete. El sistema postal nos entregará un recibo cuando pongamos el paquete en sus manos, reconociendo que ahora se hacen responsables de llevarlo a la dirección correcta. Y cuando eso ocurra, al entregar el paquete, nos enviarán un recibo firmado por el destinatario confirmando la entrega del paquete".

La calidad de servicio puede exigirse para un mensaje Will, para una suscripción (incluidas las suscripciones generales) o para un mensaje concreto. 

Mensaje Will Suscripción Mensaje
CONNECT Will QoS Opciones de suscripción SUSCRIBIRSE Bandera de nivel de QoS PUBLISH

Tabla 3. Paquetes y banderas MQTT 5.0 para los que puede establecer el nivel de calidad de servicio (QoS)

El lector atento habrá notado que QoS 1 y QoS 2 incluyen alguna forma de estado de sesión (Session state). Hablaremos sobre el estado de Session y la capa de persistencia (Persistence layer) correspondiente en otro artículo.

DUP

Si la bandera DUP está activada, significará que estamos intentando enviar nuevamente un paquete PUBLISH anterior que ha fallado. DEBEREMOS establecerlo como 0 (cero) para todos los mensajes QoS 0. La duplicación se refiere al paquete en sí, no al mensaje.


Encabezado variable del paquete MQTT 5.0 PUBLISH: nombre del tema, ID del paquete y propiedades.

El encabezado variable del paquete MQTT 5.0 PUBLISH DEBERÁ tener un nombre de tema, y si QoS es mayor que 0 (cero), también DEBERÁ tener un identificador de paquete. Estos dos campos suelen ir seguidos de un conjunto de propiedades y datos útiles, pero un paquete PUBLISH sin propiedades y con datos útiles de longitud cero es igualmente un paquete válido. En otras palabras, el paquete PUBLISH válido más simple es un paquete de encabezado fijo con QoS 0, sin banderas DUP y RETAIN, y un encabezado variable que contendrá solo el nombre del asunto.

Nombre del tema

Como todas las interacciones entre los clientes y el servidor -y, por extensión, todas las interacciones entre usuarios/dispositivos- en el protocolo de mensajería publicar/subscribir giran en torno a la publicación en un tema y la suscripción a un tema, podemos decir que el campo "Nombre del tema" merece aquí una atención especial. En muchos servicios en tiempo real, encontraremos el término "canal" en lugar del nombre del tema. Esto tiene sentido porque el título del tema representa el canal de información al que se han suscrito los clientes.

El nombre del tema es una cadena codificada en UTF-8 organizada en una estructura jerárquica de árbol. La barra ( / U+002F ) se usa como separador a nivel de tema. 

broker1/account12345/EURUSD

El nombre distingue entre mayúsculas y minúsculas. Es decir, a continuación tendremos ante nosotros dos temas diferentes.

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

Estos separadores de nivel solo son relevantes si alguno de los signos del filtro de temas (ver más abajo) está presente en la suscripción del cliente. No existe restricción en el número de niveles, salvo la restricción en la propia cadena UTF-8. Eventualmente, el nombre del tema puede ser sustituido por un Alias de Tema (Topic Alias).

"Un alias de tema es un valor entero que se usa para identificar un tema en lugar de utilizar el nombre del tema. Esto reduce el tamaño del paquete PUBLISH y es útil cuando los nombres de los temas son largos y los mismos nombres de temas se reutilizan en una conexión de red".

Identificador del paquete

El identificador del paquete es un campo entero de dos bytes necesario para los paquetes PUBLISH con QoS > 0. Se usa en todos los paquetes directamente implicados en el ciclo publicar/suscribir para gestionar el estado de la sesión. El identificador de paquete NO DEBERÁ utilizarse en PUBLISH con QoS 0.

Sirve para conectar los PUBLISH con los ACK correspondientes.

Recordemos que como el protocolo de entrega es simétrico, al usar QoS 1, nuestro Cliente puede recibir un PUBLISH de un servidor con el mismo ID de paquete antes de recibir el PUBACK asociado al PUBLISH enviado antes.

"Un cliente puede enviar un paquete PUBLISH con ID de paquete 0x1234 y luego recibir otro paquete PUBLISH con ID de paquete 0x1234 de su servidor antes de recibir el PUBACK para el paquete PUBLISH que ha enviado."

Cabe señalar que el identificador del paquete también se usa para conectar los acuses de recibo de ACK correspondientes en los paquetes SUBSCRIBE y UNSUBSCRIBE.


¿Cómo se escriben los títulos de un tema?

El nombre del tema es el primer campo del encabezado de la variable. Está codificado como una cadena UTF-8 con algunos puntos de código Unicode no válidos, y contiene una trampa. Por favor, eche un vistazo a estas tres declaraciones con algunos requisitos para la codificación de cadenas UTF-8 para MQTT 5.0.

"[...] los datos de caracteres NO DEBERÁN incluir codificaciones de puntos de código entre U+D800 y U+DFFF. Si un cliente o servidor recibe un paquete de control MQTT que contiene código malformado, se trata de un paquete Malformado (Malformed)".

"Una cadena codificada en UTF-8 NO DEBERÁ incluir el carácter nulo de codificación U+0000. Si el destinatario (cliente o servidor) recibe un paquete de control MQTT que contiene U+0000, se tratará de un paquete malformado."

"Los datos NO DEBERÁN incluir las codificaciones de puntos de código Unicode [Unicode] enumeradas a continuación. Si un destinatario (cliente o servidor) recibe un paquete de control MQTT que contiene alguno de estos elementos, PODRÁ procesar el paquete como malformado. Más abajo le mostramos los puntos de código Unicode no válidos.

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

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

Los puntos de código definidos en la especificación [Unicode] no suponen caracteres".

Como podemos ver, tanto el primer operador como el segundo son estrictos (MUST NOT), lo cual significa que cualquier implementación que cumpla la normativa comprobará los puntos de código prohibidos, mientras que el tercer operador es una recomendación (MUST NOT), lo cual significa que una implementación puede no comprobar los puntos de código prohibidos y seguir considerándose conforme.

Dado que un paquete malformado supone una DESCONEXIÓN (DISCONNECT), si permitimos estos puntos de código en nuestro Cliente y nuestro bróker decide no tratarlos como un paquete malformado, podemos provocar que otros clientes que apliquen la recomendación sean desconectados. Por tanto, aunque la exclusión de los elementos de control y los no-caracteres Unicode suponga solo una recomendación, no permitiremos su uso en nuestra implementación.

En este punto, nuestra función para codificar cadenas a UTF-8 tendrá el aspecto que sigue:

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

Si la cadena transmitida a esta función contiene código no válido, registraremos su posición en la cadena, pasaremos el búfer de destino a ZeroMemory, y retornaremos inmediatamente. Como el nombre del tema tiene una longitud mínima requerida de 1, si la cadena está vacía, haremos lo mismo: registrar, borrar el búfer y retornar.

Por cierto, tenga en cuenta que utilizamos StringToShortArray para convertir una cadena en un array Unicode. Si lo convirtiéramos en un array ASCII, utilizaríamos StringToCharArray. Encontrará una explicación detallada y mucho más en el libro añadido recientemente a la documentación o en este completo artículo sobre las cadenas MQL5.

Observe también que en esta misma llamada StringToShortArray, utilizaremos la longitud de la cadena como último parámetro en lugar de la función por defecto. Esto se debe a que no necesitaremos el carácter nulo (0x00) en nuestro array, y de acuerdo con la documentación de la función: 

" El valor por defecto es menos 1, lo cual significa copiar hasta el final del array o hasta el 0 final. El 0 final también se copiará en el array del destinatario".

mientras que el valor de retorno de StringLen será

"Un número de caracteres de una cadena sin un cero finito".

La función de comprobación de puntos de código no válidos resulta 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;
  };

Además de los puntos de código prohibidos, también deberemos comprobar dos caracteres comodín que se utilizan en los filtros de temas de suscripción pero que están prohibidos en el nombre del tema: el signo más ('+' U+002B) y el signo de número ('#' U+0023).

La función para comprobar puntos de código inválidos será ampliamente usada para codificar cualquier cadena, por lo que se colocará en el encabezado MQTT.mqh, mientras que la función para comprobar caracteres comodín será específica para el nombre del asunto, por lo que formará parte de nuestra clase 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;
  }

La función incorporada StringFind retorna la posición inicial de la subcadena correspondiente, y -1 si no se encuentra la subcadena correspondiente. Así que simplemente comprobaremos cualquier valor por encima de -1. Luego lo llamaremos desde la función 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);
  }

En esta fase, si se encuentra un comodín, haremos la misma "gestión de errores" que antes: escribiremos la información, borraremos el búfer y regresaremos inmediatamente. Más adelante podremos mejorarlo, por ejemplo emitiendo alertas.

La última línea de la función asigna la longitud restante del paquete al segundo byte de nuestro encabezado fijo, utilizando el algoritmo sugerido por el Estándar. Ya escribimos sobre ello en el primer artículo de la serie.

Nuestras pruebas también tendrán la misma estructura.

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

Si ejecutamos las pruebas, deberíamos ver algo como esto:

Figura 6. MQTT 5.0 - Nombre del tema de muestra de prueba PUBLISH MQTT 5.0

Figura 6. MQTT 5.0 - Nombre del tema de muestra de prueba PUBLISH


¿Cómo se escriben los identificadores de los paquetes?

El identificador del paquete NO deberá ser asignado por el usuario. En su lugar, el Cliente DEBERÁ asignarlo a cualquier paquete PUBLISH en el que el nivel de QoS sea > 0, y NO DEBERÁ asignarlo de otro modo. En otras palabras, cada vez que creemos un paquete PUBLISH con QoS 1 o QoS 2, deberemos establecer su ID de paquete. 

Podemos realizar las pruebas necesarias ahora mismo. Todo lo que deberemos hacer es crear un ejemplar del paquete y configurarlo con el nombre de tema requerido y QoS igual a 1 o 2. El array de bytes del paquete resultante deberá tener un identificador de paquete.

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

Tenga en cuenta que no podemos verificar el valor del identificador de paquete generado, ya que será un número generado (pseudo) aleatoriamente, como podemos ver más abajo en la implementación del stub. En su lugar, comprobaremos su disponibilidad. También deberá tener en cuenta que debemos introducir correcciones (FIX). El orden en que se llaman las funciones SetTopicName y SetQoS_X tiene un efecto inesperado en el array de bytes resultante. No recomendamos tener dependencia del orden de llamada entre funciones. Eso sería un error, pero como se suele decir, un error es una prueba no escrita. Por lo tanto, escribiremos una prueba para la ausencia de esta dependencia de orden de llamada en la próxima iteración. En este momento, solo nos interesa superar esta prueba.

Obviamente, la prueba ni siquiera se compilará hasta que tengamos una implementación de la función para establecer los identificadores de paquete. Como el ID(s) del paquete es requerido en múltiples paquetes de control, la función para escribirlo NO deberá ser un miembro de la clase CPktPublish. El encabezado MQTT.mqh parece un archivo más apropiado para su ubicación.

//+------------------------------------------------------------------+
//|            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
  }

Para generar identificadores de paquetes, utilizaremos la función MathRand incorporada. Para ello, deberemos llamar antes a MathSrand. Luego tendremos que transmitir a esta función la "semilla" para el generador de números aleatorios. Hemos elegido TimeLocal como valor inicial, siguiendo la recomendación sobre la generación de números pseudoaleatorios en MQL5 del libro añadido recientemente a la documentación.

Para establecer el ID del paquete, redimensionaremos el array de bytes inicial para hacer espacio para el ID del paquete (un entero de dos bytes), y estableceremos los valores de byte alto y bajo comenzando por la posición transmitida como argumento (start_idx). El último paso será llamar a una función de nuestra clase CPktPublish para los métodos SetQoS_1 y 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());
  }

Tras ejecutar las pruebas incluidas en los archivos adjuntos, deberíamos ver algo como esto (algunas de las líneas se han cortado por brevedad):

Figura 7. MQTT 5.0 - Identificador del paquete de muestra de prueba PUBLISH

Figura 7. MQTT 5.0 - Identificador de paquete de muestra de prueba PUBLISH

Conclusión

Dado que los paquetes PUBLISH son el corazón del protocolo, su implementación resulta un poco más compleja: tienen diferentes encabezados fijos, requieren un encabezado variable con un nombre de tema codificado como UTF-8 y protegido contra algunos puntos de código no válidos, requieren un identificador de paquete si QoS > 0, y pueden usar casi todas las propiedades personalizadas disponibles en MQTT 5.0.

En este artículo describiremos cómo crear encabezados PUBLISH válidos con banderas de publicación, el nombre del tema y el identificador del paquete. En el próximo artículo de esta serie, veremos la creación de propiedades.

Como nota sobre los cambios recientes: si ha estado siguiendo el desarrollo de este cliente MQTT, se habrá dado cuenta de que hemos cambiado varias firmas de funciones, nombres de variables, niveles de acceso a campos, accesorios de prueba, etc. Algunos de estos cambios son esperables en cualquier desarrollo de software, pero la mayoría de ellos se deben al hecho de que utilizamos el enfoque TDD y nos esforzamos por adherirnos a esta metodología tanto como sea posible. Podemos esperar grandes cambios antes de obtener el primer resultado.

Como usted sabe, ningún desarrollador conoce por sí solo todo lo necesario para desarrollar un Cliente de este tipo para nuestra base de código. El TDD ayuda mucho en cuanto a la implementación incremental de funcionalidad extensa. Si puede ofrecernos ayudar, deje un mensaje en el chat de la comunidad o en los comentarios de abajo. Cualquier ayuda será bienvenida. Gracias.

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

Archivos adjuntos |
Algoritmos de optimización de la población: Algoritmo genético binario (Binary Genetic Algorithm, BGA). Parte I Algoritmos de optimización de la población: Algoritmo genético binario (Binary Genetic Algorithm, BGA). Parte I
En este artículo, analizaremos varios métodos utilizados en algoritmos genéticos binarios y otros algoritmos poblacionales. Asimismo, repasaremos los principales componentes del algoritmo, como la selección, el cruce y la mutación, así como su impacto en el proceso de optimización. Además, estudiaremos las formas de presentar la información y su repercusión en los resultados de la optimización.
Marcado de datos en el análisis de series temporales (Parte 5): Aplicación y comprobación de asesores usando Socket Marcado de datos en el análisis de series temporales (Parte 5): Aplicación y comprobación de asesores usando Socket
En esta serie de artículos, presentaremos varias técnicas de marcado de series temporales que pueden producir datos que se ajusten a la mayoría de los modelos de inteligencia artificial (IA). El marcado dirigido de datos puede hacer que un modelo de IA entrenado resulte más relevante para las metas y objetivos del usuario, mejorando la precisión del modelo y ayudando a este a dar un salto de calidad.
Modelos de regresión de la biblioteca Scikit-learn y su exportación a ONNX Modelos de regresión de la biblioteca Scikit-learn y su exportación a ONNX
En este artículo exploraremos la aplicación de modelos de regresión del paquete Scikit-learn e intentaremos convertirlos al formato ONNX y utilizaremos los modelos resultantes dentro de programas MQL5. Adicionalmente, compararemos la precisión de los modelos originales con sus versiones ONNX tanto para precisión flotante como doble. Además, examinaremos la representación ONNX de los modelos de regresión con el fin de comprender mejor su estructura interna y sus principios de funcionamiento.
Introducción a MQL5 (Parte 2): Variables predefinidas, funciones comunes y operadores de flujo de control Introducción a MQL5 (Parte 2): Variables predefinidas, funciones comunes y operadores de flujo de control
En este artículo, seguiremos familiarizándonos con el lenguaje de programación MQL5. Esta serie de artículos no es solo un tutorial, sino también una puerta de entrada al mundo de la programación. ¿Qué hace especiales a estos artículos? Hemos procurado que las explicaciones sean sencillas para que los conceptos complejos resulten accesibles a todos. Aunque el material es accesible, para obtener los mejores resultados será necesario reproducir activamente todo lo que vamos a tratar. Solo así obtendremos el máximo beneficio de estos artículos.