English Русский 中文 Español 日本語 Português
preview
Entwicklung eines MQTT-Clients für MetaTrader 5: ein TDD-Ansatz - Teil 2

Entwicklung eines MQTT-Clients für MetaTrader 5: ein TDD-Ansatz - Teil 2

MetaTrader 5Integration | 9 Februar 2024, 13:37
176 0
Jocimar Lopes
Jocimar Lopes

Einführung

„Vorzeitige Optimierung ist die Wurzel allen Übels“ (Donald Knuth).

Im vorigen Artikel haben wir MQTT vorgestellt, ein hocheffizientes, binäres Pub/Sub-Messaging-Protokoll. Wir haben darüber gesprochen, was MQTT ist, warum seine Entwicklung vor fünfundzwanzig Jahren begann und wofür es heute in verschiedenen Branchen eingesetzt wird, von der Automobilindustrie bis zum Internet der Dinge, von der Luftfahrt bis zu einfachen Chat-Anwendungen. Wir haben gesehen, dass MQTT in jedem Kontext nützlich sein kann, in dem ein inhaltsunabhängiges Protokoll für den Nachrichtenaustausch erforderlich ist, einschließlich des Kontexts von Handelsanwendungen. Wir haben die Vorteile der Aufnahme eines nativen MQL5-Clients für MQTT in unsere Codebasis dargelegt und eine minimale Entwicklungsumgebung mit dem Open-Source-MQTT-Broker Mosquitto auf WSL (Windows Subsystem For Linux) eingerichtet.

Wir begannen die Entwicklung unseres nativen Clients nur mit einer fest kodierten Fixed Header Generator-Funktion und erreichten den Punkt, an dem wir in der Lage waren, eine Verbindung mit einem lokalen Mosquitto-Broker herzustellen, aber die Verbindung wurde vom Server aufgrund eines Protokollfehlers sofort zurückgesetzt.

Ein Zitat vom Ende des vorherigen Artikels:

„Also, ja, unser unveränderlicher Header CONNECT wurde von Mosquito erkannt, aber der <unknown> Client wurde sofort „due to protocol error“ (wegen Protokollfehler) getrennt. Der Fehler trat auf, weil wir den variablen Header mit dem Protokollnamen, der Protokollebene und anderen zugehörigen Metadaten noch nicht eingefügt haben. Wir werden dies im nächsten Schritt beheben. Wir werden dies im nächsten Schritt beheben.“

Abb. 01 - Metaeditor Experts Log Connect Response Fail

Abb. 01 - Protokoll der Registerkarte Metaeditor-Experten mit einer Verbindungsantwort: Fail


Da wir einen Testgetriebene Entwicklung (TDD) verwenden, beginnen wir mit diesem Schritt, indem wir einen Test für einen CONNECT-Paketersteller schreiben, der die zugehörigen Metadaten enthält und nicht „aufgrund eines Protokollfehlers“ zurückgewiesen wird.

Diesen Test zu schreiben, noch bevor wir den zu testenden Code schreiben, mag für viele von uns kontraintuitiv sein. Aber es scheint ganz natürlich, ja sogar der erwartete Schritt, der vor jedem Unternehmen getan werden sollte, wenn wir die Tests als objektive Beschreibung der Projektanforderungen betrachten, wenn wir sie als die objektivste Definition eines Ziels sehen, die ein Entwickler haben kann.

„Die Einheitstests sind Dokumente. Sie beschreiben die niedrigste Entwurfsebene des Systems. Sie sind eindeutig, präzise, in einer Sprache verfasst, die das Publikum versteht, und sie sind so formell, dass sie funktionieren. Sie sind die beste Art von Low-Level-Dokumentation, die es geben kann. Welcher Fachmann würde eine solche Dokumentation nicht vorlegen?“ (Robert Martin, The Clean Coder, 2011)


Code Organisation: OOP und Header-Dateien

Wie bereits erwähnt, begannen wir mit dem Aufbau unseres Verbindungspakets Fixed Header, indem wir ein Byte-Array mit den „richtigen“ Werten fest kodierten. Dann haben wir versucht, eine Verbindung zu unserem Client herzustellen, indem wir das festcodierte Byte-Array an unseren lokalen Broker gesendet haben. Unser Versuch, eine Verbindung herzustellen, scheiterte kläglich „aufgrund eines Protokollfehlers“. Aber in der Zwischenzeit lernten wir etwas über unsere Entwicklungsumgebung, über unser Mosquito-Protokoll, schrieben unsere ersten Tests, und vor allem fingen wir an, etwas zu tun, das funktionierte, zumindest in kleinen Schritten.

Wie Sie sehen können, war dieser Misserfolg beabsichtigt. Wir wissen aber, dass diese Art der Entwicklung einer komplexen Anwendung auf Dauer nicht tragfähig ist.

Die Erstellung konformer MQTT-Pakete ist nur der erste Schritt bei der Entwicklung eines robusten und wartbaren Clients. Der einfachste Teil, wie man so schön sagt. Wenn es um die Spezifikation des Betriebsverhaltens geht, werden alle Komplexitäten des Protokolls deutlich. Diese Komplexität wird uns als Entwickler mehr abverlangen. Neben dem Versenden guter Pakete müssen wir uns mit einer großen Anzahl unterschiedlicher Serverantworten und unterschiedlichen Anwendungszuständen auseinandersetzen. An diesem Punkt reichen hartkodierte Byte-Arrays – oder überhaupt alles Hartkodierte – nicht mehr aus.

Glücklicherweise ist MQL5 eine objektorientierte Programmiersprache und wir arbeiten nicht in der speicher- und rechenintensiven Umgebung, für die MQTT ursprünglich entwickelt wurde. So können wir alle Vorteile des objektorientierten Paradigmas (OOP) nutzen, um zu haben:

  • Einfache Argumentation über das Protokoll durch Wahl der richtigen Abstraktionsebene.
  • Einfaches Lesen des Codes (denken Sie daran, dass der Code viel öfter gelesen als geschrieben wird).
  • Einfache Code-Wartung.
  • Und einfache Prüfung.

