Использование WinInet в MQL5. Часть 2: POST-запросы и файлы

--- | 13 мая, 2011


Введение

В прошлом уроке "Использование WinInet.dll для обмена данными между терминалами через Интернет" мы разобрались с работой библиотеки, научились открывать web-страницы, отправлять и принимать данные по GET-запросу.

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

Как и в прошлый раз, настоятельно рекомендуем поставить локальный прокси Charles, он понадобится вам в изучении материала и ваших дальнейших экспериментах.


POST-запросы

Для отправки данных нам понадобится все те же функции wininet.dll и созданный класс CMqlNet, которые были подробно расписаны в прошлой статье.

В связи с большим количеством полей в методе CMqlNet::Request нам пришлось создать отдельную структуру tagRequest, которая содержит в себе все необходимые поля запроса. 

//------------------------------------------------------------------ struct tagRequest
struct tagRequest
{
  string stVerb;   // метод запроса GET/POST/…
  string stObject; // путь к объекту запроса например  "/index.htm" или "/get.php?a=1"  
  string stHead;   // заголовок запроса
  string stData;   // дополнительная строка данных
  bool fromFile;   // если =true, то stData обозначает имя файла данных
  string stOut;    // строка для приема ответа
  bool toFile;     // если =true, то stOut обозначает имя файла для приема ответа

  void Init(string aVerb, string aObject, string aHead, 
            string aData, bool from, string aOut, bool to); // функция инициализации всех полей
};
//------------------------------------------------------------------ Init
void tagRequest::Init(string aVerb, string aObject, string aHead, 
                      string aData, bool from, string aOut, bool to)
{
  stVerb=aVerb;     // метод запроса GET/POST/…
  stObject=aObject; // путь к странице "/get.php?a=1"  или "/index.htm"
  stHead=aHead;     // заголовок запроса, например"Content-Type: application/x-www-form-urlencoded"
  stData=aData;     // дополнительная строка данных
  fromFile=from;    // если =true, то stData обозначает имя файла данных
  stOut=aOut;       // поле для приема ответа
  toFile=to;        // если =true, то stOut обозначает имя файла для приема ответа
}

А также заменить заголовок метода CMqlNet::Request более коротким:

//+------------------------------------------------------------------+
bool MqlNet::Request(tagRequest &req)
  {
   if(!TerminalInfoInteger(TERMINAL_DLLS_ALLOWED))
     {
      Print("-DLL not allowed"); return(false);
     }
//--- проверка разрешения DLL в терминале
   if(!MQL5InfoInteger(MQL5_DLLS_ALLOWED))
     {
      Print("-DLL not allowed");
      return(false);
     }
//--- проверка разрешения DLL в терминале
   if(req.toFile && req.stOut=="")
     {
      Print("-File not specified ");
      return(false);
     }
   uchar data[]; 
    int hRequest,hSend;
   string Vers="HTTP/1.1"; 
    string nill="";

//--- читаем файл в массив
   if(req.fromFile)
     {
      if(FileToArray(req.stData,data)<0)
        {
         Print("-Err reading file "+req.stData);
         return(false);
        }
     }
   else StringToCharArray(req.stData,data);

   if(hSession<=0 || hConnect<=0)
     {
      Close();
      if(!Open(Host,Port,User,Pass,Service))
        {
         Print("-Err Connect");
         Close();
         return(false);
        }
     }
//--- создаем дескриптор запроса
   hRequest=HttpOpenRequestW(hConnect,req.stVerb,req.stObject,Vers,nill,0,
   INTERNET_FLAG_KEEP_CONNECTION|INTERNET_FLAG_RELOAD|INTERNET_FLAG_PRAGMA_NOCACHE,0);
   if(hRequest<=0)
     {
      Print("-Err OpenRequest");
      InternetCloseHandle(hConnect);
      return(false);
     }
//--- отправляем запрос
   hSend=HttpSendRequestW(hRequest,req.stHead,StringLen(req.stHead),data,ArraySize(data));
//--- отправляем файл
   if(hSend<=0)
     {
      int err=0;
      err=GetLastError(err);
      Print("-Err SendRequest= ",err);
     }
//--- читаем страницу
   if(hSend>0) ReadPage(hRequest,req.stOut,req.toFile);
//--- закрываем все хендлы
   InternetCloseHandle(hRequest); InternetCloseHandle(hSend);

   if(hSend<=0)
     {
      Close();
      return(false);
     }
   return(true);
  }

