自己动手开发多线程异步 MQL5 WebRequest(基础篇)
📘

自己动手开发多线程异步 MQL5 WebRequest(基础篇)

第 1/3 篇

「同步 WebRequest 如何拖垮 EA 事件流」

MQL5 自带的 WebRequest 是同步阻塞调用,EA 在终端里只占一条专属线程,所有分时、深度、定时器、交易与图表事件都排在同一队列里。只要 WebRequest 没返回,这条线程就卡在调用处,后续事件只能排队等。 典型代价是丢信号:一个需要实时处理分时的 EA,若中途去抓外部经济新闻,WebRequest 执行期间新来的分时事件会被跳过。虽可用 CopyTicks 事后补读,但决策时点已经错过,行情不会等你。 解决思路是把 HTTP 请求挪到另一条线程——最简单就是在 MT5 里再挂一个辅助 EA,由它跑 WebRequest 并把结果回传,主 EA 线程始终保持空闲可响应。这样主 EA 处理分时的延迟倾向降到最低,外部数据获取变成后台异步动作。 外汇与贵金属市场高波动、高杠杆,异步改造只解决技术阻塞,不消除交易本身风险,参数与站点稳定性仍需实盘验证。

用管理器统管助手EA的通信骨架

在 MT5 里每个 EA 都独占一张图表,想靠人工开一堆辅助 EA 来处理并发请求基本不现实。更合理的做法是写一个「管理器 EA」,它自己先跑起来,再开若干张子图表把自身副本当助手载入,统一调度资源与请求。第一个实例按优先级规则自认成管理器,其余副本识别到协议后降级为助手,全程不需要交易者手动去拖。 客户端、管理器、助手三者之间并不能用普通函数互调。MT5 提供的是基于 ChartID 的自定义事件系统,而事件参数空间极小——字符串类型字段上限 63 个字符,连 HTTP 方法和 URL 都经常塞不下。所以真实做法是:把请求参数写进「自定义资源」(本质是动态生成的图片二进制容器),事件里只传资源句柄链接。社区里 fxsaber 的 Resource.mqh / ResourceData.mqh 已经封装好读写,直接当黑盒用即可。 异步 Web 请求的流转顺序是:客户把参数封进本地资源并发事件给管理器;管理器从池里挑空闲助手、转交资源链接并标记占用;助手解包后调阻塞式 WebRequest 等回包,再把结果写回资源并发事件;管理器转发给客户,助手复位空闲。第 5、6 步助手其实可以直连客户窗口绕过管理器,能再省一层延迟。 管理器只需人工加载一次,终端重启会随图表自动恢复,全终端只允许一个。它按设置数量开助手窗口,助手关掉会发事件通知管理器维护列表;管理器退出也会通知助手自停。客户程序载入时要先探测管理器图表 ID 是否存在,握手拿到窗口 ID 才能开始通信。外汇与贵金属品种波动剧烈、滑点频繁,这类多实例架构在实盘前务必用模拟环境跑通事件链路。

MQL5 / C++
class="type">int WebRequest( 
  class="kw">const class="type">class="kw">string      method,        class=class="str">"cmt">// HTTP 方法 
  class="kw">const class="type">class="kw">string      url,           class=class="str">"cmt">// url 地址 
  class="kw">const class="type">class="kw">string      headers,       class=class="str">"cmt">// 请求头部  
  class="type">int               timeout,       class=class="str">"cmt">// 超时 
  class="kw">const class="type">char       &data[],        class=class="str">"cmt">// HTTP 消息体数组 
  class="type">char             &result[],      class=class="str">"cmt">// 服务器回复数据的数组 
  class="type">class="kw">string           &result_headers class=class="str">"cmt">// 服务器回复的头部 
);
class="type">int WebRequestAsync( 
  class="kw">const class="type">class="kw">string      method,        class=class="str">"cmt">// HTTP 方法 
  class="kw">const class="type">class="kw">string      url,           class=class="str">"cmt">// url 地址 
  class="kw">const class="type">class="kw">string      headers,       class=class="str">"cmt">// 请求头部  
  class="type">int               timeout,       class=class="str">"cmt">// 超时 
  class="kw">const class="type">char       &data[]         class=class="str">"cmt">// HTTP 消息体数组 
);
class="type">int WebRequestAsyncResult( 
  class="type">char             &result[],      class=class="str">"cmt">// 服务器回复数据的数组 
  class="type">class="kw">string           &result_headers class=class="str">"cmt">// 服务器回复的头部 
);

