MetaTrader 5使用了一系列新的用户界面元素,为用户开辟了独一无二的机会。因此,那些之前不具备的功能现在能被最大限度地使用了。
在本课中我们将学习:
- 使用基本的因特网技术;
- 通过服务器在终端间交换数据;
- 创建一个通用类库,在MQL5环境下操作因特网。
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); }
初始化完成后,描述符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的参数:
- string Verb – 请求的类型“GET”或者“POST”;
- string Object – 带有传入参数的页面的名称;
- string &Out – 接收应答的行字符串;
- bool toFile – 如果toFile=true,那么Out表示接受应答的文件名;
- string addData – 附加数据;
- bool fromFile – 如果fromFile = true,那么addData是需要发送的文件的名称。
读取已接收描述符的内容
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的参数:
- Int hRequest – 从中读取数据的请求描述符;
- string &Out – 接收应答的行字符串;
- bool toFile – 如果toFile=true,那么Out表示接受应答的文件的名称。
将所有这些集中到一起,我们将获得一个操作因特网的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脚本的任务是:
- 分析URL并将其分解为主机,请求和文件名;
- 发送一个请求到主机,接受和保存文件到终端文件夹 /Files下;
- 将其从Files下移动到所需的数据文件夹下:
/Experts, /Indicators, /Scripts, /Include, /Libraries, /Tester(set), /Templates.
我们用MqlNet类来解决第二个问题。我们用Kernel32.dll中的MoveFileEx来完成第三个任务
#import "Kernel32.dll" bool MoveFileExW(string &lpExistingFileName, string &lpNewFileName, int dwFlags); #import "Kernel32.dll"
对于第一个问题,让我们做一个简单的服务函数来解析URL链接。
我们要从地址中分解出三行信息:主机,站点路径以及文件名。
例如,在https://www.mysite.com/folder/page.html中
– 主机 = www.mysite.com
– 请求 = / folder / page.html
– 文件名 = page.html
MQL5网站代码库的地址也有一样的结构。例如,https://www.mql5.com/ru/code/79 页面上 ErrorDescription.mq5库的路径看上去如: https://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 https://"+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脚本结合的目的是:
- 发送一个EA的请求到服务器;
- 在服务器上形成响应页面(PHP);
- 通过此EA接收这个页面;
- 分析并将结果发送到屏幕。
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系统的目的是:
- 发送一个文本行到服务器;
- 将这行添加到共享文件中,控制文件的大小,发布到响应文件(php)上;
- 接收当前对话并且在屏幕上显示。
MetaChat的功能和之前的EA交易系统没什么两样。同样的原理,同样简单的CSV输出文件。
MetaChat 和 MetaArbitrage 在其开发者的网站上。运行他们的PHP脚本也在那里。
因此,如果你想测试或使用这项服务,你可以通过下面的链接访问它:
MetaСhat – www.fxmaster.de/metachat.php
MetaArbitrage – www.fxmaster.de/metaarbitr.php
总结
至此,我们已经熟悉了HTTP请求。我们获得了通过网络发送和接收数据的能力,并将其工作过程组织的更加舒适了。但是任何功能总是存在改进的余地。以下是可以考虑的新的潜在改进方向:
- 直接将新闻读到终端中或者在终端中接收其他信息,来给EA交易系统进行分析;
- 远程控制EA交易系统;
- 自动更新EA交易系统/指标;
- 复制/翻译交易,发送信号;
- 为EA交易系统下载模板和配置文件;
- 以及其他很多很多 …
本文中我们使用GET类型的请求。当你使用少数参数获取一个文件或者发送一个请求来分析服务器时,他们足够胜任这些任务了。
在下一课中,我们将仔细看看POST请求,发送文件到服务器或终端之间共享文件,我们会介绍他们的用例。
有用的资源
- 用于安装Apache服务器+ PHP的Denver Set https://www.denwer.ru/
- 用来浏览已发送标题的代理https://www.charlesproxy.com/
- 标题请求的类型 https://www.codenet.ru/webmast/php/HTTP-POST.php#part_3_2
- 查询类型 https://www.w3.org/TR/REC-html40/interact/forms.html#form-content-type
- 请求类型 ftp://ftp.isi.edu/in-notes/iana/assignments/media-types/media-types
- 关于WinHTTP的描述 https://msdn.microsoft.com/en-us/library/aa385331%28VS.85%29.aspx
- 关于HTTP 会话的描述 https://msdn.microsoft.com/en-us/library/aa384322%28VS.85%29.aspx
- HINTERNET结构体的使用方法 https://msdn.microsoft.com/en-us/library/aa383766%28VS.85%29.aspx
- 操作文件 https://msdn.microsoft.com/en-us/library/aa364232%28VS.85%29.aspx
- 转换到MQL的数据类型 https://msdn.microsoft.com/en-us/library/aa383751%28VS.85%29.aspx
本文译自 MetaQuotes Software Corp. 撰写的俄文原文
原文地址: https://www.mql5.com/ru/articles/73
(15.84 KB)
(17.93 KB)
(11.23 KB)
(6.35 KB)
(0.81 KB)
(12.81 KB)
MyFxtop迈投-靠谱的外汇跟单社区,免费跟随高手做交易!
免责声明:本文系转载自网络,如有侵犯,请联系我们立即删除,另:本文仅代表作者个人观点,与迈投财经无关。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
著作权归作者所有。
商业转载请联系作者获得授权,非商业转载请注明出处。