在前一课“使用 WinInet.dll 通过互联网在客户端之间交换数据”一课中,我们已经学习了如何使用库、打开网页、使用 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" 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
);
我们可以从函数的说明理解数据是作为 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,以让您感受它们之间的差异。
让我们逐一说明函数的变量:
要点 - 注意在 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/gif 和 application/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 旨在避免服务器连续请求个人资料。一旦服务器从用户收到当前工作会话所需的个人资料,它将在用户的计算机上留下一个含有该信息的文本文件。此外,当用户在页面之间移动时,服务器不再向用户请求该信息;它自动从浏览器缓存取用信息。
例如,当您在 www.mql5.com 服务器上授权时启用了“记住我”选项时,您将含有您的个人资料的 Cookie 保存到您的计算机。下一次访问该网站时,浏览器会在不询问您的情况下将该 Cookie 传递给服务器。
如果您有兴趣,您可以打开文件夹 (WinXP) C:\Documents and Settings\<User>\Cookies 并查看您访问过的不同网站的内容。
为了满足我们的需要,可以使用 Cookie 来读取 MQL5 论坛的网页。换言之,您将如您在登录网站时通过身份验证一样读取信息,然后,您将分析获得的页面。使用本地代理服务器 Charles 来分析 Cookie 是最理想的情形。它显示有关收到的/已发送的所有请求的详细信息,包括 Cookie。
例如:
要在请求中设定一个 Cookie,使用 InternetSetCookie 函数。
BOOL InternetSetCookie(
__in LPCTSTR lpszUrl,
__in LPCTSTR lpszCookieName,
__in LPCTSTR lpszCookieData
);
要设置几个 Cookie,为每个 Cookie 调用此函数。
一个有趣的功能:可以在任何时候调用 InternetSetCookie ,甚至在您没有连接到服务器时。
我们已经了解了另一类型的 HTTP 请求,又获得了发送二进制文件的可能,这允许扩大使用服务器的工具;我们也学习了使用 Cookie 的方法。
我们可以按下列进一步发展的方向前进:
本社区仅针对特定人员开放
查看需注册登录并通过风险意识测评
5秒后跳转登录页面...
移动端课程