Die Referenzdokumentation von MQL5 bietet umfangreiche Unterstützung für die objektorientierte Programmierung für MQL5, mit einem ganzen Abschnitt, der diesem Thema gewidmet ist. 


Protokoll-Definitionen

Das Protokoll für den Nachrichtenaustausch besteht aus einer Reihe von Regeln, die eine gemeinsame Grundlage für die Verständigung zwischen zwei oder mehr Einheiten schaffen. In unserem Fall zwischen zwei oder mehr Geräten. Bei vielen dieser Regeln geht es darum, was zu tun ist, indem berücksichtigt wird, was zuvor getan wurde. Sie sind zustandsorientiert. Um die nächste Aktion zu wählen, muss unser Code den aktuellen Zustand der Anwendung auswerten. Im Sprachgebrauch des MQTT-Protokolls sind dies die Regeln für das Betriebsverhalten.

Neben den zustandsbezogenen Regeln – oder besser gesagt, vor ihnen – gibt es die Definitionen von Begriffen, Werten und Berechnungen, die unabhängig vom Anwendungszustand sind. Dabei handelt es sich in der Regel um Konstanten, Enumerationen und Auswertungsalgorithmen, wie im Fall des MQTT-Protokollnamens, der Steuerpakettypen und des Bytewerts für die Restlänge des festen Headers.

Wir werden diese beiden unterschiedlichen Regelwerke in zwei unterschiedlichen Header-Dateien zusammenfassen. Die erste ist nur für die Definitionen von Begriffen und Werten, die in unseren Dateien verwendet werden. Ohne Überraschung werden wir sie Defines.mqh nennen. Bei diesen Begriffen und Werten handelt es sich in der Regel um Konstanten, und diese Datei sollte sich kaum verändern.

Die andere Header-Datei wird der Host für einige gemeinsam genutzte Enums, Strukturen und Funktionen sein. Sie erhält den Namen MQTT.mqh. Diese Enums, Strukturen und Funktionen werden sich stark weiterentwickeln, und zwar nicht nur während der Entwicklung der ersten Version. Diese Datei wird immer dann geändert, wenn wir Verbesserungen, Optimierungen und Fehlerbehebungen vornehmen. Wahrscheinlich wird diese Datei in andere, spezifischere Dateien unterteilt werden.

Die Verwendung von Header-Dateien für die Codeorganisation ist nicht an die objektorientierte Programmierung gebunden. Tatsächlich finden wir bereits in dem mittlerweile klassischen Buch „The C Programming Language“ von Brian Kernighan und Dennis Ritchie einen nützlichen Hinweis darauf. 

„(...) die Definitionen und Deklarationen, die von den Dateien gemeinsam genutzt werden. Wir wollen dies so weit wie möglich zentralisieren, sodass es nur eine Kopie gibt, die wir bei der Weiterentwicklung des Programms erhalten und pflegen müssen. (...) Bis zu einer moderaten Programmgröße ist es wahrscheinlich am besten, eine Header-Datei zu haben, die alles enthält, was zwischen zwei Teilen des Programms ausgetauscht werden soll (...). Für ein viel größeres Programm wären mehr Organisation und mehr Überschriften erforderlich.“

Aber gerade in der objektorientierten Programmierung zeigt sich die Praxis der Organisation des Codes in kleinen Kompiliereinheiten besonders deutlich. Da wir eine Bibliothek erstellen, wird außerdem fast der gesamte Code in Header-Dateien enthalten sein.

Der Header Defines

Zu diesem Zeitpunkt werden die Definitionen für Protokollname und Protokollebene nur für CONNECT-Pakete verwendet. Wenn wir wollen, können wir sie also in die spezielle Klasse CPktConnect aufnehmen (siehe unten). Wir belassen sie jedoch aus Gründen der Konsistenz in dem Header Defines. Obwohl sie an dieser Stelle nur für CONNECT-Pakete verwendet werden, können sie später in anderen Dateien verwendet werden.

Die Kommentare zum Protokoll sind wörtliche Zitate aus der offiziellen Standardspezifikation.

