English Русский 日本語
preview
Entwicklung eines MQTT-Clients für Metatrader 5: ein TDD-Ansatz - Teil 5

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

MetaTrader 5Integration | 30 April 2024, 09:17
117 0
Jocimar Lopes
Jocimar Lopes

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

Einführung

MQTT ist ein Pub/Sub-Nachrichtenaustauschprotokoll. Wir können also davon ausgehen, dass der Schwerpunkt auf PUBLISH- und SUBSCRIBE-Paketen liegt. Alle anderen Pakettypen gibt es, um sie zu erreichen.

Wir müssen nicht nur in der Lage sein, PUBLISH-Pakete zu schreiben, sondern auch, sie zu lesen, da die Nachrichten, die unser Client von anderen Clients erhält, ebenfalls PUBLISH-Pakete sind. Das liegt daran, dass das Übertragungsprotokoll symmetrisch ist.

„Ein PUBLISH-Paket wird von einem Client zu einem Server oder von einem Server zu einem Client gesendet, um eine Anwendungsnachricht zu transportieren.“

PUBLISH-Pakete haben einen anderen festen Header mit Publish Flags und einen variablen Header mit einem erforderlichen Topic Name, kodiert als UFT-8-String, und einem erforderlichen Packet Identifier (wenn QoS > 0). Darüber hinaus kann es fast alle Eigenschaften und Nutzereigenschaften verwenden, die in MQTT 5.0 eingeführt wurden, einschließlich der Eigenschaften, die sich auf den Request/Response-Interaktionsmodus beziehen.

In diesem Artikel sehen wir uns die Struktur der Header an und wie wir die Publish Flags, den/die Topic Name(s) und den/die Packet Identifier testen und implementieren. 

In den folgenden Beschreibungen werden die Begriffe MUST und MAY so verwendet, wie sie im OASIS-Standard verwendet werden, der sie wiederum wie in IETF RFC 2119 beschrieben verwendet.

Wenn nicht anders angegeben, stammen alle Zitate aus dem OASIS Standard.


Aufbau des Fixed Header eines MQTT 5.0 PUBLISH Pakets

Der feste Header des PUBLISH-Pakets folgt der gleichen Zwei-Byte-Grundstruktur wie alle anderen Kontrollpakete. Das erste Byte ist für die Übertragung des Pakettyps bestimmt. Das zweite Byte ist der Host des Pakets Verbleibende Länge kodiert als Variable Byte Integer.

Während jedoch bei allen anderen Pakettypen die ersten vier Bits des ersten Bytes den Status RESERVED haben, verwendet das PUBLISH-Paket diese vier Bits, um drei Merkmale zu kodieren: RETAIN, QoS-Stufe und DUP.

MQTT-Kontroll-Paket Feste Kopfzeilen-Flags Bit 3 Bit 2 Bit 1 Bit 0
CONNECT Reserved 0 0 0 0
CONNACK Reserved
0 0 0 0
PUBLISH Verwendet in MQTT v5.0 DUP QoS 2 QoS 1 RETAIN
PUBACK Reserved
0 0 0 0
PUBREC Reserved
0 0 0 0
PUBREL Reserved
0 0 1 0
PUBCOMP Reserved
0 0 0 0
SUBSCRIBE Reserved
0 0 1 0
SUBACK Reserved 0 0 0 0
UNSUBSCRIBE Reserved
0 0 1 0
UNSUBACK Reserved
0 0 0 0
PINGREQ Reserved
0 0 0 0
PINGRESP Reserved
0 0 0 0
DISCONNECT Reserved
0 0 0 0
AUTH Reserved
0 0 0 0

Tabelle 1 - Reproduktion der Tabelle 2-3 Flag Bits aus dem MQTT 5.0 Oasis Standard

„Wenn ein Flaggenbit als ‚Reserved‘ gekennzeichnet ist, ist es für die künftige Verwendung reserviert und MUSS auf den angegebenen Wert gesetzt werden.

Aufgrund dieses Unterschieds zwischen PUBLISH-Paketen und allen anderen Kontrollpaketen kann die Funktion, die wir zur Erzeugung fester Header verwendet haben, hier nicht eingesetzt werden.

//+------------------------------------------------------------------+
//|                     SetFixedHeader                               |
//+------------------------------------------------------------------+
void SetFixedHeader(ENUM_PKT_TYPE pkt_type, uchar& buf[], uchar& dest_buf[])
  {
   dest_buf[0] = (uchar)pkt_type << 4;
   dest_buf[1] = EncodeVariableByteInteger(buf);
  }

Wie Sie sehen können, enthalten die Funktionsparameter nur den Pakettyp und Verweise auf zwei Arrays, von denen eines die Quelle und das andere das Ziel des festen Header-Arrays ist. In der ersten Zeile wird dann der Integer-Wert des Pakettyps aus einer Enum entnommen und um vier Bits nach links verschoben, wobei das Ergebnis der bitweisen Operation dem ersten Byte des festen Header-Arrays (dest_buf[0]) zugewiesen wird. Durch diese bitweise Operation wird sichergestellt, dass die ersten vier Bits nicht zugewiesen werden oder ‚Reserved‘ sind, wie es die Norm verlangt.

Die zweite Zeile ruft die Funktion auf, die die verbleibende Länge des Pakets berechnet und den Wert dem zweiten Byte des festen Header-Arrays (dest_buf[1]) zuweist, das als Variable Byte Integer kodiert ist.

Diese Funktion bietet jedoch keine Möglichkeit, die Veröffentlichungskennzeichen zu setzen.

Fig. 1 - MQTT 5.0 PUBLISH Packet Fixed Header RETAIN, QoS Level, and DUP flags

Abb. 1 - MQTT 5.0 PUBLISH packet Fixed Header RETAIN, QoS Level, und DUP Flags

