Связь с MetaTrader 5 через именованные каналы без применения DLL
Введение
Перед многими разработчиками встает одинаковая проблема - как пробиться в песочницу торгового терминала без применения небезопасных 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 программам работать вне своей песочницы, оберегая трейдеров от угроз при использовании чужих экспертов. Но с помощью именованных каналов становится легко создавать интеграции со сторонними программами и управлять экспертами извне. Безопасно.
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Планируется ли добавить в библиотеку функцию, проверяющую открыто ли соединение с сервером? Сейчас такой функции, как я понимаю, нет.
Хэндл сохраняет ненулевое значение, даже если соединение с сервером потеряно.
Сервер для одного клиента что ли? Пытаюсь подключить второй клиент, не октрывается соединение. 5004 ошибка. Такое же имя файла, как и в другом клиенте, подключенном.
Если первый клиент отрубить, то подключается второй. То есть, один именованный канал, это только одно подключение?
Как два МТ клиента соединить через именованные каналы?
Пробовал код отсюда https://www.mql5.com/ru/articles/115. Не работает. Метод Connect зависает.