//+------------------------------------------------------------------+
//|                                                      Defines.mqh |
//|            ********* WORK IN PROGRESS **********                 |
//| **** PART OF ARTICLE https://www.mql5.com/en/articles/13334 **** |
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//|              PROTOCOL NAME AND VERSION                           |
//+------------------------------------------------------------------+
#define MQTT_PROTOCOL_NAME_LENGTH_MSB           0x00
#define MQTT_PROTOCOL_NAME_LENGTH_LSB           0x04
#define MQTT_PROTOCOL_NAME_BYTE_3               'M'
#define MQTT_PROTOCOL_NAME_BYTE_4               'Q'
#define MQTT_PROTOCOL_NAME_BYTE_5               'T'
#define MQTT_PROTOCOL_NAME_BYTE_6               'T'
#define MQTT_PROTOCOL_VERSION                   0x05
//+------------------------------------------------------------------+
//|              PROPERTIES                                          |
//+------------------------------------------------------------------+
/*
The last field in the Variable Header of the CONNECT, CONNACK, PUBLISH, PUBACK, PUBREC,
PUBREL, PUBCOMP, SUBSCRIBE, SUBACK, UNSUBSCRIBE, UNSUBACK, DISCONNECT, and
AUTH packet is a set of Properties. In the CONNECT packet there is also an optional set of Properties in
the Will Properties field with the Payload
*/
#define MQTT_PROPERTY_PAYLOAD_FORMAT_INDICATOR          0x01 // (1) Byte                  
#define MQTT_PROPERTY_MESSAGE_EXPIRY_INTERVAL           0x02 // (2) Four Byte Integer     
#define MQTT_PROPERTY_CONTENT_TYPE                      0x03 // (3) UTF-8 Encoded String  
#define MQTT_PROPERTY_RESPONSE_TOPIC                    0x08 // (8) UTF-8 Encoded String  
#define MQTT_PROPERTY_CORRELATION_DATA                  0x09 // (9) Binary Data           
#define MQTT_PROPERTY_SUBSCRIPTION_IDENTIFIER           0x0B // (11) Variable Byte Integer
#define MQTT_PROPERTY_SESSION_EXPIRY_INTERVAL           0x11 // (17) Four Byte Integer    
#define MQTT_PROPERTY_ASSIGNED_CLIENT_IDENTIFIER        0x12 // (18) UTF-8 Encoded String  
#define MQTT_PROPERTY_SERVER_KEEP_ALIVE                 0x13 // (19) Two Byte Integer      
#define MQTT_PROPERTY_AUTHENTICATION_METHOD             0x15 // (21) UTF-8 Encoded String 
#define MQTT_PROPERTY_AUTHENTICATION_DATA               0x16 // (22) Binary Data          
#define MQTT_PROPERTY_REQUEST_PROBLEM_INFORMATION       0x17 // (23) Byte                  
#define MQTT_PROPERTY_WILL_DELAY_INTERVAL               0x18 // (24) Four Byte Integer    
#define MQTT_PROPERTY_REQUEST_RESPONSE_INFORMATION      0x19 // (25) Byte                  
#define MQTT_PROPERTY_RESPONSE_INFORMATION              0x1A // (26) UTF-8 Encoded String  
#define MQTT_PROPERTY_SERVER_REFERENCE                  0x1C // (28) UTF-8 Encoded String 
#define MQTT_PROPERTY_REASON_STRING                     0x1F // (31) UTF-8 Encoded String
#define MQTT_PROPERTY_RECEIVE_MAXIMUM                   0x21 // (33) Two Byte Integer     
#define MQTT_PROPERTY_TOPIC_ALIAS_MAXIMUM               0x22 // (34) Two Byte Integer     
#define MQTT_PROPERTY_TOPIC_ALIAS                       0x23 // (35) Two Byte Integer     
#define MQTT_PROPERTY_MAXIMUM_QOS                       0x24 // (36) Byte                 
#define MQTT_PROPERTY_RETAIN_AVAILABLE                  0x25 // (37) Byte                 
#define MQTT_PROPERTY_USER_PROPERTY                     0x26 // (38) UTF-8 String Pair   
#define MQTT_PROPERTY_MAXIMUM_PACKET_SIZE               0x27 // (39) Four Byte Integer    
#define MQTT_PROPERTY_WILDCARD_SUBSCRIPTION_AVAILABLE   0x28 // (40) Byte                  
#define MQTT_PROPERTY_SUBSCRIPTION_IDENTIFIER_AVAILABLE 0x29 // (41) Byte                  
#define MQTT_PROPERTY_SHARED_SUBSCRIPTION_AVAILABLE     0x2A // (42) Byte 
//+------------------------------------------------------------------+
//|              REASON CODES                                        |
//+------------------------------------------------------------------+
/*
A Reason Code is a one byte unsigned value that indicates the result of an operation. Reason Codes less
than 0x80 indicate successful completion of an operation. The normal Reason Code for success is 0.
Reason Code values of 0x80 or greater indicate failure.

The CONNACK, PUBACK, PUBREC, PUBREL, PUBCOMP, DISCONNECT and AUTH Control Packets
have a single Reason Code as part of the Variable Header. The SUBACK and UNSUBACK packets
contain a list of one or more Reason Codes in the Payload.
*/
#define MQTT_REASON_CODE_SUCCESS                                0x00 // (0)
#define MQTT_REASON_CODE_NORMAL_DISCONNECTION                   0x00 // (0)
#define MQTT_REASON_CODE_GRANTED_QOS_0                          0x00 // (0)
#define MQTT_REASON_CODE_GRANTED_QOS_1                          0x01 // (1)
#define MQTT_REASON_CODE_GRANTED_QOS_2                          0x02 // (2)
#define MQTT_REASON_CODE_DISCONNECT_WITH_WILL_MESSAGE           0x04 // (4)
#define MQTT_REASON_CODE_NO_MATCHING_SUBSCRIBERS                0x10 // (16)
#define MQTT_REASON_CODE_NO_SUBSCRIPTION_EXISTED                0x11 // (17)
#define MQTT_REASON_CODE_CONTINUE_AUTHENTICATION                0x18 // (24)
#define MQTT_REASON_CODE_RE_AUTHENTICATE                        0x19 // (25)
#define MQTT_REASON_CODE_UNSPECIFIED_ERROR                      0x80 // (128)
#define MQTT_REASON_CODE_MALFORMED_PACKET                       0x81 // (129)
#define MQTT_REASON_CODE_PROTOCOL_ERROR                         0x82 // (130)
#define MQTT_REASON_CODE_IMPLEMENTATION_SPECIFIC_ERROR          0x83 // (131)
#define MQTT_REASON_CODE_UNSUPPORTED_PROTOCOL_VERSION           0x84 // (132)
#define MQTT_REASON_CODE_CLIENT_IDENTIFIER_NOT_VALID            0x85 // (133)
#define MQTT_REASON_CODE_BAD_USER_NAME_OR_PASSWORD              0x86 // (134)
#define MQTT_REASON_CODE_NOT_AUTHORIZED                         0x87 // (135)
#define MQTT_REASON_CODE_SERVER_UNAVAILABLE                     0x88 // (136)
#define MQTT_REASON_CODE_SERVER_BUSY                            0x89 // (137)
#define MQTT_REASON_CODE_BANNED                                 0x8A // (138)
#define MQTT_REASON_CODE_SERVER_SHUTTING_DOWN                   0x8B // (139)
#define MQTT_REASON_CODE_BAD_AUTHENTICATION_METHOD              0x8C // (140)
#define MQTT_REASON_CODE_KEEP_ALIVE_TIMEOUT                     0x8D // (141)
#define MQTT_REASON_CODE_SESSION_TAKEN_OVER                     0x8E // (142)
#define MQTT_REASON_CODE_TOPIC_FILTER_INVALID                   0x8F // (143)
#define MQTT_REASON_CODE_TOPIC_NAME_INVALID                     0x90 // (144)
#define MQTT_REASON_CODE_PACKET_IDENTIFIER_IN_USE               0x91 // (145)
#define MQTT_REASON_CODE_PACKET_IDENTIFIER_NOT_FOUND            0x92 // (146)
#define MQTT_REASON_CODE_RECEIVE_MAXIMUM_EXCEEDED               0x93 // (147)
#define MQTT_REASON_CODE_TOPIC_ALIAS_INVALID                    0x94 // (148)
#define MQTT_REASON_CODE_PACKET_TOO_LARGE                       0x95 // (149)
#define MQTT_REASON_CODE_MESSAGE_RATE_TOO_HIGH                  0x96 // (150)
#define MQTT_REASON_CODE_QUOTA_EXCEEDED                         0x97 // (151)
#define MQTT_REASON_CODE_ADMINISTRATIVE_ACTION                  0x98 // (152)
#define MQTT_REASON_CODE_PAYLOAD_FORMAT_INVALID                 0x99 // (153)
#define MQTT_REASON_CODE_RETAIN_NOT_SUPPORTED                   0x9A // (154)
#define MQTT_REASON_CODE_QOS_NOT_SUPPORTED                      0x9B // (155)
#define MQTT_REASON_CODE_USE_ANOTHER_SERVER                     0x9C // (156)
#define MQTT_REASON_CODE_SERVER_MOVED                           0x9D // (157)
#define MQTT_REASON_CODE_SHARED_SUBSCRIPTIONS_NOT_SUPPORTED     0x9E // (158)
#define MQTT_REASON_CODE_CONNECTION_RATE_EXCEEDED               0x9F // (159)
#define MQTT_REASON_CODE_MAXIMUM_CONNECT_TIME                   0xA0 // (160)
#define MQTT_REASON_CODE_SUBSCRIPTION_IDENTIFIERS_NOT_SUPPORTED 0xA1 // (161)
#define MQTT_REASON_CODE_WILDCARD_SUBSCRIPTIONS_NOT_SUPPORTED   0xA2 // (162)

