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

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

MetaTrader 5Integration | 23 August 2023, 10:47
335 0
Jocimar Lopes
Jocimar Lopes

Einführung

... zweifellos ist das die Strategie: Bring es erst zum Laufen, dann mache es richtig, und am Ende schnell.Stephen C. Johnson und Brian W. Kernighan's „The C Language and Models for Systems Programming“ in der Zeitschrift Byte (August 1983)

Der Austausch von Echtzeitdaten zwischen zwei oder mehreren Instanzen von MetaTrader ist ein häufiges Bedürfnis von Händlern und Kontoverwaltern. Der wohl am meisten geforderte Datenaustausch bezieht sich auf Handelstransaktionen durch die so genannten „Handelskopierer“. Aber man kann leicht Anfragen zur gemeinsamen Nutzung von Kontoinformationen, zum Screening von Symbolen und zu statistischen Daten für maschinelles Lernen finden, um nur einige zu nennen. Diese Funktionalität kann durch die Verwendung von Netzwerk-Sockets, Inter-Prozess-Kommunikation mit Named Pipes, Web-Diensten, lokaler Dateifreigabe und möglicherweise anderen Lösungen, die bereits getestet und/oder entwickelt wurden, erreicht werden.

Wie in der Softwareentwicklung üblich, hat jede dieser Lösungen ihre Vor- und Nachteile in Bezug auf Nutzerfreundlichkeit, Stabilität, Vertrauenswürdigkeit und die für Entwicklung und Wartung erforderlichen Ressourcen. Kurz gesagt, jedes dieser Systeme weist ein anderes Kosten-Nutzen-Verhältnis auf, das von den Anforderungen der Nutzer und ihrem Budget abhängt.

Dieser Artikel berichtet über die ersten Schritte bei der Implementierung des MQTT-Protokolls auf der Client-Seite, das zufällig eine Technologie ist, die entwickelt wurde, um genau diesen Bedarf zu decken - Echtzeit-Datenaustausch zwischen Maschinen - mit hoher Leistung, geringem Bandbreitenverbrauch, geringem Ressourcenbedarf und niedrigen Kosten.


Was ist MQTT?

„MQTT ist ein Client-Server-Publish/Subscribe-Messaging-Transportprotokoll. Es ist leichtgewichtig, offen, einfach und so konzipiert, dass sie leicht zu implementieren ist. Diese Eigenschaften machen es ideal für den Einsatz in vielen Situationen, einschließlich eingeschränkter Umgebungen, wie z.B. für die Kommunikation in Machine-to-Machine (M2M) und Internet of Things (IoT) Kontexten, in denen ein kleiner Code-Footprint erforderlich ist und/oder die Netzwerk-Bandbreite sehr knapp ist.“

Die obige Definition stammt von OASIS, dem Eigentümer und Entwickler des Protokolls, das seit 2013 ein offener Standard ist.

„Im Jahr 2013 reichte IBM MQTT v3.1 beim OASIS-Spezifikationsgremium ein, mit einer Charta, die sicherstellte, dass nur geringfügige Änderungen an der Spezifikation akzeptiert werden konnten. Nachdem die OASIS die Wartung des Standards von IBM übernommen hatte, wurde am 29. Oktober 2014 die Version 3.1.1 veröffentlicht. Ein umfangreicheres Upgrade auf MQTT Version 5, das mehrere neue Funktionen hinzufügt, wurde am 7. März 2019 veröffentlicht.“ (Wikipedia)

IBM begann 1999 mit der Entwicklung des Protokolls, um den Bedarf der Industrie zu decken, Ölpipelines mit Sensoren zu überwachen und die Daten über Satellit an entfernte Kontrollzentren zu senden. Nach Angaben von Arlen Nipper, der das Protokoll gemeinsam mit Dr. Andy Stanford-Clark, war es das Ziel, einen Echtzeit-Datenstrom für diese Kontrollzentren bereitzustellen.

„Und wir haben versucht, die damalige IBM-Middleware MQ Integrator und das, was ich damals in der Kommunikationsbranche über 1.200-Baud-Wählleitungen, 300-Baud-Wählleitungen und VSAT mit sehr eingeschränkter Bandbreite gemacht habe, miteinander zu verbinden.“

Trotz der Tatsache, dass es aufgrund der Beschränkungen des Tech-Stacks und der teuren Netzwerkkosten robust, schnell und kostengünstig sein sollte, musste es eine hochwertige Datenbereitstellung mit kontinuierlicher Sitzungserkennung bieten, die es ihm ermöglicht, mit unzuverlässigen oder sogar unterbrochenen Internetverbindungen zurechtzukommen.

Als binäres Protokoll ist MQTT sehr effizient in Bezug auf den Speicher- und Verarbeitungsbedarf. Es ist sogar kurios, dass das kleinste MQTT-Paket nur zwei Bytes umfasst!

