English Русский 中文 Español 日本語 Português
Die eigene, multi-threaded, asynchrone Web-Anfrage in MQL5

Die eigene, multi-threaded, asynchrone Web-Anfrage in MQL5

MetaTrader 5Experten | 14 Januar 2019, 07:20
797 0
Stanislav Korotky
Stanislav Korotky

Die Umsetzung von Handelsalgorithmen erfordert oft die Analyse von Daten aus verschiedenen externen Quellen, einschließlich des Internets. MQL5 stellt die Funktion WebRequest zum Senden von HTTP-Anfragen an die "Außenwelt" zur Verfügung, die hat aber leider einen spürbaren Nachteil. Die Funktion ist synchron, d.h. sie blockiert den EA-Vorgang für die gesamte Dauer einer Auftragsausführung. MetaTrader 5 weist jedem EA einen einzelnen Thread zu, der sequentiell die vorhandenen API-Funktionsaufrufe im Code ausführt, sowie eingehende Event-Handler (wie Ticks, Tiefe der Marktveränderungen in BookEvent, Timer, Handelsoperationen, Chart-Events, etc.). Es wird jeweils nur ein Codefragment ausgeführt, während alle verbleibenden "Tasks" in Warteschlangen warten, bis sie 'drankommen' und bis das aktuelle Fragment die Kontrolle an den Kernel zurückgibt.

Wenn ein EA beispielsweise neue Ticks in Echtzeit verarbeitet und regelmäßig Wirtschaftsnachrichten auf einer oder mehreren Websites überprüft, ist es unmöglich, beide Anforderungen zu erfüllen, ohne dass sie sich gegenseitig stören. Sobald WebRequest im Code ausgeführt wird, bleibt die EA auf der Funktionsaufrufzeichenkette "eingefroren", während neue Tick-Ereignisse übersprungen werden. Selbst mit der Möglichkeit, übersprungene Ticks mit der CopyTicks-Funktion zu lesen, kann der Moment für eine Handelsentscheidung verpasst werden. So zeigt sich diese Situation, veranschaulicht durch ein UML-Ablaufdiagramm:

Ablaufdiagramm der Ereignisbehandlung mit der Blockierung bei nur einem Thread

Abb. 1. Ablaufdiagramm der Ereignisbehandlung mit der Blockierung bei nur einem Thread

In diesem Zusammenhang wäre es sinnvoll, ein Werkzeug für die asynchrone, Ausführung ohne eine Blockierung von HTTP-Anfragen zu schaffen, eine Art WebRequestAsync. Natürlich müssen wir dafür zusätzliche Threads besorgen. Der einfachste Weg, dies in MetaTrader 5 zu tun, ist, zusätzliche EAs auszuführen, an die Sie zusätzliche HTTP-Anfragen senden können. Außerdem können Sie dort WebRequest aufrufen und die Ergebnisse nach etwas Zeit erhalten. Während die Anfrage in einem solchen Hilfs-EA bearbeitet wird, bleibt unser Haupt-EA für schnelle und interaktive Aktionen verfügbar. Das UML-Ablaufdiagramm würde in diesem Fall so aussehen:

Das Ablaufdiagramm, wenn die asynchrone Ereignisbehandlung an andere Threads delegiert wird.

Abb. 2. Das Ablaufdiagramm, wenn die asynchrone Ereignisbehandlung an andere Threads delegiert wird.


1. Planung

Wie Sie wissen, muss jeder EA auf seinem eigenen Chart im MetaTrader arbeiten. Daher erfordert die Erstellung von zusätzlichen EAs spezielle Charts für sie. Es ist unangenehm, das manuell zu tun. Daher ist es sinnvoll, alle Routinevorgänge an einen speziellen Manager zu delegieren - ein EA, das einen Pool von Hilfsdiagrammen und EAs verwaltet und auch einen einzigen Einstiegspunkt für die Registrierung neuer Anfragen von Client-Programmen bietet. In gewisser Weise kann man diese Architektur als eine 3-stufige Architektur bezeichnen, ähnlich der Client-Server-Architektur, bei der der EA-Manager als Server fungiert:

Die Architektur der Multiweb-Bibliothek: Client MQL-Code - Server (Assistant Pool Manager) - Hilfs-EAs

Abb. 3 Die Architektur der Multiweb-Bibliothek: Client MQL-Code <-> Server (Assistant Pool Manager) <-> Hilfs-EAs

Der Einfachheit halber können jedoch der Manager und das Hilfs-EA in Form desselben Codes (Programms) implementiert werden. Eine der beiden Rollen eines solchen "universellen" EAs - der Manager oder sein Assistent - wird durch das Prioritätsgesetz bestimmt. Die erste Instanz, die gestartet wird, erklärt sich selbst zum Manager, öffnet Hilfstabellen und startet eine bestimmte Anzahl von sich selbst in der Rolle der Assistenten.

Was genau und wie sollen der Client, der Manager und seine Assistenten miteinander 'reden'? Um dies zu verstehen, analysieren wir die Funktion WebRequest.

Wie Sie wissen, verfügt MetaTrader 5 über zwei Versionen der WebRequest-Funktion. Wir werden die zweite als die universellste betrachten. Die zweite ist wohl die universellere.

int WebRequest
( 
  const string      method,           // HTTP-Methode
  const string      url,              // Url-Adresse 
  const string      headers,          // Headers  
  int               timeout,          // Timeout 
  const char        &data[],          // HTTP Nachricht, Hauptteil (body) 
  char              &result[],        // Array mit den Daten der Antwort des Servers
  string            &result_headers   // Headers der Serverantwort
);

Die ersten fünf Parameter sind Eingabeparameter. Sie werden beim Aufruf übergeben und definieren den Inhalt der Anforderung. Die letzten beiden Parameter sind Rückgabeparameter. Sie werden von der Funktion an den aufrufenden Code übergeben und enthalten das Abfrageergebnis. Offensichtlich erfordert es, diese Funktion in eine asynchrone zu wandeln und sie in zwei Komponenten aufzuteilen: die Initialisierung der Abfrage und der Erhalt der Ergebnisse:

int WebRequestAsync
( 
  const string      method,           // HTTP-Methode
  const string      url,              // Url-Adresse 
  const string      headers,          // Headers  
  int               timeout,          // Timeout 
  const char        &data[],          // HTTP Nachricht, Hauptteil (body) 
);

int WebRequestAsyncResult
( 
  char              &result[],        // Array mit den Daten der Antwort des Servers
  string            &result_headers   // Headers der Serverantwort
);

Die Namen und Prototypen der Funktionen sind bedingt. Tatsächlich müssen wir diese Informationen zwischen verschiedenen MQL-Programmen austauschen. Normale Funktionsaufrufe sind dafür nicht geeignet. Um MQL-Programme miteinander "kommunizieren" zu lassen, können wir in MetaTrader 5 das System Nutzerereignisse verwenden. Der Ereignisaustausch erfolgt basierend auf einer Empfänger-ID unter Verwendung von ChartID - sie ist für jedes Chart eindeutig. Es kann nur ein EA in einem Chart geben, aber es gibt keine solche Einschränkung bei Indikatoren. Das bedeutet aber auch, dass ein Benutzer sicherstellen sollte, dass jedes Chart nicht mehr als einen Indikator enthält, der mit dem Manager kommuniziert.

Damit der Datenaustausch funktioniert, müssen wir alle "Funktionsparameter" in die Benutzerereignisparameter packen. Sowohl Anforderungsparameter als auch Ergebnisse können relativ große Mengen an Informationen enthalten, die nicht physisch in den begrenzten Umfang der Ereignisse passen. Selbst wenn wir uns zum Beispiel entscheiden, die HTTP-Methode und die URL als sparam, Ereignisparameter vom Typ string, zu übergeben, wäre die Begrenzung der Länge auf 63 Zeichen in den meisten Arbeitsfällen ein Problem. Das bedeutet, dass ein Ereignis-Austauschsystem durch eine Art gemeinsames Daten-Repository ergänzt werden muss und nur Links zu Datensätzen in diesem Repository in den Ereignisparametern gesendet werden sollten. Glücklicherweise bietet MetaTrader 5 einen solchen Speicher in Form von kundenspezifischen Ressourcen. Tatsächlich sind dynamisch aus MQL erstellte Ressourcen immer Bilder. Aber ein Bild ist ein Container mit binären Informationen, in dem man alles schreiben kann, was man will.

