在 MQL5 中使用 WinInet。第二部分:POST 请求和文件(基础篇)
「在 MT5 里用 WinInet 发 POST 和传文件」
MQL5 标准库没有直接封装 HTTP 客户端,想让 EA 或脚本跟外部接口打交道,得走 WinInet 这套 Windows API。本文系列第二部分专门拆 POST 请求和文件上传,区别于第一部分的 GET 拉取。 官方示例发布于 2013 年 10 月 16 日,至今在 MT5 终端里仍能编译运行,说明这套调用方式对底层 Windows 网络栈的依赖长期稳定。 实际做价格行为信号的外部推送时,POST 比 GET 更适合带 JSON 体或二进制附件;外汇与贵金属行情波动剧烈,用这套机制做跨平台告警前,先在模拟盘验证网络超时与断线重发逻辑。
从 GET 到 POST 要补的三件事
上一节已经跑通了 WinInet 的 GET 请求:打开会话、发 URL、收响应。这一节把客户端之间通信往前推一步,重点不在「读」而在「写」和「身份」。 首先要能发 POST 请求。GET 把参数挂在 URL 上,POST 把数据放进请求体,服务端脚本(比如 PHP 的 $_POST)才能按字段接住,适合传账号、密文或指令。 其次是用 multipart/form-data 把文件塞进请求体。这和纯文本表单不同,边界分隔符(boundary)要自己拼,否则服务端收不到二进制流。 最后是 Cookie 与登录态。很多站点不登录只返回游客页,带登录后写的 Cookie 再去请求,才能读到真实数据——外汇信号源、后台报价接口常是这个逻辑,杠杆品种波动剧烈,接口权限错配会直接拿不到行情。 调试环境上,本地代理 Charles 几乎必装。它能抓 MT5 发出的所有 HTTP 流量,看请求头、体、响应码,比盲调 dll 快一个数量级。
◍ 用 tagRequest 结构封装 POST 调用
在 MT5 里直接调 CMqlNet::Request 会很笨重——这个方法字段太多,散着传容易漏参。实际做法是先定义一个 tagRequest 结构,把动词、路径、报头、数据、输入输出方式一次性收进去,再用 Init 统一赋值。 结构里 stVerb 决定 GET 还是 POST;stObject 是服务端路径,比如 "/get.php?a=1";stHead 放报头,POST 场景常用 "Content-Type: application/x-www-form-urlencoded"。fromFile 为 true 时 stData 被当作文件名读字节,否则按字符串转 char 数组;toFile 同理控制 stOut 是变量还是落盘文件。 Request 方法开头就卡两道 DLL 权限:TerminalInfoInteger(TERMINAL_DLLS_ALLOWED) 和 MQL5InfoInteger(MQL5_DLLS_ALLOWED) 任一为假直接 Print 返回 false。若 toFile 为真但 stOut 为空也会拒掉。这意味着你在本机测之前,必须先在 MT5 选项里勾允许 DLL 导入,否则连会话都建不起来。 外汇与贵金属行情接口调用涉及网络与第三方服务,属于高风险操作,任何返回异常都可能让 EA 静默失效,建议先在模拟环境跑通再上真实账户。
class="kw">struct tagRequest { class="type">class="kw">string stVerb; class=class="str">"cmt">// GET/POST/…请求的方法 class="type">class="kw">string stObject; class=class="str">"cmt">// 请求实例的路径,例如:"/index.htm" 或 "/get.php?a=class="num">1" class="type">class="kw">string stHead; class=class="str">"cmt">// 请求的标题 class="type">class="kw">string stData; class=class="str">"cmt">// 附加数据字符串 class="type">bool fromFile; class=class="str">"cmt">// 如果为true,则stData代表一个数据文件的名称 class="type">class="kw">string stOut; class=class="str">"cmt">// 接收应答的字符串 class="type">bool toFile; class=class="str">"cmt">// 如果为true,则stOut代表一个接收应答的文件的名称 class="type">void Init(class="type">class="kw">string aVerb, class="type">class="kw">string aObject, class="type">class="kw">string aHead, class="type">class="kw">string aData, class="type">bool from, class="type">class="kw">string aOut, class="type">bool to); class=class="str">"cmt">// 初始化所有变量的函数 }; class="type">void tagRequest::Init(class="type">class="kw">string aVerb, class="type">class="kw">string aObject, class="type">class="kw">string aHead, class="type">class="kw">string aData, class="type">bool from, class="type">class="kw">string aOut, class="type">bool to) { stVerb=aVerb; class=class="str">"cmt">// GET/POST/…请求的方法 stObject=aObject; class=class="str">"cmt">// 页面路径:"/get.php?a=class="num">1" or "/index.htm" stHead=aHead; class=class="str">"cmt">// 请求的头部,例如:"Content-Type: application/x-www-form-urlencoded" stData=aData; class=class="str">"cmt">// 附加数据字符串 fromFile=from; class=class="str">"cmt">// 如果为true,则stData代表一个数据文件的名称 stOut=aOut; class=class="str">"cmt">// 接收应答的变量 toFile=to; class=class="str">"cmt">// 如果为true,则stOut代表一个接收应答的文件的名称 } class="type">bool MqlNet::Request(tagRequest &req) { if(!TerminalInfoInteger(TERMINAL_DLLS_ALLOWED)) { Print("-DLL not allowed"); class="kw">return(false); } if(!MQL5InfoInteger(MQL5_DLLS_ALLOWED)) { Print("-DLL not allowed"); class="kw">return(false); } if(req.toFile && req.stOut=="") { Print("-File not specified "); class="kw">return(false); } class="type">uchar data[]; class="type">int hRequest,hSend; class="type">class="kw">string Vers="HTTP/class="num">1.1"; class="type">class="kw">string nill=""; if(req.fromFile) { if(FileToArray(req.stData,data)<class="num">0) { Print("-Err reading file "+req.stData); class="kw">return(false); } } else StringToCharArray(req.stData,data); if(hSession<=class="num">0 || hConnect<=class="num">0) { Close();
「请求句柄的打开与释放路径」
这段逻辑紧接前面的连接动作,先判断是否成功连上远程主机。若 Open 返回 false,立即打印 '-Err Connect' 并调用 Close 释放资源后退出,避免空句柄继续往下跑。 连接通过后用 HttpOpenRequestW 创建请求描述符,标志位叠加了 INTERNET_FLAG_KEEP_CONNECTION、INTERNET_FLAG_RELOAD 与 INTERNET_FLAG_PRAGMA_NOCACHE,倾向保持长连接并强制服务端不读缓存。若 hRequest<=0,打印 '-Err OpenRequest' 并关掉 hConnect 后返回 false。 随后 HttpSendRequestW 把头部与数据体发出去,发送失败(hSend<=0)时通过 GetLastError 取错误码并打印 '-Err SendRequest= '。只有 hSend>0 才进 ReadPage 读取返回内容,最后无论成败都显式 InternetCloseHandle 释放 hRequest 与 hSend,发送失败的支路还会再调一次 Close 并返回 false,发送成功则返回 true。 在 MT5 里跑这段,重点看'-Err SendRequest'后面的错误码——不同券商网关对 INTERNET_FLAG_RELOAD 的兼容可能有差异,外汇与贵金属接口调用本身属高风险操作,参数组合建议先在模拟环境验证。
if(!Open(Host,Port,User,Pass,Service)) { Print("-Err Connect"); Close(); class="kw">return(false); } class=class="str">"cmt">//--- 创建请求描述符 hRequest=HttpOpenRequestW(hConnect,req.stVerb,req.stObject,Vers,nill,class="num">0, INTERNET_FLAG_KEEP_CONNECTION|INTERNET_FLAG_RELOAD|INTERNET_FLAG_PRAGMA_NOCACHE,class="num">0); if(hRequest<=class="num">0) { Print("-Err OpenRequest"); InternetCloseHandle(hConnect); class="kw">return(false); } class=class="str">"cmt">//--- 发送请求 hSend=HttpSendRequestW(hRequest,req.stHead,StringLen(req.stHead),data,ArraySize(data)); class=class="str">"cmt">//--- 发送文件 if(hSend<=class="num">0) { class="type">int err=class="num">0; err=GetLastError(err); Print("-Err SendRequest= ",err); } class=class="str">"cmt">//--- 读取页面 if(hSend>class="num">0) ReadPage(hRequest,req.stOut,req.toFile); class=class="str">"cmt">//--- 释放所有句柄 InternetCloseHandle(hRequest); InternetCloseHandle(hSend); if(hSend<=class="num">0) { Close(); class="kw">return(false); } class="kw">return(true); }