English Русский 中文 Español Deutsch Português
preview
MetaTrader 5用のMQTTクライアントの開発:TDDアプローチ(第2回)

MetaTrader 5用のMQTTクライアントの開発:TDDアプローチ(第2回)

MetaTrader 5統合 | 19 2月 2024, 09:57
144 0
Jocimar Lopes
Jocimar Lopes

はじめに

「早すぎる最適化は諸悪の根源である」(ドナルド・クヌース)

前回の記事では、非常に効率的なバイナリーパブ/サブメッセージングプロトコルであるMQTTを紹介しました。MQTTとは何か、なぜ25年前に開発が始まったのか、そして現在、自動車からモノのインターネット、航空宇宙からシンプルなチャットアプリケーションまで、さまざまな業界でMQTTが何に使われているのかについて話しました。MQTTは、コンテントタイプにとらわれないメッセージ共有プロトコルが必要とされるあらゆるコンテキスト(取引アプリケーションのコンテキストを含む)で有用であることがわかりました。私たちは、MQTT用のネイティブMQL5クライアントをコードベースに含めることの利点を述べ、WSL(Windows Subsystem For Linux)上で動作するMosquittoオープンソースMQTTブローカーを使用して最小限の開発環境を設定しました。

ハードコードされたFixed Headerジェネレーター関数のみでネイティブクライアントの開発を開始し、ローカルのMosquittoブローカーと接続できるところまで開発を進めましたが、接続はプロトコルエラーによりサーバーによって即座にリセットされました。

次は、前回の記事の最下部からの引用です。

「CONNECT固定ヘッダはMosquitoによって認識されましたが、<不明>クライアントは「プロトコルエラーのため」すぐに切断されました。このエラーは、プロトコル名、プロトコルレベル、およびその他の関連メタデータを含む可変ヘッダをまだ含めていないために発生しました。次のステップで修正します」。

図01:接続応答を示すMetaeditor [experts]タブのログ:失敗

図01:接続応答を示すMetaeditor [experts]タブのログ:失敗


テスト駆動開発(TDD)アプローチを使用しているので、関連するメタデータを含み、「プロトコルエラーのため」に拒否されないCONNECTパケットビルダーのテストを書くことから、このステップを開始します。

テストするコードを書く前にこのテストを書くことは、多くの人にとって直感に反するかもしれませんが、テストをプロジェクト要件の客観的な記述として考え、開発者が持ちうるゴールの最も客観的な定義と見なせば、それはごく自然なことであり、企業の前におこなうべき期待されたステップとさえ思えるでしょう。

「ユニットテストはドキュメントです。これらはシステムの最低レベルの設計を記述します。曖昧さがなく、正確で、オーディエンスが理解できる言語で書かれており、とてもフォーマルです。それは、存在しうる限り最高の低レベルのドキュメントです。このようなドキュメントを提供しないプロがいるでしょうか。」(ロバート・マーティン、『The Clean Coder』、2011年)


コード編成:OOPとヘッダーファイル

上述したように、「正しい」値を持つバイト配列をハードコーディングすることで、接続パケットFixed Headerの構築を開始しました。次に、ハードコードされたバイト配列をローカルブローカーに送信して、クライアントに接続することを試みました。プロトコルエラーのため」接続を試みましたが、惨めに失敗しました。しかしその間に、開発環境やMosquitoのログについて学び、最初のテストを書き、そして何よりも、少なくともベイビーステップとして、何かうまくいくことを始めました。

ご覧の通り、あの失敗はわざとでした。しかし、このような複雑なアプリケーションの開発方法は、長期的には持続不可能であることは分かっています。

準拠したMQTTパケットを構築することは、堅牢で保守可能なクライアントを書くプロセスの最初のステップに過ぎません。最も簡単なことです。操作時動作の仕様になると、プロトコルの複雑さがすべて浮かび上がってきます。この複雑さは、開発者としての私たちの仕事により多くのものを要求するでしょう。良いパケットを送るだけでなく、多数の異なるサーバーのレスポンスや異なるアプリケーションの状態を扱う必要があります。この時点で、ハードコードされたバイト配列、あるいはこの問題に関してハードコードされたものは、十分ではないでしょう。

幸いなことに、MQL5はオブジェクト指向のプログラミング言語であり、MQTTがもともと設計されたようなメモリやCPUに制約のある環境で作業することはありません。つまり、オブジェクト指向(OOP)パラダイムの利点をすべて活用することができるのです。

  • 適切な抽象化レベルを選択することで、プロトコルに関する推論を容易にします。
  • コードの読み取りが簡単(コードは書かれるより何度も多く読まれることをお忘れなく)
  • コードのメンテナンスが簡単
  • テストが簡単

MQL5のリファレンスドキュメントでは、MQL5のためのオブジェクト指向プログラミングを幅広くサポートしており、トピックに特化したセクション全体が用意されています。 


プロトコルの定義

