Desarrollando un cliente MQTT para MetaTrader 5: metodología de TDD (Parte 3)
"¿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.
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
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) |
| |
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
- almacenar mensajes Will y asociarlos con la sesión de nuestro cliente (hablaremos más sobre esto más adelante);
- proporcionar un cierto nivel de QoS, normalmente por encima de QoS 0, que es el valor por defecto si no establecemos nada;
- 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) |
| |
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
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) |
| |
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) |
| |
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) |
| |
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) |
| |
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) |
| |
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) |
| |
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
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
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
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.
- No puede escribir ningún código que se considere finalizado hasta que escriba una prueba unitaria fallida.
- No se le permite escribir más pruebas unitarias de las suficientes para fallar, y una compilación fallida implica un fracaso.
- 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.
- QoS not supported (QoS no compatible)
- 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
- Aplicaciones de trading gratuitas
- 8 000+ señales para copiar
- Noticias económicas para analizar los mercados financieros
Usted acepta la política del sitio web y las condiciones de uso