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

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

MetaTrader 5Integration | 24 Januar 2024, 17:05
183 0
Jocimar Lopes
Jocimar Lopes

„Wie können Sie sich als Profi bezeichnen, wenn Sie nicht wissen, dass Ihr Code funktioniert? Wie können Sie wissen, dass Ihr gesamter Code funktioniert, wenn Sie ihn nicht bei jeder Änderung testen? Wie können Sie es bei jeder Änderung testen, wenn Sie keine automatisierten Unit-Tests mit sehr hoher Abdeckung haben? Wie kann man automatisierte Einheitstests mit einer sehr hohen Abdeckung bekommen, ohne TDD zu praktizieren?" (Robert 'Uncle Bob' Martin, The Clean Coder, 2011)

Einführung

Bisher haben wir uns im Teil 1 und im Teil 2 dieser Serie mit einem kleinen Teil des nicht-operativen Teils des MQTT-Protokolls beschäftigt. Wir haben in zwei separaten Header-Dateien alle Protokolldefinitionen, Enumeration und einige gemeinsame Funktionen organisiert, die von unseren Klassen gemeinsam genutzt werden. Außerdem haben wir eine Schnittstelle geschrieben, die als Wurzel der Objekthierarchie dienen soll, und sie in einer Klasse implementiert, deren einziger Zweck es ist, ein konformes MQTT CONNECT-Paket zu erstellen. In der Zwischenzeit haben wir Unit-Tests für jede Funktion geschrieben, die an der Erstellung der Pakete beteiligt ist. Obwohl wir unser generiertes Paket an unseren lokalen MQTT-Broker geschickt haben, um zu prüfen, ob es als wohlgeformtes MQTT-Paket erkannt wird, war dieser Schritt technisch nicht erforderlich. Da wir unsere Funktionsparameter mit Pufferdaten fütterten, wussten wir, dass wir sie isoliert und zustandsunabhängig testen würden. Das ist gut, und wir werden uns bemühen, unsere Tests – und folglich auch unsere Funktionen – weiterhin so zu schreiben. Dies macht unseren Code flexibler und ermöglicht es uns, eine Funktionsimplementierung zu ändern, ohne unseren Testcode zu ändern, solange wir dieselbe Funktionssignatur haben.

Von nun an werden wir uns mit dem operativen Teil des MQTT-Protokolls befassen. Es überrascht nicht, dass es in der OASIS-Norm als Operatives Verhalten bezeichnet wird. Das heißt, von nun an müssen wir uns mit den vom Server gesendeten Paketen befassen. Unser Client muss in der Lage sein, den Pakettyp des Servers und dessen Semantik zu erkennen und das geeignete Verhalten in einem bestimmten Kontext zu wählen, das geeignete Verhalten in jedem möglichen Client-Zustand.

Um diese Aufgabe bewältigen zu können, müssen wir den Typ des Server-Pakets im ersten Byte der Antwort identifizieren. Handelt es sich um ein CONNACK-Paket, müssen wir seinen Connect Reason Code lesen und entsprechend reagieren.


(CONNECT) Setzen der „Connect Flags“ des Clients

Wenn unser Client eine Verbindung mit dem Server anfordert, muss er den Server über Folgendes informieren

  • einige gewünschte Fähigkeiten des Maklers,
  • wenn eine Authentifizierung mit einem Nutzernamen und einem Passwort erforderlich ist,
  • und ob es sich bei dieser Verbindung um eine neue Sitzung handelt oder um die Wiederaufnahme einer bereits geöffneten Sitzung.

Dies geschieht durch das Setzen einiger Bit-Flags am Anfang des Variablen-Kopfes, direkt nach dem Protokollnamen und der Protokollversion. Diese Bit-Flags auf dem CONNECT-Paket werden als Connect Flags bezeichnet.

Denken Sie daran, dass Bit-Flags boolesche Werte sind. Sie können unterschiedliche Namen oder Darstellungen haben, aber boolesche Werte haben nur zwei mögliche Werte, normalerweise wahr oder falsch.

Abb. 01 - Gebräuchliche Begriffe zur Darstellung boolescher Werte

Abb. 01 - Allgemeine Begriffe zur Darstellung boolescher Werte

Der OASIS-Standard verwendet durchgängig 1 (Eins) und 0 (Null). Wir werden hier die meiste Zeit „true“ und „false“ verwenden, und irgendwann werden wir auch „set“ und „unset“ verwenden. Dadurch sollte der Text besser lesbar werden. Darüber hinaus verwendet unsere öffentliche API konsequent true und false für das Setzen dieser Werte, sodass die Verwendung dieser Begriffe diesen Artikel für diejenigen Leser, die die Entwicklung der Bibliothek verfolgen, leichter verständlich machen sollte.

Abb. 02 - OASIS Connect Flag Bits

Abb. 02 - OASIS Connect Flag Bits

Wie Sie in der OASIS-Tabelle in der obigen Abbildung sehen können, ist das erste Bit (bit_0) reserviert und wir müssen es in Ruhe lassen: null, nicht angekreuzt, boolesch falsch, nicht gesetzt. Wenn wir es einstellen, haben wir ein fehlerhaftes Paket.


Clean Start (bit_1)