メッセージ共有プロトコルは、2つ以上のエンティティ間の共通理解を確立するルールの集合です。私たちの場合、2つ以上のデバイスの間で。これらのルールの多くは、過去に何がおこなわれたかを考慮した上で、何をすべきかに関するものです。これらはステートフルです。次の行動を選択するために、コードはアプリケーションの現在の状態を評価しなければなりません。MQTTプロトコル用語では、操作時動作ルールと呼びます。

ステートフルなルールのほかに、実際にはそれに先立って、アプリケーションの状態に依存しない用語、値、計算の定義があります。これらは通常、定数、列挙、評価アルゴリズムであり、それぞれMQTTプロトコル名、コントロールパケットタイプ、Fixed Header残り長バイト値の場合と同様です。

この2つの異なるルールセットを、2つの異なるヘッダーファイルにまとめます。そのうちのひとつは、ファイル間で共有される用語や値の定義についてです。驚くことなく、Defines.mqhと名付けます。これらの用語と値は一般的に定数であり、このファイルはほとんど何も進化しないはずです。

もうひとつのヘッダーファイルは、共有される列挙型、構造体、関数のホストとなります。これは、MQTT.mqhという名前になります。これらの列挙型、構造体、関数は、最初のバージョンを開発している間だけでなく、多くの進化を遂げるでしょう。このファイルは、改良、最適化、バグフィックスをおこなうたびに変更されます。おそらく、より具体的な他のファイルに細分化されるでしょう。

コード構成のためにヘッダーファイルを使うという習慣は、オブジェクト指向プログラミングとは関係ありません。実際、ブライアン・カーニガンとデニス・リッチーによる名著『The C Programming Language』には、それらに関する有益な記述があります。 

「ファイル間で共有される定義と宣言。可能な限り、ヘッダーファイルは一元化したいものです。そうすることで、プログラムが進化しても、取得するコピーは1つだけでよくなり、正しい状態を保つことができます。(中略)ある程度のプログラムサイズまでは、プログラムの2つの部分の間で共有されるものをすべて含む1つのヘッダーファイルを用意するのがベストでしょう。(中略)もっと大規模なプログラムであれば、より整理された、より多くのヘッダーが必要でしょう。」

しかし、オブジェクト指向プログラミングにおいてこそ、コードを小さなコンパイル単位でまとめるという実践が真価を発揮します。さらに、ライブラリを構築するのだから、コードのほとんどすべてがヘッダーファイルになります。

Definesヘッダー

この時点では、プロトコル名とプロトコルレベルの定義はCONNECTパケットでのみ使用されます。そこで、必要であれば、特定のCPktConnectクラス(下記参照)にそれらを置くことができます。しかし、一貫性を保つためにDefinesヘッダーに残しています。この時点ではCONNECTパケットでのみ使用されますが、後に他のファイルでも使用される可能性があります。

プロトコルに関するコメントは、公式の標準仕様からの文字通りの引用です。

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

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

すべてのプロトコル固有の定義の前にMQTTを付けていることに注意してください。これは、さらに含めるべき独自の定義と区別するためです。また、Defines.mqhファイルの一番上、PROTOCOL NAME AND VERSIONにある、識別子の命名をできるだけ明確にしようとしていることにもご注目ください。これは、いわゆるクリーンコードの原則に対処するためのものです。この実践は、コードをより読みやすく、デバッグしやすく、IDEフレンドリーにするのに役立ちます。つまり、検索しやすく、最新の IDE のオートコンプリート機能を活用するのに適しています。


MQTTヘッダー

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

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


クラスと構造

MQTTコントロールパケットインターフェイス

ここで、コントロールパケットオブジェクト階層を抽象クラスで開始するかインターフェイスで開始するかという設計上の選択をおこなう必要があります。あらゆるコントロールパケットに適した一般的な基本クラスから始めた場合、この抽象クラスは、より具体的な派生コントロールパケットクラスに特化されることになります。あるいは、コントロールパケットクラスが実装するシンプルなインターフェイスから始めることもできます。

IcontrolPacketインターフェイスから始めます。このインターフェイスはシンプルなメソッドを持つことになります。この選択は、プロトコルの操作時動作の部分を実装するときに変わるかもしれません。おそらくこのインターフェイスを、いくつかの仮想関数を持つ抽象クラスに変更することになるでしょう

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

   bool              IsControlPacket();

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

前述のように、今のところこのインターフェイスの唯一の目的は、MQTTパケットオブジェクト階層のルートとして機能することです。この時点では、単なるプレースホルダーに過ぎません。

MQTTコントロールパケット接続クラス

CONNECTコントロールパケットは、書くのが最も難しいパケットです。プロトコルにまだ慣れていないことに加え、この特定のパケットはバージョン5.0で最も大きな改良を受けました(接続プロパティとユーザープロパティ)。

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

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

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


1番目のクラスのテスト

