Connexus客户端(第七部分):添加客户端层·进阶篇
🧩

Connexus客户端(第七部分):添加客户端层·进阶篇

(2/3)·直接调WebRequest常被超时与状态码搞崩,封装一层才能让请求可复用、可测试

案例拆解新手友好 第 2/3 篇
很多人把WebRequest直接散落在EA各处的OnTick里,一旦服务端改了返回结构,全盘要改。耦合到业务里的请求代码,调试时连是哪一步超时都难定位。

「用 MT5 原生类打通一次 HTTP 请求」

想在 MT5 里把行情外的数据拉进来,第一步是确认终端真能出网。下面这段用 Connexus 封装的 HttpClient 在 OnInit 里发了一个 GET 到 httpbin.org,回测环境之外实盘终端才能跑通。

MQL5 / C++
class="macro">#include <Connexus/Core/HttpClient.mqh>
class="type">int OnInit()
  {
   CHttpRequest request;
   request.Method() = HTTP_METHOD_GET;
   request.Url().Parse("https:class=class="str">"cmt">//httpbin.org/get");
   CHttpResponse response;
   CHttpClient client;
   client.Send(request,response);
   Print(response.FormatString());
   class="kw">return(INIT_SUCCEEDED);
  }
逐行看:先 include 第三方 HTTP 核心类;OnInit 里建 request 对象并设方法为 GET、目标 URL 解析好;response 承接返回;client.Send 阻塞式发出;Print 把格式化响应打到日志。 实测一次返回 Status Code 200,Duration 845 ms,Body 里 User-Agent 暴露为 MetaTrader 5 Terminal/5.4620 (Windows NT 11.0.22631; x64),说明请求头由终端自己拼。外汇与贵金属交易本身高风险,外接 API 还叠加断网与延迟不确定性,参数 timeout 建议按网络质量调。 如果不想引库,也可用 WebRequest 原生函数:定义 method、url、headers、timeout=5000ms、data[] 与 result[] 字符数组,body 用 StringToCharArray 转 UTF8 再发。两种路径在 MT5 里都能验证,复制去 OnInit 跑一次便知哪条更顺手。

MQL5 / C++
class="macro">#include <Connexus/Core/HttpClient.mqh>
class="type">int OnInit()
  {
   CHttpRequest request;
   request.Method() = HTTP_METHOD_GET;
   request.Url().Parse("https:class=class="str">"cmt">//httpbin.org/get");
   CHttpResponse response;
   CHttpClient client;
   client.Send(request,response);
   Print(response.FormatString());
   class="kw">return(INIT_SUCCEEDED);
  }

用 WebRequest 与 MQL5 HTTP 类各跑一次 GET

MQL5 里发 HTTP 请求有两条路:直接调 WebRequest 函数,或用标准库里的 CHttpRequest / CHttpClient 封装类。两者都能在 EA 初始化阶段把远端响应打进日志,方便你当场核对接口联通性。 下面这段直接函数调用,先 ArrayRemove 把 data 数组末尾一项剔除,再发请求。status_code 拿到 HTTP 状态码,result 是字节数组,必须用 CharArrayToString 转成字符串才能肉眼看响应体。 ArrayRemove(data,ArraySize(data)-1); int status_code = WebRequest(method,url,headers,timeout,data,result,result_headers); Print("Status code: ",status_code); Print("Response: ",CharArrayToString(result)); 另一条路用类封装:CHttpRequest 设方法为 GET、Url 解析到 httpbin.org/get、Body 加一段 JSON;CHttpResponse 接返回;CHttpClient.Send 发出后直接 Print(response.FormatString()) 看整包。 开 MT5 新建 EA,把这两段分别塞进 OnInit,编译挂到图表,看终端日志里是否打出 Status code: 200 与响应原文。外汇与贵金属行情接口若走外部 HTTP,需注意网络延迟与断连风险,回测环境默认禁网。

MQL5 / C++
  ArrayRemove(data,ArraySize(data)-class="num">1);

class=class="str">"cmt">//--- Calling the function and getting the status_code
  class="type">int status_code = WebRequest(method,url,headers,timeout,data,result,result_headers);

class=class="str">"cmt">//--- Print the data
  Print("Status code: ",status_code);
  Print("Response: ",CharArrayToString(result)); class=class="str">"cmt">// We use CharArrayToString to display the response in class="type">class="kw">string form.
