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

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

MetaTrader 5Integración | 23 febrero 2024, 13:34
232 0
Jocimar Lopes
Jocimar Lopes

"¿Cómo puede llamarse profesional si no sabe si todo su código funciona? ¿Cómo puede saber que todo su código funciona si no lo prueba cada vez que realiza un cambio? ¿Cómo puede probar su código cada vez que realiza cambios sin pruebas unitarias automatizadas con una cobertura muy alta? Pero, ¿es posible crear pruebas modulares automatizadas con una cobertura muy alta sin utilizar TDD?" (Robert "Uncle Bob" Martin, Código Limpio, 2011)

Introducción

En las dos primeras partes de la serie, abordamos una pequeña parte de la sección no funcional del protocolo MQTT. Asimismo, organizamos en dos archivos de encabezado aparte todas las definiciones de protocolo, las enumeraciones y algunas funciones comunes que utilizarán todas nuestras clases. Además, escribimos una interfaz que actuará como raíz de la jerarquía de objetos y la implementamos en una sola clase con el único propósito de crear el paquete MQTT CONNECT correspondiente. Al mismo tiempo, escribimos pruebas unitarias para cada función que participa en la creación de paquetes. Aunque enviamos el paquete generado a nuestro bróker MQTT local para ver si sería reconocido como un paquete MQTT bien formado, este paso no era técnicamente necesario. Como estábamos usando datos específicos para transmitir parámetros a nuestra función, sabíamos que los estábamos probando de forma aislada e independiente del estado. Como esto es algo bueno, nos esforzaremos por seguir escribiendo nuestras pruebas (y, por tanto, nuestras funciones) de esta manera. Esto hará que nuestro código resulte más flexible y nos permitirá cambiar la implementación de una función sin siquiera cambiar el código de prueba, siempre que tengamos la misma signatura de función.

De ahora en adelante, nos ocuparemos de la parte operativa del protocolo MQTT. En el estándar OASIS se llama Comportamiento operacional (Operational Behavior). Es decir, ahora tendremos que ocuparnos de los paquetes enviados desde el servidor. Nuestro cliente deberá ser capaz de identificar el tipo de paquete del servidor y su semántica, y seleccionar el comportamiento adecuado en un contexto determinado y en cada estado posible del cliente.

Para hacer frente a esta tarea, deberemos determinar el tipo de paquete del servidor en el primer byte de la respuesta. Si se trata de un paquete CONNACK, deberemos leer su Código de Motivo de Conexión (Connect Reason Code) y responder en consecuencia.


(CONNECT) Establecimiento de las banderas de conexión del cliente

Cuando nuestro cliente solicita una conexión al servidor, deberá informar al servidor sobre

  • algunas capacidades de bróker deseadas,
  • si se requerirá autenticación usando nombre de usuario y contraseña,
  • y si esta conexión es para una nueva sesión o para reanudar una sesión previamente abierta.

Esto se logrará configurando varias banderas de bits al comienzo del encabezado de la variable, inmediatamente después del nombre y la versión del protocolo. Estas banderas de bits en el paquete CONNECT se denominarán banderas de conexión.

Recuerde que las banderas de bits son valores booleanos. Se les pueden asignar diferentes nombres o representaciones, pero los valores booleanos solo tienen dos valores posibles, normalmente true o false.

Fig. 01. Términos comunes utilizados para representar valores booleanos.

Fig. 01. Términos comunes utilizados para representar valores booleanos.

El estándar OASIS utiliza 1 (uno) y 0 (cero) de forma secuencial. La mayoría de las veces usaremos true y false, y finalmente set y unset. Esto debería hacer que el texto resulte más legible. Además, nuestra API pública utilizará consistentemente true y false para establecer estos valores, por lo que el uso de estos términos debería hacer que el presente artículo resulte más claro para aquellos lectores que han estado siguiendo el desarrollo de la biblioteca.

Fig. 02. Bits de las banderas de conexión de OASIS

Fig. 02. Bits de las banderas de conexión de OASIS

Como podemos ver en la tabla OASIS en la imagen de arriba, el primer bit (bit_0) estará reservado y deberemos dejarlo sin cambios: puesto a cero, sin marcar, valor booleano falso, unset. Si lo establecemos, tendremos un paquete mal formado.


Clean Start (bit_1)

El primer bit que podremos configurar es el segundo bit. Este se utilizará para configurar la bandera Clean Start; si es igual a True, el servidor ejecutará Clean Start y cancelará cualquier sesión existente asociada con nuestro ID de cliente. El servidor iniciará una nueva sesión. Si no se configura, el servidor reanudará nuestra conversación anterior si la hubo, o iniciará una nueva sesión si no hay ninguna sesión existente asociada con nuestro ID de cliente.