Das erste Bit, das wir setzen können, ist das zweite Bit. Es wird für das Setzen des Flags Clean Start (Sauberer Start) verwendet – wenn sie wahr ist, führt der Server einen Clean Start durch und verwirft alle bestehenden Sitzungen, die mit unserer Client-Kennung verbunden sind. Der Server wird eine neue Sitzung starten. Wenn diese Option nicht gesetzt ist, nimmt der Server die vorherige Konversation wieder auf oder beginnt eine neue Sitzung, wenn es keine bestehende Sitzung gibt, die mit unserer Client-Kennung verbunden ist.

So sieht unsere Funktion zum Setzen bzw. Zurücksetzen dieses Flags aus.

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

Wir schalten die Werte mit Hilfe von Bitoperationen um. Wir verwenden einen ternären Operator, um die booleschen und zusammengesetzten Zuweisungen umzuschalten, damit der Code kompakter ist. Dann speichern wir das Ergebnis in dem privaten Klassenmitglied m_connect_flags. Schließlich aktualisieren wir das Byte-Array, das unser CONNECT-Paket darstellt, mit den neuen Werten, indem wir die integrierte Funktion ArrayFill aufrufen. (Beachten Sie, dass dieser späte Schritt – das Füllen von Arrays – ein Implementierungsdetail ist, das wir wahrscheinlich später ändern werden).

Diese Zeile aus einem unserer Tests zeigt, wie sie aufgerufen wird.

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


Bit 7 6 5 4 3 2 1 0

User Name Flag
Password Flag
Will Retain
Will  QoS 2
Will QoS 1

Will Flag

Clean Start


Reserviert


X X X X X X 1 0

Tabelle 01 – Bit-Flag Clean Start (bit_1) auf true gesetzt – MQTT v5.0

Wir werden dieses Muster ausgiebig verwenden, um boolesche Flags umzuschalten: ternäre Operatoren und bitweise Operationen mit zusammengesetzten Zuweisungen.

Die folgenden drei Flags mit dem Namen Will „Irgendwas“ sollen unseren Wunsch ausdrücken, dass der Server einige Fähigkeiten hat. Sie teilen dem Server mit, dass wir „gewillt“ sind, den Server in die Lage zu versetzen

  1. die Will Message(s) (Will-Nachrichten) speichern und sie mit unserer Client-Sitzung verknüpfen (mehr dazu später);
  2. eine bestimmte QoS-Stufe bereitstellen, in der Regel über QoS 0, der Standardeinstellung, wenn nichts festgelegt ist;
  3. Will Message(s) aufbewahren und sie als „retained“ (aufbewahrt) veröffentlichen (siehe unten), wenn die Will Message auf „true“ gesetzt ist


Will-Flag (bit_2)

Das dritte Bit dient zum Setzen des Will-Flags – wenn es auf „true“ gesetzt ist, muss unser Client eine Will Message bereitstellen, die „in Fällen, in denen die Netzwerkverbindung nicht normal geschlossen wird“, veröffentlicht wird. Man kann sich Will Message als eine Art „letzte Worte“ des Cilents vorstellen, nachdem er im Angesicht der Abonnenten gestorben ist.

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

Sie wird auf die gleiche Weise aufgerufen wie die vorherige Funktion.

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


Bit 7 6 5 4 3 2 1 0

User Name Flag
Password Flag
Will Retain
Will  QoS 2
Will QoS 1

Will Flag

Clean Start


Reserviert


X X X X X 1 X 0

Tabelle 02 – Bit Flag Will Flag (bit_2) auf true gesetzt – MQTT v5.0

Will QoS (bit_3, bit_4) 

Anders als bei den beiden vorherigen Flags müssen bei dieser Funktion zwei Bits gesetzt werden, wenn der Client die QoS-Stufe 2 anfordert, nämlich das vierte und das fünfte Bit. QoS steht für Quality of Service (Qualität des Dienstes) und kann eine von drei Varianten sein.

Abb. 03 - OASIS QoS-Definitionen

Abbildung 03 - OASIS QoS-Definitionen

Sie reichen vom unzuverlässigsten bis zum zuverlässigsten Zustellungssystem:

QoS 0

Die QoS 0 setzt maximal eine Zustellung fest. Es ist eine Art von „fire and forget“ (feuern und vergessen). Der Absender wird es einmal versuchen. Die Nachricht kann verloren gehen. Es erfolgt keine Bestätigung durch den Server. Dies ist die Standardeinstellung, d. h., wenn in den Bits 3 und 4 nichts gesetzt ist, ist die vom Client angeforderte QoS-Stufe QoS 0.

QoS 1 

Die QoS 1 setzt mindestens eine Zustellung voraus. Es hat ein PUBACK, das die Lieferung bestätigt.

Gleiches Muster der Funktionsdefinition.

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

Gleiches Funktionsaufrufmuster.

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

Bit 7 6 5 4 3 2 1 0

User Name Flag
Password Flag
Will Retain
Will  QoS 2
Will QoS 1

Will Flag

Clean Start


Reserviert


X X X X 1 X X 0

Tabelle 03 – Bit-Flag Wird QoS 1 (bit_3) auf true gesetzt – MQTT v5.0

QoS 2 