class=class="str">"cmt">//---
  class="kw">return(INIT_SUCCEEDED);
  }
class="type">int OnInit()
  {
  class=class="str">"cmt">//--- Request object
  CHttpRequest request;
  request.Method() = HTTP_METHOD_GET;
  request.Url().Parse("https:class=class="str">"cmt">//httpbin.org/get");
  request.Body().AddString("{\"key\":\"value\"}");

  class=class="str">"cmt">//--- Response object
  CHttpResponse response;

  class=class="str">"cmt">//--- Client http object
  CHttpClient client;
  client.Send(request,response);
  Print(response.FormatString());

  class=class="str">"cmt">//--- Initialize
  class="kw">return(INIT_SUCCEEDED);
  }

◍ 把 HttpClient 从 WebRequest 里拔出来

现在 CHttpClient 里直接写死了 WebRequest 调用,Send() 和底层传输绑死,这就是典型的高耦合。改一个函数签名,或者想换一种传输方式,就得动整块类,后期加功能会越改越乱。 解决思路是接口加依赖注入。先定义一个 IHttpTransport 接口,只规定“能发 HTTP 请求”这件事;CHttpClient 不再认 WebRequest,只认 IHttpTransport。具体用真 WebRequest 还是测试用的 FakeWebRequest,由外部传进去。 这样做有三个实在好处:维护时像换汽车零件,不动发动机就能换火花塞;复用时代码块能直接搬去别的项目;测试时把 WebRequest 换成模拟对象,不联网也能跑通逻辑,顺便引出后面的 Mock 技术。外汇和贵金属 EA 开发里这类解耦能显著降低实盘调试风险,但任何自动交易仍属高风险,需充分回测。

「用模拟对象绕开真实网络依赖」

把 CHttpClient 和 WebRequest 通过接口解耦后,就能塞进一个满足同样契约的模拟版本。它不真正发 HTTP 请求,只在受控环境里返回你预设的响应,用来验证类在各种场景下的行为。 直接让 CHttpClient 调 WebRequest 时,测试被服务器连通性和返回内容绑死,本地跑一次用例都得看远端脸色。注入模拟对象后,断网也能测:正常 200、超时、JSON 畸形、空 body 都能手动构造。 这类模拟对容错验证是关键——真实接口偶尔抽风返回非预期结构,你总不能等线上炸了才发现解析分支有漏。下一段我们看具体怎么把接口和模拟对象落进代码,让 CHttpRequest 的可测性直接上一个台阶。

用接口把 WebRequest 从库里摘出来

做 MT5 网络请求封装时,直接让业务类调 WebRequest 会把代码焊死:想换传输实现或做离线测试都麻烦。办法是在调用方和函数之间插一层接口,只定契约不定实现,类再去落地。 接口就是一张合同,只写方法签名——返回类型、名字、参数。下面这段是 IHttpTransport 的声明,两个 Request 重载都只给头部,编译器知道长什么样,但不管里面怎么跑。

MQL5 / C++
interface IHttpTransport
  {
   class="type">int 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 cookie,class="kw">const class="type">class="kw">string referer,class="type">int timeout,class="kw">const class="type">char &data[],class="type">int data_size,class="type">char &result[],class="type">class="kw">string &result_headers);
   class="type">int 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="type">int timeout,class="kw">const class="type">char &data[],class="type">char &result[],class="type">class="kw">string &result_headers);
  };
上面第一处 Request 带 cookie、referer、data_size,适合带会话态的 POST;第二处用 headers 字符串代替 cookie/referer,更轻。两者都返回 int 状态码,result 和 result_headers 用引用回传,这是 MQL5 里绕开多返回值的常规写法。 实际落地时建 Interface 文件夹放 IHttpTransport.mqh,Core 里写 CHttpTransport 继承它并真调 WebRequest。CHttpClient 构造函数接收 IHttpTransport 实例存进 m_transport,默认构造若没传就 new 一个 CHttpTransport——这样老调用代码一行不改仍跑得通,但你已经能把模拟传输塞进去测异常分支。 别把耦合当省事 直接调 WebRequest 初期快,后面加代理、加Mock、换异步就很疼。接口层多写 20 行,换来的是 Connexus 这类库能在不发真实请求下跑通场景,外汇和贵金属 EA 联调时少烧几组实时行情。