Así es como se verá ahora nuestra función para establecer/eliminar esta bandera.

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

Alternaremos los valores usando operaciones bit a bit. Usaremos un operador ternario para alternar el valor booleano y un operador de asignación para hacer el código más compacto. Luego almacenaremos el resultado en un miembro de la clase privada m_connect_flags. Finalmente, actualizaremos el array de bytes que representará nuestro paquete CONNECT con los nuevos valores llamando a la función ArrayFill incorporada. (Esta etapa posterior del uso -el rellenado del array-, probablemente la cambiaremos más adelante).

Esta línea de una de nuestras pruebas muestra cómo se llama.

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


Bit 7 6 5 4 3 2 1 0

User Name Flag (bandera de nombre de usuario)
Password Flag (bandera de contraseña)
Will Retain (almacenamiento Will)
Will  QoS 2
Will QoS 1

Will Flag (bandera Will)

Clean Start (inicio limpio)


Reserved (reservado)


X X X X X X 1 0

Tabla 01. Clean Start (bit_1) la bandera de bits se ha establecido en true - MQTT v5.0

Usaremos ampliamente este patrón para alternar banderas lógicas: operadores ternarios y operadores de asignación bit a bit.

Las siguientes tres banderas con nombres que comienzan con Will tendrán como objetivo agregar ciertas capacidades al servidor. Estos le dicen al servidor que "queremos" que el servidor pueda

  1. almacenar mensajes Will y asociarlos con la sesión de nuestro cliente (hablaremos más sobre esto más adelante);
  2. proporcionar un cierto nivel de QoS, normalmente por encima de QoS 0, que es el valor por defecto si no establecemos nada;
  3. guardar los mensajes y publicarlos como "guardados" (ver más abajo) si el mensaje Will está configurado como verdadero


Will Flag (bit_2)

El tercer bit se utilizará para configurar la bandera Will. Si se establece en true, nuestro cliente debería proporcionar un mensaje Will para publicar "en los casos en que la conexión de red no se cierre normalmente". El mensaje Will sería algo así como las “últimas palabras” del cliente ante los suscriptores.

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

Se llamará de la misma forma que la función anterior.

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


Bit 7 6 5 4 3 2 1 0

User Name Flag (bandera de nombre de usuario)
Password Flag (bandera de contraseña)
Will Retain (almacenamiento Will)
Will  QoS 2
Will QoS 1

Will Flag (bandera Will)

Clean Start (inicio limpio)


Reserved (reservado)


X X X X X 1 X 0

Tabla 02. Bandera de bits (bit_2) cuando el valor es igual a true - MQTT v5.0

Will QoS (bit_3, bit_4) 

A diferencia de los dos indicadores anteriores, esta característica requerirá que se establezcan dos bits si el cliente solicita el nivel QoS 2, el cuarto y quinto bits. QoS significa (Quality of Service) Calidad de Servicio y puede ser uno de tres.

Fig. 03. Definiciones de OASIS QoS

Fig. 03. Definiciones de OASIS QoS

Del sistema de entrega menos fiable al más fiable:

QoS 0

QoS 0 se establece principalmente en el momento de la entrega. Una especie de “entrega y olvida”. El remitente hace un intento. Es posible que el mensaje se pierda. No existe confirmación del servidor. Este será el valor predeterminado, es decir, si los bits 3 y 4 no se establecen en nada, el nivel de QoS solicitado por el cliente será QoS 0.

QoS 1 

QoS 1 se establece al menos en el momento de la entrega. Hay un PUBACK que confirma la entrega.

Misma plantilla de definición de función.

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

Mismo patrón de llamada de función.

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

Bit 7 6 5 4 3 2 1 0

User Name Flag (bandera de nombre de usuario)
Password Flag (bandera de contraseña)
Will Retain (almacenamiento Will)
Will  QoS 2
Will QoS 1

Will Flag (bandera Will)

Clean Start (inicio limpio)


Reserved (reservado)


X X X X 1 X X 0

Tabla 03. Bandera de bits Will QoS 1 (bit_3) con valor true – MQTT v5.0

QoS 2 

QoS 2 se establece exactamente en el momento de la entrega. Requiere la ausencia de pérdidas y duplicación. El remitente confirmará el mensaje con PUBREC y la entrega con PUBREL.