Um die Aufgabe zu vereinfachen, verwenden wie fertige Lösungen, beliebige Daten in nutzerspezifische Ressourcen zu schreiben und zu lesen — Klassen aus Resource.mqh und ResourceData.mqh, die von fxsaber, einem Mitglied der MQL5-Community ist, bereitgestellt wurden.

Der übergebene Link führt zu einer Quelle - die TradeTransactions-Bibliothek ist nicht mit dem Thema des aktuellen Artikels verbunden, aber die Diskussion (auf Russisch) enthält ein Beispiel für Datenspeicherung und -austausch über die Ressourcen. Da sich die Bibliothek ändern kann, und auch aus Gründen der Benutzerfreundlichkeit, sind alle im Artikel verwendeten Dateien unten angehängt, aber ihre Versionen entsprechen dem Zeitpunkt des Schreibens des Artikels und können von den aktuellen Versionen abweichen, die über den obigen Link bereitgestellt werden. Außerdem verwenden die erwähnten Klassen der Ressourcen eine weitere Bibliothek in ihrer Arbeit - TypeToBytes. Deren Version ist ebenfalls dem Artikel beigefügt.

Wir brauchen uns nicht mit der internen Struktur dieser Hilfsklassen zu befassen. Hauptsache, wir können uns auf die fertige Klasse RESOURCEDATA als "Black Box" verlassen und deren Konstruktor und einige für uns geeignete Funktionen nutzen. Darauf werden wir später näher eingehen. Lassen Sie uns nun das Gesamtkonzept erläutern.

Die Reihenfolge der Interaktion der Teile unserer Architektur sieht wie folgt aus:

  1. Um eine asynchrone Webanfrage durchzuführen, sollte das Client-MQL-Programm die von uns entwickelten Klassen verwenden, um die Anfrageparameter in eine lokale Ressource zu packen und ein benutzerdefiniertes Ereignis mit einem Link zu der Ressource an den Manager zu senden; die Ressource wird innerhalb des Client-Programms erstellt und wird erst gelöscht, wenn die Ergebnisse vorliegen (und sie unnötig wird);
  2. Der Manager findet im Pool einen unbesetzten Assistent-EA und sendet ihm einen Link zur Ressource; diese Instanz ist jedoch zeitweise als besetzt markiert und kann für nachfolgende Anfragen erst nach Bearbeitung des aktuellen Auftrags ausgewählt werden;
  3. Parameter einer Webanfrage von der externen Ressource des Clients werden im Assistent-EA entpackt, der ein benutzerdefiniertes Ereignis empfangen hat;
  4. Der Assistent-EA ruft den normalen, blockierenden WebRequest auf und wartet auf eine Antwort (Header und/oder Webdokument);
  5. Der Assistent-EA packt die Anfrageergebnisse in seine lokale Ressource und sendet ein benutzerdefiniertes Ereignis an den Manager mit einem Link zu dieser Ressource;
  6. Der Manager leitet das Ereignis an den Client weiter und markiert den entsprechenden Assistenten wieder als frei;
  7. Der Client erhält eine Nachricht vom Manager und entpackt das Ergebnis der Anforderung aus der externen Ressource des Assistenten;
  8. Der Client und der Assistent können ihre lokalen Ressourcen löschen.

Die Ergebnisse können bei den Schritten 5 und 6 effizienter weitergegeben werden, da der Assistent-EA das Ergebnis direkt an das Client-Fenster sendet und den Manager umgeht.

Die oben beschriebenen Schritte beziehen sich auf die Hauptschritte der Verarbeitung von HTTP-Anfragen. Nun ist es an der Zeit, die Verknüpfung unterschiedlicher Teile zu einer einzigen Architektur zu beschreiben. Teilweise ist es auch auf Nutzerereignisse angewiesen.

Das zentrale Element der Architektur — der Manager — soll manuell gestartet werden. Wir sollten es aber nur einmal machen. Wie jeder andere laufende EA stellt er sich nach dem Neustart des Terminals automatisch zusammen mit dem Diagramm wieder her. Das Terminal erlaubt nur einen Web-Request-Manager.

Der Manager erstellt die erforderliche Anzahl von Hilfsfenstern (die in den Einstellungen einzustellen sind) und startet in ihnen Instanzen, die dank des speziellen "Protokolls" (Details finden Sie im Abschnitt Umsetzung) ihren Assistentenstatus selbst "herausfinden".

Jeder Assistent informiert den Manager über seinen Abschluss mit Hilfe eines speziellen Ereignisses. Dies ist notwendig, um eine entsprechende Liste der verfügbaren Assistenten im Manager zu pflegen. Ebenso benachrichtigt der Manager die Assistenten über den Abschluss. Die Assistenten wiederum stoppen die Arbeit und schließen ihre Fenster. Die Assistenten sind ohne den Manager nutzlos, während der Neustart des Managers zwangsläufig die Assistenten neu erstellt (z.B. wenn Sie die Anzahl der Assistenten in den Einstellungen ändern).

Fenster für Assistenten, wie die Hilfs-EAs selbst, sollen immer automatisch von dem Manager erstellt werden, und deshalb sollte unser Programm sie "bereinigen". Starten Sie den Assistenten-EA nicht manuell — Eingaben, die nicht dem Status des Managers entsprechen, werden vom Programm als Fehler betrachtet.

Während des Starts sollte das Client-MQL-Programm das Terminalfenster auf die Anwesenheit des Managers mittels Massen-Nachrichten und die Angabe seiner ChartID im Parameter überwachen. Der Manager (falls gefunden) sollte die ID seines Chartfensters an den Client zurückgeben. Danach können Client und Manager Nachrichten austauschen.

Dies sind die Hauptmerkmale. Es ist an der Zeit, zur Umsetzung überzugehen.


2. Umsetzung

Um die Entwicklung zu vereinfachen, erstellen wir eine einzige Headerdatei, multiweb.mqh, in der wir alle Klassen aufführen: einige von ihnen sind für den Client und die "Server" gemeinsam, während andere vererbt und spezifisch für jede dieser Rollen sind.

2.1. Basisklassen (Start)

Beginnen wir mit der Klasse, die die Ressourcen, IDs und Variablen für jedes Element speichert. Instanzen von daraus abgeleiteten Klassen werden vom Manager, den Assistenten und dem Client verwendet. Vom Client und den Assistenten werden solche Objekte vor allem benötigt, um die Ressourcen "über den Link" zu speichern. Beachten Sie außerdem, dass im Client mehrere Instanzen angelegt wurden, um mehrere Webanfragen gleichzeitig auszuführen. Daher sollte die Analyse des Status der aktuellen Anforderungen (zumindest ob ein Objekt bereits besetzt ist oder nicht) auf den Clients in vollem Umfang genutzt werden. Im Manager werden diese Objekte verwendet, um die Identifizierung zu implementieren und den Status der Assistenten zu verfolgen. Unten ist die Basisklasse.

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

Die Variablen:

  • chartID — ID des Charts, in dem ein MQL-Programm gestartet wurde;
  • busy — wenn die aktuelle Instanz mit der Verarbeitung einer Webanfrage beschäftigt ist;
  • Ressource — Ressource eines Objekts (zufällige Datenspeicherung); die Klasse RESOURCEDATA wird aus ResourceData.mqh übernommen;
  • Präfix — eindeutiges Präfix für jeden Status; ein Präfix wird in den Namen von Ressourcen verwendet. In einem bestimmten Clienten wird empfohlen, eine eindeutige Einstellung vorzunehmen, wie unten gezeigt. Assistant-EAs verwenden standardmäßig das Präfix "WRR_" (abgekürzt von Web Request Result).