Da MQTT auf einem Publish/Subscribe-Modell (Pub/Sub) und nicht auf einem Request/Response-Modell basiert, ist es bidirektional. Das heißt, sobald eine Client/Server-Verbindung hergestellt ist, können die Daten jederzeit vom Client zum Server und vom Server zum Client fließen, ohne dass eine vorherige Anfrage erforderlich ist, wie es bei HTTP WebRequest der Fall ist. Sobald die Daten eintreffen, leitet der Server sie sofort an die Empfänger weiter. Diese Funktion ist ein Eckpfeiler des Echtzeit-Datenaustauschs, da sie eine minimale Latenzzeit zwischen den Endpunkten ermöglicht. Einige Anbieter werben mit Latenzzeiten in der Größenordnung von Millisekunden.

Der Typ, das Format, der Codec oder irgendetwas anderes an den Daten spielt keine Rolle. MQTT ist datenunabhängig. Der Nutzer kann Rohbytes, Textformaten (XML, JSON-Objekte), Protokollpuffern, Bildern, Videofragmenten usw. senden/empfangen.

Die meisten Interaktionen zwischen dem Client und dem Server können asynchron erfolgen, d. h. MQTT ist skalierbar. In der IoT-Branche spricht man nicht selten von Tausenden oder gar Millionen von Geräten, die miteinander verbunden sind und in Echtzeit Daten austauschen. 

Die Nachrichten können zwischen den Endpunkten verschlüsselt werden und werden es in der Regel auch , da das Protokoll TLS-kompatibel ist und über integrierte Authentifizierungs- und Autorisierungsmechanismen verfügt.

Es überrascht nicht, dass MQTT nicht nur eine Reihe von hoch standardisierten Spezifikationen ist, sondern auch eine weit verbreitete Technologie in verschiedenen Branchen ist:

„MQTT wird heute in einer Vielzahl von Branchen eingesetzt, z. B. in der Automobilindustrie, der Fertigung, der Telekommunikation, der Öl- und Gasindustrie usw.“ (mqtt.org)


Hauptkomponenten

Das Pub/Sub-Modell ist ein sehr bekanntes Modell für den Nachrichtenaustausch. Ein Client stellt eine Verbindung zum Server her und veröffentlicht eine Nachricht zu einem Thema. Daraufhin erhalten alle Clients, die dieses Thema abonniert haben , die Nachricht(en). Dies ist der grundlegende Mechanismus des Modells. 

Der Server fungiert als Makler, der zwischen den Kunden steht und sowohl Abonnements als auch Veröffentlichungen entgegennimmt. TCP/IP ist das zugrundeliegende Transportprotokoll und Clients sind alle Geräte, die TCP/IP und MQTT verstehen. Bei der Nachricht handelt es sich in der Regel um eine JSON- oder XML-Daten, aber sie kann alles sein, auch eine rohe Bytefolge.

Das Thema ist eine UTF-8 kodierte Zeichenkette, die zur Beschreibung einer Namensraum-ähnlichen hierarchischen Struktur verwendet wird:

  • office/machine01/account123456

  • office/machine02/account789012

  • home/machine01/account345678

