自己动手开发多线程异步 MQL5 WebRequest·进阶篇
(2/3)· 当 EA 卡在 HTTP 请求上错过分时,三层线程架构能把延迟藏进辅助图表
把 HTTP 请求塞进资源字节流
在 MT5 里做跨图表传递或离线回放时,直接丢字符串和数组容易乱序,更稳的做法是把请求各字段按字节拼进一个 uchar 数组再存成 resource。下面这段打包逻辑先记好元数据的各段长度:t 是 int 的字节数(通常 4),b 是 body 的 ArraySize 返回值。 metadata.lengths.t = sizeof(int); metadata.lengths.b = ArraySize(body); // 分配结果数组,用于元数据和参数数据 uchar data[]; ArrayResize(data, LEADSIZE + metadata.total()); // 把元数据以字节数组方式放在数组的开始 ArrayCopy(data, metadata.sizes); // 把数据栏位挨个放到数组中 int cursor = LEADSIZE; uchar temp[]; StringToCharArray(method, temp); ArrayCopy(data, temp, cursor); ArrayResize(temp, 0); cursor += metadata.lengths.m; StringToCharArray(url, temp); ArrayCopy(data, temp, cursor); ArrayResize(temp, 0); cursor += metadata.lengths.u; StringToCharArray(headers, temp); ArrayCopy(data, temp, cursor); ArrayResize(temp, 0); cursor += metadata.lengths.h; int2chars.x = timeout; ArrayCopy(data, int2chars.b, cursor); cursor += metadata.lengths.t; ArrayCopy(data, body, cursor); // 在资源中存储数组 resource = data; 解包时反向读即可,重点是用 resource.Get(array) 把字节拉回来,再按 LEADSIZE 之后的 cursor 偏移逐段 CharArrayToString。实测打印出的 n 就是整个请求体的字节数,比如一次带 200 字节 body 的 POST 可能得到 200+LEADSIZE+各字符串长度。 void unpackRequest(string &method, string &url, string &headers, int &timeout, uchar &body[]) { uchar array[]; // 使用资源中的数据填充数组 int n = resource.Get(array); Print(ChartID(), ": Got ", n, " bytes in request"); // 从数组中读取元数据 ArrayCopy(metadata.sizes, array, 0, 0, LEADSIZE); int cursor = LEADSIZE; // 一个个读取所有的数据栏位 method = CharArrayToString(array, cursor, metadata.lengths.m); cursor += metadata.lengths.m; url = CharArrayToString(array, cursor, metadata.lengths.u); cursor += metadata.lengths.u; headers = CharArrayToString(array, cursor, metadata.lengths.h); cursor += metadata.lengths.h; ArrayCopy(int2chars.b, array, 0, cursor, metadata.lengths.t); timeout = int2chars.x; cursor += metadata.lengths.t; if(metadata.lengths.b > 0) { ArrayCopy(body, array, 0, cursor, metadata.lengths.b); } } 响应侧另有 packResponse(const string source, const uchar &result[], const string &result_headers),结构同理,只是字段换成来源标识和返回头。外汇与贵金属行情接口调用属高风险操作,字节错位可能导致请求被拒或脏数据,上线前务必在策略测试器跑一遍 Get 出来的 n 是否符合预期。
metadata.lengths.t = class="kw">sizeof(class="type">int); metadata.lengths.b = ArraySize(body); class=class="str">"cmt">// 分配结果数组,用于元数据和参数数据 class="type">uchar data[]; ArrayResize(data, LEADSIZE + metadata.total()); class=class="str">"cmt">// 把元数据以字节数组方式放在数组的开始 ArrayCopy(data, metadata.sizes); class=class="str">"cmt">// 把数据栏位挨个放到数组中 class="type">int cursor = LEADSIZE; class="type">uchar temp[]; StringToCharArray(method, temp); ArrayCopy(data, temp, cursor); ArrayResize(temp, class="num">0); cursor += metadata.lengths.m; StringToCharArray(url, temp); ArrayCopy(data, temp, cursor); ArrayResize(temp, class="num">0); cursor += metadata.lengths.u; StringToCharArray(headers, temp); ArrayCopy(data, temp, cursor); ArrayResize(temp, class="num">0); cursor += metadata.lengths.h; int2chars.x = timeout; ArrayCopy(data, int2chars.b, cursor); cursor += metadata.lengths.t; ArrayCopy(data, body, cursor); class=class="str">"cmt">// 在资源中存储数组 resource = data; class="type">void unpackRequest(class="type">class="kw">string &method, class="type">class="kw">string &url, class="type">class="kw">string &headers, class="type">int &timeout, class="type">uchar &body[]) { class="type">uchar array[]; class=class="str">"cmt">// 使用资源中的数据填充数组 class="type">int n = resource.Get(array); Print(ChartID(), ": Got ", n, " bytes in request"); class=class="str">"cmt">// 从数组中读取元数据 ArrayCopy(metadata.sizes, array, class="num">0, class="num">0, LEADSIZE); class="type">int cursor = LEADSIZE; class=class="str">"cmt">// 一个个读取所有的数据栏位 method = CharArrayToString(array, cursor, metadata.lengths.m); cursor += metadata.lengths.m; url = CharArrayToString(array, cursor, metadata.lengths.u); cursor += metadata.lengths.u; headers = CharArrayToString(array, cursor, metadata.lengths.h); cursor += metadata.lengths.h; ArrayCopy(int2chars.b, array, class="num">0, cursor, metadata.lengths.t); timeout = int2chars.x; cursor += metadata.lengths.t; if(metadata.lengths.b > class="num">0) { ArrayCopy(body, array, class="num">0, cursor, metadata.lengths.b); } } class="type">void packResponse(class="kw">const class="type">class="kw">string source, class="kw">const class="type">uchar &result[], class="kw">const class="type">class="kw">string &result_headers);
「用图表事件把 HTTP 响应搬回 EA」
在 MT5 里做异步 Web 请求,难点从来不是发得出去,而是响应怎么绕回发起图表。上面这段 ClientWebWorker 的做法,是把请求参数打包后,用 EventChartCustom 向管理图表的 managerChartID 投一个自定义事件,事件 ID 暂填 0,后续要换成独立标识避免冲突。 request() 里先存下 method 和 url,再借 ResourceMediator 的 packRequest 把头部、超时、body 塞进资源通道;busy 标记直接取 EventChartCustom 的返回值,非零才代表事件投递成功,调用方可以据此判断本轮请求是否真的排上队。 响应回来走 receiveResult() 静态函数:先用 ChartID() 打印当前图表号,再从以 resname 命名的 RESOURCEDATA<uchar> 里取出资源,交给同一个 mediator 做 unpackResponse,把 initiator、headers、text 三个字节数组填回去。你在实盘 EA 里照抄这套,开两个图表分别当管理端和客户端,就能在客户端图表直接 Print 出返回头和内容,验证跨图表通信是否通了。外汇与贵金属行情接口调用受网络与经纪商限制,异步失败概率不低,参数和事件号务必自测。
class="type">void unpackResponse(class="type">uchar &initiator[], class="type">uchar &headers[], class="type">uchar &text[]); class ClientWebWorker : class="kw">public WebWorker { ... 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; ResourceMediator mediator(allocate()); mediator.packRequest(method, url, headers, timeout, body); busy = EventChartCustom(managerChartID, class="num">0 class=class="str">"cmt">/* TODO: 特定的信息 */, chartID, class="num">0.0, getFullName()); class="kw">return busy; } 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); class="kw">const RESOURCEDATA<class="type">uchar> resource(resname); ResourceMediator mediator(&resource); mediator.unpackResponse(initiator, headers, text); } };
◍ 服务端 Worker 与池化模板怎么写
在管理器(服务端)一侧,我们需要一个从 WebWorker 派生的 ServerWebWorker 类,职责和客户端那边的 ClientWebWorker 近似,但只负责把异步请求分发给辅助 EA 实例。transfer 方法对应交互流程第二步:用客户端传来的 resname(资源名)和 clientChartID(客户图表 ID)做任务派发;receive 方法则在本地 allocate 出资源,并通过 ResourceMediator 把互联网取回的 result 与 result_headers 写进资源,完成第五步的部分动作。 由于客户端可能同时下几个文档,而管理器端一开始最好预开足够多的助手实例,否则多客户并发时容易堵。于是用 WebWorkersPool 这个模板基类来托管对象数组——客户端存 ClientWebWorker,服务端存 ServerWebWorker,类型不同所以用 template<typename T>。构造函数按指定 size 分配数组,析构逐一 delete 动态指针,findWorker / getIdleWorker 按条件检索,operator<< 动态加对象,revoke 动态删。 客户端池因事件处理特殊,又从基类扩出 ClientWebWorkersPool,三个变量要留意:managerChartID 是管理器窗口 ID,managerPoolSize 是初始数组大小,name 是所有资源通用前缀(如 WRP_)。下面代码可直接贴进 MT5 看类结构。
class ServerWebWorker : class="kw">public WebWorker { class="kw">public: ServerWebWorker(class="kw">const class="type">long id, class="kw">const class="type">class="kw">string p = "WRP_"): WebWorker(id, p) { } class="type">bool transfer(class="kw">const class="type">class="kw">string resname, class="kw">const class="type">long clientChartID) { class=class="str">"cmt">// 回应 `clientChartID` 的客户端,任务名为 `resname` class=class="str">"cmt">// 并且把任务分配给 ID 为 `chartID` 的工作单元 busy = EventChartCustom(clientChartID, TO_MSG(MSG_ACCEPTED), chartID, class="num">0.0, resname) && EventChartCustom(chartID, TO_MSG(MSG_WEB), clientChartID, class="num">0.0, resname); class="kw">return busy; } class="type">void receive(class="kw">const class="type">class="kw">string source, class="kw">const class="type">uchar &result[], class="kw">const class="type">class="kw">string &result_headers) { ResourceMediator mediator(allocate()); mediator.packResponse(source, result, result_headers); } }; class="kw">template<class="kw">typename T> class WebWorkersPool { class="kw">protected: T *workers[]; class="kw">public: WebWorkersPool() {} WebWorkersPool(class="kw">const class="type">uint size) { class=class="str">"cmt">// 分配工作单元;在客户端它们用于在资源中保存请求参数 ArrayResize(workers, size); for(class="type">int i = class="num">0; i < ArraySize(workers); i++) { workers[i] = NULL; } } ~WebWorkersPool() { for(class="type">int i = class="num">0; i < ArraySize(workers); i++) { if(CheckPointer(workers[i]) == POINTER_DYNAMIC) class="kw">delete workers[i]; } } class="type">int size() class="kw">const { class="kw">return ArraySize(workers); } class="type">void class="kw">operator<<(T *worker) { class="kw">const class="type">int n = ArraySize(workers); ArrayResize(workers, n + class="num">1); workers[n] = worker; } T *findWorker(class="kw">const class="type">class="kw">string resname) class="kw">const { for(class="type">int i = class="num">0; i < ArraySize(workers); i++) { if(workers[i] != NULL) {
Worker 池的检索与回收接口
这段模板类代码给出了多图表工作线程(worker)池的几个核心查询与销毁方法,直接决定了 EA 在 MT5 里如何复用子图表资源。
getWorkerByName 用全名字符串比对,命中即返回对象指针,否则落空返回 NULL;getIdleWorker 则先确认 ChartPeriod(chart_id) > 0(图表仍存在),再筛 !isBusy() 的空闲实例,适合调度新任务。
findWorker(const long id) 按图表 ID 精确查找,revoke(const long id) 在确认 CheckPointer == POINTER_DYNAMIC 后 delete 并置 NULL,避免野指针。available() 遍历数组统计非空槽位,复杂度 O(ArraySize),在 worker 数超 50 时可能成为微瓶颈。
下标运算符 operator[](int i) 仅作常量指针透传,调用方需自担越界与 NULL 风险。复制进 MT5 后,可先打印 available() 验证池容量是否符合预期。
if(workers[i].getFullName() == resname) { class="kw">return workers[i]; } } class="kw">return NULL; } T *getIdleWorker() class="kw">const { for(class="type">int i = class="num">0; i < ArraySize(workers); i++) { if(workers[i] != NULL) { if(ChartPeriod(workers[i].getChartID()) > class="num">0) class=class="str">"cmt">// check if exist { if(!workers[i].isBusy()) { class="kw">return workers[i]; } } } } class="kw">return NULL; } T *findWorker(class="kw">const class="type">long id) class="kw">const { for(class="type">int i = class="num">0; i < ArraySize(workers); i++) { if(workers[i] != NULL) { if(workers[i].getChartID() == id) { class="kw">return workers[i]; } } } class="kw">return NULL; } class="type">bool revoke(class="kw">const class="type">long id) { for(class="type">int i = class="num">0; i < ArraySize(workers); i++) { if(workers[i] != NULL) { if(workers[i].getChartID() == id) { if(CheckPointer(workers[i]) == POINTER_DYNAMIC) class="kw">delete workers[i]; workers[i] = NULL; class="kw">return true; } } } class="kw">return class="kw">false; } class="type">int available() class="kw">const { class="type">int count = class="num">0; for(class="type">int i = class="num">0; i < ArraySize(workers); i++) { if(workers[i] != NULL) { count++; } } class="kw">return count; } T *class="kw">operator[](class="type">int i) class="kw">const {
「客户端工作池怎么接管理器的回包」
在 MT5 里做跨图表 WebRequest 异步调用,客户端池 ClientWebWorkersPool 继承自 WebWorkersPool<T>,构造时靠 WebWorker::broadcastEvent(TO_MSG(MSG_DISCOVER), ChartID()) 向管理器图表广播发现消息,自身并不直连网络。
WebRequestAsync 先抓空闲 worker,命中后把请求转给 managerChartID 指定的管理器图表;若池内无空闲 worker 直接返回 false,这意味着并发上限由池 size 决定,超量请求会被静默丢弃。
图表事件里 MSG_DONE 代表异步结束:dparam 是结果码,sparam 是资源名,用 ClientWebWorker::receiveResult 从资源读出 initiator/headers/text,再按 initiator 名找回原 worker 调 onResult 并 release。
MSG_HELLO 是发现阶段的回包:当 managerChartID==0 且 lparam!=0 时,用 ChartPeriod(lparam)>0 校验图表有效性后记录管理器 ID 与池大小,后续请求才有投递目标。外汇与贵金属行情接口调用受经纪商服务器限制,异步失败概率随并发升高而上升,需实测池大小。
class="kw">template<class="kw">typename T> class ClientWebWorkersPool: class="kw">public WebWorkersPool<T> { class="kw">protected: class="type">long managerChartID; class="type">short managerPoolSize; class="type">class="kw">string name; class="kw">public: ClientWebWorkersPool(class="kw">const class="type">uint size, class="kw">const class="type">class="kw">string prefix): WebWorkersPool(size) { name = prefix; class=class="str">"cmt">// 尝试寻找 WebRequest 管理器图表 WebWorker::broadcastEvent(TO_MSG(MSG_DISCOVER), ChartID()); } class="type">bool WebRequestAsync(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="type">int timeout, class="kw">const class="type">char &data[]) { T *worker = getIdleWorker(); if(worker != NULL) { class="kw">return worker.request(method, url, headers, timeout, data, managerChartID); } class="kw">return class="kw">false; } class="type">void onChartEvent(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam) { if(MSG(id) == MSG_DONE) class=class="str">"cmt">// 异步请求完成,有结果或者出错 { Print(ChartID(), ": Result code ", (class="type">long)dparam); if(sparam != NULL) { class=class="str">"cmt">// 从资源中根据 sparam 中的名称读取数据 class="type">uchar initiator[], headers[], text[]; ClientWebWorker::receiveResult(sparam, initiator, headers, text); class="type">class="kw">string resname = CharArrayToString(initiator); T *worker = findWorker(resname); if(worker != NULL) { worker.onResult((class="type">long)dparam, headers, text); worker.release(); } } } ... else if(MSG(id) == MSG_HELLO) class=class="str">"cmt">// MSG_DISCOVER 广播结果找到了管理器 { if(managerChartID == class="num">0 && lparam != class="num">0) { if(ChartPeriod(lparam) > class="num">0) { managerChartID = lparam; managerPoolSize = (class="type">short)dparam; for(class="type">int i = class="num">0; i < ArraySize(workers); i++)
◍ 用 ChartID 给工作线程贴标签
上面这段把管理器图表 ID 和序号拼进对象名,本质是给每个 worker 一个可回溯的身份证。ChartID() 返回当前图表唯一整数,name 加 i+1 再加下划线,能在 MT5 终端里直接搜到对应实例。 isManagerBound() 只做一件事:managerChartID 非 0 才认为绑定成功。这个 0 值判断是哨兵,没拿到合法图表 ID 的 manager 后续逻辑不该跑。 开 MT5 新建 EA 把这段塞进类定义,编译后看 experts 日志里 name_1_、name_2_ 的构造顺序,能确认多实例是不是按预期起得来。外汇和贵金属品种波动大,这类图表对象若没随图表关闭释放,可能拖慢终端。
workers[i] = new T(ChartID(), name + (class="type">class="kw">string)(i + class="num">1) + "_"); } } } } } class="type">bool isManagerBound() class="kw">const { class="kw">return managerChartID != class="num">0; } };