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

--- | 16 十月, 2013

MetaTrader 5使用了一系列新的用户界面元素,为用户开辟了独一无二的机会。因此,那些之前不具备的功能现在能被最大限度地使用了。

在本课中我们将学习:

MQL5的代码库有一个脚本例子,它使用wininet.dll动态链接库,并列举了一个请求服务器页面的例子。不过今天我们将更进一步,使服务器不仅仅返回给我们页面,还要发送和存储这些数据,以便再次传输到其他发出请求终端上去。

注意:对于那些通过PHP配置,而没有连接到服务器的用户,我们建议下载 Denwer工具箱,使用它来作为工作平台。并且我们建议你在本地测试时使用Apache服务器和PHP。

要向服务器发送任何请求,我们需要库中的7个主要函数。

InternetAttemptConnect  试图找到并建立一个因特网连接
InternetOpen
初始化结构体来操作WinInet库函数。在使用库中的其他函数前必须先激活此函数。
InternetConnect 打开由HTTP URL或FTP地址确定的资源。返回打开的连接的描述符
HttpOpenRequest 为建立连接的HTTP请求创建描述符
HttpSendRequest 使用创建的描述符发送请求
InternetReadFile 当请求被处理后,读取从服务器返回的数据
InternetCloseHandle 释放已经传输完成的描述符


所有函数以及它们参数的详细描述可以在MSDN的帮助系统中找到。

除了使用统一码调用以及通过链接进行传输外,函数的声明和MQL4中的一样。

#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函数中),一个会话仅能创建一次。或者可以在EA交易系统运行的一开始进行,但非常重要的是,必须确保会话在关闭之前仅仅被成功创建了一次。每次迭代执行 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);
  }

初始化完成后,描述符SessionConnect就能被用在下面所有的函数中了。一但所有的工作都完成且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类来解决第二个问题。我们用Kernel32.dll中的MoveFileEx来完成第三个任务

#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

MQL5网站代码库的地址也有一样的结构。例如,https://www.mql5.com/ru/code/79 页面上 ErrorDescription.mq5库的路径看上去如: http://p.mql5.com/data/18/79/ErrorDescription.mqh。此路径很容易通过点击右键并选择“Copy Link(复制链接)”来获得。因此,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";

   // 解析url
   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/en/code上进行试验。下载下来的文件会立即出现在编辑器的文件导航中,并且不需要重启终端或编辑器,它们就能够被编译。无需为了移动这些文件,而在文件系统的冗长路径中漫步查找想要的文件夹。

注意!很多网站都有防止内容被大规模下载的安全机制,如果你的IP地址有这类大规模下载的动作,则很可能被封。因此,如果你不想被禁用的话,必须非常小心的在你经常连接的资源上使用“机器”自动下载文件。

那些想更进一步改进上述功能的读者,可以使用 Clipboard脚本来截取剪贴板的内容,并进一步实施自动化下载。

例子 2. 在一个图表上监视多个经纪商的报价

我们已经学习了如何从互联网上获取文件。现在让我们考虑一个更为有意思的问题 - 如何发送和存储这些数据到服务器上。我们需要一个额外的放置在服务器上的小PHP脚本程序。使用已经编写好的MqlNet类,我们创建一个EA交易程序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文件并且逐行进行解析,以表格的形式在屏幕上输出价格值。

实现对这个文件的处理有多种不同的方式,为每一个特定的情景选择一种方法。例如,对从MetaTrader4模拟服务器接收到的报价进行过滤,等等。


使用因特网服务器的好处是显而易见的,你发送你自己的报价,它可以被其他交易者接收和浏览。同样,你也将可以接收到发送给其他交易者的报价。也就是说,终端之间的交互是双边的,下面是实现数据交换的方案:


该方案是任意数量终端之间进行信息交换的基本方式。完整且有注释的MetaArbitrage专家交易系统和PHP脚本可以从附件的链接中下载。更多关于PHP使用到的函数,可以到这个站点php.su阅读。

例子 3. 在终端内交换信息(mini图表)MetaChat Expert Advisor

让我们暂别交易和数字,创建一个应用程序,让我们能够和几个人聊天,而不必退出终端。为了实现这一点,我们需要更多的和之前类似的PHP脚本。不同的是在这个脚本中,我们将分析文件中的行,而不是分析时间报价。这个EA系统的目的是:

MetaChat的功能和之前的EA交易系统没什么两样。同样的原理,同样简单的CSV输出文件。


MetaChat 和 MetaArbitrage 在其开发者的网站上。运行他们的PHP脚本也在那里。
因此,如果你想测试或使用这项服务,你可以通过下面的链接访问它:
MetaСhat - www.fxmaster.de/metachat.php
MetaArbitrage - www.fxmaster.de/metaarbitr.php

总结

至此,我们已经熟悉了HTTP请求。我们获得了通过网络发送和接收数据的能力,并将其工作过程组织的更加舒适了。但是任何功能总是存在改进的余地。以下是可以考虑的新的潜在改进方向:

本文中我们使用GET类型的请求。当你使用少数参数获取一个文件或者发送一个请求来分析服务器时,他们足够胜任这些任务了。

在下一课中,我们将仔细看看POST请求,发送文件到服务器或终端之间共享文件,我们会介绍他们的用例。

有用的资源