Wir können auch eine Raute (#) als Platzhalterzeichen verwenden, um ein Thema zu abonnieren. Zum Beispiel, um alle Konten von machine01 von home (zuhause) aus zu abonnieren:
  • home/machine01/# 
Oder um alle Geräte vom office (Büro) zu abonnieren:
  • office/# 

OK, MQTT wurde also für die Konversation von Maschine zu Maschine entwickelt, wird im IoT-Kontext ausgiebig genutzt und ist robust, schnell und preiswert. Aber Sie fragen sich vielleicht: Welchen Nutzen oder welche Verbesserung kann dieses Ding in einer Handelsumgebung bringen? Welche Einsatzmöglichkeiten gibt es für MQTT im MetaTrader?

Wie bereits erwähnt, sind „Handelskopierer“ der offensichtlichste Anwendungsfall für MQTT in einer Handelsumgebung. Aber man kann darüber nachdenken, maschinelle Lernpipelines mit Echtzeitdaten zu füttern, das Verhalten eines EA entsprechend den von einem Webservice bezogenen Echtzeitdaten zu ändern oder Ihre MetaTrader-Anwendung von einem beliebigen Gerät aus fernzusteuern.

Für jedes Szenario, in dem ein Echtzeit-Datenstrom zwischen Maschinen benötigt wird, können wir MQTT in Betracht ziehen.


Verwendung von MQTT im MetaTrader

Es gibt freie und quelloffene MQTT-Bibliotheken für die gängigsten Universalsprachen einschließlich der jeweiligen Varianten für mobile und eingebettete Geräte. Um MQTT von MQL5 aus zu nutzen, können wir also eine entsprechende DLL in C, C++ oder C# generieren und importieren. 

Wenn sich die auszutauschenden Daten auf Handelstransaktionen/Kontoinformationen beschränken und eine relativ große Latenzzeit akzeptabel ist, wäre eine weitere Option die Verwendung der Python MQTT-Client-Bibliothek und des MQL5-Python-Moduls als „bridge“. 

Aber wie wir wissen, hat die Verwendung von DLLs einige negative Auswirkungen auf das MQL5-Ökosystem, wobei die bemerkenswerteste ist, dass der Marktplatz keine DLL-abhängigen EAs akzeptiert. Außerdem dürfen DLL-abhängige EAs keine Backtest-Optimierungen auf der MQL5-Cloud durchführen. Um die DLL-Abhängigkeit und die Python-Bridge zu vermeiden, ist die ideale Lösung die Entwicklung einer nativen MQTT-Bibliothek auf der Client-Seite für MetaTrader.

Das ist es, was wir in den nächsten Wochen tun werden: das -Protokoll von MQTT v. 5.0 clientseitig für MetaTrader 5 implementieren.

Die Implementierung eines MQTT-Clients kann im Vergleich zu anderen Netzwerkprotokollen als „relativ einfach“ bezeichnet werden. Aber relativ einfach ist nicht unbedingt einfach. Wir beginnen also mit einem Bottom-up-Ansatz, testgetriebener Entwicklung (TDD), hoffentlich mit Tests und Feedback aus der Community.

Obwohl TDD als „Hype“ oder „Buzzword“ für fast alles verwendet werden kann (und oft auch wird), passt es sehr gut, wenn wir eine Reihe formalisierter Spezifikationen haben, was bei einem standardisierten Netzwerkprotokoll der Fall ist.

Mit einem „Bottom-up“-Ansatz können wir große Aufgaben in, sagen wir mal, kleinen Schritten angehen. Die Spezifikationen sind für MQTT nicht sehr umfangreich und die Client-Seite ist im Vergleich zur Broker-Seite am einfachsten. Aber sie haben ihre eigene Komplexität, insbesondere seit Version 5.0 mit der Aufnahme einiger zusätzlicher Funktionen.

Da wir keine bezahlte Zeit und kein Team haben, um unsere Fähigkeiten zu bündeln, scheinen kleine Schritte hier der beste Weg zu sein: Wie schicke ich eine Nachricht? Was soll ich denn schreiben? Wie kann ich mit etwas anfangen, das funktioniert, sodass ich es verbessern kann, bevor ich daran denke, es schnell zu machen?


Große Specs, kleine Schritte: Verstehen und Aufschlüsseln des MQTT-Protokolls

Wie bei vielen (wenn nicht sogar allen) Netzwerkprotokollen üblich, arbeitet das MQTT-Protokoll mit einer Aufteilung der zu übertragenden Daten in sogenannte Pakete. Wenn der Empfänger also weiß, was jede Art von Paket bedeutet, kann er je nach Art des empfangenen Pakets das richtige Betriebsverhalten wählen. Im MQTT-Sprachgebrauch wird die Art des Pakets als Control Packet Type bezeichnet, und jedes dieser Pakete besteht aus bis zu drei Teilen:

  • ein unveränderlicher Header (fixed header), der in allen Paketen enthalten ist

  • ein variabler Header (variable header), der in einigen Paketen vorhanden ist

  • eine Daten (payload), die nur in einigen Paketen enthalten ist

In MQTT-v5.0 gibt es fünfzehn Kontroll-Paket-Typen:

Tabelle 1. MQTT Control Packet Typen (Tabelle aus OASIS spec)

Name Wert Richtung des Datenstroms Beschreibung
Reserved
0 Verboten
Reserviert
CONNECT 1 Client an Server Verbindungsanfrage
CONNACK 2 Server an Client Bestätigung der Verbindung
PUBLISH 3 Client an Server oder Server an Client Nachricht veröffentlichen
PUBREC 5 Client an Server oder Server an Client

Veröffentlichen empfangen (QoS 2 Zustellung Teil 1)
PUBREL 6 Client an Server oder Server an Client
Publish (Veröffentlichung) freigegeben (QoS 2 Lieferung Teil 2)
PUBCOMP 7 Client an Server oder Server an Client

Publish abgeschlossen (QoS 2 Lieferung Teil 3)
SUBSCRIBE 8 Client an Server Anfrage abonnieren
SUBACK 9 Server an Client Bestätigung abonnieren
UNSUBSCRIBE 10 Client an Server Anfrage abbestellen
UNSUBACK 11 Server an Client
Bestätigung abbestellen
PINGREQ 12 Client an Server PING-Anfrage
PINGRESP 13 Server an Client PING-Antwort
DISCONNECT 14 Client an Server oder Server an Client Benachrichtigung beim Trennen der Verbindung
AUTH 15 Client an Server oder Server an Client Austausch der Authentifizierung

Der unveränderliche Header aller Kontrollpakete hat das gleiche Format.

Abb.1 MQTT unveränderliches Header Format

MQTT unveränderliches Header Format

Da wir nichts tun können, bevor wir eine Verbindung zwischen unserem Client und dem Server haben, und in Anbetracht der Tatsache, dass der Standard eine klare Aussage enthält, die lautet

„Nachdem eine Netzwerkverbindung von einem Client zu einem Server hergestellt wurde, MUSS das erste Paket, das vom Client zum Server gesendet wird, ein CONNECT-Paket sein. 

Schauen wir uns an, wie der unveränderliche Header eines CONNECT-Pakets formatiert werden muss.

Abb.2 MQTT unveränderliches Header Format für CONNECT-Paket

MQTT Festes Header Format des CONNECT-Pakets

Wir müssen es also mit zwei Bytes füllen: Das erste Byte muss den Binärwert 00010000 haben, und das zweite Byte muss den Wert der sogenannten „Restlänge“ haben.

Der Standard definiert die verbleibende Länge als:

„eine variable Byte Integer-Zahl, die die Anzahl der im aktuellen Kontrollpaket verbleibenden Bytes darstellt, einschließlich der Daten im variablen Kopf und der Daten selbst. Die verbleibende Länge umfasst nicht die Bytes, die zur Kodierung der verbleibenden Länge verwendet werden. Die Paketgröße ist die Gesamtzahl der Bytes in einem MQTT-Kontrollpaket, sie entspricht der Länge des unveränderlichen Headers plus der verbleibenden Länge.“ (Hervorhebung durch uns)

Der Standard definiert auch das Kodierungsschema für diese variable Byte Integer-Zahl:

„Die variable Byte Integer-Zahl wird nach einem Kodierungsschema kodiert, das für Werte bis 127 ein einzelnes Byte verwendet. Größere Werte werden wie folgt behandelt. Die niedrigstwertigen sieben Bits eines jeden Bytes kodieren die Daten, und das höchstwertige Bit wird verwendet, um anzuzeigen, ob in der Darstellung noch weitere Bytes folgen. Jedes Byte kodiert also 128 Werte und ein „Fortsetzungsbit“. Die maximale Anzahl von Bytes im Feld Variable Byte Integer beträgt vier. Der kodierte Wert MUSS die Mindestanzahl von Bytes verwenden, die zur Darstellung des Wertes erforderlich ist.

Wow! Das scheint eine Menge an Informationen zu sein, die auf einmal verarbeitet werden müssen. Und wir versuchen gerade, das zweite Byte zu füllen!

Glücklicherweise bietet der Standard den „Algorithmus für die Kodierung einer nicht-negativen ganzen Zahl (X) in das Codierungsschema der variablen Byte Integer-Zahl“.

do
   encodedByte = X MOD 128
   X = X DIV 128
   // if there are more data to encode, set the top bit of this byte
   if (X > 0)
      encodedByte = encodedByte OR 128
   endif
   'output' encodedByte
while (X > 0)

„MOD ist der Modulo-Operator (% in C), DIV ist die Ganzzahldivision (/ in C) und OR ist das bitweise Oder (| in C).“

GUT. Das haben wir jetzt:

  • die Liste aller Kontrollpakettypen, 

  • das Format des unveränderlichen Headers für ein CONNECT-Paket mit zwei Bytes,

  • den Wert des ersten Bytes,

  • und den Algorithmus zur Kodierung des variablen Bytes Integer, das das zweite Byte füllen wird.

Wir können mit dem Schreiben unseres ersten Tests beginnen. 

HINWEIS: Da wir den TDD-Ansatz von unten nach oben verfolgen, werden wir die Tests vor der Implementierung schreiben. Wir können von Anfang an davon ausgehen, dass wir 1.) Tests schreiben, die fehlschlagen, dann 2.) nur den Code implementieren, der erforderlich ist, um den Test zu bestehen , und dann 3.) den Code bei Bedarf umgestalten. Dabei spielt es keine Rolle, ob die ursprüngliche Implementierung naiv oder hässlich ist oder ob sie eine schlechte Leistung zu haben scheint. Wir werden uns mit diesen Problemen befassen, sobald wir einen funktionierenden Code haben. Die Leistung steht ganz am Ende unserer Aufgabenliste.