Die QoS 2 wird genau einmal bei der Auslieferung gesetzt. Diese QoS setzt voraus, dass es keine Verluste oder Duplikate gibt. Der Absender bestätigt die Nachricht mit einem PUBREC und die Zustellung mit einem PUBREL.

Man kann sich diese Stufe wie den Versand eines Einschreibepakets vorstellen. Die Post gibt Ihnen eine Quittung, wenn Sie das Paket in ihre Hände geben und bestätigt damit, dass sie von nun an für die Zustellung an die richtige Adresse verantwortlich ist. Und wenn das Paket zugestellt wird, erhalten wir eine vom Empfänger unterzeichnete Empfangsbestätigung über die Zustellung des Pakets.

Idem.

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

Ebenda.

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

User Name Flag
Password Flag
Will Retain
Will  QoS 2
Will QoS 1

Will Flag

Clean Start


Reserviert


X X X 1 X X X 0

Tabelle 04 – Bit-Flag Wird QoS 2 (bit_4) auf true gesetzt – MQTT v5.0

Der Server teilt uns die von ihm akzeptierte maximale QoS-Stufe in den CONNACK-Reason-Codes und in den CONNACK-Eigenschaften mit. Der Client kann eine Anfrage stellen, aber die Fähigkeiten des Servers sind obligatorisch. Wenn wir ein CONNACK mit einer maximalen QoS erhalten, müssen wir uns an diese Serverbeschränkung halten und dürfen kein PUBLISH mit einer höheren QoS senden. Andernfalls macht der Server ein DISCONNECT (Verbindung abbrechen).

QoS 2 ist die höchste bei MQTT v5.0 verfügbare QoS-Stufe, und es ist ein erheblicher Overhead damit verbunden, da das Übermittlungsprotokoll symmetrisch ist, was bedeutet, dass jede der beiden Seiten (der Server und der Client) sowohl als Sender als auch als Empfänger für diese Angelegenheit auftreten kann.

ANMERKUNG: Man kann sagen, dass die QoS aus Sicht der Nutzer das Herzstück des Protokolls ist. Es definiert das Anwendungsprofil und beeinflusst Dutzende anderer Aspekte des Protokolls. Wir werden uns also eingehend mit der QoS-Stufe und ihren Einstellungen im Zusammenhang mit der Implementierung des PUBLISH-Pakets befassen.

Es ist zu beachten, dass QoS 1 und QoS 2 für Client-Implementierungen optional sind. Wie OASIS in einem nicht-normativen Kommentar sagt:

„Ein Client muss keine QoS 1 oder QoS 2 PUBLISH-Pakete unterstützen. Wenn dies der Fall ist, beschränkt der Client einfach das maximale QoS-Feld in allen SUBSCRIBE-Befehlen, die er sendet, auf einen Wert, den er unterstützen kann.“


Will RETAIN (bit_5)

Im sechsten Byte setzen wir das Flag Will Retain. Dieses Flag ist mit dem oben genannten Will-Flag verbunden, 

  • Wenn das Will-Flag nicht gesetzt ist, darf auch Will Retain nicht gesetzt sein.
  • Wenn das Will-Flag gesetzt und die Will Retain nicht gesetzt ist, veröffentlicht der Server die Will Message als eine Nachricht ohne Beibehaltung.
  • Wenn beides eingestellt ist, veröffentlicht der Server die Will Message als behaltene Nachricht.

In diesem und den beiden folgenden Flags wird der Kürze halber kein Code angegeben, da die Funktionsdefinition und die Funktionsaufrufe streng genommen die gleichen sind wie bei den vorherigen Flags. Einzelheiten und Tests entnehmen Sie bitte den beigefügten Dateien.

Bit 7 6 5 4 3 2 1 0

User Name Flag
Password Flag
Will Retain
Will  QoS 2
Will QoS 1

Will Flag

Clean Start


Reserviert


X X 1 X X X X 0

Tabelle 05 – Bit-Flag Will Retain (bit_5) auf true gesetzt – MQTT v5.0

Wir müssen auf das CONNACK-Paket warten, um dieses Flag zu überprüfen, bevor wir mit dem Senden von PUBLISH-Paketen beginnen. Wenn der Server ein PUBLISH-Paket empfängt, bei dem Will Retain auf 1 gesetzt ist, und er keine Retain-Nachrichten unterstützt, macht der Server ein DISCONNECT. Sie werden vielleicht denken: Aber Moment mal? Ist es möglich, zu veröffentlichen, noch bevor ein CONNACK-Paket empfangen wurde? Ja, das ist möglich. Der Standard erlaubt dieses Verhalten. Aber es gibt auch eine Bemerkung dazu:

„Clients, die MQTT-Kontrollpakete senden, bevor sie CONNACK erhalten, kennen die Einschränkungen des Servers nicht“

Daher müssen wir dieses Flag in CONNACK-Paketen überprüfen, bevor wir PUBLISH-Pakete mit Will Retain auf 1 (eins) setzen. 


Password flag (bit_6)

Im siebten Bit teilen wir dem Server mit, ob wir ein Kennwort in der „PayLoad“ (Ladung) senden werden oder nicht. Wenn dieses Flag gesetzt ist, muss ein Passwortfeld in der Payload vorhanden sein. Wenn es nicht gesetzt ist, darf das Passwortfeld in der Payload fehlen.