Daher haben wir einen Switch hinzugefügt, der die PUBLISH-Pakete aufnimmt, und einen letzten Parameter, um die Publish Flags zu empfangen. Wir hätten die Funktion überladen können, um die Publish Flags zu empfangen, und ihren Körper leicht modifizieren können, um die Besonderheiten der PUBLISH-Pakete zu implementieren. Dies ist jedoch ein perfekter Anwendungsfall für einen Switch, da es nur eine Ausnahme gibt (PUBLISH) und in allen anderen Fällen die vorherige Implementierung verwendet wird.

Der letzte Parameter ist standardmäßig Null, d. h. er kann beim Setzen aller unveränderlichen Header des Pakets ignoriert werden. Es wird den dest_buf nur ändern, wenn irgendwelche Publish Flags gesetzt sind.

//+------------------------------------------------------------------+
//|                     SetFixedHeader                               |
//+------------------------------------------------------------------+
void SetFixedHeader(ENUM_PKT_TYPE pkt_type,
                    uchar& buf[], uchar& dest_buf[], uchar publish_flags = 0)
  {
   switch(pkt_type)
     {
      case PUBLISH:
         dest_buf[0] = (uchar)pkt_type << 4;
         dest_buf[0] |= publish_flags;
         dest_buf[1] = EncodeVariableByteInteger(buf);
         break;
      default:
         dest_buf[0] = (uchar)pkt_type << 4;
         dest_buf[1] = EncodeVariableByteInteger(buf);
         break;
     }
  }

Wie Sie sehen, wird der Zielpuffer, der den festen Header beherbergt, durch eine bitweise ODER-Verknüpfung in Verbindung mit der Zuweisung des ersten Bytes des Headers geändert. Wir haben dieses Muster ausgiebig verwendet, um die Connect-Flags umzuschalten, und jetzt verwenden wir das gleiche Muster, um die Publish-Flags umzuschalten.

Zum Beispiel wird das RETAIN-Flag mit dem folgenden Code gesetzt/entsetzt. 

//+------------------------------------------------------------------+
//|               CPktPublish::SetRetain                             |
//+------------------------------------------------------------------+
void CPktPublish::SetRetain(const bool retain)
  {
   retain ? m_publish_flags |= RETAIN_FLAG : m_publish_flags &= ~RETAIN_FLAG;
   SetFixedHeader(PUBLISH, m_buf, ByteArray, m_publish_flags);
  }

Das QoS_1 Level Flag (ohne ähnliche Funktionssignatur).

QoS_1 ? m_publish_flags |= QoS_1_FLAG : m_publish_flags &= ~QoS_1_FLAG;   
SetFixedHeader(PUBLISH, m_buf, ByteArray, m_publish_flags);

Das QoS_2-Level-Flag.

QoS_2 ? m_publish_flags |= QoS_2_FLAG : m_publish_flags &= ~QoS_2_FLAG;
SetFixedHeader(PUBLISH, m_buf, ByteArray, m_publish_flags);

Die DUP-Flagge.

dup ? m_publish_flags |= DUP_FLAG : m_publish_flags &= ~DUP_FLAG;
SetFixedHeader(PUBLISH, m_buf, ByteArray, m_publish_flags);

Die Werte der Flags (Flags-Masken) sind Konstanten, die in einer Enum als Zweierpotenzen entsprechend der Position des jeweiligen Bits auf dem umzuschaltenden Byte definiert sind.

//+------------------------------------------------------------------+
//|             PUBLISH - FIXED HEADER - PUBLISH FLAGS               |
//+------------------------------------------------------------------+
enum ENUM_PUBLISH_FLAGS
  {
   RETAIN_FLAG  	= 0x01,
   QoS_1_FLAG           = 0x02,
   QoS_2_FLAG           = 0x04,
   DUP_FLAG             = 0x08
  };

Die Flags haben also die folgenden binären Werte und Positionen auf dem Byte.

RETAIN

Dezimalwert 1 0 0 0 0 0 0 0 1

QoS 1

Dezimalwert 2 0 0 0 0 0 0 1 0

QoS 2

Dezimalwert 4 0 0 0 0 0 1 0 0

DUP

Dezimalwert 8 0 0 0 0 1 0 0 0

Der Dezimalwert des PUBLISH-Pakets ist 3.

Dezimalwert 3 0 0 0 0 0 0 1 1

Wir haben den Wert des Pakettyps um vier Bits nach links verschoben (dest_buf[0] = (uchar)pkt_type << 4).

Dezimalwert 48 0 0 1 1 0 0 0 0

Wenn wir die bitweise ODER-Verknüpfung ( dest_buf[0] |= publish_flags; ) auf die binäre Darstellung des Pakettypwerts und der Flags anwenden, verschmelzen wir im Wesentlichen die Bits. Die binäre Darstellung des nach links verschobenen PUBLISH-Paketwertes mit gesetztem DUP-Flag lautet also wie folgt.

Dezimalwert 56 0 0 1 1 1 0 0 0

Mit gesetzten RETAIN- und QoS 2-Flags würden die Bits des ersten Bytes des festen Headers wie folgt aussehen.

Dezimalwert 53 0 0 1 1 0 1 0 1

Umgekehrt bewirkt die bitweise UND-Verknüpfung zwischen dem Wert des Pakettyps und dem Einerkomplement (~) der Binärdarstellung der Flags das Gegenteil, d. h. das Flag wird zurückgesetzt ( m_publish_flags &= ~RETAIN_FLAG ).

Wenn also das Byte mit QoS 1 ohne DUP oder RETAIN eingestellt wurde, würde es wie folgt aussehen.

Dezimalwert 50 0 0 1 1 0 0 1 0

Das Einerkomplement des obigen QoS-1-Flags ist der Wert aller seiner Bits, die umgedreht werden.

QoS_1 Flag 0 0 1 0
~QoS_1 Flag 1 1 0 1

Da jeder Wert UND Null gleich Null ist, heben wir das Flag effektiv auf.