Öffnen wir also kurzerhand unseren MetaEditor und erstellen ein Skript namens TestFixedHeader mit folgendem Inhalt:

#include <MQTT\mqtt.mqh>
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   Print(TestFixedHeader_Connect());
  }
//---
bool TestFixedHeader_Connect()
  {
   uchar content_buffer[]; //empty
//---
   uchar expected[2];
   expected[0] = 1; //pkt type
   expected[1] = 0; //remaining length
//---
   uchar fixed_header[];
//---
   GenFixedHeader(CONNECT, content_buffer, fixed_header);
//---
   if(!ArrayCompare(expected, fixed_header) == 0)
     {
      Print(__FUNCTION__);
      for(uint i = 0; i < expected.Size(); i++)
        {
         Print("expected: ", expected[i], " result: ", fixed_header[i]);
        }
      return false;
     }
   return true;
  }

Außerdem erstellen wir den Header mqtt.mqh, in dem wir mit der Entwicklung unserer Funktionen beginnen werden, und füllen ihn mit dem unten stehenden Code.
void GenFixedHeader(uint pkt_type, uchar& buf[], uchar& head[])
  {
   ArrayFree(head);
   ArrayResize(head, 2);
//---
   head[0] = uchar(pkt_type);
//---
//Remaining Length
   uint x;
   x = ArraySize(buf);
   do
     {
      uint encodedByte = x % 128;
      x = (uint)(x / 128);
      if(x > 0)
        {
         encodedByte = encodedByte | 128;
        }
      head[1] = uchar(encodedByte);
     }
   while(x > 0);
  }