„Diese Version des Protokolls erlaubt das Senden eines Passworts ohne Nutzernamen, was bei MQTT v3.1.1 nicht möglich war. Dies spiegelt die übliche Verwendung von Password für andere Anmeldedaten als ein Passwort wider.“ (OASIS Standard, 3.1.2.9)

Bit 7 6 5 4 3 2 1 0

User Name Flag
Password Flag
Will Retain
Will  QoS 2
Will QoS 1

Will Flag

Clean Start


Reserviert


X 1 X X X X X 0

Tabelle 06 – Bit-Flag Passwort-Flag (bit_6) auf true gesetzt – MQTT v5.0


User Name flag (bit_7)

Und schließlich teilen wir dem Server mit dem 8ten Bit mit, ob wir einen User Name (Nutzernamen) in der Payload senden werden oder nicht. Wenn dieses Flag gesetzt ist, muss, wie beim Flag Password (Passwort) oben, ein Feld für den User Name in der Payload vorhanden sein. Andernfalls darf das Flag User Name nicht in der Payload enthalten sein.

Bit 7 6 5 4 3 2 1 0

User Name Flag
Password Flag
Will Retain
Will  QoS 2
Will QoS 1

Will Flag

Clean Start


Reserviert


1 X X X X X X 0

Tabelle 07 – Bit-Flag User Name (bit_7) auf true gesetzt – MQTT v5.0

Also ein Connect Flags Byte mit der folgenden Bitfolge...

Bit 7 6 5 4 3 2 1 0

User Name Flag
Password Flag
Will Retain
Will  QoS 2
Will QoS 1

Will Flag

Clean Start


Reserviert


X X 1 1 X 1 1 0

Tabelle 08 – gesetzte Bit-Flags für Clean Start, Will Flag, Will QoS2 und Will Retain – MQTT v5.0

... könnte in etwa so übersetzt werden: Öffnen Sie eine Verbindung für eine neue Sitzung mit QoS-Stufe 2, und bereiten Sie sich darauf vor, meine Will Message zu speichern und sie als aufbewahrte (retained) Nachricht zu veröffentlichen. Übrigens, Herr Server, ich muss mich nicht mit einem Nutzernamen und einem Passwort authentifizieren.

Der Server wird freundlicherweise antworten, ob er unseren Willen erfüllen kann. Er kann sie ganz, teilweise oder gar nicht erfüllen. Und der Server sendet seine Antwort in Form von Connect Reason Codes in CONNACK-Paketen.


(CONNACK) Abrufen der Reason Codes im Zusammenhang mit Connect Flags

In MQTT v5.0 gibt es vierundvierzig Reason Codes (Begründungs-Codes). Wir haben sie in unserer Kopfzeile Defines.mqh zusammengefasst. CONNACK (und andere Pakettypen) haben einen einzigen Reason Code als Teil des Variable Header. Sie werden Connect Reason Codes genannt.

Wert Hex Grund Code-Name Beschreibung
0 0x00 Success Die Verbindung wird angenommen.
128 0x80 Unspecified error Der Server möchte den Grund für den Fehler nicht preisgeben, oder keiner der anderen Reason Codes trifft zu.
129 0x81 Malformed Packet Die Daten im CONNECT-Paket konnten nicht korrekt analysiert werden. 
130 0x82 Protocol Error Die Daten im CONNECT-Paket stimmen nicht mit dieser Spezifikation überein.
131 0x83 Implementation specific error Der CONNECT ist gültig, wird aber von diesem Server nicht akzeptiert.
132 0x84 Unsupported Protocol Version Der Server unterstützt die vom Client angeforderte Version des MQTT-Protokolls nicht.
133 0x85 Client Identifier not valid Der Client Identifier ist eine gültige Zeichenfolge, die jedoch vom Server nicht zugelassen wird.
134 0x86 Bad User Name or Password Der Server akzeptiert den vom Client angegebenen Nutzernamen (User Name) oder das Passwort nicht.
135 0x87 Not authorized Der Client ist nicht berechtigt, eine Verbindung herzustellen.
136 0x88 Server unavailable Der MQTT-Server ist nicht verfügbar.
137 0x89 Server busy Der Server ist beschäftigt. Versuche es später noch einmal.
138 0x8A Banned Dieser Kunde wurde durch eine Verwaltungsmaßnahme verbannt. Wenden Sie sich an den Serveradministrator.
140 0x8C Bad authentication method Die Authentifizierungsmethode wird nicht unterstützt oder stimmt nicht mit der derzeit verwendeten Authentifizierungsmethode überein.
144 0x90 Topic Name invalid Der Name von Will Topic ist nicht fehlerhaft, wird aber von diesem Server nicht akzeptiert.
149 0x95 Packet too large Das CONNECT-Paket hat die maximal zulässige Größe überschritten.
151 0x97 Quota exceeded Eine Durchführungs- oder Verwaltungsvorgabe wurde überschritten.
153 0x99 Payload format invalid Die Will Payload entspricht nicht dem angegebenen Payload Format Indicator.
154 0x9A Retain not supported Der Server unterstützt keine aufbewahrten Nachrichten, und Will Retain wurde auf 1 gesetzt.
155 0x9B QoS not supported Der Server unterstützt die in Will QoS eingestellte Dienstgüte nicht.
156 0x9C Use another server Der Client sollte vorübergehend einen anderen Server verwenden.
157 0x9D Server moved Der Client sollte dauerhaft einen anderen Server verwenden.
159 0x9F Connection rate exceeded Der Grenzwert für die Verbindungsrate wurde überschritten.