Beachten Sie bitte, dass sich der Binärwert des Bytes natürlich ändert, wenn wir die Flags setzen. Wenn alle Flags nicht gesetzt sind, hat es nach der Linksverschiebung des Dezimalwerts 3 um vier Bits den Dezimalwert 48. Wenn wir das RETAIN-Flag setzen, hat es den Dezimalwert von 49. Mit RETAIN und QoS 1 wird der Wert 51. Und so weiter.

Diese Dezimalwerte sind die Werte, nach denen wir suchen, wenn wir in unseren Tests alle möglichen Kombinationen des Setzens/Entfernens der Flags untersuchen.

//+------------------------------------------------------------------+
//|              TEST_SetFixedHeader_DUP_QoS2_RETAIN                 |
//+------------------------------------------------------------------+
bool TEST_SetFixedHeader_DUP_QoS2_RETAIN()
  {
   Print(__FUNCTION__);
//--- Arrange
   static uchar expected[] = {61, 0};
   uchar buf[] = {};
//--- Act
   CPktPublish *cut = new CPktPublish(buf);
   cut.SetDup(true);
   cut.SetQoS_2(true);
   cut.SetRetain(true);
   uchar result[];
   ArrayCopy(result, cut.ByteArray);
//--- Assert
   bool isTrue = AssertEqual(expected, result);
//--- cleanup
   delete cut;
   ZeroMemory(result);
   return isTrue;
  }

Diese etwas naiven Tests (und andere, etwas aufwändigere), die vor der Implementierung geschrieben werden, leiten unsere Entwicklung, weil sie nicht nur dafür sorgen, dass wir uns auf die anstehende Aufgabe konzentrieren, sondern auch als „Sicherheitsnetz“ dienen, wenn wir den Code ändern oder umgestalten müssen. Sie finden viele davon in den beigefügten Dateien. 

Wenn Sie die Tests durchführen, sollten Sie in etwa Folgendes sehen.

Abb. 2 - MQTT 5.0 PUBLISH Test Output Fixed Header

Abb. 2 - MQTT 5.0 PUBLISH Test Output Fixed Header

Wenn der Publish/Subscribe-Zyklus der Kern des Protokolls ist, sind diese drei Funktionen (RETAIN, DUP und QoS) der Kern des operationellen Verhaltens des Protokolls. Sie werden wohl einen großen Einfluss auf die Verwaltung des Sitzungsstaates haben. Lassen Sie uns also ein wenig über die strenge Protokollspezifikation hinausgehen und versuchen, ein vernünftiges Verständnis ihrer Semantik zu entwickeln.

RETAIN

Wie wir im ersten Teil dieser Serie gesehen haben, ist das Veröffentlichungs-/Abonnementmuster an einen bestimmten Topic-Namen gebunden: Ein Client veröffentlicht eine Nachricht mit einem Topic-Namen oder abonniert einen Topic-Namen, und alle Clients erhalten Nachrichten, die unter dem Topic-Namen veröffentlicht werden, den sie abonniert haben. 

Bei der Veröffentlichung kann das RETAIN-Flag auf 1 (one/true) gesetzt werden, um den Server anzuweisen, die Nachricht zu speichern und sie als „aufbewahrte Nachricht“ an die neuen Abonnenten zuzustellen. Es gibt immer nur eine aufbewahrte Nachricht und wir setzen RETAIN auf 1, um vorhandene aufbewahrte Nachrichten zu speichern/zu ersetzen. Wir senden eine Null-Byte-Nutzlast mit diesem Flag auf 1 gesetzt, um zurückgehaltene Nachrichten zu bereinigen. Wir setzen ihn auf 0, um den Server anzuweisen, nichts mit zurückbehaltenen Nachrichten unter diesem Topic Name (Themennamen) zu tun, weder zu speichern, noch zu ersetzen, noch zu bereinigen.

Wenn wir einen Topic Name abonnieren, erhalten wir die erhaltene Nachricht. Bei gemeinsam genutzten Abonnements wird die aufbewahrte Nachricht nur an einen der Clients des gemeinsam genutzten Topic-Filters gesendet. Bei der Behandlung von SUBSCRIBE-Paketen werden wir uns eingehend mit dem/den gemeinsamen Abonnement(en) befassen.

Diese Funktion arbeitet in Verbindung mit den Flags Retain Available und Retain Not Supported in den vom Server gesendeten CONNACK-Paketen. 

Zurückgehaltene Meldungen verfallen wie jede andere Meldung entsprechend dem in PUBLISH oder in den Will-Eigenschaften der CONNECT-Nutzdaten eingestellten Meldungsverfallsintervall.

Wir müssen berücksichtigen, dass RETAIN eine dynamische Maklerfunktion ist, was bedeutet, dass sie in derselben Sitzung von „verfügbar“ zu „nicht unterstützt“ und umgekehrt wechseln kann.

QoS-Ebene

Wir haben bereits im Einführungsartikel dieser Serie über die QoS-Ebene gesprochen, als wir einige von den Entwicklern des Protokolls getroffene Designentscheidungen aufzählten.

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 Datenbereitstellung in Servicequalität mit kontinuierlicher Sitzungserkennung bieten, die es ermöglicht, mit unzuverlässigen oder sogar unterbrochenen Internetverbindungen umzugehen.“

Im Zusammenhang mit den Connect-Flags wurde die folgende Tabelle mit der Definition der einzelnen QoS-Stufen erstellt.

QoS-Wert Bit 2 Bit 1 Beschreibung
0 0 0 Höchstens eine Lieferung
1 0 1 Mindestens eine Lieferung
2 1 0 Genau eine Lieferung
- 1 1 Reserviert - darf nicht verwendet werden

Tabelle 2 - Wiedergabe der Tabelle 3-9 QoS-Definitionen aus dem MQTT 5.0 Oasis Standard