Теперь приступим к работе.


Отправка данных на сайт с типом application/x-www-form-urlencoded

В прошлом уроке мы рассмотрели пример - эксперт MetaArbitrage (мониторинг котировок).

Напомним, что эксперт отправлял цены Bid по своему символу через GET запрос и в ответе принимал цены остальных брокеров, которые аналогично попадали на сервер с других терминалов участников.

Чтобы сменить запрос GET на POST, необходимо всего лишь "спрятать" саму строку запроса в тело запроса, идущее после заголовков.

BOOL HttpSendRequest(
  __in  HINTERNET hRequest,
  __in  LPCTSTR lpszHeaders,
  __in  DWORD dwHeadersLength,
  __in  LPVOID lpOptional,
  __in  DWORD dwOptionalLength
);

  • hRequest [in]
     Хендл, возвращенный HttpOpenRequest.
  • lpszHeaders [in]
    Указатель на строку с содержимым заголовков для добавления к запросу. Это параметр может быть пустым.
  • dwHeadersLength [in]
    Длина заголовка в байтах.
  • lpOptional [in]
    Указатель на массив uchar с данными, которые отправляются сразу после заголовка. Этот параметр обычно используется для POST и PUT операций.
  • dwOptionalLength [in]
    Размер данных в байтах. Параметр может быть =0, что означает, что никакие дополнительные данные не отправляются.

Исходя из описания функции, данные передаются в виде байтового uchar-массива (четвертый параметр функции). Это все, что нужно знать на данном этапе.

В примере для MetaArbitrage GET-запрос на сервер выглядел следующим образом:

www.fxmaster.de/metaarbitr.php?server=Metaquotes&pair=EURUSD&bid=1.4512&time=13286794


Красным цветом выделен сам запрос. Значит, в случае POST-запроса его текст необходимо перенести в массив данных lpOptional.

Создадим скрипт MetaSwap, который будет отправлять и принимать данные по свопам символа. 

#include <InternetLib.mqh>

string Server[];        // массив имен серверов
double Long[], Short[]; // массив данных свопов
MqlNet INet;           // экземпляр класса для работы

//------------------------------------------------------------------ OnStart
void OnStart()
{
//--- открываем сессию
  if (!INet.Open("www.fxmaster.de", 80, "", "", INTERNET_SERVICE_HTTP)) return;
 
//--- обнулили массивы
  ArrayResize(Server, 0); ArrayResize(Long, 0); ArrayResize(Short, 0);
//--- файл, куда записывать пример данных свопа
  string file=Symbol()+"_swap.csv";
//--- отправляем свопы
  if (!SendData(file, "GET")) 
  { 
    Print("-err RecieveSwap"); 
    return; 
  }
//--- читаем данные из принятого файла
  if (!ReadSwap(file)) return; 
//--- обновляем информацию о свопах на графике
  UpdateInfo();               
}

Работа скрипта очень проста.

Сначала открывается интернет-сессия INet.Open. Затем функция SendData отправляет данные свопов текущего символа. Затем при успешном выполнении отправки происходит чтение принятых ответных свопов ReadSwap и вывод их на график UpdateInfo.

В данный момент нас будет интересовать только функция SendData.