Tabelle 08 - Werte der Connect Reason Codes

In der Norm wird ausdrücklich darauf hingewiesen, dass der Server verpflichtet ist, Connect Reason Codes auf dem CONNACK zu senden:

„Der Server, der das CONNACK-Paket sendet, MUSS einen der Connect Reason Code-Werte [MQTT-3.2.2-8] verwenden.“

Die Connect Reason Codes sind für uns an dieser Stelle von besonderem Interesse. Denn wir müssen sie überprüfen, bevor wir mit der Kommunikation fortfahren. Sie informieren uns über einige Fähigkeiten und Einschränkungen des Servers, wie die verfügbare QoS-Stufe und die Verfügbarkeit von aufbewahrten Nachrichten. Wie Sie den Namen und Beschreibungen in der obigen Tabelle entnehmen können, informieren sie uns auch darüber, ob unser CONNECT-Versuch erfolgreich war oder nicht.

Um die Reason Codes zu erhalten, müssen wir zunächst den Pakettyp identifizieren, da wir nur an CONNACK-Paketen interessiert sind.

Wir werden die Tatsache ausnutzen, dass wir eine sehr einfache Funktion benötigen, um den Pakettyp zu ermitteln, um zu beschreiben, wie wir die testgetriebene Entwicklung einsetzen, ein paar Überlegungen zu dieser Technik anstellen und ein paar kurze Beispiele geben. Alle Einzelheiten können Sie den beigefügten Dateien entnehmen.


(CONNACK) Identifizierung des Pakettyps des Servers

Wir wissen mit Sicherheit, dass das erste Byte jedes MQTT-Kontrollpakets den Typ des Pakets kodiert. Wir lesen also einfach dieses erste Byte so schnell wie möglich und haben den Typ des Server-Pakets.

uchar pkt_type = server_response_buffer[0];

Stop. Erledigt. Nächstes Problem. Richtig?

Nun, falsch ist es sicher nicht. Der Code ist klar, die Variablen sind gut benannt, und er sollte leistungsfähig und leichtgewichtig sein.

Doch halt! Wie soll der Code, der unsere Bibliothek verwenden wird, diese Anweisung aufrufen? Der Pakettyp wird durch einen öffentlichen Funktionsaufruf zurückgegeben? Oder kann diese Information als Implementierungsdetail hinter einem privaten Mitglied versteckt werden? Wenn sie durch einen Funktionsaufruf zurückgegeben wird, wo sollte diese Funktion gehostet werden? Bei der Klasse CPktConnect? Oder sollte sie in einer unserer Header-Dateien untergebracht werden, da sie von vielen verschiedenen Klassen verwendet wird? Wenn es in einem privaten Mitglied gespeichert ist, in welcher Klasse sollte es leben?

(TMTOWTDI*) "There is more than one way to do it" (Es gibt mehr als nur einen Weg, das zu tun) ist ein Akronym, das sehr populär wurde. TDD ist ein weiteres Akronym, das aus verschiedenen Gründen sehr populär wurde. Wie früher wurden beide Akronyme missbraucht und gehypt, und einige von ihnen wurden sogar zur „Mode“, wie im Fall von TDD:

__ „Ich sterbe, Mama! Es ist cool.“

Aber die Gruppen, die sie geprägt haben, taten dies nach jahrelanger harter Arbeit, die sich mit der gleichen grundlegenden Frage beschäftigte: Wie kann man performanteren, idiomatischeren und robusteren Code schreiben und gleichzeitig die Produktivität der Entwickler verbessern? Wie können sich die Entwickler auf das konzentrieren, was getan werden muss, anstatt sich mit dem zu beschäftigen, was getan werden kann? Wie kann sich jeder von ihnen auf eine Aufgabe – und nur auf eine Aufgabe – konzentrieren? Wie kann man sicher sein, dass das, was sie tun, nicht zu Regressionsfehlern führt und das System zerstört? 

Kurz gesagt, diese Akronyme, die Ideen, die sie transportieren, und die Techniken, die sie empfehlen, konsolidieren die jahrelange Praxis von Hunderten von sehr unterschiedlichen Personen mit Erfahrung in der Softwareentwicklung. TDD ist keine Theorie, es ist eine Praxis. Man kann sagen, dass TDD eine Technik zur Problemlösung ist, bei der der Anwendungsbereich geschlossen wird, indem das Problem in seine Bestandteile zerlegt wird. Wir müssen die einzige Aufgabe definieren, die uns einen Schritt weiterbringt. Nur ein einziger Schritt. Oftmals ein kleiner Schritt.

Was ist nun unser Problem? Wir müssen erkennen, ob eine Serverantwort ein CONNACK-Paket ist. So einfach ist das. Denn laut Spezifikation müssen wir die CONNACK Response Codes lesen, um zu entscheiden, was als Nächstes zu tun ist. Ich meine, die Identifizierung des Pakettyps, den wir vom Server als Antwort erhalten haben, ist erforderlich, damit wir vom Verbindungsstatus in den Veröffentlichungsstatus wechseln können.