Bei der Beschreibung der Verwendung von QoS-Stufen und anderen Funktionen haben wir die Begriffe „Server“ und „Broker“ verwendet, um Wenn alle Flags nicht gesetzt sind, hat es nach der Linksverschiebung des Dezimalwerts 3 um vier Bits den Dezimalwert 48.den Dienst zu bezeichnen, der unsere Nachrichten verteilen wird. Aber laut Standard:

Das Übermittlungsprotokoll ist symmetrisch, d.h. in der folgenden Beschreibung können Client und Server jeweils entweder die Rolle des Senders oder des Empfängers einnehmen. Das Zustellungsprotokoll befasst sich ausschließlich mit der Zustellung einer Anwendungsnachricht von einem einzelnen Sender zu einem einzelnen Empfänger. Wenn der Server eine Anwendungsnachricht an mehr als einen Client sendet, wird jeder Client unabhängig behandelt. Die QoS-Stufe, die für die Zustellung einer ausgehenden Anwendungsnachricht an den Client verwendet wird, kann sich von der der eingehenden Anwendungsnachricht unterscheiden.“ (Hervorhebung durch uns)

Die Verwendung der Begriffe „Server“ und „Makler“ in dem Sinne, wie wir sie bisher verwendet haben, ist also gerechtfertigt, weil wir im weitesten Sinne aus der Sicht des Kunden sprechen, aber beachten Sie diese Symmetrie im Übertragungsprotokoll.

Die Standard-QoS-Stufe ist 0, d. h., wenn wir dieses Flag nicht setzen, teilen wir dem Server mit, dass 0 (Null) die maximale QoS-Stufe ist, die wir akzeptieren. Jeder vorschriftsmäßige Makler akzeptiert dieses Niveau. Es handelt sich um eine „fire and forget“-Veröffentlichung, bei der der Absender akzeptiert, dass bei der Zustellung sowohl ein Verlust als auch eine Vervielfältigung auftreten kann.

Abb.3 - MQTT 5.0 - QoS-Level 0 Client-Server-Flussdiagramm

Abb. 3 - MQTT 5.0 - QoS Level 0 Client-Server Flussdiagramm

Die QoS-Stufe 1 akzeptiert, dass es bei der Zustellung zu Überschneidungen kommen kann, nimmt aber keine Verluste in Kauf. Der Server quittiert die Nachricht mit einem PUBACK.

Abb. 4 - MQTT 5.0 - QoS-Level 1 Client-Server-Flussdiagramm

Abb. 4 - MQTT 5.0 - QoS Level 1 Client-Server Flussdiagramm

Die QoS-Stufe 2 erfordert keine Verluste oder Duplikate. In dieser Stufe gibt es vier Pakete. Der Server erkennt, dass die Lieferung mit einem PUBREC beginnt. Dann bittet der Client mit einem PUBREL um die Freigabe dieses spezifischen Packet Identifier, und schließlich meldet der Server mit einem PUBCOMP die Fertigstellung der Lieferung.

Abb. 5 - MQTT 5.0 - QoS-Level 2 Client-Server-Flussdiagramm

Abb. 5 - MQTT 5.0 - QoS Level 2 Client-Server Flussdiagramm

Eine Analogie aus dem vorherigen Artikel, in dem wir über Connect Flags gesprochen haben:

Man kann sich diese [QoS 2]-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 sie das Paket zustellen, schicken sie Ihnen eine vom Empfänger unterzeichnete Quittung, die die Zustellung des Pakets bestätigt.

Die Dienstgüte kann für die Will-Nachricht, für ein Abonnement (einschließlich gemeinsamer Abonnements) oder für eine bestimmte Nachricht erforderlich sein. 

Will Nachricht Abonnement Nachricht
CONNECT wird QoS SUBSCRIBE Abonnement-Optionen PUBLISH QoS Level Flag

Tabelle 3 - MQTT 5.0-Pakete und Flags, bei denen die QoS-Stufe eingestellt werden kann

Der aufmerksame Leser hat vielleicht bemerkt, dass sowohl QoS 1 als auch QoS 2 eine Art Sitzungsstatus beinhalten. Mit dem Sitzungsstatus und der dazugehörigen Persistenzschicht werden wir uns in einem Artikel beschäftigen, der sich ausschließlich diesem umfangreichen Thema widmet.

DUP

Wenn das DUP-Flag gesetzt ist, bedeutet dies, dass wir erneut versuchen, ein zuvor fehlgeschlagenes PUBLISH-Paket zu senden. Er MUSS für alle QoS-0-Meldungen auf 0 (Null) gesetzt werden. Die Vervielfältigung bezieht sich auf das Paket selbst und nicht auf die Nachricht.


Variabler Header eines MQTT 5.0 PUBLISH-Pakets: Topic Name, Paketidentifikator und Eigenschaften

Der Variable Header eines MQTT 5.0 PUBLISH-Pakets MUSS einen Topic Name und, wenn die QoS größer als 0 (Null) ist, auch einen Packet Identifier enthalten. Auf diese beiden Felder folgen in der Regel eine Reihe von Eigenschaften und eine Nutzlast, aber ein PUBLISH-Paket ohne Eigenschaften und eine Nutzlast von Null-Länge ist ein gültiges Paket. Mit anderen Worten, das einfachste gültige PUBLISH-Paket ist dasjenige mit einem festen Header mit QoS 0, ohne DUP- und RETAIN-Flags und einem variablen Header mit nur einem Topic-Namen.

Topic Name

Da sich alle Interaktionen zwischen Clients und Server - und damit auch alle Interaktionen zwischen den Nutzern/Geräten - in einem Pub/Sub-Nachrichtenaustauschprotokoll um die Veröffentlichung in einem Thema und das Abonnieren eines Themas drehen, kann man sagen, dass das Feld Topic Name hier besondere Aufmerksamkeit verdient. In vielen Echtzeitdiensten findet man den Begriff „channel“ anstelle von Topic Name. Dies ist sinnvoll, da der Topic Name den Informationskanal darstellt, den die Clients abonniert haben.

