Использование WinInet в MQL5. Часть 2: POST-запросы и файлы
Введение
В прошлом уроке "Использование WinInet.dll для обмена данными между терминалами через Интернет" мы разобрались с работой библиотеки, научились открывать web-страницы, отправлять и принимать данные по GET-запросу.
В этом уроке мы научимся:
- создавать и отправлять простые POST запросы на сервер
- отправлять файлы на сервер методом представления multipart/form-data
- работать с Cookie, читать информацию с сайтов под своим логином.
Как и в прошлый раз, настоятельно рекомендуем поставить локальный прокси 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, чтобы вы могли, как говорится, "прочувствовать" их разницу.
Опишем по порядку переменные функции:
- Head - это заголовок запроса с информацией про тип содержимого. На самом деле это не весь заголовок, который находится в запросе. Остальные поля заголовка создает сама библиотека wininet.dll. Но и их можно модифицировать с помощью функции HttpAddRequestHeaders.
- Path - это путь к объекту запроса относительно начального домена www.fxmaster.de, по-простому говоря, путь к php-скрипту, который обработает запрос. Кстати, не обязательно запрашивать только php-срипт, это может быть и обычная html-страница (в первом уроке мы запрашивали даже mq5-файл).
- Data - это конкретно данные, которые передаются на сервер. Запись данных происходит согласно правилам передачи имя параметра=значение. Знак "&" является разделителем данных.
И самое главное: обратите внимание на отличия между заданием 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 в том числе.
Например:
- Написать эксперт (или внешнее приложение), который раз в час обращается на страницу https://www.mql5.com/ru/job и получать список новых предложений работы.
- Обращаться на интересующую ветку, например https://www.mql5.com/ru/forum/23, и проверять, нет ли в ней новых сообщений.
- Также можно проверять наличие новых "личных сообщений" на форумах.
Для установки 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.
В качестве направлений разработок можно обозначить:
- Организация удаленного хранилища отчетов;
- Обмен файлами между пользователями, обновление версий экспертов/индикаторов;
- Создание собственных сканеров сайтов под вашим логином, для мониторинга активности.
Полезные ресурсы
- Прокси для просмотра отсылаемых заголовков http://www.charlesproxy.com/
- Описание 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
- Комплект Денвер для локальной установки сервера Apache+PHP http://www.denwer.ru/
- Виды заголовков запросов 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.
- Структура использования 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+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
В тестере вылетает вот здесь:
С ошибкой:
На реальных данных всё работает.
Вроде помогло
Нет, не помогло.
Дурдом... после компиляции тестирование работает. При отладке в режиме тестирования не работает. А потом и просто при тестировании не работает.
После запуска в дебаг-режиме ex-файл остается от этого режима. Видимо, будет работать только релизная версия.