DIY マルチスレッド非同期 MQL5 WebRequest

Stanislav Korotky | 21 1月, 2019

トレードアルゴリズムの実装では、多くの場合、インターネットを含むさまざまな外部ソースからのデータを分析する必要があります。 MQL5 は、HTTPリクエストを "外部" に送信するためのWebRequest関数がありますが、残念ながら、ある顕著な欠点があります。 この関数は同期的であり、リクエストの実行の全期間にわたってEA操作をブロックします。 MetaTrader5 では、各EAに対して、コード内の既存の API 関数呼び出しと、受信イベントハンドラ (ティック、BookEvent、タイマー、トレード操作、チャートイベントなど) を順番に実行する単一のスレッドが割り当てられています。 一度に実行されるコードフラグメントは1つだけですが、残りのすべての "task" は、現在のフラグメントが制御をカーネルに返すまでキューの順番を待ちます。

たとえば、EAがリアルタイムで新しいティックを処理し、1つまたは複数のウェブサイトで定期的に経済ニュースをチェックする場合、両方の要件を満たすことは不可能であり、互いに干渉することはありません。 WebRequest がコード内で実行されるとすぐに、EAは関数呼び出し文字列に対して「フリーズ」したままになり、新しいティックイベントはスキップされます。 CopyTicks 関数を使用してスキップされたティックを読み取ることができる場合でも、トレード決定を下すためのモーメントが失われる可能性があります。 UML シーケンス図を使用して、この状況を示す方法を次に示します。

1つのスレッドでのブロッキングコードを特徴とするイベント処理シーケンス図

図1. 1つのスレッドでのブロッキングコードを特徴とするイベント処理シーケンス図

この点では、HTTPリクエストの非同期ノンブロッキング実行のツールを作成するのが良いでしょう、一種の WebRequestAsyncです。 明らかに、その追加のスレッドを把握する必要があります。 MetaTrader5 で行う最も簡単な方法は、追加の HTTPリクエストを送信できる追加のEAを実行することです。 また、そこに WebRequest を呼び出して、しばらくして結果を取得することができます。 リクエストがそのような補助EAで処理されている間、メインEAは、プロンプトとインタラクティブアクションに利用可能なままです。 UML シーケンス図は、そのような場合には次のようになります。

非同期イベント処理を他のスレッドに委任するシーケンス図

図2. 非同期イベント処理を他のスレッドに委任するシーケンス図


1. プラン

ご存じのように、各EAは MetaTrader の別のチャートで動作する必要があります。 したがって、補助EAの作成には専用のチャートが必要です。 手動で行うことは不便です。 したがって、補助的なチャートとEAのプールを管理し、クライアントプログラムから新しいリクエストを登録するための1つのエントリポイントを提供するEA-すべてのルーチン操作を特別マネージャーに委任することは理にかなっています。 ある意味では、このアーキテクチャは、EAmanager がサーバとして機能するクライアント/サーバアーキテクチャに似た3レベルのものと呼ぶことができます。

マルチウェブライブラリアーキテクチャ: クライアント MQL コードサーバ (アシスタントプールマネージャ)-ヘルパーEA

図3 multiweb ライブラリアーキテクチャ: クライアント MQL コード <-> サーバ (アシスタントプールマネージャ) <-> ヘルパーEAs

しかし、簡素化に、マネージャと補助EAは同じコード (プログラム) の形で実装することができます。 このような "ユニバーサル "EA-a マネージャまたはアシスタントの2つの役割のいずれかが優先法によって決定されます。 起動された最初のインスタンスは自身をマネージャとして宣言し、補助チャートを開き、指定された数のアシスタントのロールを起動します。

クライアント、マネージャ、アシスタントがお互いにどのように正確に、どのように渡すべきでしょうか? これを理解するために、WebRequest 関数を分析してみましょう。

ご存じのように、MetaTrader5 は WebRequest 関数の2つのオプションを備えています。 2番目のものが最も普遍的であると考えます。

int WebRequest
( 
  const string      method,           // HTTPメソッド
  const string      url,              //url アドレス
  const string      headers,          //ヘッダー
  int               timeout,          //タイムアウト
  const char        &data[],          // HTTPメッセージボディ配列
  char              &result[],        //サーバー応答データを持つ配列
  string            &result_headers   //サーバー応答ヘッダー
);

最初の5つのパラメータはインプットされたものです。 呼び出し元のコードからカーネルに渡され、リクエストの内容を定義します。 直近の2つのパラメータは出力ものです。 カーネルから呼び出し元のコードに渡され、クエリ結果が含まれます。 明らかに、この関数を非同期のものにするには、クエリを初期化して結果を取得するという2つのコンポーネントに分ける必要があります。

int WebRequestAsync
( 
  const string      method,           // HTTPメソッド
  const string      url,              //url アドレス
  const string      headers,          //ヘッダー
  int               timeout,          //タイムアウト
  const char        &data[],          // HTTPメッセージボディ配列
);

int WebRequestAsyncResult
( 
  char              &result[],        //サーバー応答データを持つ配列
  string            &result_headers   //サーバー応答ヘッダー
);

関数の名前とプロトタイプは条件付きです。 実際には、異なる MQL プログラム間でこの情報を渡す必要があります。 通常の関数呼び出しは適していません。 MQL プログラムが互いに「通信」できるようにするために、MetaTrader5 には、使用する custom events 交換システムがあります。 イベント交換は、 ChartIDを使用してレシーバ ID に基づいて実行されます (チャートごとに一意です)。 チャート上には1つのEAのみが存在する場合がありますが、インジケータの場合にはそのような制限はありません。 つまり、ユーザーは、各チャートには、マネージャーと通信する1つ以上のインジケータが含まれていないことを確認する必要があります。

データ交換が機能するためには、すべての "関数" パラメータをユーザーイベントパラメータにパックする必要があります。 リクエストパラメータと結果の両方には、限られた範囲のイベントに物理的に収まらない、大量の情報をインクルードすることができます。 たとえば、HTTPメソッドと URL を sparam string イベントパラメータに渡すことにした場合でも、長さを63文字に制限することは、ほとんどのタスクの場合に障害となります。 つまり、ある種の共有データリポジトリをイベント交換システムに補足する必要があり、このリポジトリ内のレコードへのリンクのみがイベントパラメータで送信される必要があります。 幸いにも、MetaTrader5 はカスタムリソースの形でこのようなストレージを提供します。 実際、MQL から動的に作成されるリソースは常にイメージです。 しかし、イメージはバイナリ情報のコンテナであり、望むものを書くことができます。

タスクを簡素化するために、ユーザーリソースに任意のデータを読み書きするための既製のソリューションを使用します-リソースからクラス. ResourceData とMQL5 コミュニティfxsaberのメンバによって開発されました。

提供されたリンクはソースにつながります— TradeTransactions ライブラリは現在の記事の主題に関連していませんが、(ロシアの) 考察には、リソースを介したデータストレージと交換の例があります。 ライブラリは変更可能で、読者の便宜に、記事で使用するすべてのファイルは以下に添付されていますが、バージョンは記事の執筆時点に対応しており、上記のリンクを介して提供される現在のバージョンとは異なる場合があります。 加えて、言及されたリソースクラスは、タスクにおいてさらに別のライブラリを使用します— TypeToBytes。 そのバージョンもこの記事に添付されています。

補助クラスの内部構造を掘り下げる必要はありません。 主なものは、「ブラックボックス」として既製の RESOURCEDATA クラスに頼ることができ、そのコンストラクタと適した関数のカップルを使用することができるということです。 これについては、後で詳しく見ていきます。 次に、全体的な概念について詳しく説明します。

アーキテクチャパーツの相互作用のオーダーは次のようになります。

  1. 非同期の web リクエストを実行するには、クライアント MQL プログラムは、ローカルリソースにリクエストパラメータをパックし、リソースへのリンクを持つマネージャーにカスタムイベントを送信するために開発したクラスを使用する必要があります。リソースはクライアントプログラム内で作成され、結果が取得されるまで削除されません (不要になった場合)。
  2. このマネージャーは、プール内の空いているアシスタントEAを検索し、リソースへのリンクを送信します。ただし、このインスタンスは一時的に占有としてマークされ、現在のリクエストが処理されるまで、トレーリングのリクエストに対して選択することはできません。
  3. クライアント外部リソースからの web リクエストのパラメータは、カスタムイベントを受け取ったアシスタントEAでアンパックされます。
  4. アシスタントEAは、標準のブロッキング WebRequest を呼び出し、応答 (ヘッダーおよび/または web ドキュメント) を待機します。
  5. アシスタントEAは、リクエストの結果をローカルリソースにパックし、このリソースへのリンクを持つカスタムイベントをマネージャに送信します。
  6. このマネージャはイベントをクライアントに転送し、適切なアシスタントを再び free としてマークします。
  7. クライアントは、マネージャからメッセージを受信し、外部アシスタントリソースからリクエストの結果をアンパックします。
  8. このクライアントとアシスタントは、ローカルリソースを削除できます。