MQL5 / C++
|--- Connexus
|--- |--- Core
|--- |--- |--- HttpTransport.mqh
|--- |--- Interface
|--- |--- |--- IHttpTransport.mqh
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//| transport interface                                                              |</span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="keyword">interface</span> IHttpTransport
  {
   <span class="keyword">class="type">int</span>                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 cookie,class="kw">const class="type">class="kw">string referer,class="type">int timeout,class="kw">const class="type">char &data[],class="type">int data_size,class="type">char &result[],class="type">class="kw">string &result_headers);
   <span class="keyword">class="type">int</span>                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="type">int timeout,class="kw">const class="type">char &data[],class="type">char &result[],class="type">class="kw">string &result_headers);
  };
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//| Include files                                                                  |</span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="preprocessor">class="macro">#include </span><span class="class="type">class="kw">string">"../Interface/IHttpTransport.mqh"</span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//| class : CHttpTransport                                                          |</span>
<span class="comment">class=class="str">"cmt">//|                                                                                |</span>
<span class="comment">class=class="str">"cmt">//| [PROPERTY]                                                                     |</span>
<span class="comment">class=class="str">"cmt">//| Name      : CHttpTransport                                                     |</span>
<span class="comment">class=class="str">"cmt">//| Heritage  : IHttpTransport                                                     |</span>

◍ 给 WebRequest 套一层可替换的传输类

把直接调用 WebRequest 的逻辑包进 CHttpTransport 类,等于在请求发起和底层函数之间加了一道中间层。这样做的好处是:上层代码只依赖 IHttpTransport 接口,哪天要换代理、加签名或改超时策略,只动这一层即可,不用碰业务主体。 类里提供了两个重载的 Request 方法。第一个带 cookie、referer 和 data_size 参数,适合带会话态和正文体的 REST 调用;第二个省略了 cookie 与 referer,由调用方把自定义头塞进 headers 字符串,更适合简单鉴权头透传。 构造函数与析构函数都是空实现,说明当前这层只做纯转发,不持有连接资源。下面这段就是类定义与第一个重载的实现,注意它最终直接 return WebRequest(...) 把参数原样下沉。 在 MT5 里实测时,timeout 传 5000 表示 5 秒,若远端黄金报价接口延迟抖动,这个数值会直接影响 EA 在极端行情下的数据更新概率,外汇与贵金属品种本身杠杆高、滑点风险大,参数须按实盘网络环境调。

MQL5 / C++
class CHttpTransport : class="kw">public IHttpTransport
  {
class="kw">public:
                    CHttpTransport(class="type">void);
                   ~CHttpTransport(class="type">void);
   class="type">int              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 cookie,class="kw">const class="type">class="kw">string referer,class="type">int timeout,class="kw">const class="type">char &data[],class="type">int data_size,class="type">char &result[],class="type">class="kw">string &result_headers);
   class="type">int              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="type">int timeout,class="kw">const class="type">char &data[],class="type">char &result[],class="type">class="kw">string &result_headers);
  };
CHttpTransport::CHttpTransport(class="type">void)
  {
  }
CHttpTransport::~CHttpTransport(class="type">void)
  {
  }
class="type">int CHttpTransport::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 cookie,class="kw">const class="type">class="kw">string referer,class="type">int timeout,class="kw">const class="type">char &data[],class="type">int data_size,class="type">char &result[],class="type">class="kw">string &result_headers)
  {
   class="kw">return(WebRequest(method,url,cookie,referer,timeout,data,data_size,result,result_headers));
  }
把请求诊断交给小布盯盘
这些诊断小布盯盘的AIGC已内置,打开对应品种页即可看到请求耗时与状态码分布,你专注决策而非埋点。

常见问题

传入已配置好的请求对象可保持不可变配置,响应对象由方法填充,避免每次new带来的堆碎片,也方便在调用处复用同一响应容器做多次重试。
参数较少的版本足以覆盖多数REST调用;当需要用自定义证书或代理时再切到扩展版本,客户端层应把这层差异屏蔽掉,不让库用户感知。
业务代码和HTTP细节纠缠后,改一个超时时间要翻多个文件,且无法单独对网络层做单元测试,回归成本随功能增长非线性上升。
小布盯盘内置的AIGC看板可记录品种页上的外部请求元数据,你把自己的Send封装接进日志流,就能在不用写额外探针的情况下观察真实发送表现。
模拟对象是用代码伪造的响应源,让Send在不联真服务器时也能跑通逻辑,主要用来验证超时处理与状态码分支是否符合预期。