//+------------------------------------------------------------------+
enum ENUM_PKT_TYPE
  {
   CONNECT      =  1, // Connection request
   CONNACK      =  2, // Connect acknowledgment
   PUBLISH      =  3, // Publish message
   PUBACK       =  4, // Publish acknowledgment (QoS 1)
   PUBREC       =  5, // Publish received (QoS 2 delivery part 1)
   PUBREL       =  6, // Publish release (QoS 2 delivery part 2)
   PUBCOMP      =  7, // Publish complete (QoS 2 delivery part 3)
   SUBSCRIBE    =  8, // Subscribe request
   SUBACK       =  9, // Subscribe acknowledgment
   UNSUBSCRIBE 	=  10, // Unsubscribe request
   UNSUBACK     =  11, // Unsubscribe acknowledgment
   PINGREQ      =  12, // PING request
   PINGRESP     =  13, // PING response
   DISCONNECT  	=  14, // Disconnect notification
   AUTH         =  15, // Authentication exchange
  };

Wenn Sie das Skript ausführen, sollten wir auf der Registerkarte „Experten“ Folgendes sehen.

Abb.3 Ausgangstest unveränderlicher Header - Test bestanden

Ausgabe Test unveränderlicher Header - Wahr

Um sicher zu sein, dass unser Test funktioniert, müssen wir sehen, wie er fehlschlägt. Es wird daher dringend empfohlen, die durch die Variable content_buffer dargestellte Eingabe zu ändern und die erwartete Variable unverändert zu lassen. Die Ausgabe auf der Registerkarte „Experten“ sollte in etwa wie folgt aussehen.

Abb.4 Ausgabe Test unveränderlicher Header - Test fehlgeschlagen

Ausgabe Test unveränderlicher Header - Test fehlgeschlagen

Wie auch immer, wir können davon ausgehen, dass unsere Tests an diesem Punkt anfällig sind, ebenso wie unser Code im mqtt.mqh-Header. Kein Problem. Wir stehen erst am Anfang, und im Laufe der Zeit werden wir die Möglichkeit haben, sie zu verbessern, aus unseren Fehlern zu lernen und folglich unsere Fähigkeiten zu verbessern.

Inzwischen können wir die Funktion TestFixedHeader_Connect auf andere Pakettypen übertragen. Wir werden diejenigen ignorieren, die nur die Flussrichtung Server zu Client haben. Diese sind CONNACK, PUBACK, SUBACK, UNSUBACK und PINGRESP. Diese ACK(S)- und Ping-Antwort-Paket-Header werden vom Server generiert und wir werden uns später mit ihnen beschäftigen.