Ein Topic Name ist eine UTF-8 kodierte Zeichenkette, die in einer hierarchischen Baumstruktur organisiert ist. Der Schrägstrich ( / U+002F ) wird als Trennzeichen der Topic Name verwendet. 

broker1/Konto12345/EURUSD

Groß- und Kleinschreibung wird beachtet. Es handelt sich also um zwei verschiedene „Topics“.

  • broker1/Konto12345/EURUSD
  • broker1/account12345/eurusd

Diese Ebenentrennzeichen sind nur von Bedeutung, wenn eines der Platzhalterzeichen des Themenfilters (siehe unten) auf dem Client-Abonnement vorhanden ist. Für die Anzahl der Ebenen gibt es keine Grenzen, außer der Grenze der UTF-8-Zeichenfolge selbst. Eventuell kann Topic Name durch einen Topic Alias (Themen-Alias) ersetzt werden.

Ein Topic Alias ist ein ganzzahliger Wert, der zur Identifizierung des Themas verwendet wird, anstatt den Topic Name zu verwenden. Dies reduziert die Größe des PUBLISH-Pakets und ist nützlich, wenn die Topic Name lang sind und dieselben Topic Name wiederholt innerhalb einer Netzwerkverbindung verwendet werden.

Packet Identifier

Packet Identifier (Paketidentifikator) ist ein Zwei-Byte-Ganzzahlfeld, das für PUBLISH-Pakete mit QoS > 0 erforderlich ist. Er wird in allen Paketen verwendet, die direkt am Pub/Sub-Zyklus zur Verwaltung des Sitzungsstatus beteiligt sind. Packet Identifier DARF NICHT in PUBLISH mit QoS 0 verwendet werden.

Er wird verwendet, um PUBLISH mit den zugehörigen ACKs zu verbinden.

Bitte denken Sie daran, dass, da das Zustellungsprotokoll symetrisch ist, bei der Verwendung von QoS 1 unser Client ein PUBLISH vom Server mit der gleichen Paket-ID erhalten kann, bevor er das PUBACK für ein zuvor gesendetes PUBLISH erhält.

„Es ist möglich, dass ein Client ein PUBLISH-Paket mit Packet Identifier 0x1234 sendet und dann ein anderes PUBLISH-Paket mit Packet Identifier 0x1234 von seinem Server empfängt, bevor er ein PUBACK für das gesendete PUBLISH-Paket erhält.“

Es ist erwähnenswert, dass der Packet Identifier auch verwendet wird, um die entsprechenden ACKs in SUBSCRIBE- und UNSUBSCRIBE-Paketen zu verbinden.


Wie wir schreiben Topic Name(s)

Der Name des Themas ist das erste Feld in der Kopfzeile der Variablen. Er ist als UTF-8-Zeichenfolge mit einigen unzulässigen Unicode-Codepunkten kodiert, und hier gibt es einen Haken. Bitte sehen Sie sich diese drei Anweisungen mit einigen der Anforderungen an die Kodierung eines UTF-8-Strings für MQTT 5.0 an.

„[...] die Zeichendaten DÜRFEN KEINE Kodierungen von Codepunkten zwischen U+D800 und U+DFFF enthalten. Wenn der Client oder Server ein MQTT-Kontrollpaket empfängt, das schlecht geformtes UTF-8 enthält, handelt es sich um ein missgebildetes Paket (Malformed Packet).“

„Eine UTF-8-kodierte Zeichenkette DARF KEINE Kodierung des Nullzeichens U+0000 enthalten. Wenn ein Empfänger (Server oder Client) ein MQTT-Kontrollpaket empfängt, das U+0000 enthält, handelt es sich um ein missgebildetes Paket.“

„Die Daten SOLLTEN KEINE Kodierungen der unten aufgeführten Unicode [Unicode]-Codepunkte enthalten. Wenn ein Empfänger (Server oder Client) ein MQTT-Kontrollpaket empfängt, das eines dieser Pakete enthält, DARF er es als missgebildetes Paket (Malformed Packet) behandeln. Dies sind die nicht zugelassenen Unicode-Codepunkte.

U+0001..U+001F Steuerzeichen

U+007F..U+009F Steuerzeichen

Codepunkte, die in der Unicode-Spezifikation [Unicode] als Nicht-Zeichen definiert sind“

Wie Sie sehen können, sind sowohl die erste als auch die zweite obige Anweisung strikt (DARF NICHT), was bedeutet, dass jede konforme Implementierung das Vorhandensein der nicht zugelassenen Codepunkte prüft, während die dritte Anweisung eine Empfehlung ist (SOLLTE NICHT), was bedeutet, dass eine Implementierung das Vorhandensein der nicht zugelassenen Codepunkte nicht prüfen muss und dennoch als konform gilt.

Da ein „Malformed Packet“ ein Grund für eine DISCONNECT ist, können wir, wenn wir diese Codepunkte in unserem Client zulassen und unser Broker sich dafür entscheidet, sie nicht als „Malformed Packet“ zu behandeln, die Disconnection anderer Clients verursachen, die die Empfehlung durchsetzen. Obwohl der Ausschluss von Unicode-Steuerzeichen und Nicht-Zeichen nur eine Empfehlung ist, lassen wir sie in unserer Implementierung nicht zu.

Im Moment sieht unsere Funktion zur Kodierung von Zeichenketten als UTF-8 wie folgt aus:

//+------------------------------------------------------------------+
//|                    Encode UTF-8 String                           |
//+------------------------------------------------------------------+
void EncodeUTF8String(string str, ushort& dest_buf[])
  {
   uint str_len = StringLen(str);
// check for disallowed Unicode code points
   uint iter_pos = 0;
   while(iter_pos < str_len)
     {
      Print("Checking disallowed code points");
      ushort code_point = StringGetCharacter(str, iter_pos);
      if(IsDisallowedCodePoint(code_point))
        {
         printf("Found disallowed code point at position %d", iter_pos);
         ZeroMemory(dest_buf);
         return;
        }
      printf("Iter position %d", iter_pos);
      iter_pos++;
     }
   if(str_len == 0)
     {
      Print("Cleaning buffer: string empty");
      ZeroMemory(dest_buf);
      return;
     }
// we have no disallowed code points and the string is not empty: encode it.
   printf("Encoding %d bytes ", str_len);
   ArrayResize(dest_buf, str_len + 2);
   dest_buf[0] = (char)str_len >> 8; // MSB
   dest_buf[1] = (char)str_len % 256; // LSB
   ushort char_array[];
   StringToShortArray(str, char_array, 0, str_len);// to Unicode
   ArrayCopy(dest_buf, char_array, 2);
   ZeroMemory(char_array);
  }

Wenn die an diese Funktion übergebene Zeichenkette einen unzulässigen Codepunkt enthält, protokollieren wir dessen Position in der Zeichenkette, übergeben den Zielpuffer an ZeroMemory und kehren sofort zurück. Da Topic Name eine Mindestlänge von 1 hat, wird bei einer leeren Zeichenkette das Gleiche getan: protokollieren, den Puffer aufräumen und zurückkehren.

Beachten Sie übrigens, dass wir StringToShortArray verwenden, um die Zeichenkette in ein Unicode-Array zu konvertieren. Wenn wir es in ein ASCII-Array konvertieren würden, würden wir StringToCharArray verwenden. Die ausführliche Erklärung und vieles mehr finden Sie in dem Buch, das seit kurzem in der Dokumentation enthalten ist, oder in diesem umfassenden Artikel über MQL5-Strings.

Beachten Sie auch, dass in diesem Aufruf von StringToShortArray die Länge der Zeichenkette als letzter Parameter verwendet wird und nicht die Standardeinstellung der Funktion. Das liegt daran, dass wir das Null-Zeichen (0x00) nicht in unserem Array haben wollen, und laut der Dokumentation der Funktion: 

Der Standardwert ist -1, was bedeutet, dass bis zum Ende des Arrays oder bis zum Terminal 0 kopiert wird. Terminal 0 wird ebenfalls in das Empfänger-Array kopiert.“

während der Rückgabewert von StringLen die

Anzahl der Symbole in einer Zeichenkette ohne die abschließende Null“ zurückgibt.

Die Funktion zur Prüfung auf unzulässige Codepunkte ist trivial.

//+------------------------------------------------------------------+
//|              IsDisallowedCodePoint                               |
//|   https://unicode.org/faq/utf_bom.html#utf16-2                   |
//+------------------------------------------------------------------+
bool IsDisallowedCodePoint(ushort code_point)
  {
   if((code_point >= 0xD800 && code_point <= 0xDFFF) // Surrogates
      || (code_point > 0x00 && code_point <= 0x1F) // C0 - Control Characters
      || (code_point >= 0x7F && code_point <= 0x9F) // C0 - Control Characters
      || (code_point == 0xFFF0 || code_point == 0xFFFF)) // Specials - non-characters
     {
      return true;
     }
   return false;
  };

Neben den nicht zulässigen Codepunkten müssen wir auch auf die beiden Platzhalterzeichen achten, die in den Topic Filters für Abonnements verwendet werden, im Topic Name jedoch verboten sind: das Pluszeichen ('+' U+002B) und das Nummernzeichen ('#' U+0023).

Die Funktion zur Prüfung auf unzulässige Codepunkte wird allgemein zur Kodierung beliebiger Zeichenketten verwendet und befindet sich daher in unserem MQTT.mqh-Header, während die Funktion zur Prüfung auf Platzhalterzeichen spezifisch für Topic Name ist und daher Teil unserer CPktPublish-Klasse ist.

//+------------------------------------------------------------------+
//|            CPktPublish::HasWildcardChar                          |
//+------------------------------------------------------------------+
bool CPktPublish::HasWildcardChar(const string str)
  {
   if(StringFind(str, "#") > -1 || StringFind(str, "+") > -1)
     {
      printf("Wildcard char not allowed in Topic Names");
      return true;
     }
   return false;
  }

Die eingebaute Funktion StringFind gibt die Anfangsposition der passenden Teilzeichenkette zurück und -1, wenn die passende Teilzeichenkette nicht gefunden wurde. Wir prüfen also einfach, ob ein Wert über -1 vorliegt. Dann rufen wir sie von der Hauptfunktion aus auf.

//+------------------------------------------------------------------+
//|            CPktPublish::SetTopicName                             |
//+------------------------------------------------------------------+
void CPktPublish::SetTopicName(const string topic_name)
  {
   if(HasWildcardChar(topic_name) || StringLen(topic_name) == 0)
     {
      ArrayFree(ByteArray);
      return;
     }
   ushort encoded_string[];
   EncodeUTF8String(topic_name, encoded_string);
   ArrayCopy(ByteArray, encoded_string, 2);
   ByteArray[1] = EncodeVariableByteInteger(encoded_string);
  }

Wenn an dieser Stelle ein Platzhalter gefunden wird, führen wir dieselbe „Fehlerbehandlung“ durch wie bisher: Wir protokollieren die Information, löschen den Puffer und kehren sofort zurück. Später können wir dies verbessern, indem wir zum Beispiel Warnungen auslösen.

Die letzte Zeile der Funktion ordnet die verbleibende Länge des Pakets dem zweiten Byte unseres festen Headers unter Verwendung des vom Standard vorgeschlagenen Algorithmus zu. Wir haben darüber im ersten Artikel dieser Serie berichtet.

Auch unsere Tests folgen genau der gleichen Struktur.