Beachten Sie, dass wir allen protokollspezifischen Definitionen das Kürzel MQTT voranstellen. Damit unterscheiden sie sich von unseren eigenen Definitionen, die wir noch aufnehmen werden. Beachten Sie auch oben in der Datei Defines.mqh, in PROTOCOL NAME UND VERSION, dass wir versuchen, unsere Bezeichner so eindeutig wie möglich zu benennen. Damit soll den Grundsätzen des so genannten Clean Code entsprochen werden. Diese Praxis sollte dazu beitragen, unseren Code leserfreundlicher, einfacher zu debuggen und IDE-freundlich zu machen, d.h. besser durchsuchbar und gut geeignet, die Autovervollständigungsfunktion moderner IDEs zu nutzen.


Der MQTT-Header

//+------------------------------------------------------------------+
//|                                                         MQTT.mqh |
//|            ********* WORK IN PROGRESS **********                 |
//| **** PART OF ARTICLE https://www.mql5.com/en/articles/13334 **** |
//+------------------------------------------------------------------+
#include "Defines.mqh"
//+------------------------------------------------------------------+
//|              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
   PUBACK      =  0x04, // Publish acknowledgment (QoS 1)
   PUBREC      =  0x05, // Publish received (QoS 2 delivery part 1)
   PUBREL      =  0x06, // Publish release (QoS 2 delivery part 2)
   PUBCOMP     =  0x07, // Publish complete (QoS 2 delivery part 3)
   SUBSCRIBE   =  0x08, // Subscribe request
   SUBACK      =  0x09, // Subscribe acknowledgment
   UNSUBSCRIBE =  0x0A, // Unsubscribe request
   UNSUBACK    =  0x0B, // Unsubscribe acknowledgment
   PINGREQ     =  0x0C, // PING request
   PINGRESP    =  0x0D, // PING response
   DISCONNECT  =  0x0E, // Disconnect notification
   AUTH        =  0x0F, // Authentication exchange
  };
//+------------------------------------------------------------------+
//|             CONNECT - VARIABLE HEADER - CONNECT FLAGS            |
//+------------------------------------------------------------------+
/*
The Connect Flags byte contains several parameters specifying the behavior of the MQTT connection. It
also indicates the presence or absence of fields in the Payload.
*/
enum ENUM_CONNECT_FLAGS
  {
   RESERVED       = 0x00,
   CLEAN_START    = 0x02,
   WILL_FLAG      = 0x04,
   WILL_QOS_1     = 0x08,
   WILL_QOS_2     = 0x10,
   WILL_RETAIN    = 0x20,
   PASSWORD_FLAG  = 0x40,
   USER_NAME_FLAG = 0x80
  };
//+------------------------------------------------------------------+
//|             CONNECT - VARIABLE HEADER - QoS LEVELS               |
//+------------------------------------------------------------------+
/*
Position: bits 4 and 3 of the Connect Flags.
These two bits specify the QoS level to be used when publishing the Will Message.
If the Will Flag is set to 0, then the Will QoS MUST be set to 0 (0x00) [MQTT-3.1.2-11].
If the Will Flag is set to 1, the value of Will QoS can be 0 (0x00), 1 (0x01), or 2 (0x02) [MQTT-3.1.2-12].
*/
enum ENUM_QOS_LEVEL
  {
   AT_MOST_ONCE   = 0x00,
   AT_LEAST_ONCE  = 0x01,
   EXACTLY_ONCE   = 0x02
  };
//+------------------------------------------------------------------+
//|                   SetProtocolVersion                             |
//+------------------------------------------------------------------+
void SetProtocolVersion(uchar& dest_buf[])
  {
   dest_buf[8] = MQTT_PROTOCOL_VERSION;
  }
//+------------------------------------------------------------------+
//|                     SetProtocolName                              |
//+------------------------------------------------------------------+
void SetProtocolName(uchar& dest_buf[])
  {
   dest_buf[2] = MQTT_PROTOCOL_NAME_LENGTH_MSB;
   dest_buf[3] = MQTT_PROTOCOL_NAME_LENGTH_LSB;
   dest_buf[4] = MQTT_PROTOCOL_NAME_BYTE_3;
   dest_buf[5] = MQTT_PROTOCOL_NAME_BYTE_4;
   dest_buf[6] = MQTT_PROTOCOL_NAME_BYTE_5;
   dest_buf[7] = MQTT_PROTOCOL_NAME_BYTE_6;
  }