Um sicher zu sein, dass unsere Tests wie erwartet funktionieren, müssen wir auch Tests einbeziehen, bei denen ein Fehlschlag zu erwarten ist. Diese Tests geben bei Fehlschlag true zurück.
#include <MQTT\mqtt.mqh>
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   Print(TestFixedHeader_Connect());
   Print(TestFixedHeader_Connect_RemainingLength1_Fail());
   Print(TestFixedHeader_Publish());
   Print(TestFixedHeader_Publish_RemainingLength1_Fail());
   Print(TestFixedHeader_Puback());
   Print(TestFixedHeader_Puback_RemainingLength1_Fail());
   Print(TestFixedHeader_Pubrec());
   Print(TestFixedHeader_Pubrec_RemainingLength1_Fail());
   Print(TestFixedHeader_Pubrel());
   Print(TestFixedHeader_Pubrel_RemainingLength1_Fail());
   Print(TestFixedHeader_Pubcomp());
   Print(TestFixedHeader_Pubcomp_RemainingLength1_Fail());
   Print(TestFixedHeader_Subscribe());
   Print(TestFixedHeader_Subscribe_RemainingLength1_Fail());
   Print(TestFixedHeader_Puback());
   Print(TestFixedHeader_Puback_RemainingLength1_Fail());
   Print(TestFixedHeader_Unsubscribe());
   Print(TestFixedHeader_Unsubscribe_RemainingLength1_Fail());
   Print(TestFixedHeader_Pingreq());
   Print(TestFixedHeader_Pingreq_RemainingLength1_Fail());
   Print(TestFixedHeader_Disconnect());
   Print(TestFixedHeader_Disconnect_RemainingLength1_Fail());
   Print(TestFixedHeader_Auth());
   Print(TestFixedHeader_Auth_RemainingLength1_Fail());
  }

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool TestFixedHeader_Connect()
  {
   uchar content_buffer[]; //empty
//---
   uchar expected[2];
   expected[0] = 1; //pkt type
   expected[1] = 0; //remaining length
//---
   uchar fixed_header[];
//---
   GenFixedHeader(CONNECT, content_buffer, fixed_header);
//---
   if(!ArrayCompare(expected, fixed_header) == 0)
     {
      Print(__FUNCTION__);
      for(uint i = 0; i < expected.Size(); i++)
        {
         Print("expected: ", expected[i], " result: ", fixed_header[i]);
        }
      return false;
     }
   Print(__FUNCTION__);
   return true;
  }

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool TestFixedHeader_Connect_RemainingLength1_Fail()
  {
   uchar content_buffer[]; //empty
   ArrayResize(content_buffer, 1);
   content_buffer[0] = 1;
//---
   uchar expected[2];
   expected[0] = 1; //pkt type
   expected[1] = 0; //remaining length should be 1
//---
   uchar fixed_header[];
//---
   GenFixedHeader(CONNECT, content_buffer, fixed_header);
//---
   if(!ArrayCompare(expected, fixed_header) == 0)
     {
      Print(__FUNCTION__);
      for(uint i = 0; i < expected.Size(); i++)
        {
         Print("expected: ", expected[i], " result: ", fixed_header[i]);
        }
      return true;
     }
   Print(__FUNCTION__);
   return false;
  }

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool TestFixedHeader_Publish()
  {
   uchar content_buffer[]; //empty
//---
   uchar expected[2];
   expected[0] = 3; //pkt type
   expected[1] = 0; //remaining length
//---
   uchar fixed_header[];
//---
   GenFixedHeader(PUBLISH, content_buffer, fixed_header);
//---
   if(!ArrayCompare(expected, fixed_header) == 0)
     {
      Print(__FUNCTION__);
      for(uint i = 0; i < expected.Size(); i++)
        {
         Print("expected: ", expected[i], " result: ", fixed_header[i]);
        }
      return false;
     }
   Print(__FUNCTION__);
   return true;
  }

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool TestFixedHeader_Publish_RemainingLength1_Fail()
  {
   uchar content_buffer[]; //empty
   ArrayResize(content_buffer, 1);
   content_buffer[0] = 1;
//---
   uchar expected[2];
   expected[0] = 3; //pkt type
   expected[1] = 0; //remaining length should be 1
//---
   uchar fixed_header[];
//---
   GenFixedHeader(PUBLISH, content_buffer, fixed_header);
//---
   if(!ArrayCompare(expected, fixed_header) == 0)
     {
      Print(__FUNCTION__);
      for(uint i = 0; i < expected.Size(); i++)
        {
         Print("expected: ", expected[i], " result: ", fixed_header[i]);
        }
      return true;
     }
   Print(__FUNCTION__);
   return false;
  }
.
.
.
(omitted for brevity)

Huch! Das ist eine Menge Standard-Code, Dutzende von Tastaturanschlägen und oder Kopieren/Einfügen! 

Ja, das ist es ganz sicher. Langfristig wird sich das aber auszahlen. Mit diesen einfachen - wenn auch simplen - Tests schaffen wir eine Art Sicherheitsnetz für unsere Entwicklung. Sie sollten uns helfen, 

  • um uns auf die anstehende Aufgabe zu konzentrieren,

  • ein Over-Engineering zu vermeiden,

  • und Regressionsfehler erkennen.

HINWEIS: Ich empfehle Ihnen dringend, es selbst zu schreiben, anstatt einfach die beigefügte Datei zu verwenden. Sie werden sehen, wie viele kleine, „harmlose“ Fehler sie von Anfang an erkennen können. Wenn wir mit dem operativen Verhalten unseres Client vorankommen, werden diese Tests (und andere spezifischere Tests) ihren Wert beweisen. Außerdem vermeiden wir eine häufige technische Schuld: die Tests erst am Ende zu schreiben. Normalerweise werden Tests nie geschrieben, wenn man sie bis zum Schluss aufschiebt.

Abb.5 Ausgangstests unveränderlicher Header - alle bestanden

Ausgabetest unveränderlicher Header - Alle bestanden

OK, schauen wir mal, ob unser zwei Byte langer CONNECT-Header von einem MQTT-Broker als gültiger Header erkannt wird.


So installieren Sie einen MQTT-Broker (und Client) für Entwicklung und Tests

Es gibt viele produktive MQTT-Broker, die online verfügbar sind, und die meisten von ihnen bieten eine Art „Sandbox“-URL für Entwicklungs- und Testzwecke an. Eine einfache Suche nach „MQTT-Broker“ in Ihrer bevorzugten Suchmaschine sollte ausreichen, um einige von ihnen zu finden.