Die Methode 'allocate' ist für die Verwendung in der abgeleiteten Klassen. Sie erstellt ein Objekt vom Typ RESOURCEDATA<uchar> in der Variable 'resource'. Die Chart-ID wird auch bei der Benennung der Ressource zusammen mit dem Präfix verwendet. Die Ressource kann mit der Methode #'release' freigegeben werden.

Besonders erwähnenswert ist die Methode getFullName, da sie den vollständigen Ressourcennamen zurückgibt, der den aktuellen MQL-Programmnamen und den Verzeichnispfad enthält. Der vollständige Name wird für den Zugriff auf Programmressourcen von Drittanbietern verwendet (nur zum Lesen). Wenn sich der EA multiweb.mq5 beispielsweise in MQL5\Experts befindet und auf dem Chart mit der ID 129912254742671346 gestartet wird, erhält die Ressource darin den vollständigen Namen "\Experts\multiweb.ex5::WRRR_129912254742671346". Wir übergeben solche Zeichenketten an Ressourcen wie einen Link unter Verwendung des sparam String-Parameters von nutzerdefinierten Ereignissen.

Die 'static' Methode broadcastEvent, die Nachrichten an alle Fenster sendet, wird in Zukunft verwendet, um den Manager zu finden.

Um mit einer Anfrage und einer zugehörigen Ressource im Client-Programm zu arbeiten, definieren wir die von WebWorker abgeleitete Klasse ClientWebWorker (im Folgenden wird der Code abgekürzt, die Vollversionen befinden sich in den angehängten Dateien).

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()? und was dann?
      ...
    }
    
    static void receiveResult(const string resname, uchar &initiator[], uchar &headers[], uchar &text[])
    {
      Print(ChartID(), ": Reading result ", resname);
      
      ...
    }
};

Zunächst ist zu beachten, dass es sich bei der Methode 'request' um eine tatsächliche Umsetzung von Schritt 1 handelt. Hier wird die Webanfrage an den Manager gesendet. Die Methodendeklaration folgt dem Prototyp des hypothetischen WebRequestAsync. Die statische Methode receiveResult führt die umgekehrte Aktion von Schritt 7 aus. Als erste Eingabe erhält sie als 'resname' den vollständigen Namen der externen Ressource, in der die Anfrageergebnisse gespeichert sind, während die Byte-Arrays 'initiator', 'headers' und 'text' innerhalb der Methode mit aus der Ressource entpackten Daten gefüllt werden sollen.

Was ist ein 'Initiator'? Die Antwort ist sehr einfach. Da alle unsere "Aufrufe" jetzt asynchron sind (und die Reihenfolge ihrer Ausführung nicht garantiert ist), sollten wir in der Lage sein, das Ergebnis mit der zuvor gesendeten Anfrage abzugleichen. Daher packen die Assistance-EAs den vollständigen Namen der Quell-Client-Ressource, die zur Einleitung der Anfrage verwendet wird, in ihre Antwortressource zusammen mit Daten aus dem Internet. Nach dem Entpacken gelangt der Name in den Parameter 'initiator' und kann verwendet werden, um das Ergebnis mit der entsprechenden Anforderung zu verknüpfen.

Die Methode receiveResult ist statisch, da sie keine Objektvariablen verwendet — alle Ergebnisse werden über die Parameter an den Aufrufcode zurückgegeben.

Beide Methoden enthalten Ellipsen, die für das Ein- und Auspacken von Daten in und aus Ressourcen erforderlich sind. Dies wird im nächsten Abschnitt behandelt.


2.2. Packen der Anfrage und der Antwort in den Ressourcen

Wie wir uns erinnern, sollen Ressourcen auf der unteren Ebene mit der Klasse RESOURCEDATA verarbeitet werden. Dies ist eine Vorlagen-Klasse, d.h. sie akzeptiert einen Parameter mit einem Datentyp, den wir in oder aus einer Ressource schreiben bzw. lesen. Da unsere Daten auch Zeichenketten enthalten, ist es sinnvoll, den kleinsten Typ uchar als Speichereinheit zu wählen. Somit wird das Objekt der Klasse RESOURCEDATA<uchar> als Datencontainer verwendet. Beim Erstellen einer Ressource wird in ihrem Konstruktor ein (für das Programm)eindeutiger 'Name' vereinbart:

RESOURCEDATA<uchar>(const string name)

Wir können diesen Namen (ergänzt durch den Programmnamen als Präfix) in benutzerdefinierten Ereignissen übergeben, so dass andere MQL-Programme auf die gleiche Ressource zugreifen können. Bitte beachten Sie, dass alle anderen Programme, mit Ausnahme desjenigen, in dem die Ressource erstellt wurde, schreibgeschützt sind.

Die Daten werden mit dem überladenen Zuordnungsoperator in die Ressource geschrieben:

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

wobei 'array' eine Art Array ist, den wir vorbereiten müssen.

Das Lesen von Daten aus der Ressource erfolgt über die Funktion:

int Get(uchar &array[]) const

Hier ist 'array' ein Ausgabeparameter, der die ursprünglichen Array-Inhalte übernimmt.

Kommen wir nun zum Anwendungsaspekt der Verwendung von Ressourcen zur Übergabe von Daten über HTTP-Anfragen und deren Ergebnisse. Wir werden eine Layerklasse zwischen Ressourcen und dem Hauptcode - ResourceMediator - erstellen. Die Klasse packt die Parameter 'method', 'url', 'headers', 'timeout' und 'data' in das Byte-Array 'array' und schreibt dann in die Ressource auf der Client-Seite. Auf der Serverseite ist es Aufgabe, die Parameter aus der Ressource zu entpacken. Ebenso wird diese Klasse die serverseitigen Parameter 'result' und 'result_headers' in das Byte-Array 'array' packen und in die Ressource schreiben, um es als Array zu lesen und auf der Client-Seite zu entpacken.

Der ResourceMediator-Konstruktor akzeptiert den Pointer auf die RESOURCEDATA-Ressource, die dann innerhalb der Methoden verarbeitet wird. Darüber hinaus enthält ResourceMediator unterstützende Strukturen zur Speicherung von Metainformationen über die Daten. Beim Ein- und Auspacken von Ressourcen benötigen wir nämlich einen bestimmten Header, der neben den Daten selbst auch die Größen aller Felder enthält.

Wenn wir beispielsweise einfach die Funktion StringToCharArray verwenden, um eine URL in ein Array von Bytes zu konvertieren, müssen wir bei der Ausführung der inversen Operation mit CharArrayToString die Array-Länge festlegen. Andernfalls werden nicht nur URL-Bytes, sondern auch das darauf folgende Headerfeld aus dem Array gelesen. Wie Sie sich vielleicht erinnern, speichern wir alle Daten in einem einzigen Array, bevor wir auf die Ressource zugreifen. Metainformationen über die Länge der Felder sollten ebenfalls in eine Folge von Bytes umgewandelt werden. Wir verwenden dafür Unions.

#define LEADSIZE (sizeof(int)*5) // 5 Felder der Web-Anfrage

class ResourceMediator
{
  private:
    const RESOURCEDATA<uchar> *resource; // Basiswert
    
    // Metadaten des Headers in Form von 5 Integer 'Längen' und/oder Bytearray der 'Größen'
    union lead
    {
      struct _l
      {
        int m; // Methode
        int u; // Url
        int h; // Headers
        int t; // timeout
        int b; // Hauptteil (body)
      }
      lengths;
      
      uchar sizes[LEADSIZE];
      