//------------------------------------------------------------------ SendData bool SendData(string file, string mode) {   string smb=Symbol();   string Head="Content-Type: application/x-www-form-urlencoded"; // заголовок   string Path="/mt5swap/metaswap.php"; // путь к странице   string Data="server="+AccountInfoString(ACCOUNT_SERVER)+               "&pair="+smb+               "&long="+DTS(SymbolInfoDouble(smb, SYMBOL_SWAP_LONG))+               "&short="+DTS(SymbolInfoDouble(smb, SYMBOL_SWAP_SHORT));   tagRequest req; // инициализация параметров   if (mode=="GET")  req.Init(mode, Path+"?"+Data, Head, "",   false, file, true);   if (mode=="POST") req.Init(mode, Path,          Head, Data, false, file, true);   return(INet.Request(req)); // посылаем запрос на сервер }

В этом скрипте мы специально для демонстрации сделали два метода отправки данных - при помощи GET и POST, чтобы вы могли, как говорится, "прочувствовать" их разницу.

Опишем по порядку переменные функции:

И самое главное: обратите внимание на отличия между заданием GET и POST запросов в tagRequest::Init.

В методе GET путь отправляется вместе с телом запроса (объединяясь знаком "?"), а поле данных lpOptional (в структуре оно имеет имя stData) остается пустым.
В методе POST
путь существует сам по себе, а тело запроса уходит в lpOptional.

Как видите, отличия несущественны. Серверный скрипт metaswap.php, который принимает этот запрос, можно скачать в приложении к статье.


Отправка данных multipart/form-data

На самом деле POST-запросы - это не аналог GET-запросов (иначе бы в них не было необходимости). POST-запросы обладают существенным преимуществом - с их помощью можно передавать файлы с бинарным содержимым.

Дело в том, что запрос с типом urlencoded имеет право отправить только разрешенный набор символов. В противном случае "запрещенные" символы будут заменены кодами. Следовательно, при отправке бинарных данных они будут искажены. То есть передать даже небольшой gif-файл через GET-запрос вы не сможете.

Чтобы выйти из этой ситуации были разработаны правила описания запроса, которые позволяют обмениваться не только текстовыми, но и бинарными данными.

Для этого тело запроса делится на разделы. Самое главное то, что каждый раздел может иметь свой тип данных. Например, первый раздел это текст, следующий раздел это image/jpeg и т.д. То есть, в одном запросе на сервер можете передавать сразу несколько разных типов данных.

Рассмотрим структуру такого описания на примере передаваемых данных скрипта MetaSwap.

Заголовок запроса Head будет иметь следующий вид:

Content-Type: multipart/form-data; boundary=РАЗДЕЛИТЕЛЬ\r\n


Ключевое слово РАЗДЕЛИТЕЛЬ – это произвольный набор символов. Но вы должны проследить, чтобы этот набор символов не находился в данных запроса. То есть, эта строка должна быть уникальной, проще говоря, какой-то абракадаброй, по типу hdsJK263shxaDFHLsdhsDdjf9 или что вы сами придумаете :). В PHP обычно данную строку формируют, используя MD5 код текущего времени.

Сам POST-запрос выглядит следующим образом (для упрощения восприятия поля выделены цветом по общему смыслу):

\r\n
--РАЗДЕЛИТЕЛЬ\r\n

Content-Disposition: form-data; name="Server"\r\n
\r\n
MetaQuotes-Demo

\r\n
--РАЗДЕЛИТЕЛЬ\r\n

Content-Disposition: form-data; name="Pair"\r\n
\r\n
EURUSD

\r\n
--РАЗДЕЛИТЕЛЬ\r\n

Content-Disposition: form-data; name="Long"\r\n
\r\n
1.02

\r\n
--РАЗДЕЛИТЕЛЬ\r\n

Content-Disposition: form-data; name="Short"\r\n
\r\n
-0.05

\r\n
--РАЗДЕЛИТЕЛЬ--\r\n


Мы специально явным образом указали места перевода строки "\r\n", так как они являются обязательными символами в запросе. Как видите, в запросе передаются те же четыре поля данных, причем они передаются обычным текстовым способом.

Важные особенности установки разделителя:

  • Перед разделителем ставится два подряд символа "--".
  • В завершающем разделителе два символа "--" добавляются также и после него.


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