//+------------------------------------------------------------------+
//|                     SetFixedHeader                               |
//+------------------------------------------------------------------+
void SetFixedHeader(ENUM_PKT_TYPE pkt_type, uchar& buf[], uchar& dest_buf[])
  {
   dest_buf[0] = (uchar)pkt_type << 4;
   dest_buf[1] = GetRemainingLength(buf);
  }
//+------------------------------------------------------------------+
//|                    GetRemainingLength                            |
//+------------------------------------------------------------------+
/*
Position: starts at byte 2.
The Remaining Length is a Variable Byte Integer that represents the number of bytes remaining within the
current Control Packet, including data in the Variable Header and the Payload. The Remaining Length
does not include the bytes used to encode the Remaining Length. The packet size is the total number of
bytes in an MQTT Control Packet, this is equal to the length of the Fixed Header plus the Remaining
Length.
*/
uchar GetRemainingLength(uchar &buf[])
  {
   uint x;
   x = ArraySize(buf);
   uint rem_len;
   do
     {
      rem_len = x % 128;
      x = (x / 128);
      if(x > 0)
        {
         rem_len = rem_len | 128;
        }
     }
   while(x > 0);
   return (uchar)rem_len;
  };

//+------------------------------------------------------------------+


Klassen und Strukturen

Die Schnittstelle von MQTT Control Packet

Hier müssen wir uns entscheiden, ob wir die Objekthierarchie der Kontrollpakete mit einer abstrakten Klasse oder mit einer Schnittstelle beginnen wollen. Wir könnten mit einer generischen Basisklasse beginnen, die für jedes Kontrollpaket geeignet ist. Diese abstrakte Klasse würde in spezifischeren abgeleiteten Control Packet-Klassen spezialisiert werden. Oder wir könnten mit einer einfachen Schnittstelle beginnen, die von diesen Control Packet-Klassen implementiert wird.

Wir beginnen mit einer Schnittstelle IcontrolPacket. Diese Schnittstelle wird über eine einfache Methode verfügen. Diese Wahl kann sich bei der Implementierung des Teils Operational Behavior des Protokolls ändern. Wir werden diese Schnittstelle wahrscheinlich in eine abstrakte Klasse mit einigen virtuellen Funktionen umwandeln.

//+------------------------------------------------------------------+
//|                                               IControlPacket.mqh |
//|            ********* WORK IN PROGRESS **********                 |
//| **** PART OF ARTICLE https://www.mql5.com/en/articles/13334 **** |
//+------------------------------------------------------------------+
#include "MQTT.mqh"
//+------------------------------------------------------------------+
//|       Interface IControlPacket                                   |
//|       The root of object hierarchy                               |
//+------------------------------------------------------------------+
interface IControlPacket
  {

   bool              IsControlPacket();

  };
//+------------------------------------------------------------------+

Wie bereits erwähnt, besteht der einzige Zweck dieser Schnittstelle darin, als Wurzel der Objekthierarchie der MQTT-Pakete zu fungieren. Zu diesem Zeitpunkt ist es nicht mehr als ein schicker Platzhalter.

Die Klasse für MQTT Control Packet Connect

Das CONNECT-Kontrollpaket ist das am schwierigsten zu schreibende Paket. Abgesehen von der Tatsache, dass wir uns mit dem Protokoll erst noch vertraut machen müssen, hat dieses spezielle Paket in der Version 5.0 die wichtigsten Verbesserungen erhalten, nämlich die Verbindungseigenschaften und die Nutzereigenschaften.

//+------------------------------------------------------------------+
//|                                                   PktConnect.mqh |
//|            ********* WORK IN PROGRESS **********                 |
//| **** PART OF ARTICLE https://www.mql5.com/en/articles/13334 **** |
//+------------------------------------------------------------------+
#include "MQTT.mqh"
#include "Defines.mqh"
#include "IControlPacket.mqh"
//+------------------------------------------------------------------+
//|        CONNECT VARIABLE HEADER                                   |
//+------------------------------------------------------------------+
/*
The Variable Header for the CONNECT Packet contains the following fields in this order:
Protocol Name,Protocol Level, Connect Flags, Keep Alive, and Properties.
*/
struct MqttClientIdentifierLength
  {
   uchar             msb;
   uchar             lsb;
  } clientIdLen;
//---
struct MqttKeepAlive
  {
   uchar             msb;
   uchar             lsb;
  } keepAlive;
//---
struct MqttConnectProperties
  {
   uint              prop_len;
   uchar             session_expiry_interval_id;
   uint              session_expiry_interval;
   uchar             receive_maximum_id;
   ushort            receive_maximum;
   uchar             maximum_packet_size_id;
   ushort            maximum_packet_size;
   uchar             topic_alias_maximum_id;
   ushort            topic_alias_maximum;
   uchar             request_response_information_id;
   uchar             request_response_information;
   uchar             request_problem_information_id;
   uchar             request_problem_information;
   uchar             user_property_id;
   string            user_property_key;
   string            user_property_value;
   uchar             authentication_method_id;
   string            authentication_method;
   uchar             authentication_data_id;
  } connectProps;
//---
struct MqttConnectPayload
  {
   uchar             client_id_len;
   string            client_id;
   ushort            will_properties_len;
   uchar             will_delay_interval_id;
   uint              will_delay_interval;
   uchar             payload_format_indicator_id;
   uchar             payload_format_indicator;
   uchar             message_expiry_interval_id;
   uint              message_expiry_interval;
   uchar             content_type_id;
   string            content_type;
   uchar             response_topic_id; // for request/response
   string            response_topic;
   uchar             correlation_data_id; // for request/response
   ulong             correlation_data[]; // binary data
   uchar             user_property_id;
   string            user_property_key;
   string            user_property_value;
   uchar             will_topic_len;
   string            will_topic;
   uchar             will_payload_len;
   ulong             will_payload[]; // binary data
   uchar             user_name_len;
   string            user_name;
   uchar             password_len;
   ulong             password; // binary data
  } connectPayload;
