Связь с MetaTrader 5 через именованные каналы без применения DLL

MetaQuotes | 15 октября, 2012

Введение

Перед многими разработчиками встает одинаковая проблема - как пробиться в песочницу торгового терминала без применения небезопасных DLL.

Одним из простых и безопасных методов является использование стандартных именованных каналов (Named Pipes), которые работают как обычные файловые операции. Они позволяют организовать межпроцессорное клиент-серверное взаимодействие между программами. Хотя ранее уже публиковалась статья "Реализация взаимодействия между клиентскими терминалами MetaTrader 5 при помощи именованных каналов (Named Pipes)", которая показывала способ использования через включение доступа к DLL библиотекам, мы воспользуемся стандартными и безопасными возможностями терминала.

Более детально об именованных каналах можно прочесть в библиотеке MSDN, а мы сразу перейдем к практическим примерам на C++ и MQL5. Реализуем сервер, клиента, обмен данными между ними и замерим производительность.


Реализация сервера

Напишем простой сервер на С++, к которому будет подключаться скрипт из терминала и обмениваться данными. Основой сервера является набор следующих WinAPI функций:

После открытия именованного канала возвращается файловый хендл, с которым можно проводить обычные файловые операции по записи и чтению. В результате получается очень простой механизм работы, не требующий особых познаний в сетевых операциях.

Особенностью именованных каналов является то, что они могут быть как локальные, так и сетевые. То есть, легко можно реализовать удаленный сервер, к которому по сети будут подключаться клиентские терминалы.

Вот простой пример создания локального сервера в виде полнодуплексного канала, работающего в режиме обмена байтами:

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

Чтобы получить клиентский коннект, нужно воспользоваться функцией ConnectNamedPipe:

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

Для обмена данными используются 4 простые функции:

Они позволяют передавать/принимать как любые бинарные данные, так и текстовые ANSI строки в совместимом с MQL5 режиме. Причем, так как по умолчанию CFilePipe в MQL5 открывает файл в ANSI режиме, то при передаче и получении строки автоматически конвертируются в Unicode. Если из MQL5 открыть файл в Unicode режиме (FILE_UNICODE), то можно обмениваться Unicode строками, не забыв про стартовую BOM сигнатуру.


Реализация клиента

Клиента напишем на MQL5 с применением обычных файловых операций через класс CFilePipe стандартной библиотеки. Этот класс практически идентичен CFileBin, но содержит важную проверку наличия данных в виртуальном файле перед их чтением.

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

У именованных каналов реализации работы в локальном и сетевом режимах очень сильно разнятся. Без этой проверки операции в сетевом режиме будут постоянно возвращать ошибку чтения при передаче больших объемом данных (более 64к).

Подключимся к серверу двойной проверкой: на удаленный компьютер по имени 'RemoteServerName' или на локальный.

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


Обмен данными

После успешного коннекта пошлем текстовую строку со своей идентификацией на сервер. Unicode строка будет автоматически сконвертирована в ANSI, так как файл открыт в ANSI режиме.

//--- send welcome message
   if(!ExtPipe.WriteString(__FILE__+" on MQL5 build "+IntegerToString(__MQ5BUILD__)))
     {
      Print("Client: sending welcome message failed");
      return;
     }

В ответ сервер пришлет свою строку "Hello from pipe server" и целое число 1234567890. Клиент еще раз пошлет строку "Test string" и целое число 1234567890.

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

На этом обмен простыми данными завершен, пора переходить к тестированию производительности.


Тестирование производительности

В качестве теста мы прокачаем с сервера на клиент 1 гигабайт данных в виде массива double чисел блоками по 8 мегабайт, проверим корректность блоков и замерим скорость передачи.

Вот как выглядит код на C++ сервере:

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

и на MQL5 клиенте:

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

Обратите внимание, что у передаваемых блоков проверяется первый и последний элементы, чтобы удостовериться, что во время передачи не было ошибок. Кроме того, после окончания передачи клиент пересылает на сервер подтверждающий сигнал об успешном приеме данных. Если не использовать финальные подтверждения, то легко можно столкнуться с потерей данных, если одна из сторон рано закроет соединение.

Запустим локально сервер PipeServer.exe и набросим скрипт PipeClient.mq5 на любой график:

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


При локальном обмене скорость получается поистине потрясающая - почти 3 гигабайта в секунду. Это означает, что через именованные каналы можно передавать практически любые объемы данных внутрь своих программ на MQL5.

Теперь замерим производительность передачи данных в обычной 1 гигабит локальной сети:

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


По локальной сети при передаче 1 гигабайта данных скорость оказалась на уровне 63 мегабайт в секунду, что является очень хорошим показателем. Фактически это 63% от максимальной пропускной способности гигабитной сети.


Заключение

Система защиты торгового терминала MetaTrader 5 не позволяет MQL5 программам работать вне своей песочницы, оберегая трейдеров от угроз при использовании чужих экспертов. Но с помощью именованных каналов становится легко создавать интеграции со сторонними программами и управлять экспертами извне. Безопасно.