アシスタントEAがマネージャをバイパスしてクライアントウィンドウに直接結果を送信するという事実により、ステップ5および6でより効率的に渡すことができます。

上記のステップは、HTTPリクエストの処理のメインステージに関連します。 ここでは、異種のパーツを1つのアーキテクチャにリンクするときについて説明します。 また、部分的にユーザーイベントに依存します。

このアーキテクチャの中央リンク (マネージャ) は、手動で起動することになっています。 一度だけ行う必要があります。 他の実行中のEAと同様に、ターミナルが再起動した後、チャートとともに自動的に回復します。 このターミナルは、1つの web リクエストマネージャーのみを許可します。

このマネージャーは、必要な数の補助ウィンドウ (設定で設定される) を作成し、特別な「プロトコル」 (詳細は実装セクションにあります) のおかげでアシスタントのステータスについて「調べる」ことができます。

アシスタントは、特別なイベントの助けをして、その終了をマネージャに通知します。 マネージャで利用可能なアシスタントの関連リストを維持するために必要です。 同様に、マネージャはアシスタントに決済を通知します。 さらに、このアシスタントはタスクをストップし、ウィンドウを閉じます。 アシスタントはマネージャなしでは使用できませんが、マネージャを再起動すると、必然的にアシスタントが再作成されます (たとえば、設定のアシスタントの数を変更した場合)。

アシスタントの Windows は、補助EA自体のように、常にマネージャから自動的に作成されることになっているので、プログラムは、「クリーンアップ」する必要があります。 アシスタントEAを手動で起動しない-マネージャステータスに対応しないインプットは、プログラムによるエラーと見なされます。

起動時に、クライアント MQL プログラムは、バルクメッセージングを使用して、パラメータにその ChartID を指定して、マネージャの存在のターミナルウィンドウを調査する必要があります。 マネージャー (見つかった場合) は、そのウィンドウの ID をクライアントに返す必要があります。 その後、クライアントとマネージャはメッセージを交換できます。

主な機能です。 実装に移る時間です。


2. 実装

開発をシンプル化するには、すべてのクラスを記述する単一のマルチウェブヘッダーファイルを作成します。いくつかは、クライアントと "サーバー " に共通していますが、その他は継承され、各ロールに固有です。

2.1. 基本クラス (開始)

各要素のリソース、id、および変数を格納するクラスから始めましょう。 派生したクラスのインスタンスは、マネージャ、アシスタント、およびクライアントで使用します。 クライアントとアシスタントでは、このようなオブジェクトは、主に "リンクによって渡された" リソースを格納するために必要です。 加えて、複数の web リクエストを同時に実行するために、クライアントで複数のインスタンスが作成されたことに注意してください。 したがって、現在のリクエストの状態 (少なくとも、オブジェクトが既にビジーであるかどうか) の分析は、クライアントで全範囲に対して使用する必要があります。 このマネージャでは、オブジェクトは、アシスタントのステータスを識別および追跡するために使用します。 以下は基本クラスです。

class WebWorker
{
  protected:
    long chartID;
    bool busy;
    const RESOURCEDATA<uchar> *resource;
    const string prefix;
    
    const RESOURCEDATA<uchar> *allocate()
    {
      release();
      resource = new RESOURCEDATA<uchar>(prefix + (string)chartID);
      return resource;
    }
    
  public:
    WebWorker(const long id, const string p = "WRP_"): chartID(id), busy(false), resource(NULL), prefix("::" + p)
    {
    }

    ~WebWorker()
    {
      release();
    }
    
    long getChartID() const
    {
      return chartID;
    }
    
    bool isBusy() const
    {
      return busy;
    }
    
    string getFullName() const
    {
      return StringSubstr(MQLInfoString(MQL_PROGRAM_PATH), StringLen(TerminalInfoString(TERMINAL_PATH)) + 5) + prefix + (string)chartID;
    }
    
    virtual void release()
    {
      busy = false;
      if(CheckPointer(resource) == POINTER_DYNAMIC) delete resource;
      resource = NULL;
    }

    static void broadcastEvent(ushort msg, long lparam = 0, double dparam = 0.0, string sparam = NULL)
    {
      long currChart = ChartFirst(); 
      while(currChart != -1)
      {
        if(currChart != ChartID())
        {
          EventChartCustom(currChart, msg, lparam, dparam, sparam); 
        }
        currChart = ChartNext(currChart);
      }
    }
};

変数:

  • chartID — MQL プログラムが起動されたチャートの ID。
  • busy - 現在のインスタンスが web リクエストの処理でビジー状態である場合。
  • resource - オブジェクトのリソース (ランダムデータストレージ)。RESOURCEDATA クラスは RESOURCEDATA から取得されます。
  • prefix —各ステータスのユニークな接頭辞。プレフィックスは、リソースの名前で使用します。 特定のクライアントでは、以下に示すように一意の設定を行うことをお勧めします。 アシスタントEAでは、デフォルトで "WRR_" (Web リクエスト結果と略記) の接頭辞を使用します。

派生クラスで使用する ' 割り当て ' メソッド。 RESOURCEDATA 型リソースのオブジェクトを<uchar>' resource ' 変数に作成します。 チャート ID は、リソースの名前付けにもプレフィックスと共に使用します。 リソースは ' release ' メソッドを使用してインプットできます。

getFullName メソッドは、現在の MQL プログラム名とディレクトリパスを含む完全なリソース名を返すため、特に言及する必要があります。 このフルネームは、サードパーティのプログラムリソースにアクセスするために使用します (読み取り専用)。 たとえば、multiwebEAが MQL5\Experts にあり、ID 129912254742671346 のチャートで起動された場合、その中のリソースはフルネーム "Experts\multiweb.ex5:: WRR_129912254742671346 " を受け取ります。 カスタムイベントの sparam 文字列パラメータを使用して、このような文字列をリンクとしてリソースに渡します。

すべてのウィンドウにメッセージを送信する broadcastEvent 静的メソッドは、マネージャを検索するために使用します。

クライアントプログラムでリクエストと関連するリソースを操作するには、WebWorker から派生した ClientWebWorker クラスを定義します (以下のコードは省略され、完全なバージョンは添付ファイルにあります)。

class ClientWebWorker : public WebWorker
{
  protected:
    string _method;
    string _url;
    
  public:
    ClientWebWorker(const long id, const string p = "WRP_"): WebWorker(id, p)
    {
    }

    string getMethod() const
    {
      return _method;
    }

    string getURL() const
    {
      return _url;
    }
    
    bool request(const string method, const string url, const string headers, const int timeout, const uchar &body[], const long managerChartID)
    {
      _method = method;
      _url = url;

      // allocate()? 次は?
      ...
    }
    
    static void receiveResult(const string resname, uchar &initiator[], uchar &headers[], uchar &text[])
    {
      Print(ChartID(), ": Reading result ", resname);
      
      ...
    }
};

まず、' request ' メソッドは前述のステップ1の実際の実装であることに注意してください。 ここでは、web リクエストがマネージャに送信されます。 このメソッド宣言は、仮説 WebRequestAsync のプロトタイプに従います。 receiveResult 静的メソッドは、ステップ7からの逆のアクションを実行します。 「resname」の最初のインプットとして、リクエスト結果が格納されている外部リソースのフルネームを受け取りますが、「イニシエータ」、「ヘッダー」、「text」のバイト配列は、リソースからアンパックされたデータを使用してメソッド内に埋め込まれることになります。

「イニシエータ」とは何でしょうか。 答えは簡単です。 すべての "呼び出し" が非同期になり、実行オーダーが保証されないため、以前に送信されたリクエストと結果を一致させることができます。 したがって、EAは、リクエストを開始するために使用するソース・クライアント・リソースのフルネームを、インターネットから取得したデータとともに応答リソースにパックします。 アンパック後、名前は ' イニシエーター ' パラメータに入り、対応するリクエストに結果を関連付けるために使用できます。