//+------------------------------------------------------------------+
//|           TEST_SetTopicName_WildcardChar_NumberSign              |
//+------------------------------------------------------------------+
bool TEST_SetTopicName_WildcardChar_NumberSign()
  {
   Print(__FUNCTION__);
//--- Arrange
   static uchar expected[] = {};
   uchar payload[] = {};
//--- Act
   CPktPublish *cut = new CPktPublish(payload);
   cut.SetTopicName("a#");
   uchar result[];
   ArrayCopy(result, cut.ByteArray);
//--- Assert
   bool isTrue = AssertEqual(expected, result);
//--- cleanup
   delete cut;
   ZeroMemory(result);
   return isTrue;
  }

Wenn Sie die Tests durchführen, sollten Sie etwa Folgendes sehen:

Abb. 6 - MQTT 5.0 - PUBLISH Testausgabe Topic Name

Abb. 6 - MQTT 5.0 - PUBLISH Testausgabe Topic Name


Wie wir den/die Packet Identifier schreiben

Der Packet Identifier ist NICHT für die Zuweisung durch den Nutzer gedacht. Stattdessen MUSS er vom Client jedem PUBLISH-Paket zugewiesen werden, bei dem die QoS-Stufe > 0 ist, und DARF sonst NICHT zugewiesen werden. Mit anderen Worten: Jedes Mal, wenn wir ein PUBLISH-Paket mit QoS 1 oder QoS 2 erstellen, müssen wir seinen Packet Identifier festlegen. 

Wir können jetzt damit beginnen, dies zu testen. Alles, was wir brauchen, ist die Instanziierung eines Pakets und die Einstellung seines erforderlichen Topic Name und seiner QoS auf 1 oder 2. Das resultierende Paket-Byte-Array sollte eine Paket-ID haben.

//+------------------------------------------------------------------+
//|            TEST_SetPacketID_QoS2_TopicName1Char                  |
//+------------------------------------------------------------------+
bool TEST_SetPacketID_QoS2_TopicName5Char()
  {
   Print(__FUNCTION__);
// Arrange
   uchar payload[] = {};
   uchar result[]; // expected {52, 9, 0, 1, 'a', 'b', 'c', 'd', 'e', pktID MSB, pktID LSB}
// Act
   CPktPublish *cut = new CPktPublish(payload);
// FIX: if we call SetQoS first this test breaks
   cut.SetTopicName("abcde");
   cut.SetQoS_2(true);
   ArrayCopy(result, cut.ByteArray);
// Assert
   ArrayPrint(result);
   bool is_true = result[9] > 0 || result[10] > 0;
// cleanup
   delete cut;
   ZeroMemory(result);
   return is_true;
  }

Beachten Sie, dass wir nicht auf den Wert der generierten Paket-ID testen können, da es sich um eine (pseudo-)zufällig generierte Zahl handelt, wie Sie unten in der Stub-Implementierung sehen können. Wir testen stattdessen auf sein Vorhandensein. Beachten Sie auch, dass wir einen FIX durchführen müssen. Die Reihenfolge der Funktionsaufrufe für SetTopicName und SetQoS_X beeinflusst das resultierende Byte-Array auf unerwartete Weise. Es ist keine gute Idee, eine Aufrufreihenfolge-Abhängigkeit zwischen Funktionen zu haben. Dies wäre ein Fehler, aber wie das Sprichwort sagt, ist ein Fehler ein nicht geschriebener Test. Daher werden wir in der nächsten Iteration einen Test schreiben, der diese Abhängigkeit von der Aufrufreihenfolge ausschließt. Im Moment geht es uns nur darum, dass dieser Test bestanden wird.

Natürlich lässt sich der Test nicht kompilieren, solange wir keine Implementierung der Funktion zum Setzen der Paket-IDs haben. Da Packet Identifier in mehreren Kontrollpaketen benötigt werden, sollte die Funktion, die sie schreibt, NICHT Mitglied der Klasse CPktPublish sein. Der MQTT.mqh-Header scheint eine geeignetere Datei zu sein, um sie aufzunehmen.

//+------------------------------------------------------------------+
//|            SetPacketID                                           |
//+------------------------------------------------------------------+
void SetPacketID(uchar& buf[], int start_idx)
  {
// MathRand - Before the first call of the function, it's necessary to call
// MathSrand to set the generator of pseudorandom numbers to the initial state.
   MathSrand((int)TimeLocal());
   int packet_id = MathRand();
   if(ArrayResize(buf, buf.Size() + 2) < 0)
     {
      printf("ERROR: failed to resize array at %s", __FUNCTION__);
      return;
     }
   buf[start_idx] = (uchar)packet_id >> 8; // MSB
   buf[start_idx + 1] = (uchar)packet_id % 256; //LSB
  }

Wir verwenden die eingebaute Funktion MathRand, um Paketbezeichner zu erzeugen. Es erfordert, dass wir vorher MathSrand aufrufen. Wir müssen dieser Funktion „seed“ für den Zufallsgenerator übergeben. Wir haben uns für TimeLocal als „seed“ entschieden und sind damit der Empfehlung gefolgt, die wir in dem kürzlich zur Dokumentation hinzugefügten Buch mit einem klaren Verweis auf die Pseudo-Zufallszahlengenerierung in MQL5 gefunden haben.

Um die Paket-ID zu setzen, wird die Größe des ursprünglichen Byte-Arrays geändert, um Platz für die Paket-ID zu schaffen (zwei Byte Integer), und die Werte des höchstwertigen Bytes und des niedrigstwertigen Bytes werden ab der als Argument übergebenen Position (start_idx) gesetzt. Der letzte Schritt ist der Aufruf der Funktion aus unserer CPktPublish-Klasse auf die Methoden SetQoS_1 und SetQoS_2.

//+------------------------------------------------------------------+
//|            CPktPublish::SetQoS_2                                 |
//+------------------------------------------------------------------+
void CPktPublish::SetQoS_2(const bool QoS_2)
  {
   QoS_2 ? m_publish_flags |= QoS_2_FLAG : m_publish_flags &= ~QoS_2_FLAG;
   SetFixedHeader(PUBLISH, m_buf, ByteArray, m_publish_flags);
   SetPacketID(ByteArray, ByteArray.Size());
  }