Unser Client ist jedoch zu diesem Zeitpunkt noch embryonal. Wir sind nicht in der Lage, eine Antwort zu empfangen und zu lesen, ohne einen Paketanalysator zu verwenden, um unseren Netzwerkverkehr zu erfassen. Dieses Tool wird später nützlich sein, aber im Moment reicht es aus, einen specs-kompatiblen MQTT-Broker auf unserem Entwicklungsrechner installiert zu haben, damit wir seine Protokolle überprüfen können, um das Ergebnis unserer Interaktionen zu sehen. Idealerweise sollte er auf einer virtuellen Maschine installiert werden, um eine andere IP als die unseres Clients zu haben. Durch die Verwendung eines Brokers mit einer anderen IP-Adresse für Entwicklung und Tests können wir Verbindungs- und Authentifizierungsprobleme früher angehen.

Auch hier gibt es mehrere Optionen für Windows, Linux und Mac. Ich habe Mosquitto auf Windows Subsystem For Linux (WSL) installiert. Mosquitto ist nicht nur kostenlos und quelloffen, sondern auch sehr praktisch, weil es zwei sehr nützliche Kommandozeilenanwendungen für die Entwicklung enthält: mosquitto_pub und mosquitto_sub zum Veröffentlichen und Abonnieren von MQTT-Themen. Ich habe es auch auf dem Windows-Entwicklungsrechner installiert, damit ich einige Fehler überprüfen kann.

Denken Sie daran, dass MetaTrader verlangt, dass Sie alle externen URLs in der Registerkarte Menü Extras > Optionen > Expert Advisors auflisten und dass Sie von MetaTrader aus nur auf die Ports 80 oder 443 zugreifen dürfen. Wenn Sie also diesen Weg der Installation des Brokers auf der WSL gehen, vergessen Sie nicht, seine Host-IP anzugeben und den Netzwerkverkehr, der an Port 80 ankommt, auf 1883 umzuleiten, den Standard-MQTT- (und Mosquitto-) Port. Es gibt ein Tool namens redir, das diese Portumleitung auf einfache und stabile Weise durchführt.

Abb.6 MetaTrader 5 Dialog - Webanforderung URL zulassen

MetaTrader 5 Dialog - Web-Anfrage-URL zulassen


Um die WSL-IP abzurufen, führen Sie den folgenden Befehl aus:

Abb.7 WSL-Befehl zum Abrufen des Hostnamens

Abb.6 - WSL-Befehl zum Abrufen des Hostnamens


Nach der Installation wird Mosquitto so konfiguriert, dass es beim Booten als „Dienst“ gestartet wird. Starten Sie also einfach Ihre WSL neu, um Mosquitto auf dem Standard-Port 1883 zu starten.

Um den Netzwerkverkehr mit redir von Port 80 auf 1883 umzuleiten, führen Sie den folgenden Befehl aus:

Abb.8 Netzwerkverkehr mit „redir“ umleiten

Portumleitung mit „redir“ Befehlszeile


Und schließlich können wir prüfen, ob unser zwei Byte langer CONNECT-unveränderlicher Header von einem specs-konformen MQTT-Broker als gültiger MQTT-Header erkannt wird. Erstellen Sie einfach ein „Scratch“-Skript und fügen Sie den folgenden Code ein. (Vergessen Sie nicht, die IP-Adresse in der Variable broker_ip entsprechend der Ausgabe des Befehls get hostname -I zu ändern).

#include <MQTT\mqtt.mqh>

string broker_ip = "172.20.155.236";
int broker_port = 80;
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
   int socket = SocketCreate();
   if(socket != INVALID_HANDLE)
     {
      if(SocketConnect(socket, broker_ip, broker_port, 1000))
        {
         Print("Connected ", broker_ip);
         //---
         uchar fixed_header[];
         uchar content_buffer[]; //empty
         //---
         GenFixedHeader(CONNECT, content_buffer, fixed_header);
         //---
         if(SocketSend(socket, fixed_header, ArraySize(fixed_header)) < 0)
           {
            Print("Failed sending fixed header ", GetLastError());
           }
        }
     }
  }

In der Ausgabe der Registerkarte „Experten“ sollten Sie Folgendes sehen...

Abb.9 Ausgabe Local Broker Connect

Ausgabe Local Broker Connect

... und die folgende Ausgabe in Ihrem Mosquitto-Protokoll.

Abb.10 Ausgabe Local Broker Connect - Mosquitto Log

Ausgabe Local Broker Connect - Mosquitto Log

Also, ja, unser unveränderlicher Header von 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.

HINWEIS: Wie Sie ganz am Anfang des obigen Befehls sehen können, verwenden wir den Befehl tail -f {pathToLogFile}. Wir können sie während der Entwicklung verwenden, um die Aktualisierungen des Mosquito-Protokolls zu verfolgen, ohne die Datei öffnen und neu laden zu müssen.