Wie kann man erkennen, ob eine Serverantwort ein CONNACK-Paket ist? Nun, das ist ganz einfach. Er hat einen bestimmten Typ, der in unserem MQTT.mqh-Header als Enumeration kodiert ist, nämlich 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
...

Vielleicht können wir also mit einer Funktion beginnen, die, wenn sie ein Netzwerk-Byte-Array von einem MQTT-Broker erhält, den Typ des Pakets zurückgibt.

Das klingt gut. Lassen Sie uns einen Test für diese Funktion schreiben.

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

Versuchen wir, die Testfunktion zu verstehen, da wir dieses Muster in allen unseren Tests verwenden werden und der Kürze halber hier nicht auf alle eingehen werden.

In den Kommentarzeilen werden die einzelnen Schritte des Musters beschrieben: 

Arrange

Zunächst initialisieren wir ein Array mit einem einzelnen Element, das den Byte-Wert darstellt, den wir von unserer Funktion zurückerwarten.

Zweitens initialisieren wir einen leeren char-Puffer mit einer Größe von eins, um das Ergebnis unseres Funktionsaufrufs zu erhalten.

Wir initialisieren unsern Puffer, das an die zu testende Funktion übergeben wird. Es steht für das erste einzelne Byte, das aus dem festen Header der Serverantwort gelesen wird, um den Pakettyp zu identifizieren. In diesem Fall hat sie ein wrong_first_byte und wir machen es explizit, indem wir die Variable entsprechend benennen.

Act

Wir instanziieren die zu testende Klasse cut und rufen unsere Funktion auf.

Assert

Wir überprüfen die Ungleichheit des erwarteten und des Ergebnis-Arrays, sowohl in Bezug auf den Inhalt als auch auf die Größe, indem wir die MQL5-Funktion ArrayCompare verwenden (siehe die beigefügte Testdatei).

Clean-Up

Zum Schluss bereinigen wir die Ressourcen, indem wir die Instanz „cut“ löschen und unseren Puffer „result“ an ZeroMemory übergeben. Dadurch sollen Speicherlecks und Testverunreinigungen vermieden werden.

Abb. 04 - TEST_CSrvResponse - FAIL - nicht deklarierter Bezeichner

Abb. 04 - TEST_CSrvResponse - FAIL - nicht deklarierter Bezeichner

Sie schlägt bei der Kompilierung fehl, weil die Funktion noch nicht existiert. Wir müssen es schreiben. Aber wo soll es untergebracht werden?

Wir wissen bereits, dass wir den Typ des Antwortpakets immer wieder identifizieren müssen. Jedes Mal, wenn wir ein Paket an unseren Broker senden, schickt er uns eines dieser „Antwort-Dings“. Und dieses „Dings“ ist eine Art MQTT Control Packet. Da es sich also um eine Art von „Ding“ handelt, erscheint es nur natürlich, dass es eine eigene Klasse unter der Gruppe der ähnlichen „Dings“ haben sollte. Sagen wir eine Klasse, die alle Serverantworten unter der Gruppe der Kontrollpakete darstellt.

Sagen wir, eine Klasse CSrvResponse, die die Schnittstelle IControlPacket implementiert.

Wir könnten versucht sein, daraus eine weitere Funktion in unserer bereits existierenden Klasse CPktConnect zu machen. Damit würden wir aber einen wichtigen Grundsatz der objektorientierten Programmierung verletzen: das Prinzip der einzigen Verantwortung (Single Responsibility Principle, SRP).

„Man sollte die Dinge, die sich aus unterschiedlichen Gründen ändern, trennen und die Dinge, die sich aus denselben Gründen ändern, zusammenfassen.“ (R. Martin, The Clean Coder, 2011).

Auf der einen Seite wird sich unsere Klasse CPktConnect ändern, wenn wir die Art und Weise ändern, wie wir Pakete CONNECT erstellen, und auf der anderen Seite wird sich unsere (nicht vorhandene) Klasse CSrvResponse ändern, wenn wir die Art und Weise ändern, wie wir unsere CONNACK-, PUBACK-, SUBACK- und andere Serverantworten lesen. Sie haben also ganz klar unterschiedliche Zuständigkeiten, und das ist in diesem Fall ziemlich leicht zu erkennen. Aber manchmal kann es schwierig sein, zu entscheiden, ob eine Entität der Domäne, die wir modellieren, in einer eigenen Klasse deklariert werden sollte. Durch die Anwendung der SVB haben wir einen objektiven Leitfaden, um über diese „Dings“ zu entscheiden.

Also schreiben wir es, gerade genug, um den Test zu bestehen.

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

Der Test lässt sich kompilieren, schlägt aber erwartungsgemäß fehl, da wir eine „falsche“ Serverantwort übergeben haben, um ihn fehlschlagen zu lassen.

Abb. 05 - TEST_CSrvResponse - FAIL - falsches Paket

Abb. 05 - TEST_CSrvResponse - FAIL - falsches Paket

Wir müssen den „richtigen“ CONNACK-Pakettyp als Serverantwort übergeben. Beachten Sie, dass wir auch hier den Puffer explizit benennen: right_first_byte. Der Name an sich ist nur ein Label. Wichtig ist nur, dass die Bedeutung für jeden, der unseren Code liest, klar ist. Auch uns, sechs Monate oder sechs Jahre später.

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