Представим, что некий эксперт в момент закрытия сделки передает снимок чарта и подробный отчет счета в текстовом файле.

\r\n
--РАЗДЕЛИТЕЛЬ\r\n

Content-Disposition: form-data; name="ExpertName"\r\n
\r\n
MACD_Sample

\r\n
--РАЗДЕЛИТЕЛЬ\r\n

Content-Disposition: file; name="screen"; filename="screen.gif"\r\n
Content-Type: image/gif\r\n
Content-Transfer-Encoding: binary\r\n
\r\n
......содержимое gif-файла.....

\r\n
--РАЗДЕЛИТЕЛЬ\r\n

Content-Disposition: form-data; name="statement"; filename="statement.csv"\r\n
Content-Type: application/octet-stream\r\n
Content-Transfer-Encoding: binary\r\n
\r\n
......содержимое csv-файла.....

\r\n
--РАЗДЕЛИТЕЛЬ--\r\n


В запросе появились два новых заголовка:

Content-Type - указывает на тип содержимого, все типы четко регламентируется стандартом RFC[2046]. Мы использовали два типа image/gif и application/octet-stream.

Два варианта записи Content-Disposition - file и form-data равноправны и корректно обрабатываются PHP в обоих случаях. То есть вы можете использовать или file, или form-data по вашему усмотрению. Более детальную разницу в их представлении можете наблюдать в Charles.

Content-Transfer-Encoding - указывает на кодировку содержимого. Для текстовых данных может отсутствовать.

Для закрепления материала напишем скрипт ScreenPost, который передает снимок экрана на сервер:

#include <InternetLib.mqh>

MqlNet INet; // экземпляр класса для работы

//------------------------------------------------------------------ OnStart
void OnStart()
{
  // открываем сессию
  if (!INet.Open("www.fxmaster.de", 80, "", "", INTERNET_SERVICE_HTTP)) return;

  string giffile=Symbol()+"_"+TimeToString(TimeCurrent(), TIME_DATE)+".gif"; // имя файла на отправку
 
  // создали скриншот 800х600px
  if (!ChartScreenShot(0, giffile, 800, 600)) { Print("-err ScreenShot "); return; }
 
  // читаем gif-файл в массив
  int h=FileOpen(giffile, FILE_ANSI|FILE_BIN|FILE_READ); if (h<0) { Print("-err Open gif-file "+giffile); return; }
  FileSeek(h, 0, SEEK_SET);
  ulong n=FileSize(h); // узнали размер файла
  uchar gif[]; ArrayResize(gif, (int)n); // создали uichar массив под размер данных
  FileReadArray(h, gif); // прочитали файл в массив
  FileClose(h); // закрыли файл
 
  // создали файл на отправку
  string sendfile="sendfile.txt";
  h=FileOpen(sendfile, FILE_ANSI|FILE_BIN|FILE_WRITE); if (h<0) { Print("-err Open send-file "+sendfile); return; }
  FileSeek(h, 0, SEEK_SET);

  // формируем запрос
  string bound="++1BEF0A57BE110FD467A++"; // разделитель данных запроса
  string Head="Content-Type: multipart/form-data; boundary="+bound+"\r\n"; // заголовок
  string Path="/mt5screen/screen.php"; // путь к странице
 
  // пишем данные
  FileWriteString(h, "\r\n--"+bound+"\r\n");
  FileWriteString(h, "Content-Disposition: form-data; name=\"EA\"\r\n"); // поле "имя эксперта"
  FileWriteString(h, "\r\n");
  FileWriteString(h, "NAME_EA");
  FileWriteString(h, "\r\n--"+bound+"\r\n");
  FileWriteString(h, "Content-Disposition: file; name=\"data\"; filename=\""+giffile+"\"\r\n"); // поле gif-файл
  FileWriteString(h, "Content-Type: image/gif\r\n");
  FileWriteString(h, "Content-Transfer-Encoding: binary\r\n");
  FileWriteString(h, "\r\n");
  FileWriteArray(h, gif); // записали gif-данные
  FileWriteString(h, "\r\n--"+bound+"--\r\n");
  FileClose(h); // закрыли файл

  tagRequest req; // инициализация параметров
  req.Init("POST", Path, Head, sendfile, true, "answer.htm", true);
 
  if (INet.Request(req)) Print("-err Request"); // посылаем запрос на сервер
  else Print("+ok Request");
} 