receiveResult メソッドは、オブジェクト変数を使用しないため、静的であり、すべての結果はパラメータを使用して呼び出し元のコードに返されます。

どちらの方法にも、リソースとの間でデータをパッキングおよびアンパックする必要があります。 これは、次のセクションで考慮されます。


2.2. リクエストのパッキングとリソースへの結果のリクエスト

覚えているかもしれませんが、リソースは RESOURCEDATA クラスを使用して下位レベルで処理されることになっています。 これは、 template クラスであり、リソースに対して読み書きを行うデータ型のパラメータを受け取ることを意味します。 データには文字列も含まれているため、ストレージユニットとして最小の uchar タイプを選択するのが妥当です。 したがって、RESOURCEDATA クラスのオブジェクト<uchar>は、データコンテナとして使用します。 リソースを作成するときに、そのコンストラクタに一意の (プログラムの) ' name ' が作成されます。

RESOURCEDATA<uchar>(const string name)

他の MQL プログラムが同じリソースにアクセスできるように、この名前 (プログラム名で接頭辞として付加されたもの) をカスタムイベントに渡すことができます。 リソースが作成された他のプログラムはすべて、読み取り専用でアクセスできることに注意してください。

データは、オーバーロード代入演算子を使用してリソースに書き込まれます。

void operator=(const uchar &array[]) const

ここで、' array ' は配列の一種であり、準備しなければなりません。

リソースからのデータの読み取りは、次の関数を使用して実行されます。

int Get(uchar &array[]) const

ここで、' array ' は元の配列の内容が置かれる出力パラメータです。

次に、リソースを使用して HTTPリクエストとその結果に関するデータを渡すアプリケーションの側面について説明します。 リソースとメインコード ResourceMediator の間にレイヤークラスを作成します。 このクラスは、「メソッド」、「url」、「ヘッダー」、「タイムアウト」、「データ」パラメータを「配列」バイト配列にパックし、クライアント側のリソースに書き込みます。 サーバー側では、リソースからパラメータをアンパックします。 同様に、このクラスは、サーバー側の ' result ' と ' result_headers ' パラメータを ' array ' バイト配列にパッケージ化し、を配列として読み取ってクライアント側でアンパックするためにリソースに書き込みます。

ResourceMediator コンストラクタは、RESOURCEDATA リソースへのポインタを受け入れ、メソッド内で処理されます。 さらに、ResourceMediator には、データに関するメタ情報を格納するためのサポート構造があります。 実際、リソースをパッキングおよびアンパックするときには、データ自体に加えて、すべてのフィールドのサイズを含む特定のヘッダーが必要です。

たとえば、 StringToCharArray 関数を使用して URL をバイト配列に変換する場合、CharArrayToString を使用して逆操作を実行するときには、配列の長さを設定する必要があります。 それ以外の場合は、URL バイトだけでなく、その後に続くヘッダーフィールドも配列から読み込まれます。 覚えているかもしれませんが、リソースに witing する前に、すべてのデータを1つの配列に格納します。 フィールドの長さに関するメタ情報も、バイトのシーケンスに変換する必要があります。 そこに適用します。

#define LEADSIZE (sizeof(int)*5) //web リクエストの5つのフィールド

class ResourceMediator
{
  private:
    const RESOURCEDATA<uchar> *resource; //原資産
    
    //ヘッダー内のメタデータは、5整数 ' の長さ ' および/またはバイト配列のサイズ ' として表されます。
    union lead
    {
      struct _l
      {
        int m; //メソッド
        int u; //Url
        int h; //ヘッダー
        int t; //タイムアウト
        int b; // body
      }
      lengths;
      
      uchar sizes[LEADSIZE];
      
      int total()
      {
        return lengths.m + lengths.u + lengths.h + lengths.t + lengths.b;
      }
    }
    metadata;
  
    //int をバイト配列として表し、その逆も同様です。
    union _s
    {
      int x;
      uchar b[sizeof(int)];
    }
    int2chars;
    
    
  public:
    ResourceMediator(const RESOURCEDATA<uchar> *r): resource(r)
    {
    }
    
    void packRequest(const string method, const string url, const string headers, const int timeout, const uchar &body[])
    {
      //パラメータデータ長を使用したメタデータのインプット
      metadata.lengths.m = StringLen(method) + 1;
      metadata.lengths.u = StringLen(url) + 1;
      metadata.lengths.h = StringLen(headers) + 1;
      metadata.lengths.t = sizeof(int);
      metadata.lengths.b = ArraySize(body);
      
      //結果として得られる配列を、メタデータとパラメータに合わせて割り当てる
      uchar data[];
      ArrayResize(data, LEADSIZE + metadata.total());
      
      //メタデータを配列の先頭にバイト配列として格納する
      ArrayCopy(data, metadata.sizes);
      
      //すべてのデータフィールドを1つずつ配列に配置する
      int cursor = LEADSIZE;
      uchar temp[];
      StringToCharArray(method, temp);
      ArrayCopy(data, temp, cursor);
      ArrayResize(temp, 0);
      cursor += metadata.lengths.m;
      
      StringToCharArray(url, temp);
      ArrayCopy(data, temp, cursor);
      ArrayResize(temp, 0);
      cursor += metadata.lengths.u;
      
      StringToCharArray(headers, temp);
      ArrayCopy(data, temp, cursor);
      ArrayResize(temp, 0);
      cursor += metadata.lengths.h;
      
      int2chars.x = timeout;
      ArrayCopy(data, int2chars.b, cursor);
      cursor += metadata.lengths.t;
      
      ArrayCopy(data, body, cursor);
      
      //リソースに配列を格納します。
      resource = data;
    }
    
    ...

まず、packRequest メソッドは、すべてのフィールドのサイズを ' メタデータ ' 構造体に書き込みます。 次に、この構造体の内容は、バイト配列の形式で ' data ' 配列の先頭にコピーされます。 その後、' data ' 配列がリソースに配置されます。 ' data ' 配列サイズは、すべてのフィールドの全長とメタデータを持つ構造体のサイズに基づいて予約されます。 文字列型パラメータは、StringToCharArray を使用して配列に変換され、対応するシフトを持つ結果の配列にコピーし、' cursor ' 変数で最新の状態に保たれます。 ' timeout ' パラメータは、int2chars 共用体を使用してシンボル配列に変換されます。 ' body ' パラメータは、すでに必要な型の配列であるため、配列にコピーされます。 最後に、共通配列の内容をリソースに移動することは、文字列で実行されます (覚えているかもしれませんが、RESOURCEDATA クラスでは演算子がオーバーロードされています)。

      resource = data;

リソースからリクエストパラメータを取得する逆の操作は、unpackRequest メソッドで実行されます。

    void unpackRequest(string &method, string &url, string &headers, int &timeout, uchar &body[])
    {
      uchar array[];
      //配列をリソースのデータで埋める  
      int n = resource.Get(array);
      Print(ChartID(), ": Got ", n, " bytes in request");
      
      //配列からメタデータを読み取る
      ArrayCopy(metadata.sizes, array, 0, 0, LEADSIZE);
      int cursor = LEADSIZE;

      //すべてのデータフィールドを1つずつ読み込む      
      method = CharArrayToString(array, cursor, metadata.lengths.m);
      cursor += metadata.lengths.m;
      url = CharArrayToString(array, cursor, metadata.lengths.u);
      cursor += metadata.lengths.u;
      headers = CharArrayToString(array, cursor, metadata.lengths.h);
      cursor += metadata.lengths.h;
      
      ArrayCopy(int2chars.b, array, 0, cursor, metadata.lengths.t);
      timeout = int2chars.x;
      cursor += metadata.lengths.t;
      
      if(metadata.lengths.b > 0)
      {
        ArrayCopy(body, array, 0, cursor, metadata.lengths.b);
      }
    }
    
    ...

ここでは、メインのタスクは、文字列呼び出しリソースによって実行されます。Get(array) 次に、メタデータバイトと基づくトレーリングのすべてのフィールドが、ステップごとに「配列」から読み取られます。

リクエスト実行結果は、packResponse および unpackResponse メソッドを使用して同様の方法でパックおよびアンパックされます (完全なコードは以下に添付されています)。

    void packResponse(const string source, const uchar &result[], const string &result_headers);
    void unpackResponse(uchar &initiator[], uchar &headers[], uchar &text[]);

ClientWebWorker のソースコードに戻り、「リクエスト」と「receiveResult」メソッドを完了することができます。

class ClientWebWorker : public WebWorker
{
    ...