      int total()
      {
        return lengths.m + lengths.u + lengths.h + lengths.t + lengths.b;
      }
    }
    metadata;
  
    // Darstelung der Integer als Bytearray und umgekehrt
    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[])
    {
      // Zuweisen der Metadaten mit den Längenparametern
      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);
      
      // bereitstellen des Ergebnisarray, passend für die Metadaten plus den Parameterdaten
      uchar data[];
      ArrayResize(data, LEADSIZE + metadata.total());
      
      // eintragen der Metadaten als Metadaten am Anfang des Arrays
      ArrayCopy(data, metadata.sizes);
      
      // eintragen aller Datenfelder in das Array, eines nach dem anderen
      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);
      
      // sichern des Array in der Resource
      resource = data;
    }
    
    ...

Zuerst schreibt die Methode packRequest die Größen aller Felder in die Struktur 'metadata'. Dann wird der Inhalt dieser Struktur in Form eines Arrays von Bytes an den Anfang des Arrays 'data' kopiert. Das Array 'data' wird anschließend der Ressource zugewiesen. Die Array-Größe 'data' wird basierend auf der Gesamtlänge aller Felder und der Größe der Struktur mit Metadaten reserviert. String-Typ-Parameter werden mit StringToCharArray in Arrays umgewandelt und mit einer entsprechenden Verschiebung in das resultierende Array kopiert, die in der Variablen 'cursor' auf dem neuesten Stand gehalten wird. Der Parameter 'timeout' wird mit Hilfe der int2chars Union in ein Symbol-Array umgewandelt. Der Parameter 'body' wird unverändert in das Array kopiert, da es sich bereits um ein Array des gewünschten Typs handelt. Schließlich wird das Verschieben des Inhalts des gemeinsamen Arrays in die Ressource in einer Zeichenkette durchgeführt (wie Sie sich vielleicht erinnern, ist der Operator '=' in der Klasse RESOURCEDATA überladen):

      resource = data;

Der umgekehrte Vorgang des Abrufs von Anforderungsparametern aus der Ressource wird in der Methode unpackRequest durchgeführt.

    void unpackRequest(string &method, string &url, string &headers, int &timeout, uchar &body[])
    {
      uchar array[];
      // ausfüllen des Arrays mit Daten aus der Resource  
      int n = resource.Get(array);
      Print(ChartID(), ": Got ", n, " bytes in request");
      
      // lesen der Metadaten aus dem Array
      ArrayCopy(metadata.sizes, array, 0, 0, LEADSIZE);
      int cursor = LEADSIZE;

      // lesen aller Datenfelder, eines nach dem anderen      
      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);
      }
    }
    
    ...

Hier wird die Hauptarbeit von der Zeile durch den Aufruf von resource.Get(array) geleistet. Anschließend werden die Metadatenbytes sowie alle darauf basierenden Folgefelder Schritt für Schritt aus dem 'array' gelesen.

Die Ergebnisse der Auftragsausführung werden auf ähnliche Weise mit den Methoden packResponse und unpackResponse gepackt und entpackt (der vollständige Code ist unten angehängt).

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

Nun können wir auf den Quellcode von ClientWebWorker zurückgreifen und die Methoden 'request' und 'receiveResult' vervollständigen.

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

Sie sind recht einfach, da die Klasse ResourceMediator die gesamte Routinearbeit übernimmt.

Die verbleibenden Fragen sind, wer und wann die Methoden WebWorker aufruft, sowie wie wir die Werte einiger Hilfsparameter, wie zum Beispiel managerChartID, in der Methode 'request' erhalten können. Obwohl ich etwas voraus bin, empfehle ich, die Verwaltung aller Objekte der Klassen der WebWorker höheren Klassen zuzuordnen, die tatsächliche Objektlisten unterstützen und Nachrichten zwischen Programmen "im Namen" der Objekte einschließlich der Manager-Suchnachrichten austauschen würden. Aber bevor wir zu dieser neuen Ebene übergehen, ist es notwendig, eine ähnliche Vorbereitung für den "Server" abzuschließen.


2.3. Basisklassen (Fortsetzung)

Deklarieren wir das nutzerdefinierte Derivat von WebWorker, um asynchrone Anfragen des "Servers" (Manager) zu behandeln, genau so wie es die Klasse ClientWebWorker auf der Client-Seite tut.

class ServerWebWorker : public WebWorker
{
  public:
    ServerWebWorker(const long id, const string p = "WRP_"): WebWorker(id, p)
    {
    }
    
    bool transfer(const string resname, const long clientChartID)
    {
      // dem Client mit `clientChartID` antworten, dass die Anforderung in `resname` akzeptiert wurde
      // und übertragen der Anforderung an den spezifischen Arbeiter mit `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);
    }
};

Die Methode 'transfer' delegiert die Bearbeitung einer Anforderung an eine bestimmte Instanz eines Assistenten EA gemäß Schritt 2 in der gesamten Interaktionssequenz. Der Parameter resname ist ein Ressourcenname, der von einem Client erhalten wurde, während clientChartID die Fenster-ID des Clients ist. Wir beziehen alle diese Parameter aus nutzerdefinierten Ereignissen. Die nutzerdefinierten Ereignisse selbst, einschließlich MSG_WEB, werden im Folgenden beschrieben.

Die Methode 'receive' erzeugt eine lokale Ressource im aktuellen WebWorker-Objekt ('allocate'-Aufruf) und schreibt dort den Namen einer ursprünglichen Initiator-Ressource für Anfragen sowie Daten aus dem Internet (result) und HTTP-Header (result_headers) über das Objekt 'mediator' der Klasse ResourceMediator. Dies ist ein Teil von Schritt 5 der gesamten Sequenz.

Deshalb haben wir die WebWorker-Klassen sowohl für den Client als auch für den "Server" definiert. In beiden Fällen werden diese Objekte höchstwahrscheinlich in großen Mengen angelegt. So kann beispielsweise ein Client mehrere Dokumente auf einmal herunterladen, während es auf Seiten des Managers zunächst wünschenswert ist, eine ausreichende Anzahl von Assistenten zu verteilen, da Anfragen von vielen Mandanten gleichzeitig kommen können. Definieren wir die Basisklasse WebWorkersPool für die Anordnung des Objektarrays. Machen wir es zu einer Vorlage, da sich die Art der gespeicherten Objekte auf dem Client und auf dem "Server" (ClientWebWorker bzw. ServerWebWorker) unterscheiden.

template<typename T>
class WebWorkersPool
{
  protected:
    T *workers[];
    
  public:
    WebWorkersPool() {}
    
    WebWorkersPool(const uint size)
    {
      // bereitstellen der Arbeiter; im Client sichern die Parameter der Anforderung in der Resource
      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) // prüfen, falls vorhanden
          {
            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];
    }
    
};

Die Idee hinter den Methoden ist einfach. Der Konstruktor und der Destruktor weisen das Array der angegebenen Größenhandler zu und geben es frei. Die Gruppe der Methoden findWorker und getIdleWorker sucht nach Objekten im Array nach verschiedenen Kriterien. Der Operator 'operator<<' ermöglicht das dynamische Hinzufügen von Objekten, während die Methode 'revoke' das dynamische Entfernen von Objekten ermöglicht.

Der Pool der Handler auf der Client-Seite sollte eine gewisse Spezifität aufweisen (insbesondere in Bezug auf die Ereignisbehandlung). Daher erweitern wir die Basisklasse um den abgeleiteten ClientWebWorkersPool.

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;
      // Versuch, den Chart des Managers von WebRequest zu finden
      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) // async. Anforderung ist fertig, mit Ergebnis oder Fehler
      {
        Print(ChartID(), ": Result code ", (long)dparam);
    
        if(sparam != NULL)
        {
          // Lesen von Daten aus der Ressource mit dem Namen in 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) // Manager wurde gefunden als Ergebnis des Rundrufs 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;
    }
};

