Использование WinInet.dll для обмена данными между терминалами через Интернет

--- | 5 мая, 2010

MetaTrader 5 открывает уникальные возможности для пользователей, применяя в своем арсенале новые элементы пользовательского интерфейса. Благодаря этому, ранее недоступный функционал можно использовать по максимуму.

В этом уроке мы научимся:

В 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:

Чтение содержимого из полученного дескриптора

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:

И, собирая все в одно целое, будем иметь библиотечный класс 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 будет:

Для решения второй задачи используем класс 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-скриптом будет:

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

Работа MetaChat не будет отличаться от предыдущего эксперта. Тот же принцип, и тот же простой файл CSV для вывода.


MetaСhat и MetaArbitrage поддерживается на сайте их разработчиков. Там же находятся установленные PHP-скрипты для их работы.
Поэтому если вы хотите протестировать работу или воспользоваться данным сервисом используйте следующую адресацию:
MetaСhat - www.fxmaster.de/metachat.php
MetaArbitrage - www.fxmaster.de/metaarbitr.php

Заключение

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

В этой статье мы использовали тип GET запросов. Их вполне достаточно, когда необходимо получить файл или отправить запрос с каким-то небольшим количеством параметров для анализа на сервере.

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

Полезные ресурсы