下载MetaTrader 5

在 MQL5 中使用 WinInet。第二部分:POST 请求和文件

16 十月 2013, 07:30
o_o
0
1 106

简介

在前一课“使用 WinInet.dll 通过互联网在客户端之间交换数据”一课中,我们已经学习了如何使用库、打开网页、使用 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" or "/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 例子(报价监控)。

让我们回想一下,EA 使用 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 - 这是要发送到服务器的信息。依据参数名称=值的解析规则编写数据。"&" 符号用作数据分隔符。

要点 - 注意在 tagRequest::Init 中进行 GET 请求和 POST 请求之间的区别。

在 GET 方法中,路径是与请求主体一起发送的(用 "?" 符号)统一起来,数据字段 lpOptional(在结构中名为 stData)是空的。
在 POST 方法中
,自身含有路径,并且请求主体移到 lpOptional

如您所见,差异并不明显。本文附带了接收请求的服务器脚本 metaswap.php


发送 "multipart/form-data" 数据

实际上,POST 请求并不是 GET 请求的模拟(否则就不需要它们)。POST 请求有显著的优势 - 使用它们,您可以发送含有二进制内容的文件。

问题在于允许 URL 加密类型的请求发送有限的交易品种。否则,将用代码代替“不允许”的交易品种。因此,当发送二进制数据时,它们会失真。这样,你甚至不能使用 GET 请求发送一个很小的 gif 图。

为了解决此问题,制定了用于描述请求的特殊规则;除了文本文件以外,它们还允许交换二进制文件。

为实现此目的,请求的主体被分为若干部分。主要原因是每一部分能够有其自己的数据类型。例如,第一部分是文本,下一部分是 image/jpeg 等。换言之,发送到服务器的一个请求可以同时包含几种数据类型。

让我们通过 MetaSwap 脚本传递的数据的例子看一看此类说明的结构。

请求的报头 Head 将看起来如下所示:

Content-Type:multipart/form-data; boundary=SEPARATOR\r\n


关键字 SEPARATOR 是一组随机符号。然而,您不应将其视为请求数据。换言之,此行应该是唯一的 - 某些诸如 hdsJK263shxaDFHLsdhsDdjf9 的咒语或者您想起的任何其他东西。:)在 PHP 中,使用当前时间的 MD5 代码形成此类代码行。

POST 请求本身看起来如下所示(为了便于理解,依据一般意义突出显示字段):

\r\n
--SEPARATOR\r\n

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

\r\n
--SEPARATOR\r\n

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

\r\n
--SEPARATOR\r\n

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

\r\n
--SEPARATOR\r\n

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

\r\n
--SEPARATOR--\r\n


我们显式指定换行符 "\r\n" 的位置,因为它在请求中是必须的符号。如您所见,在请求中传递了相同的四个字段,并且通过普通的文本方式进行。

放置分隔符的重要特性:

  • 在分隔符之前放置了两个符号 "--"。
  • 为了关闭分隔符,在其后放置了另外两个符号 "--"。


在下一个例子中,您可以看到在请求中传递文件的正确方法。

想象一个 EA 交易程序在平仓时制作图表快照,并用一个文本文件创建帐户的详细报告。

\r\n
--SEPARATOR\r\n

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

\r\n
--SEPARATOR\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
......content of the gif file.....

\r\n
--SEPARATOR\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
......content of the csv file.....

\r\n
--SEPARATOR--\r\n


在请求中出现了两个新的报头:

Content-Type - 说明内容的类型。所有可能类型实际上都是按 RFC[2046] 标准准确说明的。我们使用了两个类型 - image/gifapplication/octet-stream

编写 Content-Disposition 的两种情形 - file(文件) 和 form-data(表单数据)是相同的,在两种情况下都会被 PHP 正确处理。因此,您可以使用文件或表单数据,这由您决定。您可以在 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х600像素的截屏
  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); // 根据文件大小创建一个字符数组
  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"); //EA名称区
  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");
} 
接收信息的服务器脚本:
$ea=$_POST['EA'];
$data=file_get_contents($_FILES['data']['tmp_name']); // 文件中的信息
$file=$_FILES['data']['name'];
$h=fopen(dirname(__FILE__)."/$ea/$file", 'wb'); // 在EA文件夹下创建一个文件
fwrite($h, $data); fclose($h); // 保存数据
?>

强烈建议熟悉服务器接收文件的规则以避免安全问题!


使用 Cookie

作为上一课的补充以及深入思考其功能的题材简短地说明本主题。