Wenn Sie die in den beigefügten Dateien enthaltenen Tests ausführen, sollten Sie etwa Folgendes sehen (der Kürze halber hier gestrichen):

Abb. 7 - MQTT 5.0 - PUBLISH Test Output Packet Identifier

Abb. 7 - MQTT 5.0 - PUBLISH Test Output Packet Identifier

Schlussfolgerung

Da sie den Kern des Protokolls bilden, sind PUBLISH-Pakete etwas anspruchsvoller zu implementieren: Sie haben verschiedene feste Header, sie erfordern einen variablen Header mit dem Topic-Namen, der als UTF-8 kodiert und gegen einige unzulässige Code-Punkte geschützt ist, sie erfordern einen Paket-Identifier, wenn QoS > 0 ist, und sie können fast alle in MQTT 5.0 verfügbaren Eigenschaften und Nutzereigenschaften verwenden.

In diesem Artikel haben wir berichtet, wie wir gültige PUBLISH-Header mit Publish Flags, Topic Name und Packet Identifier erstellen. Im nächsten Artikel dieser Serie werden wir sehen, wie wir seine Eigenschaften schreiben.

Eine Randbemerkung zu den letzten Änderungen: Wenn Sie die Entwicklung dieses MQTT-Clients verfolgen, haben Sie vielleicht bemerkt, dass wir mehrere Funktionssignaturen, Variablennamen, Feldzugriffsebenen, Testfixtures usw. geändert haben. Einige dieser Änderungen sind die, die bei jeder Softwareentwicklung zu erwarten sind, aber die meisten sind auf die Tatsache zurückzuführen, dass wir einen TDD-Ansatz verwenden und uns bemühen, dieser Methodik so treu wie möglich zu bleiben, damit sie hier in diesen Artikeln berichtet werden kann. Wir können mit vielen Veränderungen rechnen, bevor wir ein erstes Ergebnis vorweisen können.

Wie Sie wissen, weiß kein Entwickler allein alles, was nötig ist, um einen Client wie diesen für unsere Codebasis zu entwickeln. TDD hilft uns sehr auf unserem Weg der „riesigen Specs, kleinen Schritte“, aber wenn Sie helfen können, schreiben Sie bitte eine Nachricht in unserem Community Chat oder in den Kommentaren unten. Jede Hilfe ist mehr als willkommen. Danke.

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

Beigefügte Dateien |
Deep Learning, Vorhersage und Aufträge mit Python, dem MetaTrader5 Python-Paket und ONNX-Modelldatei Deep Learning, Vorhersage und Aufträge mit Python, dem MetaTrader5 Python-Paket und ONNX-Modelldatei
Im Rahmen des Projekts wird Python für Deep Learning-basierte Prognosen auf den Finanzmärkten eingesetzt. Wir werden die Feinheiten des Testens der Leistung des Modells anhand von Schlüsselkennzahlen wie dem mittleren absoluten Fehler (MAE), dem mittleren quadratischen Fehler (MSE) und dem R-Quadrat (R2) erkunden und lernen, wie man alles in eine ausführbare Datei verpackt. Wir werden auch eine ONNX-Modelldatei mit seinem EA erstellen.
Modifizierter Grid-Hedge EA in MQL5 (Teil II): Erstellung eines einfachen Grid EA Modifizierter Grid-Hedge EA in MQL5 (Teil II): Erstellung eines einfachen Grid EA
In diesem Artikel wird die klassische Rasterstrategie untersucht, ihre Automatisierung mit einem Expert Advisor in MQL5 detailliert beschrieben und die ersten Backtest-Ergebnisse analysiert. Wir haben die Notwendigkeit einer hohen Haltekapazität für die Strategie hervorgehoben und Pläne für die Optimierung von Schlüsselparametern wie Abstand, TakeProfit und Losgrößen in zukünftigen Ausgaben skizziert. Die Reihe zielt darauf ab, die Effizienz der Handelsstrategien und die Anpassungsfähigkeit an unterschiedliche Marktbedingungen zu verbessern.
MQL5-Assistenten-Techniken, die Sie kennen sollten (Teil 10). Die unkonventionelle RBM MQL5-Assistenten-Techniken, die Sie kennen sollten (Teil 10). Die unkonventionelle RBM
Restriktive Boltzmann-Maschinen (RBM) sind im Grunde genommen ein zweischichtiges neuronales Netz, das durch Dimensionsreduktion eine unbeaufsichtigte Klassifizierung ermöglicht. Wir nehmen die Grundprinzipien und untersuchen, ob wir durch eine unorthodoxe Umgestaltung und ein entsprechendes Training einen nützlichen Signalfilter erhalten können.
Einführung in MQL5 (Teil 2): Navigieren zwischen vordefinierten Variablen, gebräuchlichen Funktionen und Kontrollflussanweisungen Einführung in MQL5 (Teil 2): Navigieren zwischen vordefinierten Variablen, gebräuchlichen Funktionen und Kontrollflussanweisungen
Begeben wir uns mit Teil zwei unserer MQL5-Serie auf eine aufschlussreiche Reise. Diese Artikel sind nicht einfach nur Anleitungen, sie sind die Tore zu einem verzauberten Reich, in dem Programmieranfänger und Zauberer gleichermaßen zu Hause sind. Was macht diese Reise wirklich magisch? Teil zwei unserer MQL5-Serie zeichnet sich durch seine erfrischende Einfachheit aus, die komplexe Konzepte für alle zugänglich macht. Beantworten Sie Ihre Fragen interaktiv und sorgen Sie so für eine bereichernde und individuelle Lernerfahrung. Lassen Sie uns eine Gemeinschaft aufbauen, in der das Verständnis von MQL5 für jeden ein Abenteuer ist. Willkommen in der Welt der Verzauberung!