    bool request(const string method, const string url, const string headers, const int timeout, const uchar &body[], const long managerChartID)
    {
      _method = method;
      _url = url;

      ResourceMediator mediator(allocate());
      mediator.packRequest(method, url, headers, timeout, body);
    
      busy = EventChartCustom(managerChartID, 0 /* TODO: specific message */, chartID, 0.0, getFullName());
      return busy;
    }
    
    static void receiveResult(const string resname, uchar &initiator[], uchar &headers[], uchar &text[])
    {
      Print(ChartID(), ": Reading result ", resname);
      const RESOURCEDATA<uchar> resource(resname);
      ResourceMediator mediator(&resource);
      mediator.unpackResponse(initiator, headers, text);
    }
};

ResourceMediator クラスがすべての日常的なタスクを引き継ぐため、簡単です。

残りの問題は、WebWorker メソッドを呼び出す人とタイミング、および managerChartID などのユーティリティパラメータの値を ' request ' メソッドで取得する方法です。 わずかに進んでいますが、すべての WebWorker クラスオブジェクトの管理を、実際のオブジェクトリストをサポートするより高レベルのクラスに割り当て、オブジェクトの「代わりに」プログラム間でメッセージを交換することをお勧めします。 しかし、この新しいレベルに移行する前に、「サーバー」の部分についても同様の準備を完了する必要があります。


2.3. 基本クラス (続き)

WebWorker からカスタム導関数を宣言して、ClientWebWorker クラスがクライアント側で行うのと同じように、 "サーバー " (マネージャ) 側で非同期リクエストを処理します。

class ServerWebWorker : public WebWorker
{
  public:
    ServerWebWorker(const long id, const string p = "WRP_"): WebWorker(id, p)
    {
    }
    
    bool transfer(const string resname, const long clientChartID)
    {
      //' resname ' のタスクが受け入れられたことを「clientChartID」でクライアントに応答します。
      //' chartID ' によって識別されるこの特定のタスク者にタスクを渡す 
      busy = EventChartCustom(clientChartID, TO_MSG(MSG_ACCEPTED), chartID, 0.0, resname)
          && EventChartCustom(chartID, TO_MSG(MSG_WEB), clientChartID, 0.0, resname);
      return busy;
    }
    
    void receive(const string source, const uchar &result[], const string &result_headers)
    {
      ResourceMediator mediator(allocate());
      mediator.packResponse(source, result, result_headers);
    }
};

' transfer ' メソッドは、相互作用シーケンス全体のステップ2に従って、アシスタントEAの特定のインスタンスにリクエストを処理します。 resname パラメータは、クライアントから取得したリソース名であり、clientChartID はクライアント・ウィンドウ ID です。 これらのパラメータはすべてカスタムイベントから取得します。 MSG_WEB を含むカスタムイベント自体については、以下で説明します。

' receive ' メソッドは、WebWorker 現在のオブジェクトにローカルリソースを作成し (「割り当て」呼び出し)、そこに元のリクエストイニシエータリソースの名前と、インターネットから取得したデータ (結果) と HTTPヘッダー (result_headers) を使用します。 これは、シーケンス全体のステップ5の一部です。

そこで、クライアントと "サーバー" の両方の WebWorker クラスを定義しました。 どちらの場合も、オブジェクトは大量に作成される可能性が高くなります。 たとえば、1つのクライアントが一度に複数のドキュメントをダウンロードできますが、manager 側では、リクエストが多数のクライアントから同時に実行される可能性があるため、最初は十分な数のアシスタントを配布することが望ましいと考えられます。 オブジェクト配列を配置するための WebWorkersPool 基底クラスを定義してみましょう。 格納されているオブジェクトの種類がクライアントと "サーバー" (それぞれ ClientWebWorker と ServerWebWorker) で異なるため、テンプレートとして作成してみましょう。

template<typename T>
class WebWorkersPool
{
  protected:
    T *workers[];
    
  public:
    WebWorkersPool()
    
    WebWorkersPool(const uint size)
    {
      //タスク者の割り当て。クライアントでは、リクエストパラメータをリソースに格納するために使用します。
      ArrayResize(workers, size);
      for(int i = 0; i < ArraySize(workers); i++)
      {
        workers[i] = NULL;
      }
    }
    
    ~WebWorkersPool()
    {
      for(int i = 0; i < ArraySize(workers); i++)
      {
        if(CheckPointer(workers[i]) == POINTER_DYNAMIC) delete workers[i];
      }
    }
    
    int size() const
    {
      return ArraySize(workers);
    }
    
    void operator<<(T *worker)
    {
      const int n = ArraySize(workers);
      ArrayResize(workers, n + 1);
      workers[n] = worker;
    }
    
    T *findWorker(const string resname) const
    {
      for(int i = 0; i < ArraySize(workers); i++)
      {
        if(workers[i] != NULL)
        {
          if(workers[i].getFullName() == resname)
          {
            return workers[i];
          }
        }
      }
      return NULL;
    }
    
    T *getIdleWorker() const
    {
      for(int i = 0; i < ArraySize(workers); i++)
      {
        if(workers[i] != NULL)
        {
          if(ChartPeriod(workers[i].getChartID()) > 0) //存在するかどうかの確認
          {
            if(!workers[i].isBusy())
            {
              return workers[i];
            }
          }
        }
      }
      return NULL;
    }
    
    T *findWorker(const long id) const
    {
      for(int i = 0; i < ArraySize(workers); i++)
      {
        if(workers[i] != NULL)
        {
          if(workers[i].getChartID() == id)
          {
            return workers[i];
          }
        }
      }
      return NULL;
    }
    
    bool revoke(const long id)
    {
      for(int i = 0; i < ArraySize(workers); i++)
      {
        if(workers[i] != NULL)
        {
          if(workers[i].getChartID() == id)
          {
            if(CheckPointer(workers[i]) == POINTER_DYNAMIC) delete workers[i];
            workers[i] = NULL;
            return true;
          }
        }
      }
      return false;
    }
    
    int available() const
    {
      int count = 0;
      for(int i = 0; i < ArraySize(workers); i++)
      {
        if(workers[i] != NULL)
        {
          count++;
        }
      }
      return count;
    }
    
    T *operator[](int i) const
    {
      return workers[i];
    }
    
};

メソッドの背後にある考え方は簡単です。 コンストラクタとデストラクタは、指定されたサイズハンドラの配列を割り当て、インプットします。 findWorker および getIdleWorker メソッドのグループは、さまざまな条件によって配列内のオブジェクトを詳しく見ることができます。 'operator<<' 演算子を使用すると、オブジェクトを動的に追加できますが、"revoke" メソッドでは動的に削除できます。

クライアント側のハンドラのプールには、(特にイベント処理に関して) 何らかの特異性が必要です。 したがって、派生した ClientWebWorkersPool 1 つを使用して基底クラスを拡張します。

template<typename T>
class ClientWebWorkersPool: public WebWorkersPool<T>
{
  protected:
    long   managerChartID;
    short  managerPoolSize;
    string name;
    
  public:
    ClientWebWorkersPool(const uint size, const string prefix): WebWorkersPool(size)
    {
      name = prefix;
      //WebRequest マネージャーのチャートを詳しく見ることができます。
      WebWorker::broadcastEvent(TO_MSG(MSG_DISCOVER), ChartID());
    }
    
    bool WebRequestAsync(const string method, const string url, const string headers, int timeout, const char &data[])
    {
      T *worker = getIdleWorker();
      if(worker != NULL)
      {
        return worker.request(method, url, headers, timeout, data, managerChartID);
      }
      return false;
    }
    
    void onChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
    {
      if(MSG(id) == MSG_DONE) //非同期リクエストは結果またはエラーで完了しました
      {
        Print(ChartID(), ": Result code ", (long)dparam);
    
        if(sparam != NULL)
        {
          //sparam の名前でリソースからデータを読み取ります。
          uchar initiator[], headers[], text[];
          ClientWebWorker::receiveResult(sparam, initiator, headers, text);
          string resname = CharArrayToString(initiator);
          
          T *worker = findWorker(resname);
          if(worker != NULL)
          {
            worker.onResult((long)dparam, headers, text);
            worker.release();
          }
        }
      }
      
      ...
      
      else
      if(MSG(id) == MSG_HELLO) //MSG_DISCOVER ブロードキャストの結果としてマネージャが見つかりました
      {
        if(managerChartID == 0 && lparam != 0)
        {
          if(ChartPeriod(lparam) > 0)
          {
            managerChartID = lparam;
            managerPoolSize = (short)dparam;
            for(int i = 0; i < ArraySize(workers); i++)
            {
              workers[i] = new T(ChartID(), name + (string)(i + 1) + "_");
            }
          }
        }
      }
    }
    
