在 MQL5 中使用 WinInet。第二部分:POST 请求和文件(基础篇)
📘

在 MQL5 中使用 WinInet。第二部分:POST 请求和文件(基础篇)

第 1/2 篇

「在 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 静默失效,建议先在模拟环境跑通再上真实账户。

MQL5 / C++
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 的兼容可能有差异,外汇与贵金属接口调用本身属高风险操作,参数组合建议先在模拟环境验证。

MQL5 / C++
   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);
   }

常见问题

从GET切到POST要补三件事:设Content-Type、用正确的请求体格式、显式调用发送函数。先核对这三处再排错。
每次打开请求句柄后必须走完打开-发送-读取-关闭的路径,最后调用释放函数回收句柄,否则会资源泄漏。
小布可以替你监控EA的对外请求异常并提示句柄泄漏风险,把重复劳动交给小布,你专注决策。
封装后能统一管理和复用请求参数,坑主要在结构体初始化不全导致字段为空,发前务必清零并赋值。
可在发送后读取返回状态码和响应体长度,非零长度且状态码2xx即大概率成功,注意外汇贵金属高风险。