Die Variablen:

  • managerChartID — ID des Fensters, in dem sich der arbeitende Manager befindet;
  • managerPoolSize — Anfangsgröße des Handler-Objektarrays;
  • name — allgemeines Präfix für Ressourcen in allen Poolobjekten.


2.4. Nachrichtenaustausch

Im Konstruktor von ClientWebWorkersPool sehen wir den Aufruf von WebWorker::broadcastEvent(TO_MSG(MSG_DISCOVER), ChartID()), der das Ereignis MSG_DISCOVER an alle Fenster sendet, wobei die ID des aktuellen Fensters im Ereignisparameter übergeben wird. MSG_DISCOVER ist ein reservierter Wert: Er sollte am Anfang derselben Header-Datei zusammen mit anderen Arten von Nachrichten definiert werden, die die Programme austauschen sollen.

#define MSG_DEINIT   1 // Entfernen (Manager <-> Arbeiter)
#define MSG_WEB      2 // Auftrag starten (Client -> Manager -> Arbeiter)
#define MSG_DONE     3 // Auftrag erledigt (Arbeiter -> Client, Arbeiter -> Manager)
#define MSG_ERROR    4 // Auftrag fehlgeschlagen (Manager -> Client, Arbeiter -> Client)
#define MSG_DISCOVER 5 // finden des Managers (Client -> Manager)
#define MSG_ACCEPTED 6 // Auftrag wird bearbeitet (Manager -> Client)
#define MSG_HELLO    7 // Manager gefunden (Manager -> Client)

Die Kommentare markieren die Richtung, in die eine Nachricht gesendet wird.

Das TO_MSG-Makro ist für die Transformation der aufgelisteten IDs in reale Ereigniscodes relativ zu einem zufälligen, vom Benutzer ausgewählten Basiswert ausgelegt. Wir erhalten ihn über den MessageBroadcast Input.

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

Dieser Ansatz ermöglicht es, alle Ereignisse in einen beliebigen freien Bereich zu verschieben, indem der Basiswert geändert wird. Beachten Sie, dass nutzerdefinierte Ereignisse im Terminal auch von anderen Programmen verwendet werden können. Daher ist es wichtig, Kollisionen zu vermeiden.

Die Eingabe von MessageBroadcast erscheint in allen unseren MQL-Programmen mit der Datei multiweb.mqh, d.h. in den Clients und im Manager. Geben Sie beim Start des Managers und der Clients den gleichen MessageBroadcast Wert an.

Kommen wir zurück zur Klasse ClientWebWorkersPool. Die Methode onChartEvent nimmt einen besonderen Platz ein. Sie ist aus der standardmäßigen Ereignisbehandlung durch OnChartEvent aufzurufen. Im Parameter 'id' wird der Ereignistyp übergeben. Da wir vom System Codes basierend auf dem gewählten Basiswert erhalten, sollten wir das "gespiegelte" MSG-Makro verwenden, um es wieder in den MSG_***-Bereich zu konvertieren:

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

Hier ist CHARTEVENT_CUSTOM ein Anfang des Bereichs für alle benutzerdefinierten Ereignisse im Terminal.

Wie wir sehen können, verarbeitet die Methode onChartEvent in ClientWebWorkersPool einige der oben genannten Nachrichten. Beispielsweise sollte der Manager mit der Nachricht MSG_HELLO auf das MSG_DISCOVER Massennachrichten antworten. In diesem Fall wird die Fenster-ID des Managers als lparam Parameter übergeben, während die Anzahl der verfügbaren Assistenten als dparam Parameter übergeben wird. Wenn der Manager erkannt wurde, füllt der Pool das leere Array der 'workers' mit realen Objekten des gewünschten Typs. Die aktuelle Fenster-ID sowie der eindeutige Ressourcenname in jedem Objekt wird an den Objektkonstruktor übergeben. Letzteres besteht aus dem gemeinsamen Präfix 'name' und der Seriennummer im Array.

Nachdem das Feld managerChartID einen sinnvollen Wert erhalten hat, ist es möglich, Anforderungen an den Manager zu senden. Die Methode 'request' ist für die in der Klasse ClientWebWorker reserviert, während ihre Verwendung in der Methode WebRequestAsync aus dem Pool gezeigt wird. Zuerst findet WebRequestAsync mit getIdleWorker ein freies Handler-Objekt und ruft dann worker.request(method, url, headers, timeout, data, managerChartID) dafür auf. Innerhalb der Methode 'request' haben wir einen Kommentar zur Auswahl eines speziellen Nachrichtencodes für das Senden eines Ereignisses. Nun, nachdem wir das Ereignis-Subsystem betrachtet haben, können wir die endgültige Version der ClientWebWorker::request Methode bilden:

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 ist eine Meldung über das Ausführen einer Webanfrage. Nach Erhalt sollte der Manager einen freien Assistenten EA finden und ihm den Namen der Client-Ressource (sparam) mit den Anforderungsparametern sowie die chartID (lparam) Client-Fenster-ID übergeben.

Der Assistent führt die Anforderung aus und gibt die Ergebnisse über das Ereignis MSG_DONE (falls erfolgreich) oder einen Fehlercode über MSG_ERROR (bei Problemen) an den Client zurück. Der Ergebnis- (oder Fehler-)Code wird an dparam übergeben, während das Ergebnis selbst in eine Ressource gepackt wird, die sich im Assistenten EA unter dem an sparam übergebenen Namen befindet. Im MSG_DONE-Zweig sehen wir, wie die Daten aus der Ressource abgerufen werden, indem wir die zuvor betrachtete ClientWebWorker::receiveResult(sparam, initiator, headers, text) Funktion aufrufen. Dann wird die Suche nach dem Client-Handler-Objekt (findWorker) durch den Ressourcennamen des Anforderungsinitiators durchgeführt und eine Reihe von Methoden werden für ein erkanntes Objekt aufgerufen:

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

Wir kennen die Methode 'release' bereits — sie gibt die Ressource frei, die aktuell nicht benötigt wird. Was ist onResult? Wenn wir uns den vollständigen Quellcode ansehen, werden wir sehen, dass die Klasse ClientWebWorker zwei virtuelle Funktionen ohne Implementierung bietet: onResult und onError. Dadurch wird die Klasse abstrakt. Der Client-Code sollte seine abgeleitete Klasse aus dem ClientWebWorker beschreiben und die Implementierung bereitstellen. Die Namen der Methoden besagen, dass onResult aufgerufen wird, wenn die Ergebnisse erfolgreich empfangen werden, während onError im Fehlerfall aufgerufen wird. Dies gibt Rückmeldung zwischen den Arbeitsklassen der asynchronen Anforderungen und dem Client-Programmcode, der sie verwendet. Mit anderen Worten, das Client-Programm muss nichts über die Nachrichten wissen, die der Kernel intern verwendet: Alle Interaktionen des Client-Codes mit der entwickelten API werden von den eingebauten MQL5 OOP-Tools durchgeführt.

Betrachten wir den Quellcode des Clients (multiwebclient.mq5).


2.5. Der Client-EA

Der Test EA besteht darin, mehrere Anfragen über die Multiweb-API zu senden, die auf den von einem Nutzer eingegebenen Daten basieren. Um dies zu erreichen, müssen wir die Header-Datei einbinden und die Eingaben hinzufügen:

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>

Letztendlich sind alle Parameter für die Konfiguration der ausgeführten HTTP-Anfragen vorgesehen. In der URL-Liste können wir mehrere kommagetrennte Adressen auflisten, um die Parallelität und Geschwindigkeit der Ausführung von Anfragen zu bewerten. Der URL-Parameter wird mit der Funktion StringSplit in OnInit wie folgt in Adressen unterteilt:

int urlsnum;
string urls[];
  
void OnInit()
{
  // holen der URLs für den Testauftrag
  urlsnum = StringSplit(URL, ',', urls);
  ...
}