Se puede considerar este nivel como el envío de un paquete certificado. El sistema postal nos entregará un recibo cuando pongamos el paquete en sus manos, reconociendo que ahora son responsables de llevarlo a la dirección correcta. Y cuando esto suceda, cuando entreguen el paquete, nos enviarán un recibo firmado por el destinatario confirmando que el paquete ha sido entregado.

Aquí será lo mismo.

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

Y allí mismo.

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

User Name Flag (bandera de nombre de usuario)
Password Flag (bandera de contraseña)
Will Retain (almacenamiento Will)
Will  QoS 2
Will QoS 1

Will Flag (bandera Will)

Clean Start (inicio limpio)


Reserved (reservado)


X X X 1 X X X 0

Tabla 04. Bandera de bits Will QoS 2 (bit_4) con valor true – MQTT v5.0

El servidor nos informará sobre el nivel máximo de QoS aceptado en los códigos de motivo y las propiedades CONNACK. El cliente puede solicitarlo, pero se requieren las capacidades del servidor. Si obtenemos un CONNACK con QoS máximo, deberemos respetar este límite del servidor y no enviar PUBLISH con QoS superior. De lo contrario, el servidor se desconectará (DISCONNECT).

QoS 2 es el nivel de QoS más alto disponible en MQTT v5.0 y presenta algunos desafíos porque el protocolo de entrega es simétrico, lo cual significa que cualquiera de las partes (servidor o cliente) podrá ser el remitente o el cliente y el destinatario.

NOTA: Podemos decir que QoS es el núcleo del protocolo desde el punto de vista del usuario; Define el perfil de la aplicación y afecta a decenas de otros aspectos del protocolo. Entonces, profundizaremos en la capa QoS y su configuración en el contexto de la implementación del paquete PUBLISH.

Merece la pena señalar que QoS 1 y QoS 2 son opcionales para las implementaciones de los clientes. Como dice OASIS en un comentario informal:

"El cliente no necesita admitir paquetes PUBLISH QoS 1 o QoS 2. En este caso, el Cliente simplemente limitará el campo QoS máximo en cualquier comando SUBSCRIBE que enviará a un valor que pueda admitir".


Will RETAIN (bit_5)

En el sexto byte configuramos el indicador Will Retain. Esta bandera se relaciona con la bandera Will anterior. 

  • Si la casilla Will no está marcada, Will Retain también debería estar desactivada.
  • Si el indicador Will está configurado y la opción Will Retain no lo está, el servidor publicará el mensaje Will como mensaje no guardado.
  • Si se configuran ambas opciones, el servidor publicará el mensaje Will como un mensaje guardado.

El código en este y las dos próximas banderas restantes se ha omitido para mayor brevedad porque la definición de la función y los patrones de llamada de la misma son exactamente los mismos que en las banderas anteriores. Encontrará información detallada en los archivos adjuntos. Esos también se pueden probar.

Bit 7 6 5 4 3 2 1 0

User Name Flag (bandera de nombre de usuario)
Password Flag (bandera de contraseña)
Will Retain (almacenamiento Will)
Will  QoS 2
Will QoS 1

Will Flag (bandera Will)

Clean Start (inicio limpio)


Reserved (reservado)


X X 1 X X X X 0

Tabla 05. Bandera de bits Will Retain (bit_5) con valor true – MQTT v5.0

Deberemos esperar a que el paquete CONNACK marque este indicador antes de comenzar a enviar paquetes PUBLISH. Si un servidor obtiene un paquete PUBLISH con el parámetro Will Retain establecido en 1 y no admite mensajes guardados, el servidor SE APAGARÁ. ¿Es posible comenzar la publicación antes de recibir el paquete CONNACK? Sí que podemos. La norma permite este comportamiento, pero también hace esta observación:

"Los clientes que envíen paquetes de control MQTT antes de recibir CONNACK no sabrán de las restricciones del servidor"

Por lo tanto, deberemos verificar este indicador en los paquetes CONNACK antes de enviar cualquier paquete PUBLISH con el parámetro Will Retain establecido en 1 (uno). 


Password flag (bit_6)

En el séptimo bit le diremos al servidor si enviaremos la contraseña en la carga útil (Payload) o no. Si establecemos este indicador, deberá haber un campo de contraseña en la carga útil. Si no está configurado, el campo de contraseña no debería estar presente en la carga útil.

"Esta versión del protocolo nos permite enviar una contraseña sin un nombre de usuario, cosa que MQTT v3.1.1 no hace. Esto refleja el uso común de la contraseña para credenciales sin contraseña”. (Estándar OASIS, 3.1.2.9)

Bit 7 6 5 4 3 2 1 0