CPktConnectクラスの唯一の目的は、整形式のMQTT CONNECTパケットを構築することです。したがって、これをテストするには、まず「フィクスチャ」のようなもの、つまり、整形式のCONNECTパケットを表すバイト配列のサンプルを作成することから始める必要があります。しかし、作り上げたバイト配列が正しい形式のCONNECTパケットを表していることを、どうやって確認できるのでしょうか。結局のところ、これはゼロからクラスをスタートさせるためのテストなのです。私たちの苦労は、すべてではないにせよ、フィクスチャーが悪いパケットであれば無駄になるかもしれません。

プロトコルの開発保守者であるOASISが私たちを救ってくれました。セクション3.1.2.12には、CONNECTパケットのためのVariable Headerの非規範例があります。Fixed Headerジェネレーターはすでにテスト済みなので(前回の記事を参照)、このOASISの例で始めるには十分でしょう。これによって、ブーリアンCleanSessionやKeep-Alive要求時間のようないくつかの異なる構成で、私たちのクラスが整形されたパケットを生成していることを確認することができます。

このハードコードされた手動生成バイト配列は、CpktConnectが生成したパケットと比較されます。

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

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

メモ:ご存知のように、「ヘッダー配列に先ほど入れたデータがあるかどうか確認しよう」といったテストを書くのは時間の無駄に思えるかもしれませんが、そうではありません。この「明らかな」テストのセットは、私たちのコードに永遠に付きまとうことになります。ある種のリグレッションバグや、間違ったコピーペーストのような間抜けなミスを見つけたときに、その価値を証明する継続的な自動デバッグツールと考えることができます。これが、接続アクションが機能するかどうかだけをブラックボックス的にテストしない理由です。接続アクションをテストする前に、ヘッダーが整形式であることを確認したいのです。TDDはプロセスであることを忘れてはなりません。これらのテストは、すべてではないにせよ、コードの最初の実用版ができる前に書き直されたり、削除されたりすることも多いでしょう。しかし、残る者はおそらく永遠に残るでしょう。

このテストは、CPktConnectが生成したバイト配列が、参照バイト配列であるフィクスチャとArrayCompareされたときに0を返したときのみ合格します。

基本的な接続プロパティの組み合わせをいくつかテストした後、パケットはブローカーに送信され、「プロトコルエラーのため」に拒否されません。

図02:MetaEditorのEAログTEST_CPktConnect Success

図02:CPktConnectクラスのテスト結果を示すMetaEditorの[Experts]タブのログ


ローカルのMQTTブローカーの確認

これで、WSL上でMosquittoローカルブローカーを実行し、MQTT接続が成功したかどうかを確認できます。

デフォルトのインストールに従った場合、MosquitoはLinux上でサービスとして実行されているはずです。したがって、ポートをredir (80 → 1883)し、MetaTrader 5のオプションで許可されたURLにホスト名を含めるだけでよくなります。

図03:WSL Mosquittoログ接続/切断成功

図03:WSL上のMosquittoログでの接続/切断ステータスの表示:成功


 成功です。接続を試みても、プロトコルエラーは返されません。これで、クライアントとサーバーの間でメッセージのやり取りができるようになりました。


結論

次のステップでは、CONNACK応答を扱います。このステップで、私たちは最初のメッセージを公開するための強固な基盤を手に入れることになります。そしてもちろん、そのためのテストを書き始めます。  ご期待ください。


MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/13334

添付されたファイル |
Defines.mqh (8.03 KB)
MQTT.mqh (5.01 KB)
CPktConnect.mqh (9.97 KB)
ペアトレード ペアトレード
本稿では、ペアトレードについて、その原理は何か、実用化の見込みはあるのかを考えてみたいと思います。また、ペアトレード戦略にも挑戦します。
ニューラルネットワークが簡単に(第57回):Stochastic Marginal Actor-Critic (SMAC) ニューラルネットワークが簡単に(第57回):Stochastic Marginal Actor-Critic (SMAC)
今回は、かなり新しいStochastic Marginal Actor-Critic (SMAC)アルゴリズムを検討します。このアルゴリズムは、エントロピー最大化の枠組みの中で潜在変数方策を構築することができます。
ビジュアルプログラミング言語DRAKON:MQL開発者と顧客のコミュニケーションツール ビジュアルプログラミング言語DRAKON:MQL開発者と顧客のコミュニケーションツール
DRAKONは、ロシアの宇宙プロジェクト(例えば、「Buran」再利用可能宇宙船プロジェクト)のプログラマーと、異なる分野の専門家(生物学者、物理学者、エンジニアなど)との対話を簡素化するために設計されたビジュアルプログラミング言語です。この記事では、DRAKONが、コードに触れたことがない人にとっても、アルゴリズムの作成にアクセスしやすく、直感的にし、また、顧客が取引ロボットを注文する際に自分の考えを説明しやすくし、複雑な関数でプログラマーのミスを少なくする方法についてお話します。
MQL5のALGLIB数値解析ライブラリ MQL5のALGLIB数値解析ライブラリ
この記事では、ALGLIB3.19数値分析ライブラリ、その応用、金融データ分析の効率を向上させる新しいアルゴリズムについて簡単に説明します。