如您所知,Cookie 旨在避免服务器连续请求个人资料。一旦服务器从用户收到当前工作会话所需的个人资料,它将在用户的计算机上留下一个含有该信息的文本文件。此外,当用户在页面之间移动时,服务器不再向用户请求该信息;它自动从浏览器缓存取用信息。

例如,当您在 www.mql5.com 服务器上授权时启用了“记住我”选项时,您将含有您的个人资料的 Cookie 保存到您的计算机。下一次访问该网站时,浏览器会在不询问您的情况下将该 Cookie 传递给服务器。

如果您有兴趣,您可以打开文件夹 (WinXP) C:\Documents and Settings\<User>\Cookies 并查看您访问过的不同网站的内容。

为了满足我们的需要,可以使用 Cookie 来读取 MQL5 论坛的网页。换言之,您将如您在登录网站时通过身份验证一样读取信息,然后,您将分析获得的页面。使用本地代理服务器 Charles 来分析 Cookie 是最理想的情形。它显示有关收到的/已发送的所有请求的详细信息,包括 Cookie。

例如:

  • 一个每小时请求 https://www.mql5.com/zh/job 网页一次并接收新工作机会列表的 EA(或一个外部应用程序)。
  • 它还请求一个分支,例如 https://www.mql5.com/en/forum/53,并检查是否有新的消息。
  • 此外,它还检查论坛上是否有新的“私人消息”。

要在请求中设定一个 Cookie,使用 InternetSetCookie 函数。

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

  • lpszUrl [in] - 服务器的名称,例如 www.mql5.com
  • lpszCookieName [in]- Cookie 的名称
  • lpszCookieData [in] - Cookie 的数据

要设置几个 Cookie,为每个 Cookie 调用此函数。

一个有趣的功能:可以在任何时候调用 InternetSetCookie ,甚至在您没有连接到服务器时。


总结

我们已经了解了另一类型的 HTTP 请求,又获得了发送二进制文件的可能,这允许扩大使用服务器的工具;我们也学习了使用 Cookie 的方法。

我们可以按下列进一步发展的方向前进:

  • 组织报告的远程存储;
  • 在用户之间交换文件、更新 EA 交易程序/指标的版本;
  • 在您的帐户下创建自定义扫描程序并监控网站的活动。


有用的链接

  1. 用于查看发送的报头的代理服务器 - http://www.charlesproxy.com/
  2. WinHTTP 说明 - http://msdn.microsoft.com/en-us/library/aa385331%28VS.85%29.aspx
  3. HTTP 会话说明 - http://msdn.microsoft.com/en-us/library/aa384322%28VS.85%29.aspx
  4. 针对 Apache+PHP 的本地安装的 Denwer 工具包 - 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


本文译自 MetaQuotes Software Corp. 撰写的俄文原文
原文地址: https://www.mql5.com/ru/articles/276

附加的文件 |
metaswap.zip (0.66 KB)
screenpost.zip (0.33 KB)
internetlib.mqh (23 KB)
metaswap.mq5 (9 KB)
screenpost.mq5 (4.66 KB)
MetaTrader 5 中进行测试的原理 MetaTrader 5 中进行测试的原理

MetaTrader 5 中三种测试模式有何区别?应该特别注意什么?如何测试在几个工具上同时进行交易的 EA?在测试期间何时及如何计算指标值?如何处理事件?如何在测试期间以一种仅开盘价模式同步处理来自不同工具的指标柱?本文旨在回答这些问题以及很多其他问题。

使用WinInet.dll通过网络在终端间进行数据交互 使用WinInet.dll通过网络在终端间进行数据交互

本文描述了通过HTTP请求来操作网络,以及使用一个中间服务器进行终端间数据交互的方法。引入一个MqlNet类库,在MQL5环境中操作因特网上的资源。监视不同经纪商的报价,在终端内和其他交易者进行信息交流,在互联网上查找信息——这些是本文将介绍的一些例子。

MQL5 Cookbook: 使用不同的打印模式 MQL5 Cookbook: 使用不同的打印模式

这是 MQL5 Cookbook 系列的第一篇文章,我将会从简单的实例开始,让那些刚刚开始编程的人逐渐熟悉这门新语言。我还记得我开始设计和编写交易系统时的尝试,可以说是非常困难,事实上那是我所学的第一门编程语言,然而,后来还是比我想象的简单一些,我只用了几个月的时间就能够开发相对复杂的程序了。

MQL5 Cookbook: 获取仓位属性 MQL5 Cookbook: 获取仓位属性

在本文中,我们将创建一个脚本来获得所有的仓位属性,并用对话框向用户显示它们。通过运行这个脚本,您可以从外部参数下拉列表的两种模式中选择:只看当前交易品种的仓位属性,或者查看所有交易品种的属性。