User Name Flag (bandera de nombre de usuario)
Password Flag (bandera de contraseña)
Will Retain (almacenamiento Will)
Will  QoS 2
Will QoS 1

Will Flag (bandera Will)

Clean Start (inicio limpio)


Reserved (reservado)


X 1 X X X X X 0

Tabla 06. Bandera de bits Password Flag (bit_6) con valor true – MQTT v5.0


User Name flag (bit_7)

Y finalmente, en el nivel de ocho bits, le diremos al servidor si enviaremos el nombre de usuario en la carga útil o no. Al igual que con el indicador de contraseña antes descrito, si este indicador está configurado, el campo de nombre de usuario deberá estar presente en la carga útil. De lo contrario, el campo de nombre de usuario no debería estar presente en la carga útil.

Bit 7 6 5 4 3 2 1 0

User Name Flag (bandera de nombre de usuario)
Password Flag (bandera de contraseña)
Will Retain (almacenamiento Will)
Will  QoS 2
Will QoS 1

Will Flag (bandera Will)

Clean Start (inicio limpio)


Reserved (reservado)


1 X X X X X X 0

Tabla 07. Bandera de bits User Name (bit_7) es true - MQTT v5.0

Entonces, la bandera de bits con la siguiente secuencia de bits...

Bit 7 6 5 4 3 2 1 0

User Name Flag (bandera de nombre de usuario)
Password Flag (bandera de contraseña)
Will Retain (almacenamiento Will)
Will  QoS 2
Will QoS 1

Will Flag (bandera Will)

Clean Start (inicio limpio)


Reserved (reservado)


X X 1 1 X 1 1 0

Table 08. Se establecen las banderas de bits Clean Start, Will Flag, Will QoS2 y Will Retain – MQTT v5.0

... se puede traducir así: abra la conexión para una nueva sesión con QoS nivel 2 y prepárese para guardar mi mensaje Will y publicarlo como guardado. Y por cierto, Sr. Servidor, no necesitaré autenticarme con el nombre de usuario y la contraseña.

El servidor responderá amablemente si puede cumplir con nuestra solicitud. Es posible que pueda completarla totalmente, parcialmente o en absoluto. El servidor enviará su respuesta en forma de códigos de motivo de conexión en paquetes CONNACK.


(CONNACK) Recuperación de códigos de motivo asociados con las banderas de conexión

Existen cuarenta y cuatro códigos de motivo en MQTT v5.0. Los hemos recopilado en el encabezado Defines.mqh. CONNACK (y otros tipos de paquetes) tienen un código de motivo único que forma parte del encabezado variable. Estos son los llamados códigos de motivo de conexión.

Valor Hex Nombre del código de motivo Descripción
0 0x00 Success (con éxito) Conexión aceptada.
128 0x80 Unspecified error (error no especificado) El servidor no desea revelar el motivo del error, o ninguno de los otros códigos de motivo se aplica.
129 0x81 Malformed Packet (paquete mal formado) Los datos del paquete CONNECT no se han podido analizar correctamente. 
130 0x82 Protocol Error (error de protocolo) Los datos del paquete CONNECT no cumplen las especificaciones.
131 0x83 Implementation specific error (error específico de implementación) CONNECT es válido pero no ha sido aceptado por el servidor.
132 0x84 Unsupported Protocol Version (versión de protocolo no compatible) El Servidor no admite la versión del protocolo MQTT solicitada por el Cliente.
133 0x85 Client Identifier not valid (identificador de cliente no válido) El ID del cliente es una línea válida pero no admitida por el servidor.
134 0x86 Bad User Name or Password (nombre de usuario o contraseña incorrectos) El Servidor no admite el nombre de usuario o la contraseña especificados por el Cliente.
135 0x87 Not authorized (no autorizado) El cliente no está autorizado a conectarse.
136 0x88 Server unavailable (servidor no disponible) El servidor MQTT no está disponible.
137 0x89 Server busy (servidor ocupado) El servidor está ocupado. Inténtelo más tarde.
138 0x8A Banned (prohibido) El cliente ha sido bloqueado por el administrador. Póngase en contacto con el administrador del servidor.
140 0x8C Bad authentication method (método de autenticación incorrecto) El método de autenticación no es compatible o no coincide con el método de autenticación actualmente utilizado.
144 0x90 Topic Name invalid (nombre de tema no válido) El nombre del tema Will no tiene un formato incorrecto, pero el servidor no lo admite.
149 0x95 Packet too large (paquete demasiado grande) El paquete CONNECT excede el tamaño máximo permitido.
151 0x97 Quota exceeded (cuota excedida) Se ha superado el límite establecido durante la implementación o bien por el administrador.
153 0x99 Payload format invalid (el formato de carga útil no es válido) La carga útil Will no coincide con la bandera de formato de carga útil especificada (Payload Format Indicator).
154 0x9A Retain not supported (guardado no soportado) El servidor no admite mensajes guardados y Will Retain está configurado en 1.
155 0x9B QoS not supported (QoS no compatible) El servidor no admite QoS configurado en Will QoS.
156 0x9C Use un servidor diferente El cliente debería usar temporalmente otro servidor.
157 0x9D Server moved (servidor trasladado) El cliente deberá utilizar un servidor diferente.
159 0x9F Connection rate exceeded (Velocidad de conexión superada) Se ha superado el límite de velocidad de conexión.