    bool isManagerBound() const
    {
      return managerChartID != 0;
    }
};

変数:

  • managerChartID —タスクマネージャーが見つかったウィンドウの ID。
  • managerPoolSize —ハンドラオブジェクト配列の初期サイズ。
  • name —すべてのプールオブジェクト内のリソースの共通プレフィックス。


2.4. メッセージの交換

ClientWebWorkersPool コンストラクタでは、WebWorker の呼び出しを参照してください:: broadcastEvent (MSG_DISCOVER)、ChartID()、イベントパラメータで現在のウィンドウの ID を渡すすべてのウィンドウに MSG_DISCOVER イベントを送信します。 MSG_DISCOVER は予約済みの値であり、同じヘッダー・ファイルの先頭に、プログラムが交換する他のタイプのメッセージと一緒に定義する必要があります。

#define MSG_DEINIT   1 // tear down (manager <-> worker)
#define MSG_WEB      2 // start request (client -> manager -> worker)
#define MSG_DONE     3 // request is completed (worker -> client, worker -> manager)
#define MSG_ERROR    4 // request has failed (manager -> client, worker -> client)
#define MSG_DISCOVER 5 // find the manager (client -> manager)
#define MSG_ACCEPTED 6 // request is in progress (manager -> client)
#define MSG_HELLO    7 // the manager is found (manager -> client)

このコメントは、メッセージの送信方向を示します。

TO_MSG マクロは、ユーザーが選択したランダムな基本値を基準にして、リストされた id を実際のイベント・コードに変換するように設計されています。 MessageBroadcast インプットで受信します。

sinput uint MessageBroadcast = 1;
 
#define TO_MSG(X) ((ushort)(MessageBroadcast + X))

この方法では、基本値を変更することで、すべてのイベントを自由な範囲に移動できます。 カスタムイベントは、他のプログラムでもターミナルで使用できることに注意してください。 したがって、干渉を避けることが重要です。

MessageBroadcast インプットは、multiweb ファイルをフィーチャーしたすべての MQL プログラム、すなわちクライアントとマネージャで表示されます。 マネージャとクライアントを起動するときに、同じ MessageBroadcast 値を指定します。

ClientWebWorkersPool クラスに戻りましょう。 onChartEvent メソッドは特別な場所を取ります。 標準の OnChartEvent イベントハンドラから呼び出されます。 イベントタイプは ' id ' パラメータで渡されます。 選択した基本値に基づいてシステムからコードを受信するため、 "ミラー化 " MSG マクロを使用して、MSG_ * * * 範囲に変換する必要があります。

#define MSG(x) (x - MessageBroadcast - CHARTEVENT_CUSTOM)

ここで CHARTEVENT_CUSTOM は、ターミナル内のすべてのカスタムイベントの範囲の始まりです。

ご覧の通り、ClientWebWorkersPool の onChartEvent メソッドは、上記のメッセージの一部を処理します。 たとえば、マネージャーは、メッセージ MSG_HELLO を MSG_DISCOVER の一括メッセージングに応答する必要があります。 この場合、マネージャー・ウィンドウ ID は lparam パラメータに渡され、使用可能なアシスタントの数は dparam パラメータに渡されます。 マネージャーが検出されると、プールは空の ' worker ' 配列に必要な型の実際のオブジェクトを格納します。 現在のウィンドウ ID、および各オブジェクトの一意のリソース名がオブジェクトコンストラクタに渡されます。 後者は、共通の ' name ' プレフィックスと配列内のシリアル番号で構成されます。

managerChartID フィールドが意味のある値を受け取ると、マネージャーにリクエストを送信できるようになります。 ' request ' メソッドは、ClientWebWorker クラスに予約されていますが、その使用方法はプールから WebRequestAsync メソッドで示されています。 最初に、WebRequestAsync は getIdleWorker を使用してフリーハンドラオブジェクトを検索し、対してワーカーリクエスト (メソッド、url、ヘッダー、タイムアウト、データ、managerChartID) を呼び出します。 ' request ' メソッドの内部では、イベントを送信するための特別なメッセージコードの選択に関するコメントがあります。 ここで、イベントサブシステムを検討した後、ClientWebWorker:: request メソッドの最終バージョンを形成できます。

class ClientWebWorker : public WebWorker
{
    ...

    bool request(const string method, const string url, const string headers, const int timeout, const uchar &body[], const long managerChartID)
    {
      _method = method;
      _url = url;

      ResourceMediator mediator(allocate());
      mediator.packRequest(method, url, headers, timeout, body);
    
      busy = EventChartCustom(managerChartID, TO_MSG(MSG_WEB), chartID, 0.0, getFullName());
      return busy;
    }
    
    ...
};

MSG_WEB は、WEB リクエストの実行に関するメッセージです。 受け取った後、マネージャはフリーのアシスタントEAを見つけて、chartID (lparam) クライアントウィンドウ ID と同様にリクエストパラメータを持つクライアントリソース名 (sparam) を渡します。

アシスタントはリクエストを実行し、MSG_DONE イベント (成功した場合) または MSG_ERROR を使用したエラーコード (問題が発生した場合) を使用して、結果をクライアントに返します。 この結果 (またはエラー) コードは dparam に渡され、結果自体は sparam に渡される名前の下のアシスタントEAにあるリソースにパックされます。 MSG_DONE ブランチでは、以前に考えられた ClientWebWorker:: receiveResult (sparam、イニシエーター、ヘッダー、テキスト) 関数を呼び出して、データがリソースから取得される方法を確認します。 次に、クライアント・ハンドラ・オブジェクト (findWorker) の検索は、リクエストイニシエーター・リソース名によって実行され、検出されたオブジェクトに対してメソッドが呼び出されます。

    T *worker = findWorker(resname);
    if(worker != NULL)
    {
      worker.onResult((long)dparam, headers, text);
      worker.release();
    }

すでに ' release ' メソッドを知っています-不要なリソースをインプットします。 onResult とは何でしょうか。 完全なソースコードを見れば、ClientWebWorker クラスは実装なしで2つの仮想関数 onResult と onError を備えていることがわかります。 これより、クラスが抽象化されます。 クライアントコードは、ClientWebWorker から派生クラスを記述し、実装を提供する必要があります。 メソッドの名前は、結果が正常に受信された場合に onResult が呼び出されることを意味しますが、エラーが発生した場合は onError が呼び出されます。 非同期リクエストのタスククラスと、使用するクライアントプログラムコードとの間のフィードバックが提供されます。 言い換えれば、クライアントプログラムはカーネルが内部的に使用するメッセージについて何も知る必要はありません: 開発された API とのクライアントコードのすべての相互作用は MQL5 OOP 組み込みツールによって実行します。

クライアントのソースコード (multiwebclient.mq5) を見てみましょう。


2.5. クライアントEA

テストEAは、ユーザーがインプットしたデータに基づいて、multiweb API 経由で複数のリクエストを送信します。 実現するには、ヘッダーファイルをインクルードし、インプットを追加する必要があります。

sinput string Method = "GET";
sinput string URL = "https://google.com/,https://ya.ru,https://www.startpage.com/";
sinput string Headers = "User-Agent: n/a";
sinput int Timeout = 5000;

#include <multiweb.mqh>

最終的に、すべてのパラメータは実行された HTTPrequests を設定します。 URL リストでは、リクエスト実行の並列性と速度を評価するために、コンマ区切りアドレスをリストすることができます。 URL パラメータは、次のように、OnInit の StringSplit 関数を使用してアドレスに分割されます。

int urlsnum;
string urls[];
  
void OnInit()
{
  //テストリクエストの url を取得する
  urlsnum = StringSplit(URL, ',', urls);
  ...
}

また、OnInit でリクエストハンドラオブジェクト (ClientWebWorkersPool) のプールを作成する必要があります。 しかし、これを行うためには、ClientWebWorker から派生したクラスを記述する必要があります。

class MyClientWebWorker : public ClientWebWorker
{
  public:
    MyClientWebWorker(const long id, const string p = "WRP_"): ClientWebWorker(id, p)
    {
    }
    