◍ 用 multiweb.mqh 统一声明通信类

在 MT5 里做多端通信,先别急着写逻辑,第一步建议抽一个 multiweb.mqh 头文件。把客户端和「服务器」两端要用的类都在这个头里声明,能明显压住后期维护成本。 头文件里有一部分类是两端通用的,比如底层连接、消息结构定义;另一部分则是继承通用类后,分别针对客户端角色或「服务器」角色做特化。这样编译时两边引用同一份声明,参数和接口不容易漂移。 实际开 MT5 验证时,可在 include 目录建 multiweb.mqh,先只放空类声明看能否被 Expert 与脚本同时 include 通过,再逐步补实现。外汇与贵金属 EA 涉及远端交互,实盘前务必在模拟盘跑通,这类通信模块出错概率不低。

「WebWorker 基类怎么管资源和请求状态」

做多实例并发 web 请求时,先得有个基类把图表 ID、忙闲标记和资源指针兜住。派生出的对象在管理器里跟踪助手状态,在客户端里主要存「跨连接传过来的资源」,而且一个客户端常开好几个实例同时发请求,所以 busy 标记几乎是必查项。 基类里 chartID 是 MQL 程序所在图表 ID,busy 表示实例是否正处理 web 请求,resource 指向 RESOURCEDATA<uchar> 类型的随机存储,prefix 是每个状态唯一前缀、拼在资源名前。助手 EA 默认用 "WRR_" 前缀,自己写客户端建议照这规矩改独特前缀,免得资源串台。 allocate 在派生类里调,在 resource 中建 RESOURCEDATA<uchar> 对象并用 chartID 命名;release 负责释放。getFullName 返回带 MQL 程序路径和目录的完整资源名(只读),比如 multiweb.ex5 放 MQL5\Experts、图表 ID 129912254742671346 时,全名就是 "\Experts\multiweb.ex5::WRR_129912254742671346",这串直接塞自定义事件 sparam 里当资源连接用。 broadcastEvent 是静态方法,能向所有窗口发消息,后续用来找管理器。下面代码里 WebWorker 的构造默认前缀 "WRP_",getFullName 用 MQL_PROGRAM_PATH 截掉终端路径再拼 prefix 和 chartID;broadcastEvent 从 ChartFirst 遍历到 ChartNext,跳过自己图表用 EventChartCustom 广播。 外汇和贵金属品种波动大、杠杆高,这类异步架构只解决通讯骨架,不替你过滤行情风险,上真实账户前先用策略测试器跑通多实例逻辑。

MQL5 / C++
class WebWorker
{
  class="kw">protected:
    class="type">long chartID;
    class="type">bool busy;
    class="kw">const RESOURCEDATA<class="type">uchar> *resource;
    class="kw">const class="type">class="kw">string prefix;
    
    class="kw">const RESOURCEDATA<class="type">uchar> *allocate()
    {
      release();
      resource = new RESOURCEDATA<class="type">uchar>(prefix + (class="type">class="kw">string)chartID);
      class="kw">return resource;
    }
    
  class="kw">public:
    WebWorker(class="kw">const class="type">long id, class="kw">const class="type">class="kw">string p = "WRP_"): chartID(id), busy(class="kw">false), resource(NULL), prefix("::" + p)
    {
    }
    ~WebWorker()
    {
      release();
    }
    
    class="type">long getChartID() class="kw">const
    {
      class="kw">return chartID;
    }
    
    class="type">bool isBusy() class="kw">const
    {
      class="kw">return busy;
    }
    
    class="type">class="kw">string getFullName() class="kw">const
    {
      class="kw">return StringSubstr(MQLInfoString(MQL_PROGRAM_PATH), StringLen(TerminalInfoString(TERMINAL_PATH)) + class="num">5) + prefix + (class="type">class="kw">string)chartID;
    }
    