Im nächsten Schritt werden wir den variable Header von CONNECT - und andere - implementieren, um eine stabile Verbindung mit unserem Broker aufrechtzuerhalten. Wir werden auch eine Nachricht PUBLISH (veröffentlichen) und uns mit CONNACK-Paketen befassen, die vom Broker zurückgegeben werden, sowie mit den zugehörigen Reason Codes. Im nächsten Schritt werden wir einige interessante bitweise Operationen durchführen, um unsere Connect-Flags zu füllen. Dieser nächste Schritt erfordert auch, dass wir unsere Tests erheblich verbessern, um die Komplexität zu bewältigen, die sich aus der Konversation zwischen Client und Broker ergibt.


Schlussfolgerung

In diesem Artikel haben wir einen kurzen Überblick über das MQTT-Pub/Sub-Echtzeit-Nachrichtenaustauschprotokoll, seine Ursprünge und Hauptkomponenten gesehen. Wir haben auch einige mögliche Anwendungsfälle von MQTT für Echtzeit-Nachrichten im Handelskontext aufgezeigt und wie man es für automatisierte Operationen in MetaTrader 5 verwenden kann, entweder durch den Import von DLLs, die aus C, C++ oder C# generiert wurden, oder durch die Verwendung der MQTT-Python-Bibliothek über das MetaTrader 5 Python-Modul.

In Anbetracht der Einschränkungen, die der Einsatz von DLLs auf dem MetaQuotes-Marktplatz und dem MetaQuotes Cloud Tester mit sich bringt, haben wir auch unsere ersten Schritte zur Implementierung eines nativen MQL5 MQTT-Clients vorgeschlagen und beschrieben, der einen testgetriebenen Entwicklungsansatz (TDD) verwendet.


Einige nützliche Referenzen

Wir müssen nicht alle Räder neu erfinden. Viele der Lösungen für die häufigsten Herausforderungen, mit denen Entwickler beim Schreiben von MQTT-Clients für andere Sprachen konfrontiert werden, sind als Open-Source-Bibliotheken/SDKs verfügbar.

  • Liste der Software, einschließlich Broker, Bibliotheken und Tools.
  • Liste mit verschiedenen Ressourcen zu MQTT auf GitHub.

Wenn Sie ein erfahrener MQL5-Entwickler sind und Vorschläge haben, lassen Sie bitte einen Kommentar unten. Wir werden dies sehr zu schätzen wissen.


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

Beigefügte Dateien |
TestFixedHeader.mq5 (19.48 KB)
mqtt.mqh (2.19 KB)
ONNX-Modelle in Klassen packen ONNX-Modelle in Klassen packen
Die objektorientierte Programmierung ermöglicht die Erstellung eines kompakteren Codes, der leicht zu lesen und zu ändern ist. Hier sehen wir uns das Beispiel für drei ONNX-Modelle an.
Verbessern Sie Ihre Handelscharts durch interaktiven GUI's in MQL5 (Teil II): Ein bewegliches GUI (II) Verbessern Sie Ihre Handelscharts durch interaktiven GUI's in MQL5 (Teil II): Ein bewegliches GUI (II)
Erschließen Sie das Potenzial der dynamischen Datendarstellung in Ihren Handelsstrategien und Dienstprogrammen mit unserer ausführlichen Anleitung zur Erstellung beweglicher GUIs in MQL5. Tauchen Sie ein in die grundlegenden Prinzipien der objektorientierten Programmierung und entdecken Sie, wie Sie mit Leichtigkeit und Effizienz einzelne oder mehrere bewegliche GUIs auf demselben Diagramm entwerfen und implementieren können.
Die ChatGPT-Funktionen von OpenAI im Rahmen der MQL4- und MQL5-Entwicklung Die ChatGPT-Funktionen von OpenAI im Rahmen der MQL4- und MQL5-Entwicklung
In diesem Artikel werden wir uns mit ChatGPT von OpenAI beschäftigen, um zu verstehen, welche Möglichkeiten es bietet, den Zeit- und Arbeitsaufwand für die Entwicklung von Expert Advisors, Indikatoren und Skripten zu reduzieren. Ich werde Sie schnell durch diese Technologie führen und versuchen, Ihnen zu zeigen, wie Sie sie für die Programmierung in MQL4 und MQL5 richtig einsetzen.
Entwicklung eines Wiedergabesystems — Marktsimulation (Teil 04): Anpassung der Einstellungen (II) Entwicklung eines Wiedergabesystems — Marktsimulation (Teil 04): Anpassung der Einstellungen (II)
Lassen Sie uns mit der Entwicklung des Systems und der Kontrollen fortfahren. Ohne die Möglichkeit, den Dienst zu kontrollieren, ist es schwierig, Fortschritte zu machen und das System zu verbessern.