    virtual void onResult(const long code, const uchar &headers[], const uchar &text[]) override
    {
      Print(getMethod(), " ", getURL(), "\nReceived ", ArraySize(headers), " bytes in header, ", ArraySize(text), " bytes in document");
      //コメント解除潜在的にかさばるログにつながる
      // Print(CharArrayToString(headers));
      // Print(CharArrayToString(text));
    }

    virtual void onError(const long code) override
    {
      Print("WebRequest error code ", code);
    }
};

その唯一の目的は、ステータスと取得したデータをログに記録することです。 OnInit でそのようなオブジェクトのプールを作成することができます。

ClientWebWorkersPool<MyClientWebWorker> *pool = NULL;

void OnInit()
{
  ...
  pool = new ClientWebWorkersPool<MyClientWebWorker>(urlsnum, _Symbol + "_" + EnumToString(_Period) + "_");
  Comment("Click the chart to start downloads");
}

ご覧のように、このプールは MyClientWebWorker クラスによってパラメータ化され、ライブラリコードからオブジェクトを作成することができます。 この配列サイズは、インプットされたアドレスの数と同じになります。 これはデモンストレーションに対して妥当です: より小さい数は処理待ち行列を意味し、毀損は並列実行の考えを示しますが、より大きい数はリソースの浪費になります。 実際のプロジェクトでは、プールサイズはタスクの数と同じである必要はありませんが、追加のアルゴリズムバインディングが必要です。

リソースの接頭辞は、タスクシンボルの名前とチャート期間の組み合わせとして設定されます。

初期化の直近のタッチは、マネージャーウィンドウを詳しく見ることができます。 覚えているように、検索はプール自体 (ClientWebWorkersPool クラス) によって実行されます。 このクライアントコードは、マネージャが検出されたことを確認するだけです。 これらの目的のために、検索マネージャに関するメッセージと "response" が目標を達成するために保証されるべきである妥当な時間を設定しましょう。 5秒にしましょう。 この時間のタイマーを作成します。

void OnInit()
{
  ...
  //マネージャのネゴシエーションが最大5秒間待機する
  EventSetTimer(5);
}

マネージャーがタイマーハンドラに存在するかどうかを確認します。 接続が確立されていない場合にアラートを表示します。

void OnTimer()
{
  //マネージャが5秒間に応答しなかった場合は、不足しているようです
  EventKillTimer();
  if(!pool.isManagerBound())
  {
    Alert("WebRequest Pool Manager (multiweb) is not running");
  }
}

OnDeinit ハンドラでプールオブジェクトを削除することを忘れないでください。

void OnDeinit(const int reason)
{
  delete pool;
  Comment("");
}

プールがすべてのサービスメッセージを処理しないようにするには (最初にマネージャを検索するなど)、標準の OnChartEvent チャートイベントハンドラを使用します。

void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 
{
  if(id == CHARTEVENT_CLICK) //シンプルなユーザー操作によるテストリクエストの開始
  {
    ...
  }
  else
  {
    //このハンドラは、シーンの背後にあるすべての重要なメッセージングを管理します
    pool.onChartEvent(id, lparam, dparam, sparam);
  }
}

CHARTEVENT_CLICK を除くすべてのイベントは、適用されたイベントのコードの分析に基づいて適切なアクションが実行されるプールに送信されます (onChartEvent フラグメントは上記で提供されました)。

CHARTEVENT_CLICK イベントはインタラクティブであり、ダウンロードを起動するために直接使用します。 最もシンプルなケースでは、次のようになります。

void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 
{
  if(id == CHARTEVENT_CLICK) //シンプルなユーザー操作によるテストリクエストの開始
  {
    if(pool.isManagerBound())
    {
      uchar Body[];

      for(int i = 0; i < urlsnum; i++)
      {
        pool.WebRequestAsync(Method, urls[i], Headers, Timeout, Body);
      }
    }
    ...

この例の完全なコードは、実行時間を計算し、同じアドレスのセットの標準 WebRequest のシーケンシャル呼び出しと比較するためのロジックも備えているため、少し長期です。


2.6. マネージャーEAとアシスタントEA

最終的に、 "サーバー" の部分に到達しました。 基本的なメカニズムはすでにヘッダファイルに実装されているので、マネージャとアシスタントのコードは想像するほど面倒ではありません。

覚えているかもしれませんが、マネージャとして、またはアシスタント (multiweb ファイル) として働いているEAは1つだけです。 クライアントの場合と同様に、ヘッダーファイルをインクルードし、インプットパラメータを宣言します。

sinput uint WebRequestPoolSize = 3;
sinput ulong ManagerChartID = 0;

#include <multiweb.mqh>

WebRequestPoolSize は、マネージャがアシスタントを起動するために作成する必要のある補助ウィンドウの数です。

ManagerChartID はマネージャウィンドウ ID です。 このパラメータはアシスタントとしてのみ使用でき、アシスタントがソースコードから自動的に起動されたときにマネージャでインプットされます。 マネージャの起動時に手動で ManagerChartID をインプットすると、エラーとして扱われます。

このアルゴリズムは、次の2つのグローバル変数を中心に構築されます。

bool manager;
WebWorkersPool<ServerWebWorker> pool;

' manager ' 論理フラグは、現在のEAインスタンスのロールを示します。 ' pool ' 変数は、受信タスクのハンドラオブジェクトの配列です。 WebWorkersPool は、上述した ServerWebWorker クラスに代表されます。 配列の充てんはロールに依存するため、事前に初期化されていません。

最初に起動されたインスタンス (OnInit で定義) は、マネージャロールを受け取ります。

const string GVTEMP = "WRP_GV_TEMP";

int OnInit()
{
  manager = false;
  
  if(!GlobalVariableCheck(GVTEMP))
  {
    //multiweb の最初のインスタンスが開始されると、マネージャとして扱われます
    //グローバル変数は、マネージャーが存在するフラグです。
    if(!GlobalVariableTemp(GVTEMP))
    {
      FAILED(GlobalVariableTemp);
      return INIT_FAILED;
    }
    
    manager = true;
    GlobalVariableSet(GVTEMP, 1);
    Print("WebRequest Pool Manager started in ", ChartID());
  }
  else
  {
    //multiweb の次のすべてのインスタンスは、ワーカー/ヘルパーです。
    Print("WebRequest Worker started in ", ChartID(), "; manager in ", ManagerChartID);
  }
  
  //ワーカーの遅延インスタンス化にタイマーを使用する
  EventSetTimer(1);
  return INIT_SUCCEEDED;
}

このEAは、ターミナルの特別なグローバル変数の存在をチェックします。 存在しない場合、EAは自身をマネージャに割り当て、そのようなグローバル変数を作成します。 変数が既に存在する場合は、マネージャーがそのため、このインスタンスは、アシスタントになります。 グローバル変数は一時的なものであるため、ターミナルの再起動時には保存されないことに注意してください。 しかし、マネージャーが任意のチャートに残っている場合は、変数が再び作成されます。

補助チャートの初期化には数秒かかり、OnInit からを行うことは最良の解決策ではないので、タイマーは、1秒に設定されます。 タイマーイベントハンドラでプールにインプットします。

void OnTimer()
{
  EventKillTimer();
  if(manager)
  {
    if(!instantiateWorkers())
    {
      Alert("Workers not initialized");
    }
    else
    {
      Comment("WebRequest Pool Manager ", ChartID(), "\nWorkers available: ", pool.available());
    }
  }
  else // worker
  {
    応答ヘッダーとデータを格納するリソースのホストとして使用します。
    pool << new ServerWebWorker(ChartID(), "WRR_");
  }
}

アシスタントロールの場合、さらに別の ServerWebWorker ハンドラオブジェクトが配列に追加されます。 マネージャのケースはより複雑で、別の instantiateWorkers 関数に配置されています。 見てみましょう。

bool instantiateWorkers()
{
  MqlParam Params[4];
  
  const string path = MQLInfoString(MQL_PROGRAM_PATH);
  const string experts = "\\MQL5\\";
  const int pos = StringFind(path, experts);
  
  // start itself again (in another role as helperEA)
  Params[0].string_value = StringSubstr(path, pos + StringLen(experts));
  
  Params[1].type = TYPE_UINT;
  Params[1].integer_value = 1; // 1マネージャまたはクライアントに結果を返すための新しいヘルパーEAインスタンス内のワーカー

  Params[2].type = TYPE_LONG;
  Params[2].integer_value = ChartID(); // このチャートはマネージャーです

  Params[3].type = TYPE_UINT;
  Params[3].integer_value = MessageBroadcast; // 同じカスタムイベントベース番号を使用する
  
  for(uint i = 0; i < WebRequestPoolSize; ++i)
  {
    long chart = ChartOpen(_Symbol, _Period);
    if(chart == 0)
    {
      FAILED(ChartOpen);
      return false;
    }
    if(!EXPERT::Run(chart, Params))
    {
      FAILED(EXPERT::Run);
      return false;
    }
    pool << new ServerWebWorker(chart);
  }
  return true;
}

この関数は、古い友人によって開発された Expert サードパーティのライブラリを使用しています-MQL5 コミュニティfxsaberのメンバは、ソースコードの冒頭に追加しました。

#include <fxsaber\Expert.mqh>

エキスパートライブラリを使用すると、指定されたEAパラメータを使用してTPl テンプレートを動的に生成し、指定されたチャートに適用することができ、EAの起動につながります。 この場合、すべてのアシスタントEAのパラメータは同じなので、指定された数のウィンドウを作成する前に、そのリストが1回生成されます。

パラメータ0は、実行可能なEAファイルへのパス、すなわち自体を指定します。 パラメータ1は WebRequestPoolSize です。 各アシスタントでは1です。 既に説明したように、ハンドラオブジェクトは、HTTPリクエストの結果を持つリソースを格納するためだけに、アシスタントで必要です。 各アシスタントは、ブロッキング WebRequest によってリクエストを処理します、すなわち、1つのハンドラオブジェクトだけが最大で使用します。 パラメータ2— ManagerChartID マネージャのウィンドウ ID。 パラメータ3—メッセージ・コードの基本値 (MessageBroadcast パラメータは multiweb から取得されます)。

さらに、空のチャートは ChartOpen の助けを使ってループ内に作成され、アシスタントEAはエキスパート:: Run (チャート、Params) を使用して起動されます。 ServerWebWorker (chart) ハンドラーオブジェクトは、新しいウィンドウごとに作成され、プールに追加します。 マネージャでは、HTTPリクエストはマネージャ自体で実行されず、リソースが作成されないため、ハンドラオブジェクトはアシスタントのウィンドウ id とそのステータスへのリンクにすぎません。

受信タスクは、OnChartEvent のユーザーイベントに基づいて処理されます。

void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 
{
  if(MSG(id) == MSG_DISCOVER) //新しいクライアントチャート上のワーカーEAが初期化され、このマネージャにバインドしたい
  {
    if(manager && (lparam != 0))
    {
      //マネージャのみがそのチャート id で応答し、lparam はクライアントチャート id
      EventChartCustom(lparam, TO_MSG(MSG_HELLO), ChartID(), pool.available(), NULL);
    }
  }
  else
  if(MSG(id) == MSG_WEB) //クライアントが web ダウンロードをリクエストしました
  {
    if(lparam != 0)
    {
      if(manager)
      {
        //マネージャーは、タスクをアイドル状態のタスクに委任します。
        //lparam はクライアント・チャート ID で、sparam はクライアント・リソースです。
        if(!transfer(lparam, sparam))
        {
          EventChartCustom(lparam, TO_MSG(MSG_ERROR), ERROR_NO_IDLE_WORKER, 0.0, sparam);
        }
      }
      else
      {
        //ワーカーは web リクエストを実際に処理します。
        startWebRequest(lparam, sparam);
      }
    }
  }
  else
  if(MSG(id) == MSG_DONE) //lparam のチャート ID で識別されたタスクがジョブを終了しました
  {
    WebWorker *worker = pool.findWorker(lparam);
    if(worker != NULL)
    {
      //ここではマネージャにいて、プールはリソースのないスタブワーカーを保持しています
      //したがって、このリリースは、ビジー状態をクリーンアップすることのみを目的としています
      worker.release();
    }
  }
}

まず、lparam id を持つクライアントから取得した MSG_DISCOVER への応答として、manager はそのウィンドウ id を含む MSG_HELLO メッセージを返します。

MSG_WEB を受信すると、lparam はリクエストを送信したクライアントのウィンドウ ID を格納する必要がありますが、sparam には、パックされたリクエストパラメータを持つリソースの名前をインクルードする必要があります。 マネージャとして動作しているコードは、パラメータを使用してタスクをアイドルアシスタントに渡し、' transfer ' 関数 (後述) を呼び出し、選択したオブジェクトのステータスを "ビジー" に変更します。 アイドル状態のアシスタントがない場合、MSG_ERROR イベントは ERROR_NO_IDLE_WORKER コードを使用してクライアントに送信されます。 アシスタントは、startWebRequest 関数で HTTPrequest を実行します。

MSG_DONE イベントは、後者がリクエストされたドキュメントをアップロードするときに、アシスタントからマネージャーに到着します。 このマネージャは、lparam のアシスタント ID によって適切なオブジェクトを検索し、' release ' メソッドを呼び出すことによって、その "ビジー " ステータスを無効にします。 既に説明したように、アシスタントはその操作の結果をクライアントに直接送信します。

完全なソースコードには、OnDeinit 処理に密接に関連する MSG_DEINIT イベントも含まれています。 このアイデアは、アシスタントがマネージャの削除を通知され、応答として自身をアンロードしてウィンドウを閉じ、マネージャがアシスタントの削除を通知し、マネージャのプールから削除するというものです。 読者の皆様がこのメカニズムを理解していただけると信じています。

' transfer ' 関数は、自由なオブジェクトを検索し、その「転送」メソッドを呼び出します (上記で説明しました)。

bool transfer(const long returnChartID, const string resname)
{
  ServerWebWorker *worker = pool.getIdleWorker();
  if(worker == NULL)
  {
    return false;
  }
  return worker.transfer(resname, returnChartID);
}

startWebRequest 関数は次のように定義されます。

void startWebRequest(const long returnChartID, const string resname)
{
  const RESOURCEDATA<uchar> resource(resname);
  ResourceMediator mediator(&resource);

  string method, url, headers;
  int timeout;
  uchar body[];

  mediator.unpackRequest(method, url, headers, timeout, body);

  char result[];
  string result_headers;
  
  int code = WebRequest(method, url, headers, timeout, body, result, result_headers);
  if(code != -1)
  {
    //カスタムイベントを介してクライアントに返す結果を含むリソースを作成する
    ((ServerWebWorker *)pool[0]).receive(resname, result, result_headers);
    //最初に、結果のリソースを使用して MSG_DONE をクライアントに送信します。
    EventChartCustom(returnChartID, TO_MSG(MSG_DONE), ChartID(), (double)code, pool[0].getFullName());
    //次に、MSG_DONE をマネージャに送信して、対応するワーカーをアイドル状態に設定します。
    EventChartCustom(ManagerChartID, TO_MSG(MSG_DONE), ChartID(), (double)code, NULL);
  }
  else
  {
    //dparam のエラーコード
    EventChartCustom(returnChartID, TO_MSG(MSG_ERROR), ERROR_MQL_WEB_REQUEST, (double)GetLastError(), resname);
    EventChartCustom(ManagerChartID, TO_MSG(MSG_DONE), ChartID(), (double)GetLastError(), NULL);
  }
}

ResourceMediator を使用することにより、関数はリクエストパラメータをアンパックし、標準の MQL WebRequest 関数を呼び出します。 関数が MQL エラーなしで実行された場合、結果はクライアントに送信されます。 これを行うために、 ' receive ' メソッド (前述) を使用してローカルリソースにパックされ、その名前は MSG_DONE メッセージと共に EventChartCustom 関数の sparam パラメータに渡されます。 HTTPerrors (たとえば、無効なページ404または web サーバーエラー 501) もここに HTTPしていることに注意してください-クライアントは、dparam パラメーターと応答の HTTPheaders で、状況を分析することができますが、リソースでは、この値を受信します。

WebRequest 呼び出しが MQL エラーで終了した場合、クライアントは ERROR_MQL_WEB_REQUEST コードを使用して MSG_ERROR メッセージを受信し、GetLastError 結果は dparam に配置されます。 この場合、ローカルリソースはインプットされないため、ソースリソースの名前は sparam パラメータに直接渡されるため、リソースを持つハンドラオブジェクトの特定のインスタンスをクライアント側で識別できます。

非同期および並列 WebRequest 呼び出しの multiweb ライブラリクラスの図

図4. 非同期および並列 WebRequest 呼び出しの multiweb ライブラリクラスの図


3. テスト

実装されたソフトウェア複合体のテストは、以下のように行うことができます。

まず、ターミナル設定を開き、[エキスパート] タブで許可されているURLのリストから、アクセスするすべてのサーバを指定します。

次に、multiwebEAを起動し、インプットに3アシスタントを設定します。 その結果、異なる役割で発売された同じ multiwebEAをフィーチャーした3つの新しいウィンドウが開きます。 このEAロールは、ウィンドウの左上隅にあるコメントに表示されます。

さて、別のチャート上で multiwebclient クライアントEAを起動し、チャートを1回クリックしてみましょう。 デフォルトの設定では、3つの並列 web リクエストを開始し、取得したデータのサイズと実行時間を含む診断をログに書き込みます。 TestSyncRequests 特殊パラメータが「true」のままの場合、同じページの順次リクエストは、マネージャーを介した並列 web リクエストに加えて、標準 WebRequest を使用して実行されます。 2つのオプションの実行速度を比較するために行われます。 原則として、並列処理は、sqrt (n) から n (n は使用可能なアシスタントの数) までの、順次1よりも数倍高速です。

サンプルログが以下に表示されます。

01:16:50.587    multiweb (EURUSD,H1)    OnInit 129912254742671339
01:16:50.587    multiweb (EURUSD,H1)    WebRequest Pool Manager started in 129912254742671339
01:16:52.345    multiweb (EURUSD,H1)    OnInit 129912254742671345
01:16:52.345    multiweb (EURUSD,H1)    WebRequest Worker started in 129912254742671345; manager in 129912254742671339
01:16:52.757    multiweb (EURUSD,H1)    OnInit 129912254742671346
01:16:52.757    multiweb (EURUSD,H1)    WebRequest Worker started in 129912254742671346; manager in 129912254742671339
01:16:53.247    multiweb (EURUSD,H1)    OnInit 129912254742671347
01:16:53.247    multiweb (EURUSD,H1)    WebRequest Worker started in 129912254742671347; manager in 129912254742671339
01:17:16.029    multiweb (EURUSD,H1)    Pool manager transfers \Experts\multiwebclient.ex5::GBPJPY_PERIOD_M5_1_129560567193673862
01:17:16.029    multiweb (EURUSD,H1)    129912254742671345: Reading request \Experts\multiwebclient.ex5::GBPJPY_PERIOD_M5_1_129560567193673862
01:17:16.029    multiweb (EURUSD,H1)    129912254742671345: Got 64 bytes in request
01:17:16.029    multiweb (EURUSD,H1)    129912254742671345: GET https://google.com/ User-Agent: n/a 5000 
01:17:16.030    multiweb (EURUSD,H1)    Pool manager transfers \Experts\multiwebclient.ex5::GBPJPY_PERIOD_M5_2_129560567193673862
01:17:16.030    multiweb (EURUSD,H1)    129912254742671346: Reading request \Experts\multiwebclient.ex5::GBPJPY_PERIOD_M5_2_129560567193673862
01:17:16.030    multiwebclient (GBPJPY,M5)      Accepted: \Experts\multiwebclient.ex5::GBPJPY_PERIOD_M5_1_129560567193673862 after 0 retries
01:17:16.031    multiweb (EURUSD,H1)    129912254742671346: Got 60 bytes in request
01:17:16.031    multiweb (EURUSD,H1)    129912254742671346: GET https://ya.ru User-Agent: n/a 5000 
01:17:16.031    multiweb (EURUSD,H1)    Pool manager transfers \Experts\multiwebclient.ex5::GBPJPY_PERIOD_M5_3_129560567193673862
01:17:16.031    multiwebclient (GBPJPY,M5)      Accepted: \Experts\multiwebclient.ex5::GBPJPY_PERIOD_M5_2_129560567193673862 after 0 retries
01:17:16.031    multiwebclient (GBPJPY,M5)      Accepted: \Experts\multiwebclient.ex5::GBPJPY_PERIOD_M5_3_129560567193673862 after 0 retries
01:17:16.031    multiweb (EURUSD,H1)    129912254742671347: Reading request \Experts\multiwebclient.ex5::GBPJPY_PERIOD_M5_3_129560567193673862
01:17:16.032    multiweb (EURUSD,H1)    129912254742671347: Got 72 bytes in request
01:17:16.032    multiweb (EURUSD,H1)    129912254742671347: GET https://www.startpage.com/ User-Agent: n/a 5000 
01:17:16.296    multiwebclient (GBPJPY,M5)      129560567193673862: Result code 200
01:17:16.296    multiweb (EURUSD,H1)    Result code from 129912254742671346: 200, now idle
01:17:16.297    multiweb (EURUSD,H1)    129912254742671346: Done in 265ms
01:17:16.297    multiwebclient (GBPJPY,M5)      129560567193673862: Reading result \Experts\multiweb.ex5::WRR_129912254742671346
01:17:16.300    multiwebclient (GBPJPY,M5)      129560567193673862: Got 16568 bytes in response
01:17:16.300    multiwebclient (GBPJPY,M5)      GET https://ya.ru
01:17:16.300    multiwebclient (GBPJPY,M5)      Received 3704 bytes in header, 12775 bytes in document
01:17:16.715    multiwebclient (GBPJPY,M5)      129560567193673862: Result code 200
01:17:16.715    multiwebclient (GBPJPY,M5)      129560567193673862: Reading result \Experts\multiweb.ex5::WRR_129912254742671347
01:17:16.715    multiweb (EURUSD,H1)    129912254742671347: Done in 686ms
01:17:16.715    multiweb (EURUSD,H1)    Result code from 129912254742671347: 200, now idle
01:17:16.725    multiwebclient (GBPJPY,M5)      129560567193673862: Got 45236 bytes in response
01:17:16.725    multiwebclient (GBPJPY,M5)      GET https://www.startpage.com/
01:17:16.725    multiwebclient (GBPJPY,M5)      Received 822 bytes in header, 44325 bytes in document
01:17:16.900    multiwebclient (GBPJPY,M5)      129560567193673862: Result code 200
01:17:16.900    multiweb (EURUSD,H1)    Result code from 129912254742671345: 200, now idle
01:17:16.900    multiweb (EURUSD,H1)    129912254742671345: Done in 873ms
01:17:16.900    multiwebclient (GBPJPY,M5)      129560567193673862: Reading result \Experts\multiweb.ex5::WRR_129912254742671345
01:17:16.903    multiwebclient (GBPJPY,M5)      129560567193673862: Got 13628 bytes in response
01:17:16.903    multiwebclient (GBPJPY,M5)      GET https://google.com/
01:17:16.903    multiwebclient (GBPJPY,M5)      Received 790 bytes in header, 12747 bytes in document
01:17:16.903    multiwebclient (GBPJPY,M5)      > > > Async WebRequest workers [3] finished 3 tasks in 873ms

すべてのリクエストの合計実行時間は、最もスローな場合と実行時間が等しくなることに注意してください。

マネージャのアシスタントの数を1に設定すると、リクエストは順番に処理されます。


結論

この記事では、非ブロッキングモードで HTTPリクエストを実行するための多くのクラスと既製のEAを考えてきました。 並列スレッドでインターネットからデータを取得し、HTTPリクエストに加えて、リアルタイムで分析計算を実行する必要があり、EAの効率を高めることができます。 また、このライブラリは標準 WebRequest が禁止されているインジケータでも使用できます。 アーキテクチャ全体を実装するには、ユーザーイベントの受け渡し、リソースの作成、windows の動的なオープン、およびその上でのEAの実行という幅広い MQL 関数を使用する必要がありました。

執筆時点では、EAを起動するための補助ウィンドウの作成は、HTTPリクエストを並列化する唯一のオプションですが、メタクオーツは特別なバックグラウンド MQL プログラムを開発する予定です。 MQL5/services フォルダはすでにそのようなサービスに予約されています。 この技術がターミナルに登場すると、補助ウィンドウをサービスに置き換えることで、このライブラリが改善される可能性があります。

添付ファイル:

  • MQL5/インクルード/multiweb-ライブラリ
  • MQL5/エキスパート/multiweb.mq5 —マネージャーEAとアシスタントEA 
  • MQL5/エキスパート/multiwebclient.mq5 —デモクライアントEA
  • MQL5/インクルード/fxsaber/リソースの追加-リソースを操作するための補助クラス
  • MQL5/インクルード/fxsaber/ResourceData-リソースを扱うための補助クラス
  • MQL5/インクルード/fxsaber/エキスパート:EAを起動するための補助クラス
  • MQL5/インクルード/TypeToBytes-データ変換ライブラリ