Mit MetaTrader 5 via Named Pipes ohne DDLs kommunizieren

MetaQuotes | 5 Mai, 2016

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:

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.