    class="kw">virtual class="type">void release()
    {
      busy = class="kw">false;
      if(CheckPointer(resource) == POINTER_DYNAMIC) class="kw">delete resource;
      resource = NULL;
    }
    class="kw">static class="type">void broadcastEvent(class="type">class="kw">ushort msg, class="type">long lparam = class="num">0, class="type">class="kw">double dparam = class="num">0.0, class="type">class="kw">string sparam = NULL)
    {
      class="type">long currChart = ChartFirst();
      class="kw">while(currChart != -class="num">1)
      {
        if(currChart != ChartID())
        {
          EventChartCustom(currChart, msg, lparam, dparam, sparam);
        }
        currChart = ChartNext(currChart);
      }
    }
};
class ClientWebWorker : class="kw">public WebWorker
{

给 WebWorker 套一层 HTTP 请求壳

在 MT5 里做 AIGC 行情分析,常需要 EA 向外部接口发请求。直接裸用 WebWorker 不够直观,包一层 ClientWebWorker 能把 method 和 url 存成成员变量,后面排错时一眼能看出发了什么。 下面这段 C++ 风格的 MQL5 类定义,就是给基类 WebWorker 加了一个轻量封装:protected 区放了 _method 和 _url 两个字符串,public 区提供 getMethod()、getURL() 两个只读接口,以及真正发请求的 request() 和收结果的静态函数 receiveResult()。 [CODE] protected: string _method; string _url; public: ClientWebWorker(const long id, const string p = "WRP_"): WebWorker(id, p) { } string getMethod() const { return _method; } string getURL() const { return _url; } bool request(const string method, const string url, const string headers, const int timeout, const uchar &body[], const long managerChartID) { _method = method; _url = url; // allocate()? and what's next? ... } static void receiveResult(const string resname, uchar &initiator[], uchar &headers[], uchar &text[]) { Print(ChartID(), ": Reading result ", resname); ... } }; [/CODE] 逐行看:protected 下的两个 string 存请求方法和地址;构造函数把前缀默认写成 "WRP_",传给基类做 worker 标识。getMethod()/getURL() 都带 const 修饰,保证不改动对象状态。 request() 里先把入参 method、url 赋给成员变量,注释处原文留了 allocate() 的问号——说明内存分配和后续异步投递还没写完,你开 MT5 建类时得自己补 WebRequest 或 CWebRequest 的分配逻辑。receiveResult() 是静态回调,用 Print 把 ChartID 和结果名打出来,方便在多图表环境下定位是哪张图收到的响应。外汇与贵金属杠杆高,自动化请求失败可能漏掉信号,实盘前务必在策略测试器里跑通超时与异常分支。

MQL5 / C++
class="kw">protected:
  class="type">class="kw">string _method;
  class="type">class="kw">string _url;

class="kw">public:
  ClientWebWorker(class="kw">const class="type">long id, class="kw">const class="type">class="kw">string p = "WRP_"): WebWorker(id, p)
  {
  }
  class="type">class="kw">string getMethod() class="kw">const
  {
    class="kw">return _method;
  }
  class="type">class="kw">string getURL() class="kw">const
  {
    class="kw">return _url;
  }

  class="type">bool request(class="kw">const class="type">class="kw">string method, class="kw">const class="type">class="kw">string url, class="kw">const class="type">class="kw">string headers, class="kw">const class="type">int timeout, class="kw">const class="type">uchar &body[], class="kw">const class="type">long managerChartID)
  {
    _method = method;
    _url = url;
    class=class="str">"cmt">// allocate()? and what&class="macro">#x27;s next?
    ...
  }

  class="kw">static class="type">void receiveResult(class="kw">const class="type">class="kw">string resname, class="type">uchar &initiator[], class="type">uchar &headers[], class="type">uchar &text[])
  {
    Print(ChartID(), ": Reading result ", resname);
    ...
  }
};

◍ 用 ResourceMediator 把请求塞进资源

底层资源由模板类 RESOURCEDATA 处理,传入 uchar 作存储单位最省空间,RESOURCEDATA<uchar> 对象就是数据容器。创建时构造函数要给一个程序内唯一的 name,把这个 name 加前缀通过自定义事件发出去,别的 MQL 程序就能只读访问同一份资源。 写资源用重载的 '=' 操作符,读用 Get 函数,array 作输出参数原样带回内容。直接跨程序传 HTTP 请求和结果,中间加一层 ResourceMediator:客户端把 method、url、headers、timeout、data 打包进 uchar 数组写资源,服务端解包;返回时再把 result 和 result_headers 打包写回,客户端读出来解包。 打包的坑在元信息。若只用 StringToCharArray 把 URL 转字节,反向 CharArrayToString 不指定长度就会连后面头部字段一起读出来。所以先定义 LEADSIZE = sizeof(int)*5 存 5 个栏位长度,用 union 把结构体转字节序列铺在 data 数组头部,字符串用 StringToCharArray 按 cursor 偏移拷贝,timeout 走 int2chars 联合拆字节,body 本身是 uchar 数组原样复制,最后整体通过重载 '=' 移进资源。 unpackRequest 就是反向:resource.Get(array) 先读元数据字节,再按偏移取各栏位。响应走 packResponse / unpackResponse,逻辑同构。ClientWebWorker 里的 request 和 receiveResult 因此极薄,脏活全在 mediator。剩下谁调 WebWorker、managerChartID 怎么来,留到更高层管理类再谈,但服务端准备工作得先对齐。

MQL5 / C++
RESOURCEDATA<class="type">uchar>(class="kw">const class="type">class="kw">string name)
class="type">void class="kw">operator=(class="kw">const class="type">uchar &array[]) class="kw">const
class="type">int Get(class="type">uchar &array[]) class="kw">const
class="macro">#define LEADSIZE(class="kw">sizeof(class="type">int)*class="num">5) class=class="str">"cmt">// web-request 中的5个栏位
class ResourceMediator
{
  class="kw">private:
    class="kw">const RESOURCEDATA<class="type">uchar> *resource; class=class="str">"cmt">// underlying asset
    
