English 中文 Español Deutsch 日本語 Português 한국어 Français Italiano Türkçe
Связь с MetaTrader 5 через именованные каналы без применения DLL

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

MetaTrader 5Примеры | 15 октября 2012, 21:14
15 187 56
MetaQuotes
MetaQuotes

Введение

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

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

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


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

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

  • CreateNamedPipe - создает именованный канал;
  • ConnectNamedPipe - открывает серверный коннект и ждет клиентские подключения;
  • WriteFile - записывает данные в канал;
  • ReadFile - читает данные из канала;
  • FlushFileBuffers - сбрасывает накопившиеся буферы;
  • DisconnectNamedPipe - закрывает серверный коннект;
  • CloseHandle - закрывает хендл.

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

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

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

//--- 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 простые функции:

  • 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)

Они позволяют передавать/принимать как любые бинарные данные, так и текстовые 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 программам работать вне своей песочницы, оберегая трейдеров от угроз при использовании чужих экспертов. Но с помощью именованных каналов становится легко создавать интеграции со сторонними программами и управлять экспертами извне. Безопасно.

Прикрепленные файлы |
pipeclient.mq5 (3.15 KB)
pipeserver.zip (43.59 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (56)
Juer
Juer | 14 февр. 2018 в 01:53

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

Хэндл сохраняет ненулевое значение, даже если соединение с сервером потеряно.

[Удален] | 14 февр. 2018 в 07:37
Сорри за нубский вопрос, а что это дает при использовании пайпов в таких приложениях как mt5 mt4 copier?
[Удален] | 14 февр. 2018 в 08:30
Или пайпы уже не актуальны?
leonerd
leonerd | 11 сент. 2020 в 19:03

Сервер для одного клиента что ли? Пытаюсь подключить второй клиент, не октрывается соединение. 5004 ошибка. Такое же имя файла, как и в другом клиенте, подключенном.

Если первый клиент отрубить, то подключается второй. То есть, один именованный канал, это только одно подключение?

leonerd
leonerd | 11 сент. 2020 в 21:05

Как два МТ клиента соединить через именованные каналы?

Пробовал код отсюда  https://www.mql5.com/ru/articles/115. Не работает. Метод Connect зависает.

Интервью с Франциско Гарсиа Гарсиа (ATC 2012) Интервью с Франциско Гарсиа Гарсиа (ATC 2012)
Сегодня мы берем интервью у испанца Франциско Гарсиа Гарсиа (chuliweb). Неделю назад его советник достиг 8-го места, однако досадная логическая ошибка в программировании выбросила его с первой страницы лидеров Чемпионата. Как показала статистика, такую ошибку допускают многие участники.
Как подписаться на Торговые Сигналы Как подписаться на Торговые Сигналы
"Сигналы" - это социальный трейдинг c MetaTrader 4 и MetaTrader 5. Сервис напрямую интегрирован в торговые платформы, и позволяет любому легко копировать торговые операции профессиональных трейдеров. Из тысяч провайдеров выберите понравившегося, подпишитесь в несколько кликов, и сделки моментально начнут копироваться на ваш счет.
Как подготовить описание продукта для Маркета Как подготовить описание продукта для Маркета
В MQL5 Маркете представлено много продуктов, однако их описания оставляют желать лучшего. Многие тексты непонятны обычному трейдеру и нуждаются в улучшении. Данная статья поможет вам представить свой продукт в выгодном свете. Воспользуйтесь ею и создайте хорошее описание, которое доходчиво объяснит вашим покупателям, что именно вы продаете.
Интервью с Андреем Бариновым (ATC 2012) Интервью с Андреем Бариновым (ATC 2012)
Еще в пятницу в первую неделю соревнований торговый робот Андрея Баринова (Wahoo) занимал пятое место в TOP-10. Андрей в первый раз участвует в Чемпионате, но успел уже выполнить более 100 заказов в Работе, а также выставил десяток продуктов в Маркете. Мы пообщались с ним и узнали, что создать "простой мультивалютный советник" не просто, а достаточно просто.