Außerdem müssen wir in OnInit einen Pool von Request-Handler-Objekten (ClientWebWorkersPool) anlegen. Aber um dies zu erreichen, müssen wir unsere Klasse beschreiben, die von ClientWebWorker abgeleitet ist.

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");
      // Entkommentieren führt u.U. zu umfangreichen Logs
      // Print(CharArrayToString(headers));
      // Print(CharArrayToString(text));
    }

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

Dessen einzige Aufgabe ist es, den Status zu protokollieren und die Daten zu holen. Jetzt können wir den Pool mit diesen Objekten erstellen.

ClientWebWorkersPool<MyClientWebWorker> *pool = NULL;

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

Wie Sie sehen können, wird der Pool durch die Klasse MyClientWebWorker parametrisiert, was es ermöglicht, unsere Objekte aus dem Bibliothekscode zu erstellen. Die Array-Größe wird gleich der Anzahl der eingegebenen Adressen gewählt. Dies ist für Demonstrationszwecke sinnvoll: Eine kleinere Anzahl würde eine Verarbeitungswarteschlange bedeuten und die Idee der parallelen Ausführung diskreditieren, während eine größere Anzahl eine Verschwendung von Ressourcen wäre. In realen Projekten muss die Poolgröße nicht gleich der Anzahl der Aufgaben sein, aber das erfordert zusätzliche algorithmische Bindung.

Das Präfix für Ressourcen wird als Kombination aus dem Namen des Arbeitssymbols und dem Diagrammzeitraum gesetzt.

Der letzte Schritt zur Initialisierung ist die Suche nach dem Fenster des Managers. Wie Sie sich erinnern, wird die Suche vom Pool selbst (der Klasse ClientWebWorkersPool) durchgeführt. Der Code des Client muss nur sicherstellen, dass der Manager gefunden wird. Setzen wir zu diesem Zweck eine angemessene Zeit ein, innerhalb derer die Nachricht über den Suchmanager und die "Antwort" zur Erreichung der Ziele garantiert sein soll. Setzen wir 5 Sekunden fest. Erstellen wir jetzt einen Timer für diesen Zeitraum:

void OnInit()
{
  ...
  // warten auf den Manager für längsten 5 Sekunden
  EventSetTimer(5);
}

Prüfen wir in der Ereignisbehandlung, ob der Manager läuft. Es erfolgt ein Warnhinweis, wenn der Manager nicht regiert hat.

void OnTimer()
{
  // wenn der Manager micht innerhalb von 5 Sekunden antwortet, scheint es ihn nicht zu geben
  EventKillTimer();
  if(!pool.isManagerBound())
  {
    Alert("WebRequest Pool Manager (multiweb) is not running");
  }
}

Es darf nicht vergessen werden, in OnDeinit die Objekte im Pool wieder zu löschen.

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

Um den Pool alle Servicemeldungen ohne unsere Beteiligung bearbeiten zu lassen, einschließlich der Suche nach dem Manager, verwenden wir die standardmäßige Ereignisbehandlung durch OnChartEvent:

void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 
{
  if(id == CHARTEVENT_CLICK) // Initiieren des Testauftrages durch den Nutzer selbst
  {
    ...
  }
  else
  {
    // dieser Handler regelt alle wichtigen Nachrichten im Hintergrund
    pool.onChartEvent(id, lparam, dparam, sparam);
  }
}

Alle Ereignisse, mit Ausnahme von CHARTEVENT_CLICK, werden an den Pool gesendet, wo die entsprechenden Aktionen basierend auf der Analyse der Codes der angewandten Ereignisse durchgeführt werden (das onChartEvent-Fragment wurde oben bereitgestellt).

Das Ereignis CHARTEVENT_CLICK ist interaktiv und wird direkt zum Starten des Downloads verwendet. Im einfachsten Fall kann es wie folgt aussehen:

void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 
{
  if(id == CHARTEVENT_CLICK) // Initiieren des Testauftrages durch den Nutzer selbst
  {
    if(pool.isManagerBound())
    {
      uchar Body[];

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

Der vollständige Code des Beispiels ist etwas umfangreicher, da er auch die Logik zur Berechnung der Ausführungszeit und zum Vergleich mit einem sequentiellen Aufruf der Standard WebRequest für den gleichen Adressensatz enthält.


2.6. Der Manager-EA und sein Assistent-EA

Endlich sind wir beim "Server" angelangt. Da die grundlegenden Mechanismen bereits in der Header-Datei implementiert sind, ist der Code des Managers und siner Assistenten nicht so umständlich, wie man glauben könnte.

Wie Sie sich vielleicht erinnern, haben wir nur einen EA, der als Manager oder als sein Assistent arbeitet (die Datei multiweb.mq5). Wie beim Client fügen wir die Header-Datei ein und deklarieren die Eingabeparameter:

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

#include <multiweb.mqh>

WebRequestPoolSize ist eine Reihe von Hilfsfenstern, die der Manager erstellen sollte, um Assistenten auf ihnen zu starten.

ManagerChartID ist eine Managerfenster-ID. Dieser Parameter ist nur als Assistent verwendbar und wird mit dem Manager gefüllt, wenn Assistenten automatisch aus dem Quellcode gestartet werden. Das manuelle Zuweisen von ManagerChartID beim Start des Managers wird als Fehler behandelt.

Der Algorithmus basiert auf zwei globalen Variablen:

bool manager;
WebWorkersPool<ServerWebWorker> pool;

Das logische Flag 'manager' zeigt die Rolle der aktuellen EA-Instanz an. Die Variable 'pool' ist ein Array von Handler-Objekten eingehender Aufgaben. WebWorkersPool wird durch die oben beschriebene Klasse ServerWebWorker charakterisiert. Das Array wird nicht im Voraus initialisiert, da seine Füllung von der Rolle abhängt.

Die erste gestartete Instanz (definiert in OnInit) erhält die Managerrolle.

const string GVTEMP = "WRP_GV_TEMP";

int OnInit()
{
  manager = false;
  
  if(!GlobalVariableCheck(GVTEMP))
  {
    // when first instance of multiweb is started, it's treated as manager
    // die globalen Variablen ist ein Flag, dass der Manager läuft
    if(!GlobalVariableTemp(GVTEMP))
    {
      FAILED(GlobalVariableTemp);
      return INIT_FAILED;
    }
    
    manager = true;
    GlobalVariableSet(GVTEMP, 1);
    Print("WebRequest Pool Manager started in ", ChartID());
  }
  else
  {
    // allw eiteren Instanzen des multiweb sind Arbeiter/Helfer
    Print("WebRequest Worker started in ", ChartID(), "; manager in ", ManagerChartID);
  }
  
  // Timer für die verspätete Instantiierung der Arbeiter
  EventSetTimer(1);
  return INIT_SUCCEEDED;
}

Der EA überprüft das Vorhandensein einer speziellen globalen Variablen des Terminals. Wenn sie nicht vorhanden ist, installiert sich der EA als Manager und erstellt eine solche globale Variable. Wenn die Variable bereits vorhanden ist, dann gibt es auch den Manager, und somit wird diese Instanz zu einem Assistenten. Bitte beachten Sie, dass die globale Variable temporär ist, d.h. sie wird beim Neustart des Terminals nicht gespeichert. Wenn der Manager jedoch auf einem Chart belassen wird, erstellt er die Variable erneut.

Der Timer wird dann auf eine Sekunde eingestellt, da die Initialisierung von Hilfsdiagrammen in der Regel einige Sekunden dauert und dies von OnInit aus nicht die beste Lösung ist. Befüllen des Pools im Timer-Ereignishandler:

void OnTimer()
{
  EventKillTimer();
  if(manager)
  {
    if(!instantiateWorkers())
    {
      Alert("Workers not initialized");
    }
    else
    {
      Comment("WebRequest Pool Manager ", ChartID(), "\nWorkers available: ", pool.available());
    }
  }
  else // Arbeiter
  {
    // das wird als Gast der Resource verwendet, um die Antwortdaten des Headers zu sichern
    pool << new ServerWebWorker(ChartID(), "WRR_");
  }
}

Im Falle einer Assistentenrolle wird dem Array einfach ein weiteres ServerWebWorker-Handler-Objekt hinzugefügt. Als Manager ist es komplizierter und wird in der separaten Funktion instantiateWorkers implementiert. Schauen wir es uns mal an.

bool instantiateWorkers()
{
  MqlParam Params[4];
  
  const string path = MQLInfoString(MQL_PROGRAM_PATH);
  const string experts = "\\MQL5\\";
  const int pos = StringFind(path, experts);
  
  // sich noch mal starten (in einer anderen Rolle als Hilfs-EA)
  Params[0].string_value = StringSubstr(path, pos + StringLen(experts));
  
  Params[1].type = TYPE_UINT;
  Params[1].integer_value = 1; // 1 Arbeiter innerhalb der Instanz des Hilfs-EAs für die Rückgabe der Ergebnisse für den Manager oder Client

  Params[2].type = TYPE_LONG;
  Params[2].integer_value = ChartID(); // Chart des Managers

  Params[3].type = TYPE_UINT;
  Params[3].integer_value = MessageBroadcast; // Verwendung derselben Zahl für das Ereignis
  
  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;
}

Diese Funktion verwendet die Bibliothek Expert, die von unserem alten Freund - dem Mitglied der MQL5-Community fxsaber - entwickelte wurde, daher wird am Anfang des Quellcodes die entsprechende Header-Datei eingebunden.

#include <fxsaber\Expert.mqh>

Die Bibliothek Expert ermöglicht es uns, tpl-Vorlagen mit den Parametern bestimmter EAs dynamisch zu generieren und auf bestimmte Charts anzuwenden, was zum Start der EAs führt. In unserem Fall sind die Parameter aller Assistenten-EAs gleich, so dass ihre Liste einmalig erstellt wird, bevor eine bestimmte Anzahl von Fenstern erstellt wird.

Der Parameter 0 gibt den Pfad zur ausführbaren EA-Datei an, also zu sich selbst. Parameter 1 ist WebRequestPoolSize. Er ist 1 bei jedem Assistenten. Wie ich bereits erwähnt habe, wird das Handler-Objekt im Assistenten nur zum Speichern einer Ressource mit HTTP-Request-Ergebnissen benötigt. Jeder Assistent bearbeitet die Anfrage durch eine blockierende WebRequest, d.h. es wird maximal ein Handle-Objekt verwendet. Parameter 2 — ManagerChartID Manager Fenster-ID. Parameter 3 — Basiswert für die Nachrichtencodes (MessageBroadcast Parameter wird aus multiweb.mqh übernommen).

Weiterhin werden in der Schleife mit Hilfe von ChartOpen leere Charts erstellt und in ihnen mit EXPERT::Run (Chart, Params) die Assistenten-EAs gestartet. Das ServerWebWorker(chart)-Handle-Objekt wird für jedes neue Fenster erstellt und dem Pool hinzugefügt. Im Manager sind die Handler-Objekte nichts anderes als Links zu den Fenster-IDs der Assistenten und deren Status, da HTTP-Anfragen nicht im Manager selbst ausgeführt werden und keine Ressourcen für sie angelegt werden.

Eingehende Aufgaben werden basierend auf Benutzerereignissen in OnChartEvent behandelt.

void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 
{
  if(MSG(id) == MSG_DISCOVER) // a Arbeiter-EA auf einem neuen Chart eines Client wird initialisiert und will sich mit dem Manager verbinden
  {
    if(manager && (lparam != 0))
    {
      // nur der Manager antwortet mit seiner Chart-ID, lparam ist die Chart-ID des Client
      EventChartCustom(lparam, TO_MSG(MSG_HELLO), ChartID(), pool.available(), NULL);
    }
  }
  else
  if(MSG(id) == MSG_WEB) // ein Client hat einen Web-Auftrag erteilt
  {
    if(lparam != 0)
    {
      if(manager)
      {
        // der Manager leitet die Anfrage weiter an einen wartenden Arbeiter
        // lparam ist die Chart-ID des Client, sparam ist des Client Resource
        if(!transfer(lparam, sparam))
        {
          EventChartCustom(lparam, TO_MSG(MSG_ERROR), ERROR_NO_IDLE_WORKER, 0.0, sparam);
        }
      }
      else
      {
        // der Arbeiter führt den Web-Auftrag tatsächlich durch
        startWebRequest(lparam, sparam);
      }
    }
  }
  else
  if(MSG(id) == MSG_DONE) // ein Arbeiter mit der Chart-ID in lparam ist fertig
  {
    WebWorker *worker = pool.findWorker(lparam);
    if(worker != NULL)
    {
      // jetzt sind wir im Manager, und der Pool enthält Sub-Arbeiter ohne Resourcen
      // daher ist diese Version ausschließlich dazu gedacht, den Belegt-Zustand zu bereinigen.
      worker.release();
    }
  }
}

Zuerst gibt der Manager als Antwort auf MSG_DISCOVER, das er vom Client mit der lparam-ID erhalten hat, die MSG_HELLO-Nachricht mit seiner Window-ID zurück.

Beim Empfang von MSG_WEB sollte lparam die Fenster-ID des Clients enthalten, der die Anfrage gesendet hat, während sparam einen Namen der Ressource mit gepackten Anfrageparametern enthalten sollte. Als Manager versucht der Code, die Aufgabe mit diesen Parametern an einen inaktiven Assistenten weiterzugeben, indem er die Funktion 'transfer' (siehe unten) aufruft und dadurch den Status des ausgewählten Objekts auf "busy" (beschäftigt) ändert. Wenn es keine wartenden Assistenten gibt, wird das Ereignis MSG_ERROR mit dem Code ERROR_NO_IDLE_WORKER an den Mandanten gesendet. Der Assistent führt den HTTP-Request in der Funktion startWebRequest aus.

Das Ereignis MSG_DONE kommt vom Assistenten zum Manager, wenn dieser das angeforderte Dokument hochlädt. Der Manager findet das entsprechende Objekt über die Assistenten-ID in lparam und deaktiviert den Status "busy" durch Aufruf der Methode "release". Wie bereits erwähnt, sendet der Assistent die Ergebnisse seiner Arbeit direkt an den Kunden.

Der vollständige Quellcode enthält auch das Ereignis MSG_DEINIT, das eng mit der OnDeinit-Verarbeitung verbunden ist. Die Idee ist, dass die Assistenten über das Entfernen des Managers informiert werden und sich daraufhin selbst löschen und ihr Fenster schließen, während der Manager aus der Ferne über das Entfernen des Assistenten informiert wird und ihn aus dem Pool des Managers löscht. Ich glaube, jeder kann diesen Mechanismus selbst verstehen.

Die Funktion 'transfer' sucht nach einem freien Objekt und ruft dessen Methode 'transfer' auf (siehe oben).

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

The startWebRequest function is defined as follows:

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)
  {
    // Erstellen der Resource mit den Ergebnissen, um sie als Nutzerereignis dem Client zuzuführen
    ((ServerWebWorker *)pool[0]).receive(resname, result, result_headers);
    // zuerst wird MSG_DONE mit der Ergebnisresource dem Clienten gesendet
    EventChartCustom(returnChartID, TO_MSG(MSG_DONE), ChartID(), (double)code, pool[0].getFullName());
    // danach wird dem Manager MSG_DONE gesendet, damit er seine Arbeiter in den Wartezustand setzen kann
    EventChartCustom(ManagerChartID, TO_MSG(MSG_DONE), ChartID(), (double)code, NULL);
  }
  else
  {
    // Fehlernummer in dparam
    EventChartCustom(returnChartID, TO_MSG(MSG_ERROR), ERROR_MQL_WEB_REQUEST, (double)GetLastError(), resname);
    EventChartCustom(ManagerChartID, TO_MSG(MSG_DONE), ChartID(), (double)GetLastError(), NULL);
  }
}

Mittels dem ResourceMediator entpackt die Funktion die Anforderungsparameter und ruft die standardmäßige MQL-WebRequest-Funktion auf. Wenn die Funktion ohne MQL-Fehler ausgeführt wird, werden die Ergebnisse an den Client gesendet. Dazu werden sie mit der oben beschriebenen Methode 'receive' in eine lokale Ressource gepackt und ihr Name wird mit der MSG_DONE-Nachricht im sparam-Parameter der EventChartCustom-Funktion übergeben. Beachten Sie, dass HTTP-Fehler (z.B. ungültige Seite 404 oder Webserver-Fehler 501) auch hier auftreten — der Client erhält den HTTP-Code im Parameter dparam und die HTTP-Header der Antwort in der Ressource, so dass Sie die Situation analysieren können.

Wenn der Aufruf von WebRequest mit einem MQL-Fehler endet, erhält der Client die MSG_ERROR-Nachricht mit dem Code ERROR_MQL_WEB_REQUEST, während das Ergebnis von GetLastError auf dparam gesetzt wird. Da die lokale Ressource in diesem Fall nicht gefüllt wird, wird der Name einer Quellressource direkt im sparam-Parameter übergeben, so dass eine bestimmte Instanz eines Handler-Objekts mit einer Ressource auf der Client-Seite noch identifiziert werden kann.

Diagramm der Bibliothek der Klassen des multiweb für den asynchronen und parallelen Aufruf von WebRequest

Abb. 4. Diagramm der Bibliothek der Klassen des multiweb für den asynchronen und parallelen Aufruf von WebRequest


3. Tests

Das Testen des implementierten Softwarekomplexes kann wie folgt durchgeführt werden.

Öffnen Sie zunächst die Terminaleinstellungen und geben Sie alle Server an, auf die zugegriffen werden soll, in der Liste der zulässigen URLs auf der Registerkarte Experten.

Als nächstes starten Sie den multiweb EA und bestimmen Sie 3 Assistenten in die Eingaben. Dadurch werden 3 neue Fenster mit dem gleichen Multiweb EA geöffnet, die in der anderen Rolle gestartet wurde. Die EA-Rolle wird im Kommentar in der linken oberen Ecke des Fensters angezeigt.

Starten Sie nun den Multiwebclient Client EA auf einem anderen Chart und klicken einmal auf das Chart. Mit den Standardeinstellungen initiiert er 3 parallele Webanforderungen und schreibt Diagnosen in das Protokoll, einschließlich der Größe der erhaltenen Daten und der benötigten Zeit. Wenn der Spezialparameter TestSyncRequests auf 'true' gesetzt wird, werden neben parallelen Webanfragen über den Manager auch sequentielle Anfragen derselben Seiten mit dem Standard WebRequest ausgeführt. Dies geschieht, um die Ausführungsgeschwindigkeiten der beiden Optionen zu vergleichen. In der Regel ist die Parallelverarbeitung um ein Vielfaches schneller als die sequentielle - von Wurzel(N) bis N, wobei N eine Anzahl von verfügbaren Assistenten ist.

Das Beispiel-Log wird unten angezeigt.

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

Beachten Sie, dass die Gesamtausführungszeit aller Anfragen gleich der Ausführungszeit des langsamsten ist.

Wenn wir die Anzahl der Assistenten im Manager auf eins einstellen, werden die Anfragen sequentiell abgearbeitet.


Schlussfolgerung

In diesem Artikel haben wir eine Reihe von Klassen und vorgefertigten EAs für die Ausführung von HTTP-Anfragen im nicht-blockierenden Modus entwickelt. Dies ermöglicht es, Daten aus dem Internet in mehreren, parallelen Threads zu erhalten und die Effizienz von EAs zu erhöhen, die neben HTTP-Anfragen auch analytische Berechnungen in Echtzeit durchführen sollen. Darüber hinaus kann diese Bibliothek auch in Indikatoren verwendet werden, bei denen der standardmäßige WebRequest verboten ist. Um die gesamte Architektur zu implementieren, mussten wir eine Vielzahl von MQL-Features verwenden: Benutzerereignisse übergeben, Ressourcen erstellen, Fenster dynamisch öffnen und EAs darauf ausführen.

Zum Zeitpunkt des Schreibens ist die Erstellung von Hilfsfenstern zum Starten von Assistenten-EAs die einzige Möglichkeit, HTTP-Anfragen zu parallelisieren, aber MetaQuotes plant die Entwicklung spezieller Hintergrund-MQL-Programme. Der Ordner MQL5/Services ist bereits für diese Dienste reserviert. Wenn diese Technologie im Terminal erscheint, kann diese Bibliothek wahrscheinlich verbessert werden, indem Hilfsfenster durch Dienste ersetzt werden.

Beigefügte Dateien:

  • MQL5/Include/multiweb.mqh — Bibliothek
  • MQL5/Experts/multiweb.mq5 — Manager-EA und Assistant-EA 
  • MQL5/Experts/multiwebclient.mq5 — Demo Client-EA
  • MQL5/Include/fxsaber/Resource.mqh — Hilfsklasse für die Arbeit mit Ressourcen
  • MQL5/Include/fxsaber/ResourceData.mqh — Hilfsklasse für die Arbeit mit Ressourcen
  • MQL5/Include/fxsaber/Expert.mqh — Hilfsklasse für den Start der EAs
  • MQL5/Include/TypeToBytes.mqh — Bibliothek für die Datenkonversion

Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/5337

Beigefügte Dateien |
MQL5.zip (17.48 KB)
Verwenden von OpenCL, um Kerzenmuster zu testen Verwenden von OpenCL, um Kerzenmuster zu testen
Der Artikel beschreibt den Algorithmus, um die Kerzenmuster von OpenCL für den Tester im Modus "1 Minute OHLC" zu implementieren. Wir werden auch die Geschwindigkeiten des integrierten Strategietesters, gestartet in schnellen und langsamen Modi, vergleichen.
Umkehrung: Formalisieren des Einstiegspunktes und die Entwicklung eines Algorithmus für den manuellen Handel Umkehrung: Formalisieren des Einstiegspunktes und die Entwicklung eines Algorithmus für den manuellen Handel
Dies ist der letzte Artikel innerhalb der Serie, der sich mit der Strategie des Umkehrhandels beschäftigt. Hier werden wir versuchen, das Problem zu lösen, das die Instabilität der Testergebnisse in früheren Artikeln verursacht hat. Wir werden auch unseren eigenen Algorithmus für den manuellen Handel in jedem Markt mit der Reverse-Strategie entwickeln und testen.
Entwicklung eines Symbolauswahl- und Navigationsprogramms in MQL5 und MQL4 Entwicklung eines Symbolauswahl- und Navigationsprogramms in MQL5 und MQL4
Erfahrene Händler sind sich der Tatsache bewusst, dass die meisten zeitaufwendigen Dinge im Handel nicht das Öffnen und Verfolgen von Positionen sind, sondern das Auswählen von Symbolen und das Suchen von Einstiegspunkten. In diesem Artikel werden wir einen EA entwickeln, das die Suche nach Einstiegspunkten für Handelsinstrumente Ihres Brokers vereinfacht.
Umkehrmuster: Testen des Musters Kopf und Schulter Umkehrmuster: Testen des Musters Kopf und Schulter
Dieser Artikel ist eine Fortsetzung des vorherigen: "Umkehrmuster: Testen des Musters Doppelspitze/Doppelboden". Nun werden wir uns ein weiteres, bekanntes Umkehrmuster namens Kopf und Schulter ansehen, die Handelseffizienz der beiden Muster vergleichen und sie zu einem einzigen Handelssystem kombinieren.