Tabla 08. Significados del código de motivo de conexión

El estándar establece claramente que el servidor deberá enviar códigos de motivo de conexión a través de CONNACK:

"El servidor que envía el paquete CONNACK DEBERÁ utilizar uno de los valores del código de motivo de conexión [MQTT-3.2.2-8]".

En esta etapa, los códigos de motivo de conexión resultan de particular interés para nosotros. Necesitaremos verificarlos antes de continuar la comunicación. Nos informarán sobre algunas de las capacidades y limitaciones del servidor, como el nivel de QoS disponible y la disponibilidad de mensajes guardados. Además, como podemos ver en sus nombres y descripciones en la tabla anterior, nos dirán si nuestro intento de CONEXIÓN ha tenido éxito o no.

Para obtener los códigos de motivo, primero deberemos determinar el tipo de paquete, ya que solo nos interesarán los paquetes CONNACK.

Usaremos el hecho de que necesitamos una función muy simple para obtener el tipo de paquete para describir cómo usaremos el desarrollo basado en pruebas, hablaremos un poco sobre la técnica y daremos un par de ejemplos breves. Toda la información detallada se podrá obtener en los archivos adjuntos.


(CONNACK) Determinar el tipo de paquete del servidor

Sabemos con exactitud que el primer byte de cualquier paquete de control MQTT codifica el tipo de paquete. Entonces leeremos ese primer byte lo más rápidamente posible y tendremos el tipo de paquete del servidor.

uchar pkt_type = server_response_buffer[0];

El código es claro, las variables se han nombrado correctamente. No se esperan problemas.

¡Pero espere un segundo! ¿Cómo debería llamar a este operador el código que usará nuestra biblioteca? ¿La llamada a la función pública retornará el tipo de paquete? ¿O el miembro privado podría ocultar esta información como un detalle de implementación? Si se retorna una llamada a una función, ¿dónde se deberá colocar la función? ¿En la clase CPktConnect? ¿O debería colocarse en cualquiera de nuestros archivos de encabezado ya que será usado por muchas clases diferentes? Si se guarda en un miembro privado, ¿en qué clase debería estar?

Existe un acrónimo muy conocido: TMTOWTDI* (there is more than one way to do it - hay varias formas de hacerlo). TDD es otro acrónimo que se ha vuelto muy popular por diversos motivos. Ambas siglas se han promocionaron activamente e incluso se pusieron de moda:

__ "I’m tddying, mom! It’s cool" ("Mamá, estoy tddeando! ¡Es genial!")

Esto se logró tras años de arduo trabajo sobre la misma pregunta básica: ¿cómo escribir código más eficaz, idiomático y fiable y al mismo tiempo hacer que los desarrolladores sean más productivos? ¿Cómo conseguir que los desarrolladores se concentren en lo que se debe hacer, en lugar de deambular por lo que se puede hacer? ¿Cómo lograr que cada uno de ellos se concentre en una tarea (y solo en una) al mismo tiempo? ¿Cómo podemos estar seguro de que sus acciones no provocarán errores de regresión ni fallos del sistema? 

En resumen, estas siglas, las ideas que transmiten y los métodos que recomiendan son el resultado de años de trabajo de cientos de personas distintas con experiencia en el desarrollo de software. El TDD no es una teoría, sino una práctica. Podemos decir que el TDD es un método para resolver problemas reduciendo el alcance al dividir el problema en componentes. Deberemos identificar el único desafío que nos llevará un paso adelante. Solo un paso. Con frecuencia, incluso un paso pequeño.

Entonces, ¿cuál es nuestro problema ahora? Necesitaremos determinar si la respuesta del servidor es un paquete CONNACK. Es sencillo. Porque según las especificaciones, necesitaremos leer los códigos de respuesta de CONNACK para decidir qué hacer a continuación. Lo que quiero decir es que identificar el tipo de paquete que recibimos del servidor como respuesta será necesario para pasar del estado de conexión al estado de publicación.