Серверный скрипт, который принимает данные:

<?php
$ea=$_POST['EA'];
$data=file_get_contents($_FILES['data']['tmp_name']); // данные файла
$file=$_FILES['data']['name'];
$h=fopen(dirname(__FILE__)."/$ea/$file", 'wb'); // создаем файл в папке эксперта 
fwrite($h, $data); fclose($h); // сохраняем данные
?>

Настоятельно рекомендуем ознакомиться с правилами приема файлов на сервере чтобы избежать возможных проблем с безопасностью!


Работа с Cookie

Эту тему мы рассмотрим обзорно, в качестве дополнения к предыдущему уроку и пищи для размышления над их возможностями.

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

Например, когда вы при логине на сервер www.mql5.com  ставите галку "Запомнить меня", то вы тем самым сохраняете Cookie с вашими данными на компьютере. При следующем посещении сайта браузер просто передаст серверу эти Cookie, не спрашивая вас.

Можете для интереса открыть папку (WinXP) C:\Documents and Settings\<Юзер>\Cookies и просмотреть содержимое от различных серверов, которые вы посещаете.

Применительно к нашим потребностям Cookie можно использовать для чтения своих страниц с форума MQL5. То есть читать данные таким образом, как будто вы зашли на сайт под своим логином и дальше анализировать полученные страницы. Но оптимальным будет анализ Cookie с помощью локального прокси Charles. Он очень детально покажет все отправляемые/принимаемые запросы и Cookie в том числе.

Например:

Для установки Cookie в запрос используется функция InternetSetCookie.

BOOL InternetSetCookie(
  __in  LPCTSTR lpszUrl,
  __in  LPCTSTR lpszCookieName,
  __in  LPCTSTR lpszCookieData
);

  • lpszUrl [in] - Имя сервера, например, www.mql5.com
  • lpszCookieName [in]- Имя куки
  • lpszCookieData [in] - Данные этого куки

Чтобы установить несколько Cookie, нужно просто вызвать эту функцию для каждого из них.

Интересная особенность: вызов InternetSetCookie может происходить в любой момент, даже когда вы не соединились с сервером.


Заключение

Итак, мы познакомились с еще одним типом HTTP-запросов, получили возможность отправлять бинарные файлы, что позволяет расширить возможности работы с вашими серверами, а также узнали про методы работы с Cookie.

В качестве направлений разработок можно обозначить:


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

  1. Прокси для просмотра отсылаемых заголовков http://www.charlesproxy.com/
  2. Описание WinHTTP http://msdn.microsoft.com/en-us/library/aa385331%28VS.85%29.aspx
  3. Описание HTTP Session http://msdn.microsoft.com/en-us/library/aa384322%28VS.85%29.aspx
  4. Комплект Денвер для локальной установки сервера Apache+PHP http://www.denwer.ru/
  5. Виды заголовков запросов http://www.codenet.ru/webmast/php/HTTP-POST.php#part_3_2
  6. Типы запросов http://www.w3.org/TR/REC-html40/interact/forms.html#form-content-type
  7. Типы запросов ftp://ftp.isi.edu/in-notes/iana/assignments/media-types/media-types.
  8. Структура использования HINTERNET http://msdn.microsoft.com/en-us/library/aa383766%28VS.85%29.aspx
  9. Работа с файлами http://msdn.microsoft.com/en-us/library/aa364232%28VS.85%29.aspx
  10. Типы данных для перевода в MQL http://msdn.microsoft.com/en-us/library/aa383751%28VS.85%29.aspx