//+------------------------------------------------------------------+
//| Class CPktConnect.                                               |
//| Purpose: Class of MQTT Connect Control Packets.                  |
//|          Implements IControlPacket                               |
//+------------------------------------------------------------------+
class CPktConnect : public IControlPacket
  {
private:
   bool              IsControlPacket() {return true;}
protected:
   void              InitConnectFlags() {ByteArray[9] = 0;}
   void              InitKeepAlive() {ByteArray[10] = 0; ByteArray[11] = 0;}
   void              InitPropertiesLength() {ByteArray[12] = 0;}
   uchar             m_connect_flags;

public:
                     CPktConnect();
                     CPktConnect(uchar &buf[]);
                    ~CPktConnect();
   //--- methods for setting Connect Flags
   void              SetCleanStart(const bool cleanStart);
   void              SetWillFlag(const bool willFlag);
   void              SetWillQoS_1(const bool willQoS_1);
   void              SetWillQoS_2(const bool willQoS_2);
   void              SetWillRetain(const bool willRetain);
   void              SetPasswordFlag(const bool passwordFlag);
   void              SetUserNameFlag(const bool userNameFlag);
   void              SetKeepAlive(ushort seconds);
   void              SetClientIdentifierLength(string clientId);
   void              SetClientIdentifier(string clientId);

   //--- member for getting the byte array
   uchar             ByteArray[];
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CPktConnect::CPktConnect(uchar &buf[])
  {
   ArrayFree(ByteArray);
   ArrayResize(ByteArray, buf.Size() + 2, UCHAR_MAX);
   SetFixedHeader(CONNECT, buf, ByteArray);
   SetProtocolName(ByteArray);
   SetProtocolVersion(ByteArray);
   InitConnectFlags();
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void CPktConnect::SetClientIdentifier(string clientId)
  {
   SetClientIdentifierLength(clientId);
   StringToCharArray(clientId, ByteArray,
                     ByteArray.Size() - StringLen(clientId), StringLen(clientId));
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void CPktConnect::SetClientIdentifierLength(string clientId)
  {
   clientIdLen.msb = (char)StringLen(clientId) >> 8;
   clientIdLen.lsb = (char)StringLen(clientId) % 256;
   ByteArray[12] = clientIdLen.msb;
   ByteArray[13] = clientIdLen.lsb;
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void CPktConnect::SetKeepAlive(ushort seconds) // MQTT max is 65,535 sec
  {
   keepAlive.msb = (uchar)(seconds >> 8) & 255;
   keepAlive.lsb = (uchar)seconds & 255;
   ByteArray[10] = keepAlive.msb;
   ByteArray[11] = keepAlive.lsb;
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void CPktConnect::SetPasswordFlag(const bool passwordFlag)
  {
   passwordFlag ? m_connect_flags |= PASSWORD_FLAG : m_connect_flags &= ~PASSWORD_FLAG;
   ArrayFill(ByteArray, sizeof(ByteArray), 1, m_connect_flags);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void CPktConnect::SetUserNameFlag(const bool userNameFlag)
  {
   userNameFlag ? m_connect_flags |= USER_NAME_FLAG : m_connect_flags &= (uchar) ~USER_NAME_FLAG;
   ArrayFill(ByteArray, sizeof(ByteArray), 1, m_connect_flags);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void CPktConnect::SetWillRetain(const bool willRetain)
  {
   willRetain ? m_connect_flags |= WILL_RETAIN : m_connect_flags &= ~WILL_RETAIN;
   ArrayFill(ByteArray, sizeof(ByteArray), 1, m_connect_flags);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void CPktConnect::SetWillQoS_2(const bool willQoS_2)
  {
   willQoS_2 ? m_connect_flags |= WILL_QOS_2 : m_connect_flags &= ~WILL_QOS_2;
   ArrayFill(ByteArray, sizeof(ByteArray), 1, m_connect_flags);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void CPktConnect::SetWillQoS_1(const bool willQoS_1)
  {
   willQoS_1 ? m_connect_flags |= WILL_QOS_1 : m_connect_flags &= ~WILL_QOS_1;
   ArrayFill(ByteArray, sizeof(ByteArray), 1, m_connect_flags);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void CPktConnect::SetWillFlag(const bool willFlag)
  {
   willFlag ? m_connect_flags |= WILL_FLAG : m_connect_flags &= ~WILL_FLAG;
   ArrayFill(ByteArray, sizeof(ByteArray), 1, m_connect_flags);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void CPktConnect::SetCleanStart(const bool cleanStart)
  {
   cleanStart ? m_connect_flags |= CLEAN_START : m_connect_flags &= ~CLEAN_START;
   ArrayFill(ByteArray, 9, 1, m_connect_flags);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CPktConnect::CPktConnect()
  {
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CPktConnect::~CPktConnect()
  {
  }
//+------------------------------------------------------------------+


Testen unserer ersten Klasse

Der einzige Zweck der Klasse CPktConnect ist die Erstellung eines wohlgeformten MQTT CONNECT-Pakets. Um es zu testen, müssen wir also zunächst eine Art „Vorrichtung“ erstellen, ein Beispiel-Byte-Array, das ein wohlgeformtes CONNECT-Paket darstellt. Aber wie können wir sicher sein, dass unser erfundenes Byte-Array ein wohlgeformtes CONNECT-Paket darstellt? Letztendlich ist dies der Test, mit dem wir unsere Klasse von Grund auf neu beginnen werden. Viele, wenn nicht alle unsere Bemühungen können umsonst gewesen sein, wenn unser Vorrichtung ein schlechtes Paket darstellt.

Der Entwickler und Betreuer des Protokolls, OASIS, kam uns zu Hilfe. In Abschnitt 3.1.2.12 finden Sie ein nicht-normatives Beispiel für den variablen Header eines CONNECT-Pakets. Da wir unseren Generator für feste Überschriften bereits getestet haben (siehe vorheriger Artikel), reicht dieses OASIS-Beispiel für den Anfang aus. Damit können wir sicherstellen, dass unsere Klasse ein wohlgeformtes Paket mit verschiedenen Konfigurationen wie dem booleschen CleanSession und der angeforderten Keep-Alive-Zeitspanne erzeugt.

Dieses hartkodierte, manuell erzeugte Byte-Array wird dann mit dem von CpktConnect erzeugten Paket verglichen.

//+------------------------------------------------------------------+
//|                                  TEST_CControlPacket_Connect.mq5 |
//|                                                                  |
//|            ********* WORK IN PROGRESS **********                 |
//| **** PART OF ARTICLE https://www.mql5.com/en/articles/13334 **** |
//+------------------------------------------------------------------+
#include <MQTT\CPktConnect.mqh>

//+------------------------------------------------------------------+
//| Tests for CControlPacketConnect class                            |
//+------------------------------------------------------------------+
void OnStart()
  {
   Print(TEST_SetCleanStart_KeepAlive_ClientIdentifier());
   Print(TEST_SetClientIdentifier());
   Print(TEST_SetClientIdentifierLength());
   Print(TEST_SetCleanStart_and_SetKeepAlive());
   Print(TEST_SetKeepAlive());
   Print(TEST_SetCleanStart());
  }
/* REFERENCE ARRAY (FIXTURE)
{16, 24, 0, 4, 77, 81, 84, 84, 5, 2, 0, 10, 0, 4, 7, 17, 0, 0, 0, 10, 25, 1, 77, 81, 76, 53}
*/
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool TEST_SetCleanStart_KeepAlive_ClientIdentifier()
  {
   Print(__FUNCTION__);
//--- Arrange
   static uchar expected[] =
     {16, 16, 0, 4, 77, 81, 84, 84, 5, 2, 0, 10, 0, 4, 77, 81, 76, 53};
   uchar buf[expected.Size() - 2];
   CPktConnect *cut = new CPktConnect(buf);
//--- Act
   cut.SetCleanStart(true);
   cut.SetKeepAlive(10);//10 sec
   cut.SetClientIdentifier("MQL5");
   uchar result[];
   ArrayCopy(result, cut.ByteArray);
//--- Assert
   bool isTrue = Assert(expected, result);
//--- cleanup
   delete cut;
   ZeroMemory(result);
   return  isTrue ? true : false;
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool TEST_SetClientIdentifier()
  {
   Print(__FUNCTION__);
//--- Arrange
   static uchar expected[] =
     {16, 16, 0, 4, 77, 81, 84, 84, 5, 0, 0, 0, 0, 4, 77, 81, 76, 53};
   uchar buf[expected.Size() - 2];
   CPktConnect *cut = new CPktConnect(buf);
//--- Act
   cut.SetClientIdentifier("MQL5");
   uchar result[];
   ArrayCopy(result, cut.ByteArray);
//--- Assert
   bool isTrue = Assert(expected, result);
//--- cleanup
   delete cut;
   ZeroMemory(result);
   return  isTrue ? true : false;
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool TEST_SetClientIdentifierLength()
  {
   Print(__FUNCTION__);
//--- Arrange
   static uchar expected[] =
     {16, 12, 0, 4, 77, 81, 84, 84, 5, 0, 0, 0, 0, 4};
   uchar buf[expected.Size() - 2];
   CPktConnect *cut = new CPktConnect(buf);
//--- Act
   cut.SetClientIdentifierLength("MQL5");
   uchar result[];
   ArrayCopy(result, cut.ByteArray);
//--- Assert
   bool isTrue = Assert(expected, result);
//--- cleanup
   delete cut;
   ZeroMemory(result);
   return  isTrue ? true : false;
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool TEST_SetCleanStart_and_SetKeepAlive()
  {
   Print(__FUNCTION__);
//--- Arrange
   static uchar expected[] =
     {16, 10, 0, 4, 77, 81, 84, 84, 5, 2, 0, 10};
   uchar buf[expected.Size() - 2];
   CPktConnect *cut = new CPktConnect(buf);
//--- Act
   cut.SetCleanStart(true);
   cut.SetKeepAlive(10); //10 secs
   uchar result[];
   ArrayCopy(result, cut.ByteArray);
//--- Assert
   bool isTrue = Assert(expected, result);
//--- cleanup
   delete cut;
   ZeroMemory(result);
   return  isTrue ? true : false;
  }
//+------------------------------------------------------------------+
bool TEST_SetKeepAlive()
  {
   Print(__FUNCTION__);
//--- Arrange
   static uchar expected[] =
     {16, 10, 0, 4, 77, 81, 84, 84, 5, 0, 0, 10};
   uchar buf[expected.Size() - 2];
   CPktConnect *cut = new CPktConnect(buf);
//--- Act
   cut.SetKeepAlive(10); //10 secs
   uchar result[];
   ArrayCopy(result, cut.ByteArray);
//--- Assert
   bool isTrue = Assert(expected, result);
//--- cleanup
   delete cut;
   ZeroMemory(result);
   return  isTrue ? true : false;
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool TEST_SetCleanStart()
  {
   Print(__FUNCTION__);
//--- Arrange
   static uchar expected[] =
     {16, 8, 0, 4, 77, 81, 84, 84, 5, 2};
   uchar buf[expected.Size() - 2];
   CPktConnect *cut = new CPktConnect(buf);
//--- Act
   cut.SetCleanStart(true);
   uchar result[];
   ArrayCopy(result, cut.ByteArray);
//--- Assert
   bool isTrue = Assert(expected, result);
//--- cleanup
   delete cut;
//ZeroMemory(result);
   return  isTrue ? true : false;
  }
//+------------------------------------------------------------------+
bool Assert(uchar& expected[], uchar& result[])
  {
   if(!ArrayCompare(expected, result) == 0)
     {
      for(uint i = 0; i < expected.Size(); i++)
        {
         printf("expected\t%d\t\t%d result", expected[i], result[i]);
        }
      printf("expected size %d <=> %d result size", expected.Size(), result.Size());
      Print("Expected");
      ArrayPrint(expected);
      Print("Result");
      ArrayPrint(result);
      return false;
     }
   return true;
  }
//+------------------------------------------------------------------+

HINWEIS: Wie Sie wissen, kann das Schreiben von Tests für Dinge wie „Mal sehen, ob das Header-Array die Daten hat, die ich gerade hineingetan habe“ als Zeitverschwendung erscheinen. Aber das ist es nicht. Dieser Satz von „offensichtlichen“ Tests wird unseren Code für immer begleiten. Man kann sie als ein kontinuierliches und automatisiertes Debugging-Tool betrachten, das seinen Wert unter Beweis stellt, wenn man einen Regressionsfehler oder sogar einen dummen Fehler wie falsches Kopieren/Einfügen findet. Aus diesem Grund testen wir nicht nur, ob die Aktion „Verbinden“ in einer Blackbox funktioniert. Wir wollen sicherstellen, dass unsere Kopfzeile wohlgeformt ist, bevor wir die Verbindungsaktion testen. Es ist zu bedenken, dass TDD ein Prozess ist. Viele, wenn nicht alle dieser Tests werden umgeschrieben oder sogar gelöscht, bevor wir eine erste funktionierende Version unseres Codes haben. Aber diejenigen, die bleiben, werden wahrscheinlich für immer bleiben.

Dieser Test ist nur dann erfolgreich, wenn das von CPktConnect' erzeugte Byte-Array beim ArrayCompare(d) mit unserem Referenz-Byte-Array, unserem Fixture, 0 (Null) ergibt.    ​

Nachdem wir einige Kombinationen der grundlegenden Verbindungseigenschaften getestet haben, wird das Paket an den Broker gesendet und darf nicht „wegen eines Protokollfehlers“ zurückgewiesen werden.

Abb. 02 - Metaeditor Experts Log TEST_CPktConnect Erfolg

Abb. 02 - Protokoll der Registerkarte Metaeditor-Experten mit Testergebnissen der Klasse CPktConnect


Überprüfung mit unserem lokalen MQTT-Broker

Jetzt können wir unseren lokalen Mosquitto-Broker auf der WSL ausführen, um zu prüfen, ob unsere MQTT-Verbindung erfolgreich war.

Wenn Sie die Standardinstallation befolgt haben, sollte Mosquito als Dienst unter Linux laufen. Sie müssen also nur die Ports umleiten (80 → 1883) und den Hostnamen in die zulässigen URLs in Ihren Metatrader 5-Optionen aufnehmen.

Abb. 03 - WSL Mosquitto Log Verbunden/Trennend Erfolg

Abb. 03 - Mosquitto-Protokoll auf der WSL mit Anzeige des Verbindungs-/Trennungsstatus: Erfolg.


 Ja! Unser Verbindungsversuch gibt keinen Protokollfehler zurück. Jetzt können wir versuchen, Nachrichten zwischen Client und Server auszutauschen.


Schlussfolgerung

Im nächsten Schritt werden wir uns mit Antworten von CONNACK beschäftigen. Mit diesem Schritt haben wir eine solide Grundlage für die Veröffentlichung unserer ersten Nachricht. Und natürlich werden wir anfangen, einen Test dafür zu schreiben! :)  Bleiben Sie dran!


Übersetzt aus dem Englischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/en/articles/13334

Beigefügte Dateien |
Defines.mqh (8.03 KB)
MQTT.mqh (5.01 KB)
CPktConnect.mqh (9.97 KB)
Der Handel von Paaren Der Handel von Paaren
In diesem Artikel werden wir uns mit dem Handel von Paaren befassen, d. h. mit den Grundsätzen und den Aussichten für seine praktische Anwendung. Wir werden auch versuchen, dafür eine Handelsstrategie zu entwickeln.
Integrieren Sie Ihr eigenes LLM in Ihren EA (Teil 1): Die bereitgestellte Hardware und Umgebung Integrieren Sie Ihr eigenes LLM in Ihren EA (Teil 1): Die bereitgestellte Hardware und Umgebung
Angesichts der rasanten Entwicklung der künstlichen Intelligenz sind Sprachmodelle (language models, LLMs) heute ein wichtiger Bestandteil der künstlichen Intelligenz, sodass wir darüber nachdenken sollten, wie wir leistungsstarke LLMs in unseren algorithmischen Handel integrieren können. Für die meisten Menschen ist es schwierig, diese leistungsstarken Modelle auf ihre Bedürfnisse abzustimmen, sie lokal einzusetzen und sie dann auf den algorithmischen Handel anzuwenden. In dieser Artikelserie werden wir Schritt für Schritt vorgehen, um dieses Ziel zu erreichen.
Die visuelle Programmiersprache DRAKON - Kommunikationswerkzeug für MQL-Entwickler und Kunden Die visuelle Programmiersprache DRAKON - Kommunikationswerkzeug für MQL-Entwickler und Kunden
DRAKON ist eine visuelle Programmiersprache, die entwickelt wurde, um die Interaktion zwischen Fachleuten aus verschiedenen Bereichen (Biologen, Physiker, Ingenieure...) und Programmierern in russischen Raumfahrtprojekten (z.B. im Projekt für das wiederverwendbare Raumschiff Buran) zu vereinfachen. In diesem Artikel werde ich darüber sprechen, wie DRAKON die Erstellung von Algorithmen zugänglich und intuitiv macht, selbst wenn Sie noch nie mit Code in Berührung gekommen sind, und wie es für Kunden einfacher ist, ihre Gedanken zu erklären, wenn sie Handelsroboter bestellen, und für Programmierer, weniger Fehler bei komplexen Funktionen zu machen.
Neuronale Netze leicht gemacht (Teil 57): Stochastic Marginal Actor-Critic (SMAC) Neuronale Netze leicht gemacht (Teil 57): Stochastic Marginal Actor-Critic (SMAC)
Hier werde ich den relativ neuen Algorithmus Stochastic Marginal Actor-Critic (SMAC) vorstellen, der es ermöglicht, Strategien mit latenten Variablen im Rahmen der Entropiemaximierung zu entwickeln.