¿Cómo podremos determinar si la respuesta del servidor es un paquete CONNACK? Fácilmente. Tiene un tipo específico codificado en el encabezado MQTT.mqh como enumeración (Enumeration), a saber, ENUM_PKT_TYPE.

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

Intentaremos comenzar con una función que, al transmitir una matriz de bytes de red procedente de un bróker MQTT, retornará el tipo de paquete.

La verdad es que suena bien. Vamos a escribir una prueba para esta función.

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

Intentaremos comprender la función de prueba, ya que usaremos este patrón en todas nuestras pruebas y no entraremos en detalles sobre todas ellas aquí por razones de brevedad.

En los comentarios describiremos cada paso del patrón: 

Arrange (organizar)

Primero inicializaremos el array con un solo elemento, que representará el valor de byte que esperamos que retorne nuestra función.

En segundo lugar, inicializaremos un búfer de caracteres vacío de tamaño uno para obtener el resultado de nuestra llamada a la función.

Luego inicializaremos nuestro dispositivo para pasarlo a la función bajo prueba. Indica el primer byte que deberá leerse del encabezado de respuesta fijo del servidor para determinar el tipo de paquete. En este caso tendrá el valor wrong_first_byte. Lo haremos explícito nombrando la variable de la forma correspondiente.

Acto (acción)

Crearemos un ejemplar de la clase bajo prueba (cut) y llamaremos a nuestra función.

Assert (afirmar)

Afirmaremos la desigualdad de los arrays esperados y resultantes tanto en contenido como en tamaño usando la función ArrayCompare de MQL5 (ver archivo de prueba adjunto).

Clean-up (limpiar)

Finalmente, limpiaremos los recursos eliminando el ejemplar "cortado" y transmitiendo nuestro búfer de "resultado" a ZeroMemory. Esto evitará pérdidas de memoria y contaminación de las pruebas.

Fig. 04. TEST_CSrvResponse - FAIL - identificador no declarado

Fig. 04. TEST_CSrvResponse - FAIL - identificador no declarado

La compilación falla debido a que la función aún no existe. Necesitaremos escribirla. ¿Pero dónde deberíamos colocarla?

Ya sabemos que necesitaremos determinar constantemente el tipo del paquete de respuesta. Cada vez que enviamos un paquete a nuestro bróker, este nos enviará una de estas "respuestas". Y dicha "respuesta" será una especie de paquete de control MQTT. Entonces, como se trata de un tipo de "respuesta", debería tener su propia clase en un grupo de "respuestas" similares. Digamos que tenemos una clase para representar todas las respuestas del servidor en un grupo de paquetes de control;

esta será la clase CSRvResponse que implementará la interfaz IControlPacket.

Podríamos sentirnos tentados de convertirla en otra función de nuestra clase CPktConnect ya existente, pero estaríamos violando un principio importante de la programación orientada a objetos: el Principio de Responsabilidad Única (Single Responsibility Principle, SRP).

“Deberá separar aquellas cosas que cambian por diferentes motivos, y agrupar aquellas cosas que cambian por los mismos motivos” (R. Martin, “Código Limpio”, 2011).

Por un lado, nuestra clase CPktConnect cambiará cada vez que cambiemos la forma en que construimos los paquetes CONNECT y, por otra parte, nuestra (inexistente) clase CSRvResponse cambiará cada vez que cambiemos la forma en que leemos nuestros CONNACK, PUBACK, SUBACK y otras respuestas del servidor. De esta forma, tendrán responsabilidades muy diferentes y en este caso resultan bastante fáciles de ver. Pero a veces puede ser difícil decidir si declaramos el objeto de dominio que estamos modelando en la clase adecuada. Al usar SRP, recibiremos orientación objetiva para la toma de decisiones.

Así que intentaremos realizar cambios mínimos para pasar la prueba.

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

La prueba se compilará, pero fallará (como se esperaba) porque hemos transmitido intencionalmente una respuesta del servidor "incorrecta".

Fig. 05. TEST_CSrvResponse - FAIL - paquete no válido

Fig. 05. TEST_CSrvResponse - FAIL - paquete no válido

Vamos a transmitir el tipo de paquete CONNACK "correcto" como respuesta del servidor. Tenga en cuenta que nuevamente asignaremos el nombre de forma explícita: right_first_byte. El nombre en sí será solo una etiqueta. Lo importante es que su significado quede claro para todo el que lea nuestro código, incluyéndonos a nosotros mismos seis meses o seis años después.

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

