Mit MetaTrader 5 via Named Pipes ohne DDLs kommunizieren
Einleitung
Viele Entwickler sehen sich mit demselben Problem konfrontiert: Wie kreiert man eine Sandboxumgebung für ein Handelsterminal ohne unsichere DLLs zu benutzen.
Eine der leichtesten und zugleich sichersten Methoden besteht in der Verwendung standardisierter Named Pipes, die normale Dateioperationen gewährleisten. Diese ermöglichen eine Interprozessor-Client-Server basierte Kommunikation zwischen Programmen. Obwohl es bereits einen Artikel mit dem Titel Eine DLL-freie Lösung für die Kommunikation zwischen Terminals von MetaTrader 5 mithilfe von Named Pipes zu diesem Thema gibt, der den Zugang zu DDLs demonstriert, werden wir standardisierte und sichere Handelsterminal-Features verwenden.
In der MSDN-Bibliothek finden sich noch einige weitere Hinweise über Named Pipes. Allerdings werden wir uns mit praktischen C++- und MQL5-Beispielen beschäftigen. Wir werden einen Server, Client, den Datenaustausch zwischen beiden implementieren und dann einen Benchmark-Test durchführen.
Zur Serverimplementierung
Lassen Sie uns einen simplen Server mithilfe von C++ aufsetzen. Ein Skript des Terminals wird eine Serververbindung herstellen und dann einen Datenaustausch vornehmen. Der betreffende Server Core weist folgenden Satz von WinAPI-Funktionen auf:
- NamedPipeKreieren - kreiert eine Named Pipe
- NamedPipeVerbindung - ordnet einem Server an, auf eine Client-Verbindung zu warten
- DateiSchreiben - füllt eine Pipe mit Daten
- DateiLesen - Dateninhalt der Pipe auslesen
- DateispeicherLeeren - leert den Speicher
- NamedPipeTrennen - Serververbindung trennen
- DeskriptorSchließen - schließt einen Dateideskriptor
Sobald eine Named Pipe geöffnet wird, gibt sie einen Dateideskriptor frei, der für simple Lese-/Schreib-Dateioperationen verwendet werden kann. Als Folge hieraus erhalten Sie einen simplen Mechanismus, für den keinerlei besondere Netzwerkoperationskenntnisse notwendig sind.
Named Pipes weisen dabei eine spezifische Besonderheit auf - beides kann auf sie zutreffen: lokal oder Netzwerk. Das bedeutet, dass es ein Leichtes ist, einen Remote-Server zu implementieren, der Netzwerkverbindung seitens Kundenterminals zulässt.
Hier nun ein einfaches Beispiel dafür, wie Sie einen lokalen Server so einrichten, dass er als ein Vollduplex-Kanal fungiert, mit dessen Hilfe Bytes ausgetauscht werden können:
//--- open CPipeManager manager; if(!manager.Create(L"\\\\.\\pipe\\MQL5.Pipe.Server")) return(-1); //+------------------------------------------------------------------+ //| Create named pipe | //+------------------------------------------------------------------+ bool CPipeManager::Create(LPCWSTR pipename) { //--- check parameters if(!pipename || *pipename==0) return(false); //--- close old Close(); //--- create named pipe m_handle=CreateNamedPipe(pipename,PIPE_ACCESS_DUPLEX, PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT, PIPE_UNLIMITED_INSTANCES,256*1024,256*1024,1000,NULL); if(m_handle==INVALID_HANDLE_VALUE) { wprintf(L"Creating pipe '%s' failed\n",pipename); return(false); } //--- ok wprintf(L"Pipe '%s' created\n",pipename); return(true); }
Um eine Verbindung mit dem Client herzustellen, müssen Sie sich der ConnectNamedPipe-Funktion bedienen:
//+------------------------------------------------------------------+ //| Connect client | //+------------------------------------------------------------------+ bool CPipeManager::ConnectClient(void) { //--- pipe exists? if(m_handle==INVALID_HANDLE_VALUE) return(false); //--- connected? if(!m_connected) { //--- connect if(ConnectNamedPipe(m_handle,NULL)==0) { //--- client already connected before ConnectNamedPipe? if(GetLastError()!=ERROR_PIPE_CONNECTED) return(false); //--- ok } m_connected=true; } //--- return(true); }
Ein Datenaustausch wird mittels den folgenden 4 simplen Funktionen organisiert:
- CPipeManager::Send(void *data,size_t data_size)
- CPipeManager::Read(void *data,size_t data_size)
- CPipeManager::SendString(LPCSTR command)
- CPipeManager::ReadString(LPSTR answer,size_t answer_maxlen)
Diese Funktionen ermöglichen das Senden und Empfangen von Daten als Binärdaten oder ANSI-Textfolgen, die mit MQL5 kompatibel sind. Darüber hinaus, da CFilePipe via MQL5 automatisch eine Datei im ANSI-Modus öffnet, werden die Zeichenketten während des Sendens und Empfangens automatisch in Unicode konvertiert. Falls Ihr MQL5-Programm eine Datei im Unicode-Modus (FILE_UNICODE) öffnet, dann kann es Unicode--Zeichenketten austauschen (mit BOM als Startsignatur).
Zur Client-Implementierung
Wir werden unseren eigenen Client in MQL5 schreiben. Er wird dazu in der Lage sein, reguläre Operationen durchzuführen, die auf der CFilePipe-Klasse der Standardbibliothek basieren. Diese Klasse ist beinahe mit CFileBin identisch, allerdings nimmt sie eine Verifizierung der Datenverfügbarkeit in einer virtuellen Datei vor, bevor sie die Daten liest.
//+------------------------------------------------------------------+ //| Wait for incoming data | //+------------------------------------------------------------------+ bool CFilePipe::WaitForRead(const ulong size) { //--- check handle and stop flag while(m_handle!=INVALID_HANDLE && !IsStopped()) { //--- enough data? if(FileSize(m_handle)>=size) return(true); //--- wait a little Sleep(1); } //--- failure return(false); } //+------------------------------------------------------------------+ //| Read an array of variables of double type | //+------------------------------------------------------------------+ uint CFilePipe::ReadDoubleArray(double &array[],const int start_item,const int items_count) { //--- calculate size uint size=ArraySize(array); if(items_count!=WHOLE_ARRAY) size=items_count; //--- check for data if(WaitForRead(size*sizeof(double))) return FileReadArray(m_handle,array,start_item,items_count); //--- failure return(0); }
Named Pipes weisen signifikante Unterschiede bei der Implementierung ihrer lokalen beziehungsweise Netzwerkmodi auf. Ohne eine derartige Verifizierung, werden Netzwerkmodus-Operationen immer einen Fehler ausgeben, wenn man größere Datenmengen (über 64K) zu versenden sucht.
Lassen Sie uns nun eine Serververbindung herstellen: entweder mittels eines Remotecomputers namens „RemoteServerName“ oder via einer lokalen Maschine.
void OnStart() { //--- wait for pipe server while(!IsStopped()) { if(ExtPipe.Open("\\\\RemoteServerName\\pipe\\MQL5.Pipe.Server",FILE_READ|FILE_WRITE|FILE_BIN)!=INVALID_HANDLE) break; if(ExtPipe.Open("\\\\.\\pipe\\MQL5.Pipe.Server",FILE_READ|FILE_WRITE|FILE_BIN)!=INVALID_HANDLE) break; Sleep(250); } Print("Client: pipe opened");
Datenaustausch
Nachdem erfolgreich eine Verbindung hergestellt werden konnte, wollen wir eine Textfolge samt Identifikation in Richtung des Servers schicken. Da die Datei im ANSI-Modus geöffnet worden ist, wird eine jede Unicode-Zeichenfolge automatisch in ANSI konvertiert werden.
//--- send welcome message if(!ExtPipe.WriteString(__FILE__+" on MQL5 build "+IntegerToString(__MQ5BUILD__))) { Print("Client: sending welcome message failed"); return; }
Als Antwort werden wir also seitens des Servers folgendes zu erwarten haben: „Hello from pipe server“ als auch den Integer 1234567890. Der Client wird - erneut - „Test string“ sowie den Integer 1234567890 senden.
//--- read data from server string str; int value=0; if(!ExtPipe.ReadString(str)) { Print("Client: reading string failed"); return; } Print("Server: ",str," received"); if(!ExtPipe.ReadInteger(value)) { Print("Client: reading integer failed"); return; } Print("Server: ",value," received"); //--- send data to server if(!ExtPipe.WriteString("Test string")) { Print("Client: sending string failed"); return; } if(!ExtPipe.WriteInteger(value)) { Print("Client: sending integer failed"); return; }
Der simple Datenaustausch ist hiermit beenden. Nun wird es Zeit, einen Benchmark-Test durchzuführen.
Einen Benchmark-Test durchführen
Als Test werden wir 1 Gigabyte an Daten (eine Reihe bestehend aus double type-Nummern in Blöcken von jeweils 8 Megabyte) vom Server in Richtung Client senden. Im Anschluss daran überprüfen wir die Korrektheit der Blöcke und messen die Transferrate.
Hier ist der Code für den C++-Server:
//--- benchmark double volume=0.0; double *buffer=new double[1024*1024]; // 8 Mb wprintf(L"Server: start benchmark\n"); if(buffer) { //--- fill the buffer for(size_t j=0;j<1024*1024;j++) buffer[j]=j; //--- send 8 Mb * 128 = 1024 Mb to client DWORD ticks=GetTickCount(); for(size_t i=0;i<128;i++) { //--- setup guard signatures buffer[0]=i; buffer[1024*1024-1]=i+1024*1024-1; //--- if(!manager.Send(buffer,sizeof(double)*1024*1024)) { wprintf(L"Server: benchmark failed, %d\n",GetLastError()); break; } volume+=sizeof(double)*1024*1024; wprintf(L"."); } wprintf(L"\n"); //--- read confirmation if(!manager.Read(&value,sizeof(value)) || value!=12345) wprintf(L"Server: benchmark confirmation failed\n"); //--- show statistics ticks=GetTickCount()-ticks; if(ticks>0) wprintf(L"Server: %.0lf Mb sent at %.0lf Mb per second\n",volume/1024/1024,volume/1024/ticks); //--- delete[] buffer; }
und hier für den MQL5-Client:
//--- benchmark double buffer[]; double volume=0.0; if(ArrayResize(buffer,1024*1024,0)==1024*1024) { uint ticks=GetTickCount(); //--- read 8 Mb * 128 = 1024 Mb from server for(int i=0;i<128;i++) { uint items=ExtPipe.ReadDoubleArray(buffer); if(items!=1024*1024) { Print("Client: benchmark failed after ",volume/1024," Kb, ",items," items received"); break; } //--- check the data if(buffer[0]!=i || buffer[1024*1024-1]!=i+1024*1024-1) { Print("Client: benchmark invalid content"); break; } //--- volume+=sizeof(double)*1024*1024; } //--- send confirmation value=12345; if(!ExtPipe.WriteInteger(value)) Print("Client: benchmark confirmation failed "); //--- show statistics ticks=GetTickCount()-ticks; if(ticks>0) printf("Client: %.0lf Mb received at %.0lf Mb per second\n",volume/1024/1024,volume/1024/ticks); //--- ArrayFree(buffer); }
Nehmen Sie zur Kenntnis, dass das erste und letzte Element der transferierten Blöcke überprüft wird, um sicherzustellen, dass keine Fehler während des Transfers auftraten. Ferner sendet der Client, sobald der Transfer abgeschlossen ist, ein Bestätigungssignal an den Server betreffend eines erfolgreichen Datenempfangs. Falls Sie sich gegen eine abschließende Bestätigung entscheiden, werden Sie wahrscheinlich einen Datenverlust erleiden, wenn eine der beiden Seiten die Verbindung vorzeitig unterbricht.
Führen Sie die PipeServer.exe auf dem Server lokal aus und ordnen Sie das PipeClient.mq5-Skript irgendeinem Chart zu.
PipeServer.exe | PipeClient.mq5 |
---|---|
MQL5 Pipe Server Copyright 2012, MetaQuotes Software Corp. Pipe '\\.\pipe\MQL5.Pipe.Server' created Client: waiting for connection... Client: connected as 'PipeClient.mq5 on MQL5 build 705' Server: send string Server: send integer Server: read string Server: 'Test string' received Server: read integer Server: 1234567890 received Server: start benchmark ...................................................... ........ Server: 1024 Mb sent at 2921 Mb per second |
PipeClient (EURUSD,H1) Client: pipe opened PipeClient (EURUSD,H1) Server: Hello from pipe server received PipeClient (EURUSD,H1) Server: 1234567890 received PipeClient (EURUSD,H1) Client: 1024 Mb received at 2921 Mb per second |
Für einen lokalen Datenaustausch ist der Transfer ohne Zweifel überragend - beinahe 3 Gigabyte pro Sekunde. Das bedeutet, dass Named Pipes verwendet werden können, um nahezu jede beliebe Datenmenge in MQL5-Programme zu transportieren.
Lassen Sie uns nun die Datentransferperformance eines normalen 1-Gigabyte-LAN testen:
PipeServer.exe | PipeClient.mq5 |
---|---|
MQL5 Pipe Server Copyright 2012, MetaQuotes Software Corp. Pipe '\\.\pipe\MQL5.Pipe.Server' created Client: waiting for connection... Client: connected as 'PipeClient.mq5 on MQL5 build 705' Server: send string Server: send integer Server: read string Server: 'Test string' received Server: read integer Server: 1234567890 received Server: start benchmark ...................................................... ........ Server: 1024 Mb sent at 63 Mb per second |
PipeClient (EURUSD,H1) Client: pipe opened PipeClient (EURUSD,H1) Server: Hello from pipe server received PipeClient (EURUSD,H1) Server: 1234567890 received PipeClient (EURUSD,H1) Client: 1024 Mb received at 63 Mb per second |
In einem lokalen Netzwerk wurde 1 Gigabyte an Daten mit einer Rate von 63 Megabyte pro Sekunde transportiert. Auch das ist gar nicht mal so schlecht. Tatsächlich handelt es sich um 63% der maximalen Bandbreite des Gigabit-Netzwerks.
Fazit
Da das Sicherheitssystem von MetaTrader 5 es MQL5-Programmen nicht erlaubt, außerhalb ihrer Sandboxen ausgeführt zu werden, schützt es Trader vor Bedrohungen, während diese nicht vertrauenswürdige Handelsroboter verwenden. Indem Sie Named Pipes verwenden, können Sie kinderleicht Integrationen mit Drittanbieter-Software herbeiführen und EAs von draußen aus verwalten. Sicherheit.
Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/503
- Freie Handelsapplikationen
- Über 8.000 Signale zum Kopieren
- Wirtschaftsnachrichten für die Lage an den Finanzmärkte
Sie stimmen der Website-Richtlinie und den Nutzungsbedingungen zu.