    class=class="str">"cmt">// 头部的元数据可以是5个整数的‘lengths’或者是一个字节数组`sizes`
    union lead
    {
      class="kw">struct _l
      {
        class="type">int m; class=class="str">"cmt">// 方法
        class="type">int u; class=class="str">"cmt">// url
        class="type">int h; class=class="str">"cmt">// 头部
        class="type">int t; class=class="str">"cmt">// 超时
        class="type">int b; class=class="str">"cmt">// 主体
      }
      lengths;
      
      class="type">uchar sizes[LEADSIZE];
      
      class="type">int total()
      {
        class="kw">return lengths.m + lengths.u + lengths.h + lengths.t + lengths.b;
      }
    }
    metadata;

    class=class="str">"cmt">// 整数和字节数组
    union _s
    {
      class="type">int x;
      class="type">uchar b[class="kw">sizeof(class="type">int)];
    }
    int2chars;
    

  class="kw">public:
    ResourceMediator(class="kw">const RESOURCEDATA<class="type">uchar> *r): resource(r)
    {
    }
    
    class="type">void packRequest(class="kw">const class="type">class="kw">string method, class="kw">const class="type">class="kw">string url, class="kw">const class="type">class="kw">string headers, class="kw">const class="type">int timeout, class="kw">const class="type">uchar &body[])
    {
      class=class="str">"cmt">// 使用参数数据长度填充元数据
      metadata.lengths.m = StringLen(method) + class="num">1;
      metadata.lengths.u = StringLen(url) + class="num">1;
      metadata.lengths.h = StringLen(headers) + class="num">1;

常见问题

同步WebRequest会阻塞EA事件流,导致OnTick被拖慢。改用异步多线程管理器统管请求,把网络调用移出主事件循环即可。
用multiweb.mqh统一声明通信类,所有助手EA引用同一头文件,由管理器协调收发,避免重复连接和资源争用。
小布可以接入你的EA日志流,实时标记请求超时、资源泄漏等异常,并把多 worker 状态汇总成一眼能看的面板,省去你手动翻日志。
至少存请求ID、状态码、所属资源句柄和完成标志。基类统一管这些字段,子类只填业务参数,避免状态错乱。
多线程并发写入资源池会覆盖数据。Mediator用内部队列加锁串行化入队,保证每个请求拿到独立资源槽位不冲突。