Fig. 06. TEST_CSrvResponse - PASS

Fig. 06. TEST_CSrvResponse - PASS

Bien, ahora se ha superado la prueba y sabemos que al menos para estos dos argumentos, uno incorrecto y otro correcto, o bien se ha fallado o bien se ha superado, respectivamente. Más adelante, si fuera necesario, podremos probarlo más a fondo.

Estos sencillos pasos establecerán las tres "leyes" básicas del TDD, como las resume R. Martin.

  1. No puede escribir ningún código que se considere finalizado hasta que escriba una prueba unitaria fallida.
  2. No se le permite escribir más pruebas unitarias de las suficientes para fallar, y una compilación fallida implica un fracaso.
  3. No se le permite escribir más código que se considere finalizado del que sea suficiente para superar la prueba unitaria actualmente fallida.

Bien, dejemos el tema del TDD por ahora. Regresemos a nuestra tarea y leamos los códigos de motivo de conexión en los paquetes CONNACK provenientes del servidor.


(Connect Reason Codes) ¿Qué hacer con las funciones no disponibles en el servidor?

En esta etapa, deberemos dirigir nuestra atención a dos códigos de motivo de conexión.

  1. QoS not supported (QoS no compatible)
  2. Retain not supported (guardado no soportado)

Son especiales porque no indican un error, sino una limitación del servidor. Si nuestro CONNACK tiene alguno de estos códigos de motivo de conexión, el servidor informará de que nuestra conexión de red ha tenido éxito, que nuestro CONNECT es un paquete bien formado y que el servidor está en línea y ejecutándose pero no puede cumplir con nuestras solicitudes. Necesitaremos tomar medidas y elegir qué hacer a continuación.

¿Qué deberemos hacer si enviamos CONNECT con QoS 2 y el servidor responde con QoS Maximum 1? ¿Deberíamos reenviar CONNECT con la bandera de QoS bajado? ¿O deberíamos desconectarnos (DISCONNECT) antes de bajar la versión? Si este fuera el caso de la función RETAIN, ¿podemos simplemente ignorarla como irrelevante y seguir adelante con la publicación de todos modos? ¿O deberíamos reenviar CONNECT con banderas reducidas antes de la publicación?

¿Qué debemos hacer después de recibir un mensaje CONNACK exitoso, indicando que el servidor ha aceptado nuestra conexión y tiene todas las capacidades solicitadas? ¿Deberíamos comenzar a enviar paquetes PUBLISH inmediatamente? ¿O podemos dejar la conexión abierta, enviando paquetes PINGREQ sucesivos hasta que estemos listos para publicar el mensaje? Por cierto, ¿necesitaremos suscribirnos (SUSCRIBE) a un tema antes de publicarlo?

La mayoría de estas preguntas son respondidas por el Estándar. Deberemos implementar AS-IS para tener un cliente compatible con MQTT v5.0. Los desarrolladores de aplicaciones tienen muchas opciones a elegir. Por ahora solo nos ocuparemos de lo necesario para conseguir el cliente adecuado lo más rápido posible.

Según el estándar, un cliente solo puede solicitar un nivel de QoS > 0 si el indicador Will también está configurado en 1, lo cual significa que solo podremos solicitar un nivel de QoS > 0 si también enviamos un mensaje Will en el paquete CONNECT. Pero no queremos, o mejor aún, no necesitamos lidiar con los mensajes Will en este momento. Entonces, nuestra solución supondrá un compromiso entre comprender lo que necesitamos saber ahora y tratar de comprender todas las complejidades del Estándar, escribiendo finalmente código que tal vez no necesitemos más adelante. 

Solo necesitaremos saber qué hará nuestro cliente si el nivel de QoS solicitado o almacenado no está disponible en el servidor. Y debemos saber esto tan pronto como llegue el nuevo CONNACK. Por ello, realizaremos la comprobación en el constructor CSRvResponse. Si la respuesta es CONNACK, el constructor llamará al método protegido GetConnectReasonCode.

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

Si el código de motivo de la conexión es MQTT_REASON_CODE_QOS_NOT_SUPPORTED o MQTT_REASON_CODE_RETAIN_NOT_SUPPORTED, esta información se guardará en el perfil del servidor. Por ahora solo guardaremos esta información sobre el servidor y esperaremos una desconexión (DISCONNECT). Más tarde lo utilizaremos al solicitar una nueva conexión en el mismo servidor. Tenga en cuenta que este "más tarde" puede ocurrir unos milisegundos después del primer intento de conexión, o tal vez en unas pocas semanas. El hecho es que esta información se guardará en el perfil del servidor.