Abb. 06 - TEST_CSrvResponse - PASS

Abb. 06 - TEST_CSrvResponse - PASS

Gut. Jetzt ist der Test durchlaufen, und wir wissen, dass er zumindest für diese beiden Argumente, eines falsch und eines richtig, nicht bestanden bzw. passiert hat. Später können wir das bei Bedarf ausführlicher testen.

In diese einfachen Schritte sind die drei grundlegenden „Gesetze“ von TDD eingebettet, wie sie von R. Martin zusammengefasst wurden.

  1. Sie dürfen keinen Produktionscode schreiben, bevor Sie nicht einen fehlgeschlagenen Einheitstest geschrieben haben.
  2. Es ist nicht erlaubt, mehr von einem Unit-Test zu schreiben, als zum Scheitern ausreicht, und nicht zu kompilieren ist ein Scheitern.
  3. Sie dürfen nicht mehr Produktionscode schreiben, als zum Bestehen des aktuell fehlgeschlagenen Einheitstests erforderlich ist.

Gut. Genug über TDD für den Moment. Kehren wir zu unserer eigentlichen Aufgabe zurück und lesen wir die Connect Reason Codes in den CONNACK-Paketen, die vom Server ankommen.


(Connect Reason Codes) Was tun bei nicht verfügbaren Funktionen auf dem Server?

Zwei der Connect Reason Codes verdienen an dieser Stelle unsere Aufmerksamkeit.

  1. QoS not supported
  2. Retain not supported

Sie sind etwas Besonderes, weil sie nicht auf einen Fehler, sondern auf eine Serverbeschränkung hinweisen. Wenn unser CONNACK einen dieser Connect Reason Codes hat, sagt der Server, dass unsere Netzwerkverbindung erfolgreich war, dass unser CONNECT ein wohlgeformtes Paket war und dass der Server online ist und funktioniert, aber nicht in der Lage ist, unsere Anforderungen zu erfüllen. Wir müssen Maßnahmen ergreifen. Wir müssen entscheiden, was wir als Nächstes tun.

Was sollen wir tun, wenn wir ein CONNECT mit Will QoS 2 senden und der Server mit QoS Maximum 1 antwortet? Sollen wir das CONNECT mit dem heruntergestuften QoS-Flag erneut senden? Oder sollten wir vor der Herabstufung ein DISCONNECT machen? Wenn das bei der RETAIN-Funktion der Fall war, können wir sie dann einfach als irrelevant ignorieren und trotzdem mit der Veröffentlichung beginnen? Oder sollten wir das CONNECT vor der Veröffentlichung mit herabgestuften Flags erneut senden?

Was sollten wir tun, nachdem wir ein erfolgreiches CONNACK erhalten haben, was bedeutet, dass der Server unsere Verbindung akzeptiert hat und über alle von uns angeforderten Fähigkeiten verfügt? Müssen wir sofort mit dem Senden von PUBLISH-Paketen beginnen? Oder können wir die Verbindung offen lassen, indem wir aufeinanderfolgende PINGREQ-Pakete senden, bis wir bereit sind, eine Nachricht zu veröffentlichen? Müssen wir übrigens vor der Veröffentlichung ein Thema ABONNIEREN?

Die meisten dieser Fragen werden durch den Standard beantwortet. Sie müssen AS-IS implementieren, um einen MQTT v5.0-konformen Client zu haben. Viele Entscheidungen werden den Anwendungsentwicklern überlassen. Vorerst werden wir uns nur mit dem beschäftigen, was erforderlich ist, damit wir so bald wie möglich einen konformen Client haben können.

Gemäß dem Standard kann der Client nur dann eine QoS-Stufe > 0 anfordern, wenn das Will-Flag ebenfalls auf 1 gesetzt ist, d. h. wir dürfen nur dann eine QoS-Stufe > 0 anfordern, wenn wir auch eine Will-Meldung im CONNECT-Paket senden. Aber wir wollen, oder besser gesagt, wir müssen uns jetzt nicht mit Will Message(s) beschäftigen. Unsere Entscheidung ist also ein Kompromiss zwischen der Entscheidung, nur das zu verstehen, was wir jetzt wissen müssen, und dem Versuch, alle Feinheiten der Norm zu verstehen und dabei möglicherweise Code zu schreiben, der später nicht mehr nützlich ist. 

Wir müssen nur wissen, was unser Client tun wird, wenn die angeforderte QoS-Stufe oder Retain auf dem Server nicht verfügbar ist. Und das müssen wir wissen, sobald ein neues CONNACK eintrifft. Daher wird im Konstruktor der CSrvResponse danach gesucht. Wenn die Antwort ein CONNACK ist, ruft der Konstruktor die geschützte Methode GetConnectReasonCode auf.

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

Wenn der Connect Reason Code einer von MQTT_REASON_CODE_QOS_NOT_SUPPORTED oder MQTT_REASON_CODE_RETAIN_NOT_SUPPORTED ist, wird diese Information im Serverprofil gespeichert. Vorerst werden wir nur diese Informationen über den Server speichern und auf einen DISCONNECT warten. Später werden wir sie verwenden, wenn wir eine neue Verbindung zu diesem Server anfordern. Beachten Sie, dass dieses „später“ einige Millisekunden nach dem ersten Verbindungsversuch liegen kann. Oder es kann Wochen zu spät sein. Der Punkt ist, dass wir diese Informationen im Serverprofil gespeichert haben werden.


