
Использование WinInet.dll для обмена данными между терминалами через Интернет
MetaTrader 5 открывает уникальные возможности для пользователей, применяя в своем арсенале новые элементы пользовательского интерфейса. Благодаря этому, ранее недоступный функционал можно использовать по максимуму.
В этом уроке мы научимся:
- использовать базовые интернет технологии;
- обмениваться данными между терминалами посредством сервера;
- создадим обобщенный библиотечный класс для работы с интернет в среде MQL5.
В Codebase MQL4 давно находится пример скрипта, работающего с библиотекой wininet.dll, показан пример обращения к странице сервера. Но сегодня мы пойдем намного дальше и заставим сервер не только отдавать нам страницу, но и отправлять и хранить эти данные для последующей передачи их другому запрашивающему терминалу.
На заметку: для тех, у кого нет доступа к какому-либо серверу с настроенным PHP, предлагаем в качестве рабочей площадки установить комплект Denwer. И использовать для тестирования сервер Apache и PHP на своём localhost.
Для отправки любого запроса на сервер нам понадобится 7 основных функций из библиотеки.
InternetAttemptConnect | Проверка наличия подключения к интернету и попытка его создать |
InternetOpen | Инициализирует структуры для работы функций библиотеки WinInet. Эта функция должна вызываться перед вызовом всех остальных функций библиотеки. |
InternetConnect | Открывает указанный ресурс по указанному адресу HTTP URL или FTP. Возвращает дескриптор на открытое соединение |
HttpOpenRequest | Создает дескриптор для HTTP запросов на созданное соединение |
HttpSendRequest | Отправляет запрос с использованием созданного дескриптора |
InternetReadFile | Читает данные, полученные от сервера после запроса |
InternetCloseHandle | Освобождает передаваемый дескриптор |
Подробное описание всех функций и их параметров вы можете найти в справочной системе MSDN.
Объявление заголовков функций осталось таким же, как и в MQL4 за исключением – использования Unicode вызовов и передачи строк по ссылке.
#import "wininet.dll" int InternetAttemptConnect(int x); int InternetOpenW(string &sAgent,int lAccessType,string &sProxyName,string &sProxyBypass,int lFlags); int InternetConnectW(int hInternet,string &szServerName,int nServerPort,string &lpszUsername,string &lpszPassword,int dwService,int dwFlags,int dwContext); int HttpOpenRequestW(int hConnect,string &Verb,string &ObjectName,string &Version,string &Referer,string &AcceptTypes,uint dwFlags,int dwContext); int HttpSendRequestW(int hRequest,string &lpszHeaders,int dwHeadersLength,uchar &lpOptional[],int dwOptionalLength); int HttpQueryInfoW(int hRequest,int dwInfoLevel,int &lpvBuffer[],int &lpdwBufferLength,int &lpdwIndex); int InternetReadFile(int hFile,uchar &sBuffer[],int lNumBytesToRead,int &lNumberOfBytesRead); int InternetCloseHandle(int hInet); #import //Также для эстетики кода определим используемые имена констант из wininet.h. #define OPEN_TYPE_PRECONFIG 0 // использовать конфигурацию по умолчанию #define FLAG_KEEP_CONNECTION 0x00400000 // не разрывать соединение #define FLAG_PRAGMA_NOCACHE 0x00000100 // не кешировать страницу #define FLAG_RELOAD 0x80000000 // получать страницу с сервера при обращении к ней #define SERVICE_HTTP 3 // требуемый протокол
Подробное описание флагов находится в том же разделе MSDN для каждой вызываемой функции. Если вы желаете посмотреть объявления других констант и функций, то исходный файл wininet.h можете скачать внизу статьи.
1. Правила создания и удаления интернет-сессии
Первое, что мы должны сделать, это создать сессию и открыть соединение с хостом. Создание сессии желательно делать один раз при инициализации программы (например, в функции OnInit). Либо в самом начале работы эксперта, но главное проконтролировать, чтоб её успешное выполнение было только один раз до момента закрытия сессии. И она не вызывалась повторно без надобности каждый раз на новой итерации выполнения OnStart или OnTimer. Это важно для того, чтоб не нагружать слишком частыми вызовами и создание для каждого вызова требуемых структур.
Поэтому будем использовать только один глобальный экземпляр класса для описания дескриптора сессии и соединения.
string Host; // имя хоста int Port; // порт int Session; // дескриптор сессии int Connect; // дескриптор соединения bool MqlNet::Open(string aHost,int aPort) { if(aHost=="") { Print("-Host is not specified"); return(false); } // проверка разрешения DLL в терминале if(!TerminalInfoInteger(TERMINAL_DLLS_ALLOWED)) { Print("-DLL is not allowed"); return(false); } // если сессия была опеределена, то закрываем if(Session>0 || Connect>0) Close(); // сообщение про попытку открытия в журнал Print("+Open Inet..."); // если не удалось проверить имеющееся соединение с интернетом, то выходим if(InternetAttemptConnect(0)!=0) { Print("-Err AttemptConnect"); return(false); } string UserAgent="Mozilla"; string nill=""; // открываем сессию Session=InternetOpenW(UserAgent,OPEN_TYPE_PRECONFIG,nill,nill,0); // если не смогли открыть сессию, то выходим if(Session<=0) { Print("-Err create Session"); Close(); return(false); } Connect=InternetConnectW(Session,aHost,aPort,nill,nill,SERVICE_HTTP,0,0); if(Connect<=0) { Print("-Err create Connect"); Close(); return(false); } Host=aHost; Port=aPort; // иначе все проверки завершились успешно return(true); }
После инициализации дескрипторы Session и Connect можно использовать во всех последующих функциях. При завершении работы и деинициализации MQL-программы их нужно обязательно удалить. Делается это с помощью функции InternetCloseHandle.
void MqlNet::CloseInet() { Print("-Close Inet..."); if(Session>0) InternetCloseHandle(Session); Session=-1; if(Connect>0) InternetCloseHandle(Connect); Connect=-1; }
Важно! При работе с интернет-функциями необходимо освобождать все получаемые от них дескрипторы с помощью InternetCloseHandle.
2. Отправка запроса на сервер и получение страницы
Для отправки запроса и получения страницы в ответ на запрос нам понадобятся оставшиеся три функции HttpOpenRequest, HttpSendRequest и InternetReadFile. Суть получения страницы в ответ на запрос заключается в обычном сохранении её содержимого в локальный файл.
Для удобства работы с запросами и содержимым создадим две универсальные функции.
Отправка запроса:
bool MqlNet::Request(string Verb,string Object,string &Out,bool toFile=false,string addData="",bool fromFile=false) { if(toFile && Out=="") { Print("-File is not specified "); return(false); } uchar data[]; int hRequest,hSend,h; string Vers="HTTP/1.1"; string nill=""; if(fromFile) { if(FileToArray(addData,data)<0) { Print("-Err reading file "+addData); return(false); } } // прочитали файл в массив else StringToCharArray(addData,data); if(Session<=0 || Connect<=0) { Close(); if(!Open(Host,Port)) { Print("-Err Connect"); Close(); return(false); } } // создаем дескриптор запроса hRequest=HttpOpenRequestW(Connect,Verb,Object,Vers,nill,nill,FLAG_KEEP_CONNECTION|FLAG_RELOAD|FLAG_PRAGMA_NOCACHE,0); if(hRequest<=0) { Print("-Err OpenRequest"); InternetCloseHandle(Connect); return(false); } // отправляем запрос // заголовок на отправку string head="Content-Type: application/x-www-form-urlencoded"; // отправили файл hSend=HttpSendRequestW(hRequest,head,StringLen(head),data,ArraySize(data)-1); if(hSend<=0) { Print("-Err SendRequest"); InternetCloseHandle(hRequest); Close(); } // читаем страницу ReadPage(hRequest,Out,toFile); // закрыли все хендлы InternetCloseHandle(hRequest); InternetCloseHandle(hSend); return(true); }
Параметры функции MqlNet::Request:
- string Verb – тип запроса “GET” или “POST”;
- string Object – имя страницы с передаваемыми ей параметрами;
- string &Out – строка, куда получаем ответ;
- bool toFile – если toFile=true, то Out обозначает имя файла куда надо получить ответ;
- string addData – дополнительные данные;
- bool fromFile – если fromFile=true, то addData обозначает имя файла который надо отправить.
Чтение содержимого из полученного дескриптора
void MqlNet::ReadPage(int hRequest,string &Out,bool toFile) { // читаем страницу uchar ch[100]; string toStr=""; int dwBytes,h; while(InternetReadFile(hRequest,ch,100,dwBytes)) { if(dwBytes<=0) break; toStr=toStr+CharArrayToString(ch,0,dwBytes); } if(toFile) { h=FileOpen(Out,FILE_BIN|FILE_WRITE); FileWriteString(h,toStr); FileClose(h); } else Out=toStr; }
Параметры функции MqlNet::ReadPage:
- Int hRequest – дескриптор запроса, откуда читаем данные;
- string &Out – строка, куда получаем ответ;
- bool toFile – если toFile=true, то Out обозначает имя файла куда надо получить ответ.
И, собирая все в одно целое, будем иметь библиотечный класс MqlNet для работы с интернет.
class MqlNet { string Host; // имя хоста int Port; // порт int Session; // дескриптор сессии int Connect; // дескриптор соединения public: MqlNet(); // конструктор класса ~MqlNet(); // деструктор bool Open(string aHost,int aPort); // создаем сессию и открываем соединение void Close(); // закрываем сессию и соединение bool Request(string Verb,string Request,string &Out,bool toFile=false,string addData="",bool fromFile=false); // отправляем запрос bool OpenURL(string URL,string &Out,bool toFile); // просто читаем страницу в файл или в переменную void ReadPage(int hRequest,string &Out,bool toFile); // читаем страницу int FileToArray(string FileName,uchar &data[]); // копируем файл в массив для отправки };
Вот в принципе и все требуемые функции, которые вполне могут удовлетворить самые разнообразные потребности при работе с интернетом. Рассмотрим примеры их использования.
Пример 1. Автоматическое скачивание MQL-программ в папки терминала. Скрипт MetaGrabber
Для разминки и проверки работы класса начнем с самого простого – чтения страницы и сохранение её содержимого в указанную папку. Но простое чтение страниц наверно не очень интересное занятие, поэтому, чтобы был какой-то прок от работы скрипта, дадим ему функционал граббера mql программ с сайтов. Задачей скрипта MetaGrabber будет:
- анализ URL и разделение его на хост, запрос и имя файла;
- отправление запроса на хост, получение и сохранение файла в папке терминала \Files;
- перемещение его из Files в одну из требуемых папок данных:
\Experts, \Indicators, \Scripts, \Include, \Libraries, \Tester(set), \Templates.
Для решения второй задачи используем класс MqlNet. Для третьей задачи функцию MoveFileEx из Kernel32.dll
#import "Kernel32.dll" bool MoveFileExW(string &lpExistingFileName, string &lpNewFileName, int dwFlags); #import "Kernel32.dll"
Для первой задачи сделаем небольшую сервисную функцию разбора строки URL.
Нам требуется выделить из адреса три строки: хост, путь к файлу на сайте и имя файла.
Например, в строке http://www.mysite.com/folder/page.html
- Хост = www.mysite.com
- Запрос = /folder/page.html
- Имя файла = page.html
В случае с CodeBase на сайте MQL5 пути имеют похожую структуру. Например, путь к файлу библиотеки ErrorDescription.mq5 на странице https://www.mql5.com/ru/code/79 имеет вид http://p.mql5.com/data/18/79/ErrorDescription.mqh. Этот путь легко получить – нажав правой кнопкой на ссылке и выбрав команду «Копировать ссылку». Таким образом, этот URL разделяется на две части для запроса плюс имя файла для удобства сохранения.
- Хост = p.mql5.com
- Запрос = /data/18/79/5/ErrorDescription.mqh
- Имя файла = ErrorDescription.mqh
Именно таким разбором строки и будет заниматься следующая функция ParseURL.
void ParseURL(string path,string &host,string &request,string &filename) { host=StringSubstr(URL,7); // убрали int i=StringFind(host,"/"); request=StringSubstr(host,i); host=StringSubstr(host,0,i); string file=""; for(i=StringLen(URL)-1; i>=0; i--) if(StringSubstr(URL,i,1)=="/") { file=StringSubstr(URL,i+1); break; } if(file!="") filename=file; }
Во внешних параметрах скрипта сделаем всего два параметра – URL (путь mql5 файла) и тип папки последующего размещения – то есть, в какую папку терминала его требуется переместить.
Итого в результате получим небольшой, но очень полезный скрипт.
//+------------------------------------------------------------------+ //| MetaGrabber.mq5 | //| Copyright © 2010 www.fxmaster.de | //| Coding by Sergeev Alexey | //+------------------------------------------------------------------+ #property copyright "www.fxmaster.de © 2010" #property link "www.fxmaster.de" #property version "1.00" #property description "Download files from internet" #property script_show_inputs #include <InternetLib.mqh> #import "Kernel32.dll" bool MoveFileExW(string &lpExistingFileName,string &lpNewFileName,int dwFlags); #import #define MOVEFILE_REPLACE_EXISTING 0x1 enum _FolderType { Experts=0, Indicators=1, Scripts=2, Include=3, Libraries=4, Files=5, Templates=6, TesterSet=7 }; input string URL=""; input _FolderType FolderType=0; //------------------------------------------------------------------ OnStart int OnStart() { MqlNet INet; // переменная для работы в интернете string Host,Request,FileName="Recieve_"+TimeToString(TimeCurrent())+".mq5"; // разделили адрес на запрос ParseURL(URL,Host,Request,FileName); // открыли сессию if(!INet.Open(Host,80)) return(0); Print("+Copy "+FileName+" from http://"+Host+" to "+GetFolder(FolderType)); // получили файл if(!INet.Request("GET",Request,FileName,true)) { Print("-Err download "+URL); return(0); } Print("+Ok download "+FileName); // перемещаем в требуемую папку string to,from,dir; // если никуда перемещать не надо if(FolderType==Files) return(0); // откуда from=TerminalInfoString(TERMINAL_DATA_PATH)+"\\MQL5\\Files\\"+FileName; // куда to=TerminalInfoString(TERMINAL_DATA_PATH)+"\\"; if(FolderType!=Templates && FolderType!=TesterSet) to+="MQL5\\"; to+=GetFolder(FolderType)+"\\"+FileName; // перемещаем if(!MoveFileExW(from,to,MOVEFILE_REPLACE_EXISTING)) { Print("-Err move to "+to); return(0); } Print("+Ok move "+FileName+" to "+GetFolder(FolderType)); return(0); } //------------------------------------------------------------------ GetFolder string GetFolder(_FolderType foldertype) { if(foldertype==Experts) return("Experts"); if(foldertype==Indicators) return("Indicators"); if(foldertype==Scripts) return("Scripts"); if(foldertype==Include) return("Include"); if(foldertype==Libraries) return("Libraries"); if(foldertype==Files) return("Files"); if(foldertype==Templates) return("Profiles\\Templates"); if(foldertype==TesterSet) return("Tester"); return(""); } //------------------------------------------------------------------ ParseURL void ParseURL(string path,string &host,string &request,string &filename) { host=StringSubstr(URL,7); // убрали int i=StringFind(host,"/"); request=StringSubstr(host,i); host=StringSubstr(host,0,i); string file=""; for(i=StringLen(URL)-1; i>=0; i--) if(StringSubstr(URL,i,1)=="/") { file=StringSubstr(URL,i+1); break; } if(file!="") filename=file; } //+------------------------------------------------------------------+
Опыты предлагаем проводить над любимым разделом https://www.mql5.com/ru/code. Скачанные файлы сразу будут появляться в навигаторе редактора, и их можно будет компилировать, не перезагружая терминал и редактор. И не блуждая по длинным путям файловой системы в поиске требуемой папки для переноса в неё файла.
Важно! Многие сайты ставят защиту от массового скачивания контента, и ваш IP-адрес, в
случае такого массового скачивания может быть заблокирован данным ресурсом. Поэтому внимательно отнеситесь к использованию "машинного" скачивания файлов с ресурсов, которые вы часто посещаете и не хотите быть заблокированным от дальнейшего его посещения.
Для тех, кто желает пойти еще дальше, улучшив предлагаемый сервис, может воспользоваться скриптом Clipboard с перехватом содержимого буфера обмена и дальнейшему автоматическому скачиванию.
Пример 2. Мониторинг котировок от нескольких брокеров на одном графике
Итак, получать файлы из интернета мы научились. Теперь рассмотрим вопрос интереснее – как эти данные отправить и сохранить на сервере. Для этого нам понадобится дополнительный небольшой PHP-скрипт, который будет размещаться на сервере. Используя написанный класс MqlNet, создадим эксперт мониторинга - MetaArbitrage. Задачей эксперта в связке с PHP-скриптом будет:
- отправка экспертом запроса на сервер;
- формирование на сервере ответной страницы (PHP);
- прием этой страницы экспертом;
- её анализ и выдача результата на экран.
Принципиальная схема взаимодействия между MQL-модулем и PHP-скриптом следующая:
Для решения задач - используем класс MqlNet.
Чтоб избежать дублирования данных, а также отсеять устаревшие котировки – будем передавать 4 главных параметра: имя сервер брокера (источник текущих котировок), валюта, цена и время котировки в UTC. Например, запрос для обращения к скрипту на ресурсе нашей компании выглядит так:
www.fxmaster.de/metaarbitr.php?server=Metaquotes&pair=EURUSD&bid=1.4512&time=13286794
Эти параметры и сама котировка сохраняются на сервере и будут выдаваться в ответной странице вместе со всеми другими хранящимися котировками данной валюты.
«Побочное» удобство такого обмена – котировки можно отправлять как из MT5, так и из MT4!
Страница, которая формируется сервером – это обычный CSV файл. Выглядит он в данном скрипте так:
ServerName1; Bid1; Time1
ServerName 2; Bid2; Time2
ServerName 3; Bid3; Time3
…
ServerName N; BidN; TimeN
Но вы можете добавить в него свои дополнительные параметры (например, тип сервера - демо или реал). Этот CSV-файл сохраняем и разбираем построчно с выводом на экран таблицы значений и линий цены.
Обрабатывать данный файл можно самыми разнообразными способами, которые требуются в вашем индивидуальном случае. Например, отсеять котировки, которые приходят от демо-серверов MetaTrader 4 и т.д.
Преимущества использования интернет-сервера очевидны – вы отправляете свои котировки, которые может получить и просмотреть другой трейдер. Аналогично и вы, будете принимать котировки, которые отправляют другие трейдеры. То есть, взаимодействие между терминалами идет двустороннее, происходит обмен данными на примере приведенной схемы:
Эта схема будет основой для принципа обмена информацией между любым количеством терминалов. Полный эксперт MetaArbitrage и PHP-скрипт с комментариями скачайте внизу статьи. Подробно про используемые PHP-функции можете прочитать на сайте php.su
Пример 3. Обмен сообщениями (мини-чат) в терминале. Эксперт MetaChat
Отойдем немного от торговли и цифр и сделаем приложение, которое даст возможность, не «выходя» из терминала, общаться в чате одновременно с несколькими людьми. Для этого нам понадобится еще один скрипт на PHP, но он, в общем, похож на предыдущий. За исключением того, что в новом вместо анализа времени котировок будет анализ числа строк в файле. Задачей эксперта будет:
- отправка текстовой строки на сервер;
- добавление этой строки в общий файл, контроль размера файла, выдача ответного файла (php);
- получение текущего чата и отображение его на экране.
Работа MetaChat не будет отличаться от предыдущего эксперта. Тот же принцип, и тот же простой файл CSV для вывода.
MetaСhat и MetaArbitrage поддерживается на сайте их разработчиков. Там же находятся установленные PHP-скрипты для их работы.
Поэтому если вы хотите протестировать работу или воспользоваться данным сервисом используйте следующую адресацию:
MetaСhat - www.fxmaster.de/metachat.php
MetaArbitrage - www.fxmaster.de/metaarbitr.php
Заключение
Итак, мы познакомились с HTTP-запросами. Получили возможность отправлять и принимать данные из сети интернет, организовывать рабочий процесс более комфортно. Но любые возможности всегда можно улучшать. В качестве направлений их продвижения можно обозначить следующие:
- чтение новостей или получение иной информации напрямую в терминал для анализа в экспертах;
- удаленное управление экспертом;
- автоматическое обновление версий экспертов/индикаторов;
- копировщики/трансляторы сделок, рассылка сигналов;
- скачивание шаблонов в комплекте с индикаторами и set-файлов для экспертов;
- И многое, многое другое…
В этой статье мы использовали тип GET запросов. Их вполне достаточно, когда необходимо получить файл или отправить запрос с каким-то небольшим количеством параметров для анализа на сервере.
В следующем уроке мы рассмотрим работу с POST запросами - отправка файлов на сервер или обмен файлами между терминалами и покажем примеры их использования.
Полезные ресурсы
- Комплект Денвер для установки сервера Apache+PHP http://www.denwer.ru/
- Прокси для просмотра отсылаемых заголовков http://www.charlesproxy.com/
- Виды заголовков запросов http://www.codenet.ru/webmast/php/HTTP-POST.php#part_3_2
- Типы запросов http://www.w3.org/TR/REC-html40/interact/forms.html#form-content-type
- Типы запросов ftp://ftp.isi.edu/in-notes/iana/assignments/media-types/media-types
- Описание WinHTTP http://msdn.microsoft.com/en-us/library/aa385331%28VS.85%29.aspx
- Описание HTTP Session http://msdn.microsoft.com/en-us/library/aa384322%28VS.85%29.aspx
- Структура использования HINTERNET http://msdn.microsoft.com/en-us/library/aa383766%28VS.85%29.aspx
- Работа с файлами http://msdn.microsoft.com/en-us/library/aa364232%28VS.85%29.aspx
- Типы данных для перевода в MQL http://msdn.microsoft.com/en-us/library/aa383751%28VS.85%29.aspx





- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Вот такой не проходит ec2-18-217-190-217.us-east-2.compute.amazonaws.com
А как вызвать скрипт из индикатора? Это как мне известно тоже нормальными способами невозможно.
https://www.mql5.com/ru/articles/5337
Вот такое стало вылазить при поптыке проверить.