¿Cómo probaremos los métodos protegidos?

Para probar los métodos protegidos, crearemos una clase en nuestro caso de prueba que derivará de nuestra clase bajo prueba, en este caso CSRvResponse. Luego llamaremos a los métodos protegidos de CSRvResponse a través de esta clase derivada, creada "con fines de prueba", a la que llamaremos TestProtectedMethods.

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

Tenga en cuenta que no almacenaremos nada en el perfil del servidor, este ni siquiera existe todavía. Simplemente imprimiremos un mensaje diciendo que el perfil del servidor se está actualizando. Esto se debe a que el perfil del servidor deberá persistir entre las sesiones de nuestros clientes y todavía no estamos haciendo ningún almacenamiento. Más adelante, al implementar el almacenamiento, podremos modificar esta función stub para conservar el perfil del servidor, por ejemplo, en una base de datos SQLite, sin siquiera tener que eliminar el mensaje impreso (o registrado). Simplemente no estará implementado ahora. Como hemos indicado antes, en esta etapa solo necesitaremos saber qué hacer si el servidor no cumple con las capacidades solicitadas: guardaremos la información para su reutilización.


Conclusión

En este artículo hemos descrito cómo comenzar con la parte operativa del protocolo MQTT v5.0 como lo requiere el estándar OASIS para poder iniciar el cliente correspondiente lo más rápido posible. Hemos descrito cómo implementar la clase CSRvResponse para determinar el tipo de respuesta del servidor y los códigos de motivo vinculados. También hemos descrito cómo respondería nuestro cliente a las capacidades del servidor no disponibles.

En el siguiente paso implementaremos PUBLISH, comprenderemos mejor el comportamiento operativo para los niveles de QoS y analizaremos las sesiones (Sessions) y el almacenamiento requerido.

** Otras siglas útiles: DRY (don’t repeat yourself - no te repitas), KISS (keep it simple, stupid - hazlo todo sencillo, atontado), YAGNI (you aren't gonna need it - no vas a necesitar esto). Cada uno de ellos posee cierta sabiduría práctica, pero YMMV (your mileage may vary - sobre gustos no hay nada escrito) :) 

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

Archivos adjuntos |
headers.zip (7.16 KB)
tests.zip (3 KB)
Algoritmos de optimización de la población: Algoritmo Mind Evolutionary Computation (Computación Evolutiva Mental, (MEC) Algoritmos de optimización de la población: Algoritmo Mind Evolutionary Computation (Computación Evolutiva Mental, (MEC)
En este artículo, analizaremos un algoritmo de la familia MEC llamado algoritmo MEC Simple de evolución mental (Simple MEC, SMEC). El algoritmo se caracteriza por la belleza de la idea expuesta y su sencillez de aplicación.
Teoría de categorías en MQL5 (Parte 21): Transformaciones naturales con ayuda de LDA Teoría de categorías en MQL5 (Parte 21): Transformaciones naturales con ayuda de LDA
Este artículo, el número 21 de nuestra serie, continuaremos analizando las transformaciones naturales y cómo se pueden implementar mediante el análisis discriminante lineal. Como en el artículo anterior, la implementación se presentará en formato de clase de señal.
Integración de modelos ML con el simulador de estrategias (Conclusión): Implementación de un modelo de regresión para la predicción de precios Integración de modelos ML con el simulador de estrategias (Conclusión): Implementación de un modelo de regresión para la predicción de precios
Este artículo describe la implementación de un modelo de regresión de árboles de decisión para predecir precios de activos financieros. Se realizaron etapas de preparación de datos, entrenamiento y evaluación del modelo, con ajustes y optimizaciones. Sin embargo, es importante destacar que el modelo es solo un estudio y no debe ser usado en operaciones reales.
Algoritmos de optimización de la población: Algoritmo de salto de rana aleatorio (Shuffled Frog-Leaping, SFL) Algoritmos de optimización de la población: Algoritmo de salto de rana aleatorio (Shuffled Frog-Leaping, SFL)
El artículo presenta una descripción detallada del algoritmo de salto de rana aleatorio (SFL) y sus capacidades para resolver problemas de optimización. El algoritmo SFL se inspira en el comportamiento de las ranas en su entorno natural y ofrece un enfoque innovador para la optimización de características. El algoritmo SFL supone una herramienta eficaz y flexible que puede gestionar una gran variedad de tipos de datos y alcanzar soluciones óptimas.