Wie wir geschützte Methoden testen

Um geschützte Methoden zu testen, haben wir in unserem Testskript eine Klasse erstellt, die von unserer zu testenden Klasse, in diesem Fall CSrvResponse, abgeleitet ist. Dann rufen wir die geschützten Methoden von CSrvResponse über diese abgeleitete Klasse „nur zu Testzwecken“ auf, die wir TestProtectedMethods nennen.

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

Beachten Sie, dass wir nichts im Serverprofil speichern. Tatsächlich gibt es das Serverprofil noch gar nicht. Wir drucken lediglich eine Meldung aus, dass das Serverprofil aktualisiert wird. Das liegt daran, dass das Serverprofil zwischen den Sitzungen unseres Clients persistiert werden soll, und wir uns noch nicht mit Persistenz beschäftigen. Später, bei der Implementierung der Persistenz, können wir diese Stub-Funktion ändern, um das Serverprofil z. B. in einer SQLite-Datenbank zu persistieren, ohne die gedruckte (oder protokollierte) Nachricht entfernen zu müssen. Es ist nur so, dass es im Moment nicht umgesetzt wird. Wie bereits erwähnt, müssen wir zu diesem Zeitpunkt nur wissen, was zu tun ist, wenn der Server nicht den von uns angeforderten Fähigkeiten entspricht: Wir speichern die Informationen, um sie später wieder zu verwenden.


Schlussfolgerung

In diesem Artikel haben wir beschrieben, wie wir uns mit dem Kapitel über das operative Verhalten des MQTT v5.0-Protokolls befassen, wie es der OASIS-Standard vorschreibt, um so bald wie möglich einen konformen Client zur Verfügung zu haben. Wir haben beschrieben, wie wir die Klasse CSrvResponse implementieren, um den Antworttyp des Servers und die damit verbundenen Reason Codes zu identifizieren. Wir haben auch beschrieben, wie unser Client auf nicht verfügbare Serverkapazitäten reagieren wird.

Im nächsten Schritt werden wir PUBLISH implementieren, das Betriebsverhalten für die QoS-Stufe(n) besser verstehen und uns mit der/den Sitzung(en) und ihrer nahezu erforderlichen Persistenz befassen.

** Andere nützliche Akronyme: DRY, KISS, YAGNI. Jeder von ihnen enthält einige praktische Weisheiten, aber YMMV :) 

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

Beigefügte Dateien |
headers.zip (7.16 KB)
tests.zip (3 KB)
Schätzung der zukünftigen Leistung mit Konfidenzintervallen Schätzung der zukünftigen Leistung mit Konfidenzintervallen
In diesem Artikel befassen wir uns mit der Anwendung von Bootstrapping-Techniken (Bootstrapping: am eigenen Schopf aus dem Sumpf ziehen) als Mittel zur Schätzung der künftigen Leistung einer automatisierten Strategie.
Neuronale Netze leicht gemacht (Teil 56): Nuklearnorm als Antrieb für die Erkundung nutzen Neuronale Netze leicht gemacht (Teil 56): Nuklearnorm als Antrieb für die Erkundung nutzen
Die Untersuchung der Umgebung beim Verstärkungslernen ist ein dringendes Problem. Wir haben uns bereits mit einigen Ansätzen beschäftigt. In diesem Artikel werden wir uns eine weitere Methode ansehen, die auf der Maximierung der Nuklearnorm beruht. Es ermöglicht den Agenten, Umgebungszustände mit einem hohen Maß an Neuartigkeit und Vielfalt zu erkennen.
Wie man einen einfachen EA für mehrere Währungen mit MQL5 erstellt (Teil 2): Indikator-Signale: Multi-Zeitrahmen Parabolic SAR Indikator Wie man einen einfachen EA für mehrere Währungen mit MQL5 erstellt (Teil 2): Indikator-Signale: Multi-Zeitrahmen Parabolic SAR Indikator
Der Expert Advisor für mehrere Währungen in diesem Artikel ist ein Expert Advisor oder Handelsroboter, der handeln kann (z.B. Aufträge öffnen, schließen und verwalten, Trailing Stop Loss und Trailing Profit) für mehr als 1 Symbolpaar von nur einem Symbolchart aus. Dieses Mal werden wir nur 1 Indikator verwenden, nämlich den Parabolic SAR oder iSAR in mehreren Zeitrahmen von PERIOD_M15 bis PERIOD_D1.
Erstellung eines Dashboards zur Anzeige von Daten in Indikatoren und EAs Erstellung eines Dashboards zur Anzeige von Daten in Indikatoren und EAs
In diesem Artikel werden wir eine Dashboard-Klasse erstellen, die in Indikatoren und EAs verwendet werden kann. Dies ist ein einleitender Artikel in einer kleinen Serie von Artikeln mit Vorlagen für die Einbeziehung und Verwendung von Standardindikatoren in Expert Advisors. Ich beginne mit der Erstellung eines Panels, das dem MetaTrader 5-Datenfenster ähnelt.